@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,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-043 — A standing rule is pinned, not ranked, and the budget says what it dropped"
|
|
4
|
+
description: "The operator's own rule was written, stored, correct and never delivered. Three causes, each invisible on its own: an authored lesson carried `priority: 0.8` on a 70..100 scale, so it contributed 0.004 where a seed contributes 0.45; the char budget fits about two blocks while `maxInjectSession` promises five, silently; and ranking is the wrong mechanism for an instruction the operator did not want ranked."
|
|
5
|
+
tags: [decision, lessons, ranking, visibility]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-043 — A standing rule is pinned, not ranked
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`priority: 0.8` on an authored lesson becomes `80`.** Every other producer uses 70..100 and `relevanceScore`
|
|
17
|
+
divides by 200. The old value was written as if priority were the 0..1 scale `confidence` uses.
|
|
18
|
+
2. **A lesson can be pinned.** `tlc harness lessons add --pin` sets `pinned: true`; pinned lessons are placed before
|
|
19
|
+
every scored lesson, in store order.
|
|
20
|
+
3. **Pinning changes order, not eligibility.** Staleness, the validity window, mode filtering and the char budget
|
|
21
|
+
all still bind on a pinned lesson.
|
|
22
|
+
4. **`selectLessons` returns `omitted`**, and the injected block names it when it is non-zero.
|
|
23
|
+
5. **The note is silent when nothing was dropped.**
|
|
24
|
+
6. **`lessons list` marks a pinned lesson `PINNED`.**
|
|
25
|
+
|
|
26
|
+
## Why, and how the three causes hid each other
|
|
27
|
+
|
|
28
|
+
The intent was one rule: never declare work done without an end-to-end run pasted into the reply. It was written
|
|
29
|
+
to the global tier, stored correctly, and `lessons list` showed it active. It never reached a turn.
|
|
30
|
+
|
|
31
|
+
Running `selectLessons` against another repository and printing what came out gave the answer in one line — the
|
|
32
|
+
rule was absent and two core seeds were present. Then three separate causes had to be peeled apart:
|
|
33
|
+
|
|
34
|
+
**The priority scale.** `buildAuthoredLesson` set `priority: 0.8`. `relevanceScore` adds `priority / 200`, so an
|
|
35
|
+
authored lesson contributed **0.004** where `core:gate:stagnation` contributes 0.5. Combined with confidence 0.8
|
|
36
|
+
against 1, and no decay on core, every authored lesson sat at the bottom of every ranking. Shipped in AD-035 and
|
|
37
|
+
invisible for as long as nobody looked at an ordered list — the observed scores, 0.203 and 0.305 against 0.700,
|
|
38
|
+
match the arithmetic exactly.
|
|
39
|
+
|
|
40
|
+
**The silent budget.** `maxInjectSession` defaults to 5, `maxCharsSession` to 900, and a rendered block is four
|
|
41
|
+
lines. Two fit. So the count promised five, delivered two, and nothing anywhere said so. That is the same defect
|
|
42
|
+
family as a config value nothing reads, pointed the other way: a config value that reads as a promise the
|
|
43
|
+
mechanism cannot keep.
|
|
44
|
+
|
|
45
|
+
**Ranking as the wrong mechanism.** Fixing the scale was not enough, and the correct response was not to keep
|
|
46
|
+
raising numbers until the rule won. Ranking exists for lessons the harness *inferred* from failures — it weighs
|
|
47
|
+
recurrence, decay and gate match, all of which are estimates. An instruction the operator wrote deliberately has
|
|
48
|
+
no recurrence to accumulate and is not an estimate. Making it compete on score was a category error.
|
|
49
|
+
|
|
50
|
+
## Why pinning is bounded rather than absolute
|
|
51
|
+
|
|
52
|
+
A pinned lesson still obeys the char budget. The alternative — pinned content bypassing the budget — would let a
|
|
53
|
+
handful of pins consume the whole session context and make the budget a suggestion.
|
|
54
|
+
|
|
55
|
+
The worst case is therefore an operator pinning more than fits, and the omission note is what makes that visible
|
|
56
|
+
instead of silent. Staleness and validity still apply, because a pinned rule that names a renamed file is exactly
|
|
57
|
+
as wrong as an unpinned one.
|
|
58
|
+
|
|
59
|
+
## Trade-offs
|
|
60
|
+
|
|
61
|
+
**A pinned lesson displaces a higher-scoring one.** That is the point, and it is the operator's explicit choice.
|
|
62
|
+
|
|
63
|
+
**Nothing caps how many lessons can be pinned.** A cap would need a number nobody can justify. The budget already
|
|
64
|
+
bounds delivery, and `lessons list` shows every `PINNED` row.
|
|
65
|
+
|
|
66
|
+
**The priority fix changes the ranking of every authored lesson already on disk.** They move up, which is what the
|
|
67
|
+
0..100 scale always meant.
|
|
68
|
+
|
|
69
|
+
**The omission note costs a line whenever the budget binds.** With the default 900-char budget and any real store,
|
|
70
|
+
that is most turns — which is honest: it says the budget is the binding constraint, and raising `maxCharsSession`
|
|
71
|
+
is the operator's call.
|
|
72
|
+
|
|
73
|
+
## Not decided here
|
|
74
|
+
|
|
75
|
+
- **Raising the default `maxCharsSession`.** It is per-project config, and a default that spends more context by
|
|
76
|
+
itself is a decision for whoever installs the harness.
|
|
77
|
+
- **`lessons pin <id>` / `unpin <id>`.** Today pinning is set when the lesson is written; rewriting the same
|
|
78
|
+
instruction updates it in place, which covers the case.
|
|
79
|
+
- **Whether core seeds should ever be pinnable.** They ship immutable, so there is nowhere to record it.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-044 — Only an injection a gate could grade can be unproven"
|
|
4
|
+
description: "The effectiveness reading counted session-start injections, which nothing ever grades — a lesson whose gate is `any` is not even eligible on a retry. So a healthy pinned standing rule reported `unproven` forever and `doctor` warned about it on every run, in every repository on the machine. A separate `gradeableCount` is incremented where the pending credit is written."
|
|
5
|
+
tags: [decision, lessons, measurement, visibility]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-044 — Only an injection a gate could grade can be unproven
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`gradeableCount` counts injections a later gate run can grade** — incremented where the pending credit is
|
|
17
|
+
written, so the counter and the credit cannot disagree.
|
|
18
|
+
2. **`unproven` means `gradeableCount > 0` and nothing graded yet.** It no longer reads `injectedCount`.
|
|
19
|
+
3. **A lesson injected only at session start reads `session-only`**, rendered as
|
|
20
|
+
`session-only (injected 12x, never for a gate)`. It is not a warning.
|
|
21
|
+
4. **`doctor` says "1 lesson has been injected … nothing shows it helped"** with real agreement, instead of
|
|
22
|
+
"1 lesson have been injected … nothing shows it help".
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
`injectedCount` is incremented for every injection, in both modes. Only the retry path writes a pending credit,
|
|
27
|
+
because only there is there a gate whose next run can decide. So a session-start injection can never be graded.
|
|
28
|
+
|
|
29
|
+
The two facts combine badly for exactly the case AD-043 just made possible. A pinned standing rule is written
|
|
30
|
+
with gate `any`; `allowedForMode` on a retry requires `failedGate === gate`, so an `any` lesson is **never
|
|
31
|
+
eligible on a retry**. It is injected at every session start, `injectedCount` climbs, `gradeableCount` stays at
|
|
32
|
+
zero, and the old reading called that `unproven` — forever, in every repository on the machine, as a `doctor`
|
|
33
|
+
warning.
|
|
34
|
+
|
|
35
|
+
That is an alarm firing on a healthy install, which AD-034 named as its own defect family: a warning the reader
|
|
36
|
+
learns to skip takes the next real one with it. The measurement was also simply wrong — the lesson is not
|
|
37
|
+
unproven, it is unprovable by this mechanism, and those are different claims.
|
|
38
|
+
|
|
39
|
+
## How it was found
|
|
40
|
+
|
|
41
|
+
By running `tlc harness doctor` from a second repository after pinning the rule, and reading the output. The
|
|
42
|
+
grammar defect was in the same line.
|
|
43
|
+
|
|
44
|
+
## Trade-offs
|
|
45
|
+
|
|
46
|
+
**A third reading to explain.** `not-injected`, `session-only`, `unproven`, `helped`, `neutral`. The alternative
|
|
47
|
+
is one reading that is wrong for the most common case.
|
|
48
|
+
|
|
49
|
+
**`session-only` is not actionable.** Deliberately: it states that this lesson's usefulness cannot be measured by
|
|
50
|
+
the current mechanism, which is information, not a task.
|
|
51
|
+
|
|
52
|
+
**Records written before this field read `gradeableCount: 0`**, so an already-graded lesson keeps its `helped` or
|
|
53
|
+
`neutral` reading and an ungraded one drops from `unproven` to `session-only` until it is next injected for a
|
|
54
|
+
gate. No migration; the reading self-corrects.
|
|
55
|
+
|
|
56
|
+
## Not decided here
|
|
57
|
+
|
|
58
|
+
- **Grading session-start injections.** It would need a notion of "the session went well", which the harness does
|
|
59
|
+
not have. Recorded in AD-039 and still true.
|
|
60
|
+
- **Making an `any` lesson eligible on a retry.** It would be boosted by gates it was never about, which is the
|
|
61
|
+
reason AD-035 chose a neutral gate in the first place.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-045 — A gate verdict is reused when the content hash of its inputs did not change"
|
|
4
|
+
description: "A read-only question in a repository with uncommitted work re-ran the whole suite, because the trigger read the state of the tree rather than what the turn did. The verdict is now keyed on a content hash of the command and the files, which is the monorepo-tooling rule: same inputs, replay the result. Making a reused failure advisory instead of blocking was tried and rejected — the suite showed it silences the stagnation rail."
|
|
5
|
+
tags: [decision, gate, cost, caching]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-045 — A gate verdict is reused when its inputs did not change
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`computeInputsHash` keys a verdict** on the gate command plus each file's repository-relative path, size and
|
|
17
|
+
**contents**.
|
|
18
|
+
2. **A matching hash reuses the verdict** and the command does not execute.
|
|
19
|
+
3. **Contents, not mtime.** A `touch`, a branch switch restoring identical bytes, or a clock that moved all change
|
|
20
|
+
mtime without changing anything the gate reads.
|
|
21
|
+
4. **An unreadable input or a cap overrun is incomplete, and incomplete is never a hit.** Unknown means run.
|
|
22
|
+
5. **An incomplete hash is not recorded either.** It hashes over the subset it could read, which collides with the
|
|
23
|
+
hash of that subset alone.
|
|
24
|
+
6. **The gate name is compared as well as the hash**, in a function with its own test, because end to end the
|
|
25
|
+
check is unfalsifiable while the command is part of the key.
|
|
26
|
+
7. **A reused verdict changes nothing but execution.** It still blocks on failure, still advances the fingerprint,
|
|
27
|
+
still grades lessons.
|
|
28
|
+
8. **`gate.outcome` carries `reused`,** and the session report counts reused verdicts in their own column and adds
|
|
29
|
+
no time for them.
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
`stop.ts` decided whether to run the gates from `listChangedRepoFiles` — `git diff HEAD`, `--cached`,
|
|
34
|
+
`ls-files --others`. That is the state of the tree. With any uncommitted work present, **every** turn re-ran the
|
|
35
|
+
suite, including turns that changed nothing.
|
|
36
|
+
|
|
37
|
+
The report: a colleague asked "check the tasks and status" in a repository mid-feature. The agent answered, the
|
|
38
|
+
stop hook ran the suite, returned `BLOCKED: test failed`, and the agent abandoned the question to fix tests nobody
|
|
39
|
+
had asked about. The same mechanism is the likely cause of the earlier "more than thirty minutes running jest"
|
|
40
|
+
report, which was diagnosed as a slow machine and measured as hook latency. It was not the machine, and that
|
|
41
|
+
earlier conclusion is wrong.
|
|
42
|
+
|
|
43
|
+
The 2026 stop-hook pattern acts only when code was modified, detected from the turn's own tool calls. The
|
|
44
|
+
monorepo-tooling rule keys a task result on a content hash of its inputs and replays on a match. This takes the
|
|
45
|
+
second: a hash covers a turn that wrote nothing *and* a turn that wrote through a shell command, which
|
|
46
|
+
transcript-scanning does not.
|
|
47
|
+
|
|
48
|
+
## The divergence that stays
|
|
49
|
+
|
|
50
|
+
The published guidance is that the hook should detect and decide, not perform the validation — keep it under ten
|
|
51
|
+
seconds and let the agent run the suite when it acts on the block reason.
|
|
52
|
+
|
|
53
|
+
This harness does the opposite deliberately. If the agent executes the verification, it can skip it or misreport
|
|
54
|
+
it, and the whole thesis is that the verdict is not the model's to give. The cost of that choice is what the
|
|
55
|
+
colleague paid; the cache is how it stops being paid for questions rather than for work.
|
|
56
|
+
|
|
57
|
+
## What was tried and rejected
|
|
58
|
+
|
|
59
|
+
A reused **failure** was going to be advisory — surfaced as context rather than blocking — on the reasoning that
|
|
60
|
+
blocking on a failure the turn did not cause hijacks the operator's question.
|
|
61
|
+
|
|
62
|
+
The suite refused it. Four existing tests failed, and the reason is structural: with the advisory in place a
|
|
63
|
+
reused failure never reaches `failGate`, so `trackFingerprint` never runs, so the **stagnation rail stops firing
|
|
64
|
+
exactly in the case it exists for** — an agent that changes nothing and stops again. An agent could escape a grind
|
|
65
|
+
loop by doing nothing.
|
|
66
|
+
|
|
67
|
+
The discriminator that would make it safe is "was the operator asking, or was the agent told to fix this" — and
|
|
68
|
+
the stop payload does not carry it. `handoff.last_gate_result` and the loop counter are both proxies that break on
|
|
69
|
+
one provider or the other. So the advisory is not shipped, and the cost fix stands on its own: the suite does not
|
|
70
|
+
re-run, which is the part that was expensive.
|
|
71
|
+
|
|
72
|
+
No published source discusses replaying a cached **failure** at all; every example demonstrates a cache hit on a
|
|
73
|
+
passing task. That gap is real and this decision does not close it.
|
|
74
|
+
|
|
75
|
+
## Trade-offs
|
|
76
|
+
|
|
77
|
+
**Hashing costs a read per changed file per stop.** Bounded at 400 files and 12 MB; over either, the result is
|
|
78
|
+
incomplete and the gate runs. A hash of a few files is orders of magnitude cheaper than the command it replaces.
|
|
79
|
+
|
|
80
|
+
**A verdict can be stale in a way the hash cannot see.** An external input — a database, a network service, an
|
|
81
|
+
environment variable, the clock — can change without any file changing. Turborepo has the same exposure and
|
|
82
|
+
handles it by declaring inputs; here the changed-file set is the declaration, and a gate depending on the world
|
|
83
|
+
outside it will reuse a verdict that no longer holds.
|
|
84
|
+
|
|
85
|
+
**One artifact per repository, not per gate.** `readLastGate` returns the most recent artifact of any gate, so a
|
|
86
|
+
lint run overwrites the test verdict and the next test stop re-runs. Correct but weaker than it could be; a
|
|
87
|
+
per-gate artifact is a bigger change to a file other things read.
|
|
88
|
+
|
|
89
|
+
## Not decided here
|
|
90
|
+
|
|
91
|
+
- **Advisory for a reused failure.** Above. It needs a signal the harness does not have.
|
|
92
|
+
- **A per-gate artifact**, which would let lint and test both stay cached across a turn.
|
|
93
|
+
- **Declaring extra inputs** — a lock file, a config file, an environment variable — so a gate that depends on more
|
|
94
|
+
than the changed files can say so.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-046 — The runtime path is an artifact, and update never touches what it does not own"
|
|
4
|
+
description: "`tlc harness update` failed for a colleague because the harness itself had dirtied the checkout: update rebuilt `dist/` in place, and Bun and esbuild produce different bytes for the same source. The message then named the wrong cause, offered a remedy that runs the command that just failed, and told the operator to `git reset --hard` a path that on a contributor's machine is a symlink to their own repository. Ownership now decides what update may write."
|
|
5
|
+
tags: [decision, update, install, safety]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-046 — The runtime path is an artifact
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`classifyRuntimePath` names what the runtime path is**: `managed` (a checkout the installer created),
|
|
17
|
+
`linked` (a symlink to someone's clone), `unmanaged` (no `.git`), `absent`.
|
|
18
|
+
2. **The symlink test comes first**, decided by the path and never by its contents. A linked clone has a `.git`
|
|
19
|
+
too, so testing for that first would classify a contributor's repository as the harness's own.
|
|
20
|
+
3. **A managed artifact is moved with `git reset --hard`**, not a fast-forward merge. It is the harness's own, so a
|
|
21
|
+
local change in it is not the operator's work and not a conflict for them to resolve.
|
|
22
|
+
4. **A linked path receives no git command at all** — not fetch, not merge, not reset. Update refreshes only what
|
|
23
|
+
is machine-local (CLI link, skill link, provider hooks) and exits zero.
|
|
24
|
+
5. **`update` builds only when a bundle is missing.** `dist/` is committed and the gate keeps it matching `src/`,
|
|
25
|
+
so the pulled revision already carries the right bundles.
|
|
26
|
+
6. **Every message that named a remedy the harness does not perform is deleted.**
|
|
27
|
+
7. **`doctor` reports the path kind** as one `ok` row for both supported kinds.
|
|
28
|
+
|
|
29
|
+
## Why
|
|
30
|
+
|
|
31
|
+
Five defects, each measured.
|
|
32
|
+
|
|
33
|
+
**The harness created the dirt.** `update` ran `bin/tlc-build`, which rebuilds `dist/` inside the checkout.
|
|
34
|
+
`tlc-build` uses Bun when present and esbuild otherwise. Measured on the same source:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
bun 1.3.14 → 223,390 bytes
|
|
38
|
+
esbuild 0.28.1 → 228,018 bytes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Any toolchain differing from the one that produced the committed bundles leaves the checkout permanently dirty, so
|
|
42
|
+
**every later update fails to fast-forward**. That is every user, not an unlucky one.
|
|
43
|
+
|
|
44
|
+
**The diagnosis was wrong.** Git reported uncommitted local modifications; the message said "the runtime checkout
|
|
45
|
+
has commits that upstream does not". Two different situations, and the operator was sent looking for commits that
|
|
46
|
+
did not exist.
|
|
47
|
+
|
|
48
|
+
**One of the two remedies did not exist.** "Re-run the installer, which replaces the checkout" — `install.sh` runs
|
|
49
|
+
`git -C "$DEST" pull --ff-only`, the command that had just failed. It replaces nothing.
|
|
50
|
+
|
|
51
|
+
**The safety judgement was inverted.** It warned that `reset --hard` "throws work away". What it would have
|
|
52
|
+
discarded was build output the harness generated.
|
|
53
|
+
|
|
54
|
+
**And the remedy was destructive.** `install.sh` links `$DEST` to the clone it was run from, so on a contributor's
|
|
55
|
+
machine `~/.tlc/harness` **is a symlink to their working repository**. Verified on the author's machine while
|
|
56
|
+
diagnosing this. The printed `git reset --hard` would have destroyed uncommitted work. The colleague who reported
|
|
57
|
+
it was safe only because he had installed with the curl one-liner — by install method, not by design.
|
|
58
|
+
|
|
59
|
+
## The standard this adopts
|
|
60
|
+
|
|
61
|
+
The named smell is conflating a **package artifact** — replaceable, nobody edits it — with a **working tree**, which
|
|
62
|
+
assumes a human resolves conflicts. The published fixes, in order of strength: do not track generated files in the
|
|
63
|
+
install path; revert them before pulling; or replace the pull with a versioned release download and an atomic swap,
|
|
64
|
+
which is what rustup, nvm and deno do.
|
|
65
|
+
|
|
66
|
+
This change adopts the principle those all rest on — **the install path is an artifact the tool owns, and the
|
|
67
|
+
updater never touches anything it does not own** — without the release machinery. `git` stays the transport; what
|
|
68
|
+
changes is that the checkout is now genuinely disposable, because nothing is written into it that upstream does not
|
|
69
|
+
already ship.
|
|
70
|
+
|
|
71
|
+
## What the demo and CI each caught, after the tests were green
|
|
72
|
+
|
|
73
|
+
**The demo, before pushing.** `resolveHarnessRoot` calls `realpathSync`, and the first version classified that
|
|
74
|
+
resolved path — so a linked clone read as `managed` and `update` ran `git fetch` inside a contributor's repository.
|
|
75
|
+
Sixteen unit tests passed through it, because they called the classifier directly with the path they meant. Only
|
|
76
|
+
driving the real command against a real linked install showed it. Classification now takes the **configured** home.
|
|
77
|
+
|
|
78
|
+
**macOS CI, after pushing.** The fix for that came with a second condition: treat "the path resolves elsewhere" as
|
|
79
|
+
linked too, to catch a symlinked ancestor — justified in a comment with macOS as the example. macOS is what refuted
|
|
80
|
+
it. `/var` is a symlink to `/private/var`, so every path under the system temp directory resolves elsewhere, and a
|
|
81
|
+
**managed** checkout classified as `linked`: updates would have stopped silently on the platform the reporter uses.
|
|
82
|
+
Only the last hop decides now, which is the one thing `install.sh` actually creates, and a regression test asserts
|
|
83
|
+
that a managed checkout under a symlinked ancestor stays managed.
|
|
84
|
+
|
|
85
|
+
Both belong here because the shape repeats: a guess about the filesystem, argued from an example, and only a real
|
|
86
|
+
platform can settle it.
|
|
87
|
+
|
|
88
|
+
## Trade-offs
|
|
89
|
+
|
|
90
|
+
**A hard reset discards local commits at a managed runtime path silently.** That is the point: it is not a place to
|
|
91
|
+
develop. Anyone developing there has a `linked` install, which is never touched.
|
|
92
|
+
|
|
93
|
+
**No rollback.** A versioned-directory layout would make reverting a symlink flip. Deferred below.
|
|
94
|
+
|
|
95
|
+
**`state/` and `config.json` still live inside the artifact.** They are gitignored, so a reset cannot remove them —
|
|
96
|
+
asserted by a test rather than trusted, because the whole model rests on it.
|
|
97
|
+
|
|
98
|
+
**Building only when a bundle is missing trusts the committed `dist/`.** If upstream ever shipped a stale bundle, a
|
|
99
|
+
Node-fallback user would run stale code until a bundle went missing. `check-dist-fresh` in the gate is what makes
|
|
100
|
+
that impossible by construction, and it is a repository invariant rather than a runtime one.
|
|
101
|
+
|
|
102
|
+
## Not decided here
|
|
103
|
+
|
|
104
|
+
- **Versioned artifact directories with an atomic swap** (`~/.tlc/versions/<rev>`, `harness → versions/…`). The full
|
|
105
|
+
form of the standard, and it buys rollback. It needs release artifacts in CI, checksum verification, junctions on
|
|
106
|
+
Windows, and a migration for every existing install.
|
|
107
|
+
- **Moving `state/` and `config.json` out of the artifact.** The right end state — user data should not live inside
|
|
108
|
+
a disposable directory — but it is a data migration, and every moved file is a chance to lose an operator's global
|
|
109
|
+
lessons or obs history. It gets its own change, with a verified migration.
|
|
110
|
+
- **Making the build deterministic** by pinning one bundler. It would let `dist/` be rebuilt anywhere without dirt,
|
|
111
|
+
but pinning Bun breaks the esbuild fallback that exists for machines without Bun ([/decisions/ad-012.md](/decisions/ad-012.md)).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-047 — An instruction is not an affordance, and a refusal names the route that works"
|
|
4
|
+
description: "The bootstrap told the agent to read `.tlc/harness/state/handoff.json`, a path the floor guards, and the obvious command — `test -f … && head -c 2000 …` — was refused with advice about writing policy. Three layers disagreed: the instruction asked for something the runtime denies, and the denial could not be planned around. The instruction now names `tlc harness handoff`, `test` and `[` join the proven readers, and each refusal carries the remedy for its own case."
|
|
5
|
+
tags: [decision, floor, affordance, agent-experience]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-047 — An instruction is not an affordance
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`tlc harness handoff [--json]`** is the sanctioned way to read handoff state. Nothing has to reach into the
|
|
17
|
+
protected path.
|
|
18
|
+
2. **Every instruction naming the handoff names the command**, not the file — session start, session end, and the
|
|
19
|
+
operator bootstrap. The path still appears, as *where state lands*, never as the route.
|
|
20
|
+
3. **`test` and `[` join the proven readers.** They evaluate a predicate and produce an exit code; neither can write
|
|
21
|
+
a file, which makes them strictly safer than `echo`, already on the list.
|
|
22
|
+
4. **A policy-surface refusal carries the remedy for its own case.** A read refusal says how to read; a write
|
|
23
|
+
refusal says who may write.
|
|
24
|
+
5. **Allowlisting a verb does not allowlist a redirect.** `test -f x > config.json` is still denied by the redirect
|
|
25
|
+
rule.
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
29
|
+
A colleague's agent was told, by the harness, to read the handoff. It ran the natural command:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
test -f /…/.tlc/harness/state/handoff.json && head -c 2000 …
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`head` is a proven reader. `test` was not on the list, so the segment was denied — and the denial ended with the
|
|
36
|
+
sentence every policy-surface refusal ended with:
|
|
37
|
+
|
|
38
|
+
> Set a gate command with `tlc harness gate test-command` or `gate lint-command`, and run policy changes from your
|
|
39
|
+
> own terminal rather than from inside this session.
|
|
40
|
+
|
|
41
|
+
That is advice about *writing* policy, handed to an agent trying to *read* a file the harness had just pointed it
|
|
42
|
+
at. The operator's report was exact: "ele diz pra usar o handoff, mas tem comandos que não são permitidos — aí
|
|
43
|
+
confunde o agente."
|
|
44
|
+
|
|
45
|
+
Three layers had drifted apart: the instruction text, the set of permitted verbs, and the refusal.
|
|
46
|
+
|
|
47
|
+
## What the research changed
|
|
48
|
+
|
|
49
|
+
The 2026 literature on agent tool use names both halves of this.
|
|
50
|
+
|
|
51
|
+
The authorization work frames the general rule as **existence is not permission** — a tool being callable is not the
|
|
52
|
+
same as it being authorized, and prompt text is not a boundary. The inverse is the case here: **an instruction is
|
|
53
|
+
not an affordance.** Telling a model to do something is not the same as granting it, and the fix is to stop exposing
|
|
54
|
+
a route the runtime will refuse.
|
|
55
|
+
|
|
56
|
+
The recommended handling of the refusal itself is to *make the deny legible*: return structured, self-describing
|
|
57
|
+
refusals the model can plan around, rather than opaque ones. Work on tool documentation
|
|
58
|
+
(OpaqueToolsBench / ToolObserver) reaches the same place from the documentation side — descriptions are
|
|
59
|
+
load-bearing infrastructure and must cover failure modes, not only the happy path, because a model cannot predict
|
|
60
|
+
what a stateful environment will do.
|
|
61
|
+
|
|
62
|
+
Without that research this would have been fixed by adding `test` to the allowlist and stopping. The allowlist gap
|
|
63
|
+
is the smallest of the three defects; the refusal giving unrelated advice, and the harness pointing at a path it
|
|
64
|
+
guards, are the ones that confused the agent.
|
|
65
|
+
|
|
66
|
+
## Why `test` was missing, and why adding it is not a loosening
|
|
67
|
+
|
|
68
|
+
The allowlist is deliberately an allowlist: the ways a shell can write are unbounded — `python3 -c`, `perl -pi`,
|
|
69
|
+
`ex`, any interpreter — so enumerating writers guarantees a hole, and enumerating readers puts an unknown verb on
|
|
70
|
+
the deny side without anyone predicting it. `awk` and `sort` are excluded on purpose, because `awk '{print > f}'`
|
|
71
|
+
and `sort -o f` write a file the head verb never reveals.
|
|
72
|
+
|
|
73
|
+
`test` and `[` have no such form. There is no flag, no redirect of their own, no output beyond an exit code. Their
|
|
74
|
+
absence was an incomplete list rather than a decision.
|
|
75
|
+
|
|
76
|
+
## Trade-offs
|
|
77
|
+
|
|
78
|
+
**Two remedies to keep true instead of one.** A branch that denies without passing a remedy falls back to the write
|
|
79
|
+
advice, which is right for the write cases and wrong for a read case somebody adds later. The sensor covers the
|
|
80
|
+
distinction in both directions.
|
|
81
|
+
|
|
82
|
+
**`tlc harness handoff` is another surface to keep honest.** It reads the same file the hooks read, through the
|
|
83
|
+
facade, so it cannot report something different from what a hook sees.
|
|
84
|
+
|
|
85
|
+
**The bootstrap line is longer.** It now carries the command and the path, because the path is still where state
|
|
86
|
+
lands and an operator looking for the file should find it.
|
|
87
|
+
|
|
88
|
+
## Not decided here
|
|
89
|
+
|
|
90
|
+
- **Whether the floor should refuse `tlc harness handoff` mid-session.** It reads and writes nothing, so it is not
|
|
91
|
+
in `MUTATING_SUBCOMMANDS`.
|
|
92
|
+
- **Auditing every other instruction the harness emits against what the floor permits.** This one was found by a
|
|
93
|
+
report. A mechanical check — every path named in injected prose is one the floor allows a proven reader on —
|
|
94
|
+
would catch the class, and is not built.
|
|
95
|
+
- **`--json` shape stability.** The report is a projection over the handoff file, and the handoff schema is
|
|
96
|
+
versioned; the projection is not.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-048 — A broken updater cannot deliver its own fix, so the installer is the recovery route"
|
|
4
|
+
description: "AD-046 fixed `tlc harness update`, and the fix could not reach anyone who needed it: the new code lives in the revision the broken updater cannot fetch. A colleague updated, got the old message from his stale CLI, and typed `--force`, which was accepted in silence and did nothing. The installer now recovers a managed checkout with a hard reset — the one route that does not run through the installed CLI — and `update` says what `--force` is not."
|
|
5
|
+
tags: [decision, update, install, delivery]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-048 — A broken updater cannot deliver its own fix
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`install.sh` and `install.ps1` recover a managed checkout** with `fetch` + `reset --hard origin/main`, rather
|
|
17
|
+
than `pull --ff-only`.
|
|
18
|
+
2. **Neither installer runs git against a linked runtime.** `[[ -L "$DEST" ]]` on Unix and `LinkType` on Windows are
|
|
19
|
+
checked *before* the `.git` test, because `[[ -d "$DEST/.git" ]]` follows a symlink and would have matched a
|
|
20
|
+
contributor's clone.
|
|
21
|
+
3. **`update` validates its flags.** `--force` is answered with what it is not and what to do instead; any other
|
|
22
|
+
unknown flag is named.
|
|
23
|
+
4. **The README and the diagnose runbook name the one-liner as the recovery**, because it is the only route
|
|
24
|
+
independent of the installed CLI.
|
|
25
|
+
|
|
26
|
+
## Why
|
|
27
|
+
|
|
28
|
+
AD-046 was correct and undeliverable.
|
|
29
|
+
|
|
30
|
+
`tlc harness update` runs from the installed runtime. The fix for `update` is in a revision that `update` has to
|
|
31
|
+
fetch and fast-forward to — and the bug being fixed is precisely that the fast-forward aborts. So every install that
|
|
32
|
+
had ever rebuilt `dist/` with a different bundler was stuck at its current revision, and the new code could not
|
|
33
|
+
arrive.
|
|
34
|
+
|
|
35
|
+
The report shows it exactly. A colleague ran `tlc harness update --force` and got:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Updating 297b623..b453d8b
|
|
39
|
+
error: Your local changes to the following files would be overwritten by merge:
|
|
40
|
+
dist/… (17 bundles)
|
|
41
|
+
update: fast-forward failed (origin/main) — the runtime checkout has commits that upstream does not.
|
|
42
|
+
or re-run the installer from the README, which replaces the checkout
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Three separate things are visible in that output:
|
|
46
|
+
|
|
47
|
+
- **The message is one this repository no longer contains.** `ffFailureMessage` was deleted in AD-046. He was
|
|
48
|
+
running the old CLI, which is the proof that the fix had not landed and could not.
|
|
49
|
+
- **`--force` was swallowed.** `route` filtered flags for every other command and not for `update`, so a flag that
|
|
50
|
+
does not exist was accepted without a word. He had every reason to read the unchanged failure as a refusal to
|
|
51
|
+
force.
|
|
52
|
+
- **The remedy it offered still did not work.** "Re-run the installer, which replaces the checkout" —
|
|
53
|
+
`install.sh` ran `git pull --ff-only`, the same command that had just aborted. AD-046 deleted that sentence from
|
|
54
|
+
the CLI and left the installer as it was, so the sentence stopped being printed without becoming true.
|
|
55
|
+
|
|
56
|
+
## What this says about the shape of the change
|
|
57
|
+
|
|
58
|
+
A self-updating tool needs a recovery path that does not run through the thing being updated. `curl | bash` is that
|
|
59
|
+
path here: it is fetched fresh from upstream every time, so it carries the current logic regardless of what is
|
|
60
|
+
installed. That makes the installer, not the CLI, the place where "get me to a known-good state" has to work.
|
|
61
|
+
|
|
62
|
+
The same reasoning is why the deferred versioned-artifact layout in AD-046 matters more than it looked: with
|
|
63
|
+
`~/.tlc/versions/<rev>` and a symlink swap, recovery is a link flip and there is nothing to reconcile. This change
|
|
64
|
+
does not build that; it makes the route that exists actually work.
|
|
65
|
+
|
|
66
|
+
## Trade-offs
|
|
67
|
+
|
|
68
|
+
**The installer now discards local changes at a managed runtime path.** That is the ownership model of AD-046
|
|
69
|
+
applied where recovery happens. `config.json` and `state/` are gitignored, so a reset cannot reach them — asserted
|
|
70
|
+
by a test that writes both and checks they survive, because the whole recovery rests on it.
|
|
71
|
+
|
|
72
|
+
**Existing installs need one manual step.** There is no way around it: the code that would do it for them cannot be
|
|
73
|
+
delivered by the mechanism that is broken. The step is the documented install one-liner.
|
|
74
|
+
|
|
75
|
+
**`--force` is refused rather than implemented.** A managed runtime is already hard-reset, and a linked clone must
|
|
76
|
+
never be written to, so there is nothing left for a force flag to mean. Refusing it with that explanation is more
|
|
77
|
+
honest than accepting it as a synonym.
|
|
78
|
+
|
|
79
|
+
## Not decided here
|
|
80
|
+
|
|
81
|
+
- **Versioned artifact directories with an atomic swap.** Still the full form, still deferred, and this decision is
|
|
82
|
+
the second argument for it.
|
|
83
|
+
- **A check that the CLI and the installer agree.** They now implement the same ownership rule in TypeScript and in
|
|
84
|
+
bash and PowerShell, three times. Nothing enforces that they stay in agreement.
|
|
85
|
+
- **Telling an operator their runtime is behind.** `update --check` reports it, and nothing surfaces it unprompted.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-049 — An empty synced file says which of four reasons made it empty"
|
|
4
|
+
description: "An operator reported that the harness never updates its lessons file. It was being rewritten every session with the same sentence — \"No active project lessons yet.\" — because lessons were switched off. That sentence covered four different situations: off, on-but-nothing-repeated, candidates-not-promoted, and active-but-withheld. Each now names itself."
|
|
5
|
+
tags: [decision, lessons, visibility]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-049 — An empty synced file says which of four reasons made it empty
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`emptySyncReason` names the reason** the synced markdown has no lessons in it, from the store and the config.
|
|
17
|
+
2. **Four distinct reasons**, asserted distinct by a test: the capability is off; on with an empty store; candidates
|
|
18
|
+
recorded but none promoted; active but withheld.
|
|
19
|
+
3. **The switched-off reason names the config field and the route to change it** — the init skill, not the file.
|
|
20
|
+
4. **`renderLessonsMarkdown` takes the whole config** rather than one number from it, because the reason needs
|
|
21
|
+
`enabled` and `promoteHitCount` too.
|
|
22
|
+
5. **The docs name `.specs/LESSONS.md` as belonging to the `/sdd` skill**, so the two files stop being compared.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
The report: "uma coisa que eu percebi é que ele não tá atualizando nunca esse arquivo, mas atualiza o `.specs` do
|
|
27
|
+
tlc." Followed by "mandei um init all default".
|
|
28
|
+
|
|
29
|
+
Both halves are informative.
|
|
30
|
+
|
|
31
|
+
`intelligence.lessons.enabled` defaults to **false**, and an all-defaults init leaves it off. With
|
|
32
|
+
`syncRulesFile` on, `gardenAndPersistLessons` rewrote the file at every session end — with the same empty text,
|
|
33
|
+
forever. From the outside that is indistinguishable from a file nothing writes, which is exactly how it was
|
|
34
|
+
reported.
|
|
35
|
+
|
|
36
|
+
And the sentence could not have told him anything, because it was the same sentence for four situations. Reaching a
|
|
37
|
+
non-empty file requires all of: the capability on, the **same** gate failure repeating inside one session
|
|
38
|
+
(`hits >= 2`), and the resulting candidate promoted across `promoteHitCount` distinct sessions — `rankLessonsForSync`
|
|
39
|
+
filters on `status === "active"`. Three hurdles, one message.
|
|
40
|
+
|
|
41
|
+
The other half of the report is a naming collision the harness does not own. `.specs/LESSONS.md` is the `/sdd`
|
|
42
|
+
skill's own lessons layer. Seeing one update and the other not is reasonable evidence that the second is broken,
|
|
43
|
+
and the answer is documentation rather than code: the harness must not couple to another tool's files, and AD-035
|
|
44
|
+
already refused to read a project's documentation at all.
|
|
45
|
+
|
|
46
|
+
## Trade-offs
|
|
47
|
+
|
|
48
|
+
**Four strings to keep true.** A fifth state — say, every lesson quarantined — falls through to the original
|
|
49
|
+
sentence. The test asserts the four are distinct, not that the set is complete.
|
|
50
|
+
|
|
51
|
+
**The reason is computed on every sync.** It reads the store that was just gardened and does no extra I/O.
|
|
52
|
+
|
|
53
|
+
**It says "ask the agent to run the harness-init skill" rather than naming a command.** There is no CLI to toggle a
|
|
54
|
+
capability, because `tlc harness` mutations are the operator's from a terminal ([/decisions/ad-022.md](/decisions/ad-022.md)) and the init skill is the
|
|
55
|
+
sanctioned interactive route. A line pointing at a command that does not exist would be the AD-047 defect again.
|
|
56
|
+
|
|
57
|
+
## Not decided here
|
|
58
|
+
|
|
59
|
+
- **Whether the file should be written at all when lessons are off.** It is, and it says so. Not writing it would
|
|
60
|
+
leave `syncRulesFile: true` silently doing nothing, which is the same defect one layer down.
|
|
61
|
+
- **Surfacing the off state in `doctor`.** `checkLessonHealth` is silent when the capability is off, deliberately —
|
|
62
|
+
a row about a disabled feature is noise on every healthy run ([/decisions/ad-034.md](/decisions/ad-034.md)). The synced file is the place an operator
|
|
63
|
+
who enabled `syncRulesFile` is already looking.
|
|
64
|
+
- **Whether `promoteHitCount: 2` is the right default** for a capability whose first visible output needs two
|
|
65
|
+
sessions. It is the recurrence threshold AD-038 argued for; the interaction with time-to-first-output is not
|
|
66
|
+
measured.
|