@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,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-050 — Lesson transport is a provider capability, not an operator preference"
|
|
4
|
+
description: "A question about duplicated lessons found the opposite of duplication: on the host that drops context returned from its session-start hook, the durable rules file was the only route to the model, and it was off by default. Transport becomes a declared capability, the advisory stops being rendered into a field that host ignores, and the cost report names which of the two is paid."
|
|
5
|
+
tags: [decision, lessons, providers, capabilities, observability]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-050 — Lesson transport is a provider capability, not an operator preference
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`sessionStartContextReliable` is declared per provider** and states whether context returned from the
|
|
17
|
+
session-start hook reaches the model. It is not the same question as whether the field exists.
|
|
18
|
+
2. **`contextAtStop` is declared per provider** and read by `degrade`, so a `context` decision raised on `stop`
|
|
19
|
+
abstains where the host has no field to carry it.
|
|
20
|
+
3. **`intelligence.lessons.syncRulesFile` becomes `auto` | `always` | `never`, defaulting to `auto`.** Under `auto`
|
|
21
|
+
the durable view is written where the host does not deliver hook context, and withheld where it does.
|
|
22
|
+
4. **The legacy boolean is coerced** — `true` reads as `always`, `false` as `never` — and `lessons list` names the
|
|
23
|
+
coercion once, next to the value it produced.
|
|
24
|
+
5. **`durableViewVerdict` takes the capability as data** and returns a reason, following `appendFilesVerdict`. Core
|
|
25
|
+
does not import a provider to decide it.
|
|
26
|
+
6. **The durable view is written at session start as well as session end**, from `allLessons` rather than from the
|
|
27
|
+
project store, filtered by `appliesHere` as well as `isInjectable`.
|
|
28
|
+
7. **The cost report says which transport is paid.** `durable_chars` is measured from the file that was written and
|
|
29
|
+
`hook_context_reliable` records what the provider declares.
|
|
30
|
+
8. **`session.start` keeps carrying context on every provider**, because the drop is a race rather than a refusal.
|
|
31
|
+
|
|
32
|
+
## Why
|
|
33
|
+
|
|
34
|
+
The question was whether it is correct to hold the same lessons in `.cursor/rules/harness-lessons.mdc` and in
|
|
35
|
+
`.tlc/harness/lessons.md`. Measuring it found the reverse of duplication.
|
|
36
|
+
|
|
37
|
+
Cursor's own staff answered it, on the `sessionStart` `additional_context` bug report (forum thread 158452,
|
|
38
|
+
2026-04-20): *"This is a bug on our side. `additional_context` from `sessionStart` gets dropped due to a timing issue
|
|
39
|
+
between when the hook runs and when the composer handle is created."* `env` from the same payload arrives, because
|
|
40
|
+
it follows a different code path. It was reported again against 3.14.7 on 2026-08-02 (thread 167274), and no
|
|
41
|
+
changelog entry fixes it.
|
|
42
|
+
|
|
43
|
+
So on that host the hook route is unreliable and the rules file is the durable one — which AD-011 item 4 had already
|
|
44
|
+
reasoned, without a citation. What nobody had noticed is that `syncRulesFile` defaulted to `false`. An all-defaults
|
|
45
|
+
init with lessons switched on therefore delivered lessons by **no** route at all, which is consistent with the
|
|
46
|
+
report that opened AD-049. One boolean was deciding transport for two hosts with different capabilities: on the one
|
|
47
|
+
that delivers hook context it was a convenience, and on the one that does not it was the only route.
|
|
48
|
+
|
|
49
|
+
Two further defects came out of reading the schemas rather than assuming them.
|
|
50
|
+
|
|
51
|
+
Cursor's `stop` output schema carries `followup_message` and nothing else, and `cursorRender` maps every `context`
|
|
52
|
+
decision to `additional_context`. The docs gate's non-blocking advisory was therefore rendered into a field that
|
|
53
|
+
host ignores. The test covering it asserted the text on a Cursor stop event, which is how it passed. `degrade`
|
|
54
|
+
already abstained a `context` decision a provider could not carry — it only knew about `tool.before` and
|
|
55
|
+
`tool.after`. Claude Code's `Stop` *does* accept `hookSpecificOutput.additionalContext` for feedback that continues
|
|
56
|
+
the turn, so the advisory survives where it can be read.
|
|
57
|
+
|
|
58
|
+
And the durable view was rendering from `readProjectLessons`, so it carried one of the three tiers AD-040
|
|
59
|
+
introduced. On the host where that file is the only route, every shipped core lesson and every global lesson
|
|
60
|
+
reached nothing.
|
|
61
|
+
|
|
62
|
+
## Trade-offs
|
|
63
|
+
|
|
64
|
+
**`auto` changes behaviour for an existing Cursor project.** A file appears in `.cursor/rules/` that was not there
|
|
65
|
+
before. That is the fix, and `never` is the way to decline it — which is why the mode has three values rather than
|
|
66
|
+
being inferred outright.
|
|
67
|
+
|
|
68
|
+
**A file written into someone's repo.** `.cursor/rules/` is tracked by default in this project, on the reasoning
|
|
69
|
+
that lessons are worth sharing with a team. In another repo that is the operator's call, and `never` is how they
|
|
70
|
+
make it.
|
|
71
|
+
|
|
72
|
+
**Two transports pay twice when the race is won.** Emitting at `session.start` on a host that usually drops the text
|
|
73
|
+
means that when it does arrive, the same lessons arrive twice on that turn. The alternative is to stop emitting and
|
|
74
|
+
lose the cases that work; the emission is bounded by `maxCharsSession`, so the cost of the overlap is bounded too.
|
|
75
|
+
|
|
76
|
+
**`sessionStartContextReliable` is a claim about someone else's bug.** It will be wrong the day Cursor fixes the
|
|
77
|
+
race, and nothing here will notice. The adapter cites the thread and the version so the claim can be re-checked
|
|
78
|
+
rather than inherited.
|
|
79
|
+
|
|
80
|
+
**`lessons list` carries a migration line.** It retires itself once the config is updated, and until then it is one
|
|
81
|
+
line on a command an operator runs deliberately. `doctor` was the other candidate and was rejected: a row about a
|
|
82
|
+
coerced field on every healthy run is the AD-034 defect.
|
|
83
|
+
|
|
84
|
+
## Not decided here
|
|
85
|
+
|
|
86
|
+
- **Whether the `alwaysApply` file's real token cost can be reported.** The harness cannot see the assembled prompt.
|
|
87
|
+
It reports the characters it wrote and states that the host is *asked* to include them on every request.
|
|
88
|
+
- **Whether the advisory should reach the operator by another route on a host that cannot carry it.** It is in the
|
|
89
|
+
gate artifact, which is where `tlc harness` reads it. A handoff blocker for a non-blocking advisory was not
|
|
90
|
+
written.
|
|
91
|
+
- **Whether the emission should stop once the Cursor race is fixed.** That is a capability flip, not a code change,
|
|
92
|
+
and the flip needs evidence that does not exist yet.
|
|
93
|
+
- **Whether Claude Code should get the durable pointer by default.** `always` writes it. Under `auto` it does not,
|
|
94
|
+
because the hook delivers and the pointer would be a second copy of a working route.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-051 — A warning fails the gate, and a suppression states what breaks without it"
|
|
4
|
+
description: "Three fixable warnings sat in this repository across several green gates, because a warn-level biome rule does not change the exit code. The gate now runs biome with --error-on-warnings, and a new checker fails it on any suppression whose reason is not a reason. Escalating every rule group to error was measured first and rejected."
|
|
5
|
+
tags: [decision, gate, lint, quality]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-051 — A warning fails the gate, and a suppression states what breaks without it
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **The gate runs `biome check --error-on-warnings`.** A warn-level diagnostic fails it like an error.
|
|
17
|
+
2. **`linter.rules` stays on the `recommended` preset.** Group-level `error` severity was measured and rejected.
|
|
18
|
+
3. **`tools/check-suppressions.ts` is a gate step.** A `biome-ignore`, `@ts-ignore` or `@ts-expect-error` must carry
|
|
19
|
+
a reason opening with `why:`, `hazard:` or `invariant:` — the words this project already requires of any comment
|
|
20
|
+
that stays — followed by more than four words.
|
|
21
|
+
4. **`@ts-nocheck` fails regardless of its reason**, because it silences a whole file rather than a diagnostic.
|
|
22
|
+
5. **The three warnings are fixed rather than suppressed**, and one of them was a real defect.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
`npx biome check .` exited 0 while printing "Found 2 warnings. Found 1 info." Warn-level diagnostics do not change
|
|
27
|
+
biome's exit code unless `--error-on-warnings` is passed, so the gate was green and the output said otherwise. Three
|
|
28
|
+
findings had survived that way long enough that nobody knew when they arrived.
|
|
29
|
+
|
|
30
|
+
One was not cosmetic. `linkedRuntimeMessage(dest, target)` accepted `dest` and never used it, so the message read
|
|
31
|
+
"the runtime path is a link to a working clone" without naming the path — while both sibling messages,
|
|
32
|
+
`unmanagedRuntimeMessage` and `resetFailureMessage`, name theirs. An operator with more than one runtime could not
|
|
33
|
+
tell which link was meant. `noUnusedFunctionParameters` had been pointing at it in the warning list.
|
|
34
|
+
|
|
35
|
+
The other two were `useIndexOf` (a `findIndex` with an equality predicate) and `useOptionalChain`.
|
|
36
|
+
|
|
37
|
+
**Group-level severity was tried and measured.** Setting every group to `error` in `biome.json` produced **3763**
|
|
38
|
+
findings, because a group severity enables that group's non-recommended rules as well. Among them: `noBarrelFile`
|
|
39
|
+
and `noReExportAll`, which forbid precisely the core facade this architecture is built on ([/decisions/ad-004.md](/decisions/ad-004.md)), and
|
|
40
|
+
`noNodejsModules` in a Node CLI. Adopting that would have meant a config full of exclusions — the opposite of
|
|
41
|
+
removing clutter — so the curated preset stays and the flag does the enforcing.
|
|
42
|
+
|
|
43
|
+
**`--error-on-warnings` cannot see a rule that was suppressed instead of fixed.** Biome 2 requires text after the
|
|
44
|
+
colon, so `biome-ignore lint/complexity/useIndexOf: needed` parses and says nothing. That is exactly the escape a
|
|
45
|
+
stricter exit code creates pressure to use, which is why the checker ships in the same change rather than later.
|
|
46
|
+
|
|
47
|
+
## Trade-offs
|
|
48
|
+
|
|
49
|
+
**A prose rule enforced by a regex.** The checker cannot tell a real reason from four words that look like one. It
|
|
50
|
+
raises the cost of a thoughtless suppression; it does not make one impossible.
|
|
51
|
+
|
|
52
|
+
**`__test__` is scanned like everything else.** Excluding it would be the route around the rule. What keeps the
|
|
53
|
+
comment-policy fixtures quiet is `isInComment`, which tracks the opening quote character rather than counting
|
|
54
|
+
quotes — the first version counted, and reported three findings that were all string literals inside this project's
|
|
55
|
+
own tests. A checker whose findings are noise is a checker somebody switches off, so it was calibrated against a
|
|
56
|
+
deliberately injected suppression before being wired in.
|
|
57
|
+
|
|
58
|
+
**Four words is arbitrary.** It was chosen because `why: needed` and `hazard: unavoidable` both pass a
|
|
59
|
+
non-empty test and neither says what breaks.
|
|
60
|
+
|
|
61
|
+
## Not decided here
|
|
62
|
+
|
|
63
|
+
- **Whether biome's own unused-suppression diagnostic should also fail.** It is part of the recommended preset and
|
|
64
|
+
now fails through `--error-on-warnings`, so no separate decision was needed.
|
|
65
|
+
- **Whether individual non-recommended rules are worth enabling one at a time.** `useConsistentTypeDefinitions`
|
|
66
|
+
(8 findings) would force `interface` on a codebase that uses `type` throughout; `noMagicNumbers` accounts for most
|
|
67
|
+
of the 3763. Neither was adopted and neither was excluded by name — the preset simply does not include them.
|
|
68
|
+
- **Whether the same reason discipline should apply to `.mjs` and shell files.** The checker scans the tracked
|
|
69
|
+
TypeScript surface, which is where suppressions have appeared.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-052 — The repository moves to the org and the runtime paths do not"
|
|
4
|
+
description: "The project moves to github.com/tech-leads-club/harness-toolkit. Only the identity layer changes — package name, repository URLs, installer source. The CLI, the runtime home and the project policy path stay exactly as AD-002 set them, because renaming those would re-initialise every install for no gain."
|
|
5
|
+
tags: [decision, naming, install, delivery]
|
|
6
|
+
timestamp: "2026-08-05"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-052 — The repository moves to the org and the runtime paths do not
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-05
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **The repository is `github.com/tech-leads-club/harness-toolkit`**, superseding that one clause of
|
|
17
|
+
[/decisions/ad-002.md](/decisions/ad-002.md). The package is `harness-toolkit`.
|
|
18
|
+
2. **Nothing else in AD-002 changes.** The CLI is still `tlc` with a `harness` subcommand group, the runtime home is
|
|
19
|
+
still `~/.tlc/harness/`, and the project source of truth is still `.tlc/harness/config.json`.
|
|
20
|
+
3. **`felipfr/tlc-agent-harness` is frozen as it stands**, not archived and not amended.
|
|
21
|
+
4. **While the repository is private to the org, the documented install fetches the script through `gh`**, because
|
|
22
|
+
`raw.githubusercontent.com` is unauthenticated and cannot read it. The public one-liner is documented alongside,
|
|
23
|
+
labelled for when it applies.
|
|
24
|
+
5. **A clone that fails names the credential it needs** — `gh auth login`, `gh auth setup-git`, org membership —
|
|
25
|
+
rather than passing git's own error through.
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
29
|
+
**The paths stay because renaming them costs users and buys nothing.** `TLC` is Tech Leads Club, so `tlc` and
|
|
30
|
+
`.tlc/` read as *more* accurate under the org than they did under one person's account. Changing them would move
|
|
31
|
+
`~/.tlc/harness`, `TLC_HOME`, `.tlc/harness/config.json` — which is also the literal string the floor's
|
|
32
|
+
`policy-surface-write` rule matches — and every already-initialised project would need a manual re-init. Two
|
|
33
|
+
colleagues are mid-onboarding on the current layout.
|
|
34
|
+
|
|
35
|
+
**The private phase is an install problem, not a code problem.** `install.sh` clones over HTTPS and works against a
|
|
36
|
+
private repository as soon as git has a credential; what does not work is the *bootstrap*, because the one-liner
|
|
37
|
+
fetches the script from `raw.githubusercontent.com` unauthenticated. `gh api …/contents/install.sh` carries the
|
|
38
|
+
credential and reaches the same script, so both phases run identical code and going public later requires no change
|
|
39
|
+
at all.
|
|
40
|
+
|
|
41
|
+
**The old repository is left alone by choice.** It keeps its history, and it keeps receiving `tlc harness update`
|
|
42
|
+
from every install that predates this move — silently, from a repository that has stopped moving. That is a known
|
|
43
|
+
consequence: the affected installs have to re-run the installer against the new repository, and being told is the
|
|
44
|
+
only mechanism, because a stale install cannot learn about a move on its own. This is the same shape as
|
|
45
|
+
[/decisions/ad-048.md](/decisions/ad-048.md), one level up.
|
|
46
|
+
|
|
47
|
+
## Trade-offs
|
|
48
|
+
|
|
49
|
+
**Two install routes in the docs at once.** One of them does not work today and the other will not be needed later,
|
|
50
|
+
and each is labelled with which. The alternative — documenting only the working one — means the public one-liner has
|
|
51
|
+
to be reinstated in a later commit that nobody will remember to make.
|
|
52
|
+
|
|
53
|
+
**`gh api … | base64 -d | bash` is not memorable.** It is also the only unattended route into a private repository
|
|
54
|
+
that does not ask anyone to paste a token into a shell.
|
|
55
|
+
|
|
56
|
+
**A frozen repository under a personal account still answers `git fetch`.** Anyone who never re-installs stays on
|
|
57
|
+
the last revision it received, and the harness has no way to tell them so.
|
|
58
|
+
|
|
59
|
+
## Not decided here
|
|
60
|
+
|
|
61
|
+
- **Publishing to npm.** It would remove `curl | bash`, the clone, the private-repository problem and the committed
|
|
62
|
+
`dist/` in one move — and it replaces the update mechanism that AD-046 and AD-048 are built on, so it is its own
|
|
63
|
+
decision rather than part of a move.
|
|
64
|
+
- **When the repository becomes public.** The docs carry both routes so the switch is a visibility change and
|
|
65
|
+
nothing else.
|
|
66
|
+
- **Renaming the local working directory.** It is the operator's filesystem, and a runtime linked to it would follow
|
|
67
|
+
the rename only if the link is re-pointed.
|
|
68
|
+
- **`projectName` in this repository's own `.tlc/harness/config.json`.** It still reads `tlc-agent-harness`. The
|
|
69
|
+
file is on the policy surface, so it is the operator's edit from a terminal ([/decisions/ad-022.md](/decisions/ad-022.md)).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-053 — The harness ships no model allowlist, and a list that names nothing enforces nothing"
|
|
4
|
+
description: "An agent read `enforceAllowlist: true` with `allowedModels: []` and concluded that every model was blocked, then offered to switch the rail off. The list it was actually being refused by was shipped by the adapter and already stale. No adapter ships one now, an empty list enforces nothing, doctor reports that combination, and the refusal names the key it came from."
|
|
5
|
+
tags: [decision, subagents, policy, defaults]
|
|
6
|
+
timestamp: "2026-08-06"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-053 — The harness ships no model allowlist, and a list that names nothing enforces nothing
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-06
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`ProviderPolicyDefaults` no longer carries `allowedModels`**, and no adapter declares one. The effective
|
|
17
|
+
allowlist is exactly what the project configured.
|
|
18
|
+
2. **An empty or absent list enforces nothing.** `enforceAllowlist` with no entries does not deny.
|
|
19
|
+
3. **`doctor` reports that combination as a fault**, naming `subagents.allowedModels`, and is silent when the rail
|
|
20
|
+
is off or the list has entries.
|
|
21
|
+
4. **The refusal names the key the list came from**, and says `inherit` is a value that list may contain when
|
|
22
|
+
`inherit` is what was refused.
|
|
23
|
+
5. **`blockedPatterns` keeps its shipped `-fast` entries.** It is the opposite mechanism and stays.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
The report came from another project, where an agent read:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
"subagents": { "enforceAllowlist": true, "requireModel": false, "allowedModels": [] }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
and concluded that every model fell out, `inherit` and a `-thinking-high` variant included. It offered to set
|
|
34
|
+
`enforceAllowlist: false`.
|
|
35
|
+
|
|
36
|
+
The conclusion was wrong and the remedy would have switched off a rail. `effectiveAllowedModels` fell back to
|
|
37
|
+
`provider.policyDefaults().allowedModels` whenever the project's list was empty, so the spawn was refused by a
|
|
38
|
+
**shipped** list — three model slugs on Claude Code, five on Cursor — that appears nowhere in the project. The
|
|
39
|
+
requested model genuinely was not on it, because that list had already gone stale.
|
|
40
|
+
|
|
41
|
+
That a careful reader got it exactly backwards is the finding, not a footnote. The refusal said `Use one of: <list>`
|
|
42
|
+
and named no source. An operator who opens their own config, sees `[]`, and is told a spawn was refused by an
|
|
43
|
+
allowlist can only conclude that empty means none.
|
|
44
|
+
|
|
45
|
+
**No shipped list at all** is the operator's decision, and it removes the cause rather than the symptom: a list the
|
|
46
|
+
harness invents is a list that goes stale, and enforcing one nobody wrote is what made the message unreadable.
|
|
47
|
+
|
|
48
|
+
**An empty list cannot deny**, because it says nothing about what is permitted. With the shipped fallback gone, the
|
|
49
|
+
old code would have refused every spawn from a rule listing nothing — the behaviour the reader mistook for a bug,
|
|
50
|
+
where they would have been right.
|
|
51
|
+
|
|
52
|
+
**`inherit` is not a model name.** It means the parent's model, and before this the only occurrences of the string
|
|
53
|
+
in this repository were `stdio: "inherit"`. It is now an ordinary value an operator may put on the list, and the
|
|
54
|
+
refusal says so instead of answering with slugs.
|
|
55
|
+
|
|
56
|
+
## Trade-offs
|
|
57
|
+
|
|
58
|
+
**A project that relied on the shipped list loses it.** `enforceAllowlist: true` with no list now permits every
|
|
59
|
+
model instead of the three or five the adapter named. `doctor` fails on exactly that state, which is the only
|
|
60
|
+
signal available — a spawn cannot warn without denying, and denying is what this removes.
|
|
61
|
+
|
|
62
|
+
**`-fast` protection is unaffected, and the asymmetry is deliberate.** `effectiveBlockedPatterns` concatenates the
|
|
63
|
+
project's patterns with the adapter's rather than replacing them, so the `-fast` denial survives an empty project
|
|
64
|
+
config. A blocklist that ships entries adds protection; an allowlist that ships entries invents a policy.
|
|
65
|
+
|
|
66
|
+
**`doctor` gains a `fail` row that some projects will hit immediately.** It is a fault: a rail declared on and
|
|
67
|
+
enforcing nothing is a claim of protection that is not being given.
|
|
68
|
+
|
|
69
|
+
## Not decided here
|
|
70
|
+
|
|
71
|
+
- **Resolving `inherit` to the recorded parent model and judging that.** More precise than treating it as a listable
|
|
72
|
+
value, and it is inference the operator did not ask for. `blockParentFast` already covers the case that makes
|
|
73
|
+
`inherit` risky.
|
|
74
|
+
- **Whether the init skill should offer a starting list.** It asks; it does not propose slugs, because proposing
|
|
75
|
+
them is shipping a default one conversation later.
|
|
76
|
+
- **Whether `requireModel` should default on.** Unchanged by this decision.
|
|
77
|
+
- **Prefix or wildcard matching for model families.** `isModelAllowlisted` still matches exactly or on `prefix[`,
|
|
78
|
+
so `-thinking-high` needs its own entry. Widening the match is a separate question from who owns the list.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-054 — npm is the distribution, and a merged release PR is the only thing that publishes"
|
|
4
|
+
description: "Distribution moves from a git clone of the repository to a published npm package. Release Please computes the version from Conventional Commits and opens a release PR; merging it tags, releases and publishes through npm trusted publishing, with no stored token and no bot commit on main. The runtime path does not move."
|
|
5
|
+
tags: [decision, release, npm, distribution, ci]
|
|
6
|
+
timestamp: "2026-08-07"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-054 — npm is the distribution, and a merged release PR is the only thing that publishes
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-07
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **The product is published as `@tech-leads-club/harness-toolkit`.** The unscoped name was already taken on
|
|
17
|
+
npm by an unrelated package; the scope is the one `@tech-leads-club/agent-skills` already uses.
|
|
18
|
+
2. **Release Please v4 computes the version** from Conventional Commits, which this repository already writes,
|
|
19
|
+
and opens a release PR. It writes no changelog — `skip-changelog` is set, and
|
|
20
|
+
[/decisions/ad-055.md](/decisions/ad-055.md) says where the changelog comes from instead.
|
|
21
|
+
3. **Merging that PR is the release.** It tags, creates the GitHub Release, and only then does the publish job
|
|
22
|
+
run. An ordinary push to `main` publishes nothing. *(The clause making that merge a human approval is
|
|
23
|
+
superseded by [/decisions/ad-057.md](/decisions/ad-057.md): the bot merges it.)*
|
|
24
|
+
4. **Publishing uses npm trusted publishing (OIDC).** No npm token exists anywhere — the workflow requests a
|
|
25
|
+
short-lived credential with `id-token: write`, and provenance is generated automatically from a public
|
|
26
|
+
repository.
|
|
27
|
+
5. **The publish job rebuilds `dist/` and runs the full gate against the tag** before publishing.
|
|
28
|
+
6. **No bot commits to `main`.** The one generated file a release touches is corrected inside the release PR.
|
|
29
|
+
7. **The runtime path does not move.** `~/.tlc/harness` stays what hooks point at, and what holds `config.json`,
|
|
30
|
+
`flags/` and `state/` ([/decisions/ad-002.md](/decisions/ad-002.md),
|
|
31
|
+
[/decisions/ad-052.md](/decisions/ad-052.md)).
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
**A git clone is not a distribution channel, and this repository has the bug reports to prove it.** Two of the
|
|
36
|
+
last five fixes were to the updater: a checkout that dirtied itself on rebuild and broke every later update
|
|
37
|
+
([/decisions/ad-046.md](/decisions/ad-046.md)), and an updater unable to deliver its own fix
|
|
38
|
+
([/decisions/ad-048.md](/decisions/ad-048.md)). Both are properties of running `git` against a user's machine.
|
|
39
|
+
A registry has solved fetch, integrity and rollback already, and neither of those failures has an analogue in
|
|
40
|
+
`npm install`.
|
|
41
|
+
|
|
42
|
+
**Trusted publishing rather than a token, because the deadline is real.** npm is restricting tokens that bypass
|
|
43
|
+
2FA — account changes in August 2026, direct publishing in January 2027 — so a token-based pipeline would need
|
|
44
|
+
replacing within months of being built. OIDC also produces the provenance attestation for free, which is the
|
|
45
|
+
artifact that says this tarball was built from this commit by this workflow.
|
|
46
|
+
|
|
47
|
+
**A release PR rather than publish-on-merge.** semantic-release publishes on every merge to `main` with nothing
|
|
48
|
+
in between. Release Please's PR is where the version bump and the rendered changelog are assembled before either
|
|
49
|
+
reaches `main`, and it costs nothing to author because the commit convention is already in place. It was also
|
|
50
|
+
where a human could approve; [/decisions/ad-057.md](/decisions/ad-057.md) removed that and the bot merges it,
|
|
51
|
+
which leaves the PR doing the assembly job it was chosen for.
|
|
52
|
+
|
|
53
|
+
**Changesets was the other candidate and lost on shape.** Its strength is monorepos and its cost is a third
|
|
54
|
+
artifact per PR — a changeset file, alongside the commit message and the decision record. This is one package
|
|
55
|
+
with a decision-record habit already. The pause it sells is a pause this repository already takes.
|
|
56
|
+
|
|
57
|
+
**A breaking change stays in `0.x`.** Release Please bumps a breaking change straight to `1.0.0` by default,
|
|
58
|
+
even from `0.1.0`, so a single `feat!:` would have declared this product stable on its first release.
|
|
59
|
+
`bump-minor-pre-major` turns that into a minor bump. Going to `1.0.0` becomes a decision someone makes, which
|
|
60
|
+
is the only way it can mean anything.
|
|
61
|
+
|
|
62
|
+
**Version numbers are now a promise, and the number stays below 1.0.0.**
|
|
63
|
+
[/decisions/ad-003.md](/decisions/ad-003.md) declined backward compatibility and declined a version for exactly
|
|
64
|
+
that reason. A registry requires one. `0.x` is the honest encoding of that stance: it is a version without a
|
|
65
|
+
compatibility promise, which is what AD-003 said this product offers.
|
|
66
|
+
|
|
67
|
+
## Trade-offs
|
|
68
|
+
|
|
69
|
+
**Publishing makes the source public, whatever the repository's visibility.** The tarball is world-readable, so
|
|
70
|
+
the decision to publish is the decision to open the code. Provenance additionally requires a public repository,
|
|
71
|
+
so publishing from a private one silently drops the attestation — the one thing the pipeline exists to produce.
|
|
72
|
+
|
|
73
|
+
**The licence does not become permissive.** PolyForm Noncommercial 1.0.0 is a valid SPDX identifier and npm
|
|
74
|
+
carries it, but a package on a public registry will be installed by people who never read it.
|
|
75
|
+
|
|
76
|
+
**Two things must be done by hand, once, and the pipeline cannot do them.** A trusted publisher must be
|
|
77
|
+
configured on npmjs.com naming the organisation, the repository and `release.yml`, and the repository must be
|
|
78
|
+
public for provenance. A configuration created after 20 May 2026 must also state explicitly which operations it
|
|
79
|
+
permits.
|
|
80
|
+
|
|
81
|
+
**The version becomes a thing that can be wrong.** A `fix:` commit that changes behaviour ships as a patch and
|
|
82
|
+
says compatibility held when it did not. The gate does not check this and nothing here proposes that it could.
|
|
83
|
+
|
|
84
|
+
**`dist/` is now built twice** — committed for the Node fallback path and rebuilt in the publish job. The
|
|
85
|
+
duplication is deliberate: `check-dist-fresh` proves the committed bundles match `src/`, and a release is the one
|
|
86
|
+
moment where building again costs nothing and removes the question.
|
|
87
|
+
|
|
88
|
+
## Not decided here
|
|
89
|
+
|
|
90
|
+
- **How an existing install migrates.** Two things were measured as wrong for the npm route: `doctor` reported
|
|
91
|
+
`not a git checkout` as a failure, and the runtime cache was written inside the package directory npm replaces
|
|
92
|
+
on update. Both are settled in [/decisions/ad-056.md](/decisions/ad-056.md).
|
|
93
|
+
- **Whether to publish a library entry point.** Nothing is exported for programmatic use. The package is a CLI
|
|
94
|
+
and a runtime; `src/` ships because Bun executes it directly, not as an API.
|
|
95
|
+
- **Prereleases and dist-tags.** No `next` channel, no beta flow. One tag, `latest`.
|
|
96
|
+
- **Signing beyond provenance.** Provenance attests the build; it is not a signature over the contents by a key
|
|
97
|
+
this project holds, and key management stays out of scope for the same reason
|
|
98
|
+
[/decisions/ad-028.md](/decisions/ad-028.md) declined it for attestations.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-055 — The changelog is rendered from the decision records, and git says which release each landed in"
|
|
4
|
+
description: "CHANGELOG.md is generated from docs/decisions/ rather than from commit messages, and the mapping from decision to version is read out of git rather than recorded anywhere. Release Please writes no changelog. The gate fails when the file drifts."
|
|
5
|
+
tags: [decision, release, changelog, docs]
|
|
6
|
+
timestamp: "2026-08-07"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-055 — The changelog is rendered from the decision records, and git says which release each landed in
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-07
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`CHANGELOG.md` is generated** by `tools/render-changelog.ts` from `docs/decisions/ad-*.md`. It is not
|
|
17
|
+
edited by hand and Release Please does not write it (`skip-changelog: true`).
|
|
18
|
+
2. **Which release a decision landed in is read from git** — the commit that added the file, and the tag whose
|
|
19
|
+
range contains it. Nothing records it, so nothing can disagree about it.
|
|
20
|
+
3. **A `migration:` note becomes a "Needs your action" line.** Everything else is a title.
|
|
21
|
+
4. **`node tools/render-changelog.ts --check` is a gate step**, so the file cannot drift from the records.
|
|
22
|
+
5. **`--release <version>` renames the pending section**, and the release workflow runs it inside the release
|
|
23
|
+
PR so the merged commit already carries the final document.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
[/decisions/ad-031.md](/decisions/ad-031.md) already decided that the substance of a changelog exists here as
|
|
28
|
+
decision records — each carrying why, the trade-offs and what was refused — and that a second index would be a
|
|
29
|
+
thing to maintain and a thing that can go stale. Publishing to a registry does not change that; it only adds a
|
|
30
|
+
requirement that the document exist as a file.
|
|
31
|
+
|
|
32
|
+
**Commit messages describe the commit; decision records describe the decision.** Generating from commits would
|
|
33
|
+
produce a list where `fix(lessons): the migration line names the file the old boolean is actually in` is an
|
|
34
|
+
entry, which tells a reader nothing they can act on. The decision record for the same change says what changed
|
|
35
|
+
and what it costs.
|
|
36
|
+
|
|
37
|
+
**The decision-to-version mapping is already in git.** Writing a version into each record's frontmatter would be
|
|
38
|
+
the second index AD-031 refused, and it would be hand-maintained at exactly the moment — cutting a release —
|
|
39
|
+
when nobody is looking at it. Walking the tag ranges answers the same question from the thing that cannot lie.
|
|
40
|
+
|
|
41
|
+
**`--release` exists because the release PR bumps the version before the tag is created.** Without it, the first
|
|
42
|
+
push after every release would fail its own `--check`: the file on disk would say `Unreleased` while a
|
|
43
|
+
regeneration, now seeing the tag, would say `v0.2.0`. A test asserts the two renderings are byte-identical, so
|
|
44
|
+
this stays true rather than being a claim in a comment.
|
|
45
|
+
|
|
46
|
+
**And `--check` accepts two renderings, because the merge commit is a real window.** `ci.yml` and `release.yml`
|
|
47
|
+
both fire on a push to `main` and run in parallel, so on the commit that merges a release PR the gate reads a
|
|
48
|
+
document naming `v0.2.0` while the tag is still being created in the other workflow. A plain comparison would
|
|
49
|
+
fail there on every release, on a file that is already correct. The tolerance is exactly one state and closes on
|
|
50
|
+
its own: the pending rendering is accepted only while no tag of that name exists, so it cannot hold a stale file.
|
|
51
|
+
A mutant that removes the closing condition kills a test.
|
|
52
|
+
|
|
53
|
+
## Trade-offs
|
|
54
|
+
|
|
55
|
+
**A change with no decision record is invisible in the changelog.** That is the intent — a change worth telling
|
|
56
|
+
users about is worth a record — but it means a bug fix that warranted no record leaves no trace here. The commit
|
|
57
|
+
history remains the complete account; the GitHub Release carries Release Please's own commit-derived notes for
|
|
58
|
+
anyone who wants them.
|
|
59
|
+
|
|
60
|
+
**The first release lists every decision at once.** Fifty-odd entries under one version, which AD-031 argues is
|
|
61
|
+
the same as no message. It is accepted for exactly one release: subsequent ones carry the handful that landed.
|
|
62
|
+
|
|
63
|
+
**Generating from git makes the tool require git.** It shells out to `git log` and `git tag`, so it runs in a
|
|
64
|
+
checkout and not from an unpacked tarball. It is a build-time tool and never runs in a hook, so the latency rule
|
|
65
|
+
that governs the runtime does not reach it.
|
|
66
|
+
|
|
67
|
+
## Not decided here
|
|
68
|
+
|
|
69
|
+
- **Grouping entries by kind** (feature, fix, security). Decision records carry tags and none of them is a
|
|
70
|
+
release-note category; inventing one would be a second taxonomy.
|
|
71
|
+
- **Linking each entry to its pull request.** The record links to the code it decided about, which is the more
|
|
72
|
+
durable pointer.
|
|
73
|
+
- **Rendering the changelog for other products.** The harness deliberately reads no project's documentation
|
|
74
|
+
convention ([/decisions/ad-035.md](/decisions/ad-035.md)); this tool is this repository's, not a feature.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-056 — The package delivers the runtime, and the runtime path stays where hooks already point"
|
|
4
|
+
description: "npm becomes the install route. The published package is a delivery vehicle: `tlc harness install` materialises the code at ~/.tlc/harness, which keeps config.json, state/ and flags/ out of a directory npm replaces. Update bumps the package and re-materialises; no git command runs against an npm-delivered runtime."
|
|
5
|
+
tags: [decision, install, update, npm, runtime]
|
|
6
|
+
timestamp: "2026-08-07"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-056 — The package delivers the runtime, and the runtime path stays where hooks already point
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-07
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`install.sh` installs the package globally and then runs `tlc harness install`.** The clone route stays,
|
|
17
|
+
and the installer falls back to it — with a line saying so — whenever npm cannot deliver.
|
|
18
|
+
2. **`tlc harness install` copies a declared payload** from wherever the package sits into `~/.tlc/harness`.
|
|
19
|
+
Each entry is replaced wholesale, so a file deleted upstream does not survive. Nothing outside the payload
|
|
20
|
+
is touched, which is what keeps `config.json`, `state/` and `flags/` the operator's.
|
|
21
|
+
3. **The runtime path does not move** ([/decisions/ad-002.md](/decisions/ad-002.md),
|
|
22
|
+
[/decisions/ad-052.md](/decisions/ad-052.md)). Hooks keep naming `~/.tlc/harness/bin/tlc-exec.mjs`, so the
|
|
23
|
+
hot path never resolves anything.
|
|
24
|
+
4. **An npm-installed launcher defers to that runtime** when one is installed there, and runs from the package
|
|
25
|
+
only while it is not — the package has to be able to run itself in order to install anything.
|
|
26
|
+
5. **`runtimePathKind` gains `npm`**, told apart by a marker the installer writes rather than guessed from the
|
|
27
|
+
contents. `doctor` reports it as `ok`; `update` bumps the package and re-materialises, and runs no git
|
|
28
|
+
command against it, for the same reason it runs none against a linked clone.
|
|
29
|
+
6. **The runtime cache is never written into a packaged copy**, and an unwritable home degrades to probing
|
|
30
|
+
rather than throwing.
|
|
31
|
+
|
|
32
|
+
## Why
|
|
33
|
+
|
|
34
|
+
**A directory npm owns cannot hold state.** `npm install -g` replaces the package directory, so the global
|
|
35
|
+
lesson tier, the cross-repository spool and the runtime cache would be deleted by an ordinary update. This was
|
|
36
|
+
measured, not predicted: the first packed tarball wrote `runtime-cache.json` inside the package on its first
|
|
37
|
+
run, and `doctor` reported `FAIL runtime ownership — not a git checkout` on a perfectly healthy install.
|
|
38
|
+
|
|
39
|
+
**Materialising beats splitting the paths.** The alternative was to teach the runtime that code and data live in
|
|
40
|
+
different places — a change to `runtimeHome()`, the floor's protected paths, the lesson store, the spool and the
|
|
41
|
+
policy loader, all on the hot path, on every machine that already has this installed. Copying four megabytes
|
|
42
|
+
once per update buys the same correctness and touches one new file. The conflation of "where the code is" and
|
|
43
|
+
"where the data is" was only ever safe because the runtime was a checkout the harness owned; the package
|
|
44
|
+
restores that property instead of removing the assumption.
|
|
45
|
+
|
|
46
|
+
**The marker, rather than inference.** A directory with no `.git` was classified `unmanaged`, which `doctor`
|
|
47
|
+
calls a failure and `update` answers with "re-install with the one-liner". A directory can be many things and
|
|
48
|
+
only whatever created it knows which, so the installer says so in a file.
|
|
49
|
+
|
|
50
|
+
**The installer falls back rather than failing.** Before the first publish the package genuinely does not
|
|
51
|
+
exist, and an installer that breaks because a future route is not ready yet is broken by its own roadmap. The
|
|
52
|
+
fallback prints which route it took, so nothing is silent. This was caught by the existing test that pipes the
|
|
53
|
+
installer the way the README documents.
|
|
54
|
+
|
|
55
|
+
## Trade-offs
|
|
56
|
+
|
|
57
|
+
**The code exists twice.** Roughly four megabytes in global `node_modules` and again under the runtime home.
|
|
58
|
+
The duplication buys a stable path for hooks and a directory npm will not delete, and disk is the cheapest
|
|
59
|
+
thing being traded here.
|
|
60
|
+
|
|
61
|
+
**`~/.tlc/harness` can be stale relative to the installed package.** `npm i -g` alone does not update the
|
|
62
|
+
runtime; `tlc harness install` does, and `tlc harness update` runs both. An operator who bumps the package by
|
|
63
|
+
hand and stops there keeps running the previous runtime, and nothing currently reports that gap.
|
|
64
|
+
|
|
65
|
+
**The npm-installed CLI is not the code that runs.** `tlc` resolves to the package's shim, which defers to the
|
|
66
|
+
materialised runtime. That is the same relationship the git route has — the CLI runs whatever is at the runtime
|
|
67
|
+
home — but it is surprising the first time.
|
|
68
|
+
|
|
69
|
+
**One env var of coupling.** `TLC_ORIGIN` carries where the launcher physically lives, because the resolved home
|
|
70
|
+
is no longer that. `TLC_HOME_FROM_ENV` distinguishes an operator's choice from the launcher's own resolution,
|
|
71
|
+
and it is derived once by the outermost launcher and inherited after that: deriving it twice made the nested
|
|
72
|
+
invocation read the parent's `TLC_HOME` as a choice and install the runtime on top of itself. Measured against
|
|
73
|
+
the packed tarball, and now asserted.
|
|
74
|
+
|
|
75
|
+
## Not decided here
|
|
76
|
+
|
|
77
|
+
- **Reporting a runtime older than the installed package.** `doctor` could compare the two `package.json`
|
|
78
|
+
versions and say so. Worth doing; it is a check, not a distribution decision.
|
|
79
|
+
- **Removing the git route.** It stays for contributors and for anyone already on it. Whether it is eventually
|
|
80
|
+
retired depends on how the npm route behaves in the field.
|
|
81
|
+
- **A postinstall script.** It would remove the second command, and `--ignore-scripts` is common and postinstall
|
|
82
|
+
scripts are exactly the supply-chain surface this project should not add to.
|
|
83
|
+
- **Windows.** `install.ps1` is unchanged and still clones. The CLI half works — `bin/tlc.mjs` is why the
|
|
84
|
+
published entry points are `.mjs` rather than the shell wrappers — but the installer is not converted and
|
|
85
|
+
neither is CI's coverage of it.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-057 — The release PR is a mechanism, not a gate, and the bot merges it"
|
|
4
|
+
description: "Publishing is unattended. Release Please still opens a release PR, because that is where the version bump and the rendered changelog are assembled, and the release bot merges it as soon as they are. Supersedes the human-approval clause of AD-054."
|
|
5
|
+
tags: [decision, release, ci, npm]
|
|
6
|
+
timestamp: "2026-08-07"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-057 — The release PR is a mechanism, not a gate, and the bot merges it
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-07
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **The release is unattended.** A merge to `main` carrying a `feat:` or `fix:` reaches npm with no human
|
|
17
|
+
step. This supersedes item 3 of [/decisions/ad-054.md](/decisions/ad-054.md), which made merging the release
|
|
18
|
+
PR an approval.
|
|
19
|
+
2. **The release PR stays**, because it is where the version bump and the changelog rendered at the pending
|
|
20
|
+
version are assembled. A `merge` job merges it once the `changelog` job has committed into it.
|
|
21
|
+
3. **`--squash --auto` first, direct merge as the fallback.** Auto-merge waits for the required checks; a
|
|
22
|
+
repository with none, or with auto-merge disabled, gets the direct merge. Which one happened is printed.
|
|
23
|
+
4. **The publish job carries `environment: publish`.** Empty, it changes nothing. Given required reviewers it
|
|
24
|
+
becomes an approval again, without touching any code.
|
|
25
|
+
|
|
26
|
+
## Why
|
|
27
|
+
|
|
28
|
+
**The operator asked for it, having been told the cost, and restated it.** That is the whole of the reason,
|
|
29
|
+
and recording it as anything else would be dressing up someone else's call as an engineering finding. The
|
|
30
|
+
sibling repository `agent-skills` already publishes this way, and one product in an organisation releasing by
|
|
31
|
+
hand while the other does not is a difference nobody chose.
|
|
32
|
+
|
|
33
|
+
**Merging beats replacing the PR flow.** The alternative was to publish directly from `main` the way
|
|
34
|
+
`agent-skills` does — compute the version, commit the bump, tag and publish in one run. That discards the
|
|
35
|
+
changelog-in-PR mechanism ([/decisions/ad-055.md](/decisions/ad-055.md)) and puts the bot's bump commit on
|
|
36
|
+
`main` from a workflow rather than through a reviewed branch. Merging a PR the bot opened costs one job.
|
|
37
|
+
|
|
38
|
+
**It terminates, and that is a property rather than a hope.** The merge is a push to `main`, which runs the
|
|
39
|
+
workflow again; that run finds the merged PR, creates the tag and the release, and the publish job runs off it.
|
|
40
|
+
A third run finds no releasable commit — the changelog commit is `docs(...)`, which is not a releasable type —
|
|
41
|
+
so no PR is opened, the merge job is skipped, and nothing re-triggers.
|
|
42
|
+
|
|
43
|
+
## Trade-offs
|
|
44
|
+
|
|
45
|
+
**A bad release reaches other people's machines with nobody having looked.** This runtime installs itself into
|
|
46
|
+
editors and writes their hook files. Two defences remain and neither is review: the full gate runs against the
|
|
47
|
+
tag inside the publish job before `npm publish`, and CI runs on the release PR before the merge — which works
|
|
48
|
+
only because the App token opens it, since a PR opened with `GITHUB_TOKEN` triggers no workflows.
|
|
49
|
+
|
|
50
|
+
**The fallback merges without waiting for checks.** `--auto` needs required checks configured on `main`; with
|
|
51
|
+
none, there is nothing to wait for and the direct merge is what happens. Configuring CI as a required check on
|
|
52
|
+
`main` is what turns that branch off, and it is a repository setting rather than a change here.
|
|
53
|
+
|
|
54
|
+
**Recovering a failed publish is manual.** If the gate fails inside the publish job the tag and the GitHub
|
|
55
|
+
Release already exist and npm does not. Re-running the whole workflow does not help — `release_created` is
|
|
56
|
+
false on a second pass — so the recovery is "Re-run failed jobs", which preserves the upstream outputs.
|
|
57
|
+
|
|
58
|
+
**The version is now decided entirely by commit messages.** A `fix:` that changes behaviour ships as a patch
|
|
59
|
+
and says compatibility held when it did not. Nothing checks this, and with the human step gone there is no
|
|
60
|
+
longer a moment where someone would notice.
|
|
61
|
+
|
|
62
|
+
## Not decided here
|
|
63
|
+
|
|
64
|
+
- **Required checks on `main`.** A repository setting that would make the auto-merge path the real one. Worth
|
|
65
|
+
doing; not a code change.
|
|
66
|
+
- **Reviewers on the `publish` environment.** The hook is in place and deliberately empty.
|
|
67
|
+
- **A cool-off or a `next` dist-tag.** One channel, `latest`, published immediately
|
|
68
|
+
([/decisions/ad-054.md](/decisions/ad-054.md)).
|