@tech-leads-club/harness-toolkit 0.2.1 → 0.2.4
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/bin/tlc-cli.ts +20 -5
- package/dist/compact-before.mjs +86 -8
- package/dist/doctor.mjs +116 -10
- package/dist/init-project.mjs +13 -7
- package/dist/lessons-cli.mjs +86 -8
- package/dist/obs-cli.mjs +86 -8
- package/dist/prompt-submit.mjs +86 -8
- package/dist/response-after.mjs +86 -8
- package/dist/run.mjs +86 -8
- package/dist/session-end.mjs +86 -8
- package/dist/session-start.mjs +86 -8
- package/dist/shim.mjs +6976 -15
- package/dist/stop.mjs +86 -8
- package/dist/subagent-start.mjs +86 -8
- package/dist/subagent-stop.mjs +86 -8
- package/dist/support.mjs +86 -8
- package/dist/tlc-cli.mjs +106 -12
- package/dist/tool-after.mjs +86 -8
- package/dist/tool-before.mjs +86 -8
- package/dist/tool-failure.mjs +86 -8
- package/docs/log.md +5 -0
- package/package.json +3 -2
- package/src/core/core.facade.ts +11 -0
- package/src/core/index.ts +2 -0
- package/src/core/release/release.version.ts +147 -0
- package/src/core/shim/shim.precedence.ts +72 -0
- package/src/core/skill/skill.link.ts +81 -0
- package/src/entrypoints/shim.ts +41 -9
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/paths.ts +27 -0
- package/tools/doctor.ts +50 -3
- package/tools/init-project.ts +27 -7
- package/CHANGELOG.md +0 -95
- package/docs/decisions/ad-001.md +0 -32
- package/docs/decisions/ad-002.md +0 -51
- package/docs/decisions/ad-003.md +0 -30
- package/docs/decisions/ad-004.md +0 -37
- package/docs/decisions/ad-005.md +0 -36
- package/docs/decisions/ad-006.md +0 -49
- package/docs/decisions/ad-007.md +0 -36
- package/docs/decisions/ad-008.md +0 -54
- package/docs/decisions/ad-009.md +0 -61
- package/docs/decisions/ad-010.md +0 -45
- package/docs/decisions/ad-011.md +0 -59
- package/docs/decisions/ad-012.md +0 -71
- package/docs/decisions/ad-013.md +0 -87
- package/docs/decisions/ad-014.md +0 -56
- package/docs/decisions/ad-015.md +0 -33
- package/docs/decisions/ad-016.md +0 -98
- package/docs/decisions/ad-017.md +0 -65
- package/docs/decisions/ad-018.md +0 -77
- package/docs/decisions/ad-019.md +0 -75
- package/docs/decisions/ad-020.md +0 -88
- package/docs/decisions/ad-021.md +0 -57
- package/docs/decisions/ad-022.md +0 -120
- package/docs/decisions/ad-023.md +0 -87
- package/docs/decisions/ad-024.md +0 -85
- package/docs/decisions/ad-025.md +0 -92
- package/docs/decisions/ad-026.md +0 -131
- package/docs/decisions/ad-027.md +0 -90
- package/docs/decisions/ad-028.md +0 -90
- package/docs/decisions/ad-029.md +0 -73
- package/docs/decisions/ad-030.md +0 -97
- package/docs/decisions/ad-031.md +0 -94
- package/docs/decisions/ad-032.md +0 -83
- package/docs/decisions/ad-033.md +0 -89
- package/docs/decisions/ad-034.md +0 -86
- package/docs/decisions/ad-035.md +0 -86
- package/docs/decisions/ad-036.md +0 -68
- package/docs/decisions/ad-037.md +0 -47
- package/docs/decisions/ad-038.md +0 -52
- package/docs/decisions/ad-039.md +0 -69
- package/docs/decisions/ad-040.md +0 -89
- package/docs/decisions/ad-041.md +0 -98
- package/docs/decisions/ad-042.md +0 -82
- package/docs/decisions/ad-043.md +0 -79
- package/docs/decisions/ad-044.md +0 -61
- package/docs/decisions/ad-045.md +0 -94
- package/docs/decisions/ad-046.md +0 -111
- package/docs/decisions/ad-047.md +0 -96
- package/docs/decisions/ad-048.md +0 -85
- package/docs/decisions/ad-049.md +0 -66
- package/docs/decisions/ad-050.md +0 -94
- package/docs/decisions/ad-051.md +0 -69
- package/docs/decisions/ad-052.md +0 -69
- package/docs/decisions/ad-053.md +0 -78
- package/docs/decisions/ad-054.md +0 -98
- package/docs/decisions/ad-055.md +0 -74
- package/docs/decisions/ad-056.md +0 -85
- package/docs/decisions/ad-057.md +0 -68
- package/docs/decisions/ad-058.md +0 -97
- package/docs/decisions/ad-059.md +0 -82
- package/docs/decisions/ad-060.md +0 -75
- package/docs/decisions/ad-061.md +0 -68
- package/docs/decisions/ad-062.md +0 -72
- package/docs/decisions/ad-063.md +0 -84
- package/docs/decisions/ad-064.md +0 -79
- package/docs/decisions/ad-065.md +0 -81
- package/docs/decisions/ad-066.md +0 -111
- package/docs/decisions/ad-067.md +0 -64
- package/docs/decisions/ad-068.md +0 -79
- package/docs/decisions/ad-069.md +0 -74
- package/docs/decisions/ad-070.md +0 -86
- package/docs/decisions/ad-071.md +0 -93
- package/docs/decisions/ad-072.md +0 -82
- package/docs/decisions/ad-073.md +0 -102
- package/docs/decisions/ad-074.md +0 -91
- package/docs/decisions/ad-075.md +0 -79
- package/docs/decisions/ad-076.md +0 -102
- package/docs/decisions/ad-077.md +0 -94
- package/docs/decisions/ad-078.md +0 -84
- package/docs/decisions/ad-079.md +0 -73
- package/docs/decisions/ad-080.md +0 -86
- package/docs/decisions/ad-081.md +0 -70
- package/docs/decisions/ad-082.md +0 -79
- package/docs/decisions/ad-083.md +0 -88
- package/docs/decisions/index.md +0 -111
- package/tools/test-env.mjs +0 -28
- package/tools/test-env.names.d.mts +0 -1
- package/tools/test-env.names.mjs +0 -14
package/docs/decisions/ad-042.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-042 — The suite gets an empty runtime home, and one renderer renders a lesson"
|
|
4
|
-
description: "Two defects the previous change shipped, both found by running it rather than by any check. `TLC_HOME` was deliberately left alone in the hermetic setup, which was safe only while nothing machine-wide lived under it — the global lesson tier does, so a test reading `allLessons` read whichever lessons the developer had promoted. And `support.ts` carried a copy of the lesson renderer, so the tier added to the core block never reached the text a model receives."
|
|
5
|
-
tags: [decision, testing, lessons, hermetic]
|
|
6
|
-
timestamp: "2026-08-04"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-042 — The suite gets an empty runtime home, and one renderer renders a lesson
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-08-04
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
1. **`tools/test-env.mjs` points `TLC_HOME` at a fresh empty temp directory.** Redirected, not deleted — deleting
|
|
17
|
-
it would send every test at the developer's real `~/.tlc/harness`, which is the opposite of hermetic.
|
|
18
|
-
2. **A test that needs the real runtime says so.** `e2e-consistency.test.ts` drives the actual launcher and CLI, so
|
|
19
|
-
it passes `TLC_HOME: repoRoot` explicitly.
|
|
20
|
-
3. **`renderLessonLine` in `src/entrypoints/support.ts` delegates to the core renderer**, exposed on the facade as
|
|
21
|
-
`coreFacade.lesson.renderLessonBlock`. The copy is deleted.
|
|
22
|
-
4. **An end-to-end test asserts the tier appears in the text the hook returns**, not only in the core block.
|
|
23
|
-
|
|
24
|
-
## Why the runtime home had to change
|
|
25
|
-
|
|
26
|
-
`TLC_HOME` was excluded from the hermetic setup on an explicit rationale, written in
|
|
27
|
-
`tools/test-env.names.mjs`: it names *which runtime*, not which project, CI sets it deliberately, and the suite
|
|
28
|
-
exercises it.
|
|
29
|
-
|
|
30
|
-
That was true, and it stopped being sufficient the moment something machine-wide lived under it. AD-040 put the
|
|
31
|
-
global lesson tier at `<runtime home>/state/lessons.json`. From then on, any test calling `allLessons` without
|
|
32
|
-
pinning a home read whichever lessons the operator of that machine happened to have promoted.
|
|
33
|
-
|
|
34
|
-
The failure profile is what makes this worth a decision record: **green on a fresh machine, green in CI, green on
|
|
35
|
-
mine at commit time** — because my global store was empty — and red minutes later, when I promoted five lessons
|
|
36
|
-
into it. The gate ran three times and passed three times before the store had contents. Nothing about the change
|
|
37
|
-
was wrong; the test's assumption was, and the assumption only became visible once the feature was used.
|
|
38
|
-
|
|
39
|
-
`obs.globalSpool` ([/decisions/ad-018.md](/decisions/ad-018.md)) had already put a file under the runtime home and did not expose this, because no test
|
|
40
|
-
asserts a count over the spool. The lesson store is read by `allLessons`, which almost every lesson test calls.
|
|
41
|
-
|
|
42
|
-
## Why one renderer
|
|
43
|
-
|
|
44
|
-
`support.ts` carried `renderLessonLine`, a duplicate of `renderLessonBlock`, with a comment explaining the choice:
|
|
45
|
-
the core's version is presentation-only and was not on the facade, so reaching into core internals was avoided by
|
|
46
|
-
copying it.
|
|
47
|
-
|
|
48
|
-
The copy is the one the model actually receives. So `[gate/status/tier]` — added in AD-040 precisely so a turn can
|
|
49
|
-
tell that a lesson was written about a different repository — rendered in `lessons list` and in nothing an agent
|
|
50
|
-
ever saw. Two derivations of one string is the same defect as a consumer without a producer, pointed sideways: both
|
|
51
|
-
halves work, and the one that matters is the one nobody checked.
|
|
52
|
-
|
|
53
|
-
Exposing it on the facade is the sanctioned route ([/decisions/ad-004.md](/decisions/ad-004.md)). "Presentation does not belong in core" was the right
|
|
54
|
-
instinct applied to the wrong thing — the block's shape *is* a core contract, because ranking, budgeting and the
|
|
55
|
-
synced markdown all depend on its length.
|
|
56
|
-
|
|
57
|
-
## How both were found
|
|
58
|
-
|
|
59
|
-
By running the thing and reading the output — the practice AD-034 records, not a mechanism. `check-wiring` ([/decisions/ad-041.md](/decisions/ad-041.md))
|
|
60
|
-
catches a declared member nothing writes; neither of these is that shape. Stated plainly: after this change the
|
|
61
|
-
harness mechanically refuses one defect family, and the other two found here still depend on someone exercising the
|
|
62
|
-
feature and reading what comes out.
|
|
63
|
-
|
|
64
|
-
## Trade-offs
|
|
65
|
-
|
|
66
|
-
**Every test now pays a `mkdtemp` at startup.** One syscall per test process, twice per gate run.
|
|
67
|
-
|
|
68
|
-
**A test that silently depended on the ambient runtime home now fails.** Three did, all in the E2E file, and all
|
|
69
|
-
three were better off stating the dependency. A future test that needs the real runtime will fail loudly rather
|
|
70
|
-
than pass for an ambient reason.
|
|
71
|
-
|
|
72
|
-
**The temp home is not cleaned up.** The OS reclaims `TMPDIR`; a directory per test process with nothing in it is
|
|
73
|
-
cheaper to leak than to track.
|
|
74
|
-
|
|
75
|
-
## Not decided here
|
|
76
|
-
|
|
77
|
-
- **Whether CI should keep setting `TLC_HOME` to the workspace.** It still does, and the setup module now overrides
|
|
78
|
-
it for the suite while `check-dist-fresh` and the CLI steps still read it.
|
|
79
|
-
- **A mechanical check that a test does not read machine state.** It would need to know which paths are
|
|
80
|
-
machine-wide; the hermetic setup covers the two that exist.
|
|
81
|
-
- **Auditing every other machine-wide read for the same assumption.** The runtime home is now empty for the suite,
|
|
82
|
-
which covers the class rather than the instances.
|
package/docs/decisions/ad-043.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
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.
|
package/docs/decisions/ad-044.md
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
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.
|
package/docs/decisions/ad-045.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
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.
|
package/docs/decisions/ad-046.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
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)).
|
package/docs/decisions/ad-047.md
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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.
|
package/docs/decisions/ad-048.md
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
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.
|