@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,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-066 — Uninstall reads the artefact, and the plan is the confirmation"
|
|
4
|
+
description: "The harness merges eleven hook groups into a settings.json that also holds the operator's permissions, sandbox and env, and offered no way out. The established answer is a receipt written at install time; that record would desync on the first npm upgrade or hand-edit, and every artefact this installer creates already names itself."
|
|
5
|
+
tags: [decision, install, safety, operator]
|
|
6
|
+
timestamp: "2026-08-12"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-066 — Uninstall reads the artefact, and the plan is the confirmation
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-12
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **Ownership is read from the artefact, never from a record.** No manifest is written at install time and none
|
|
17
|
+
is required at uninstall time.
|
|
18
|
+
2. **A shared file is un-merged, never deleted.** `settings.json` keeps every key and every foreign hook group;
|
|
19
|
+
an event left with no groups loses its key rather than keeping an empty array.
|
|
20
|
+
3. **A symlinked runtime home is unlinked and never followed.**
|
|
21
|
+
4. **The split install already draws is the split uninstall honours.** `RUNTIME_PAYLOAD` goes, `OPERATOR_OWNED`
|
|
22
|
+
stays, and `--purge` is a separate word that `--yes` never implies.
|
|
23
|
+
5. **The plan is the confirmation and there is no prompt.** Without `--yes` the command prints the full plan and
|
|
24
|
+
changes nothing.
|
|
25
|
+
6. **The npm global package and per-project `.tlc/` are reported, not removed.**
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
29
|
+
**The exit was hand-editing a file most operators should not hand-edit.** `~/.claude/settings.json` carries
|
|
30
|
+
`permissions`, `sandbox`, `env` and `statusLine` next to our hook groups. Reverting the install meant finding
|
|
31
|
+
eleven groups in that file by eye. An install with no exit is one people try on one machine and never a second.
|
|
32
|
+
|
|
33
|
+
**The established pattern is a receipt, and it is the wrong one here.** CMake's `install_manifest.txt` is the
|
|
34
|
+
minimal form; package managers write the same record into a local store, and the schema people converge on
|
|
35
|
+
carries the path, a content hash, a `pre_existed` flag and the artefact type. The sharpest statement of the case
|
|
36
|
+
comes from a project that **rejected** an uninstall command outright: never infer ownership, because a bulk
|
|
37
|
+
command guessing which files are safe to remove will eventually guess wrong, and `--yes` turns that guess into a
|
|
38
|
+
recursive delete.
|
|
39
|
+
|
|
40
|
+
That objection is right about inference and does not describe this installer. Every artefact here names itself —
|
|
41
|
+
a hook group whose command *is* the launcher path, a symlink resolving inside the runtime home, a `hooks.json`
|
|
42
|
+
carrying the marker `isCursorWired` already checks. Matching an exact launcher path is not a heuristic. And a
|
|
43
|
+
receipt would be **strictly worse**, because the install routes are npm, git clone and a symlink to a working
|
|
44
|
+
checkout: three ways for a written record to desync from the disk on the first `npm i -g`, `git reset --hard` or
|
|
45
|
+
hand-edit. `isHarnessGroup` cannot desync — it reads what is there.
|
|
46
|
+
|
|
47
|
+
So the receipt's findings are adopted and its mechanism is not. `pre_existed` becomes "this group does not carry
|
|
48
|
+
the marker". "Skip a modified file" becomes "never follow a link into a checkout". "Remove a directory only when
|
|
49
|
+
empty" becomes the `OPERATOR_OWNED` split, which
|
|
50
|
+
[/decisions/ad-056.md](/decisions/ad-056.md) had already drawn for the installer.
|
|
51
|
+
|
|
52
|
+
**The symlinked home is the reason this is careful rather than a one-liner.** `~/.tlc/harness` is a link to the
|
|
53
|
+
contributor's working clone on every development machine, including the one this was written on. `rm -rf`
|
|
54
|
+
follows it and deletes the repository. `install.sh` refuses to touch a symlinked runtime for exactly this reason
|
|
55
|
+
([/decisions/ad-046.md](/decisions/ad-046.md)), and the uninstall inherits the refusal: `unlinkSync`, never
|
|
56
|
+
`rmSync` recursive.
|
|
57
|
+
|
|
58
|
+
**No prompt, because the operator reaching for this may have no terminal.** A confirmation prompt needs a TTY and
|
|
59
|
+
assumes an interactive session; the person undoing an install is as likely to be in CI, or in a shell the harness
|
|
60
|
+
itself has made unpleasant. Printing the plan and requiring `--yes` gives the same protection, works headless,
|
|
61
|
+
and leaves the plan in the log of whatever ran it.
|
|
62
|
+
|
|
63
|
+
**The npm package is reported rather than removed** because a global prefix owned by root needs `sudo`, and an
|
|
64
|
+
`npm uninstall` failing halfway through a teardown leaves a worse state than one that never started. Per-project
|
|
65
|
+
`.tlc/` is reported for a different reason: finding them means walking the operator's disk, which is a larger
|
|
66
|
+
risk than the residue.
|
|
67
|
+
|
|
68
|
+
## Trade-offs
|
|
69
|
+
|
|
70
|
+
**Running it found a defect the tests would not have.** On the machine it was written on,
|
|
71
|
+
`~/.claude/skills/harness-init` pointed at a `/tmp` install deleted weeks earlier. Under a target-based ownership
|
|
72
|
+
rule that dangling link reads as "not ours" and survives every uninstall forever. So the two link families now
|
|
73
|
+
answer to different rules: `tlc` on PATH is a name anybody may own and keeps the target test; `skills/harness-init`
|
|
74
|
+
is a path only this installer writes, so its **location** is the identification. Two rules is worse than one, and
|
|
75
|
+
leaving residue that carries our name is worse than two rules.
|
|
76
|
+
|
|
77
|
+
**A file that does not parse is left alone and reported.** The un-merge cannot preserve what it cannot read, and
|
|
78
|
+
the alternative — rewriting from a template — is precisely the data loss this exists to avoid. The operator gets
|
|
79
|
+
the path and the parse error.
|
|
80
|
+
|
|
81
|
+
**Two CI failures paid for the cross-platform rules, and both were the same mistake.** macOS: `realpathSync`
|
|
82
|
+
throws on a path whose tail is absent, so a dangling link fell back to its literal text and was compared against
|
|
83
|
+
a home that had been resolved — and the OS temp directory there sits under `/var`, a symlink to `/private/var`.
|
|
84
|
+
Windows: ownership was decided with `target.startsWith(`${root}/`)`, and that platform separates with `\`. In
|
|
85
|
+
both cases the harness's own artefact read as somebody else's and survived. One rule now: resolve both sides the
|
|
86
|
+
same way, and compare with `relative` rather than a string prefix. The path API is a parameter, so the win32
|
|
87
|
+
rules are asserted from any machine.
|
|
88
|
+
|
|
89
|
+
**And `install.ps1` does not write what `install.sh` writes.** It resolves the home from `USERPROFILE`, copies
|
|
90
|
+
`tlc.cmd` instead of linking it, and places one skill junction at `~/.tlc/skills/harness-init` rather than one
|
|
91
|
+
per provider directory. Reading the POSIX layout on Windows finds none of it and reports a clean machine, which
|
|
92
|
+
is the worst answer an uninstaller can give. A regular file is now identified by the launcher marker its one
|
|
93
|
+
command line carries, the same way every other artefact identifies itself.
|
|
94
|
+
|
|
95
|
+
**An artefact the operator renamed is invisible.** Ownership read from the artefact means an artefact that no
|
|
96
|
+
longer identifies itself is not ours to remove. This is the honest cost of decision 1, and it is quieter than a
|
|
97
|
+
receipt's failure mode, which is deleting something that has since become somebody else's.
|
|
98
|
+
|
|
99
|
+
**A partly-applied run leaves a mixed state, and that is why every step is idempotent.** A second run resumes
|
|
100
|
+
from wherever the first stopped and reports nothing to do once there is nothing left — including a link left
|
|
101
|
+
dangling by a payload removal that ran before it.
|
|
102
|
+
|
|
103
|
+
## Not decided here
|
|
104
|
+
|
|
105
|
+
- **Removing the harness from repositories.** `.tlc/` exists in every project it ran in. Searching for them is
|
|
106
|
+
the risk; the plan names the one-line removal instead.
|
|
107
|
+
- **Whether the Windows launcher copy is a bug.** `install.ps1` copies `bin\tlc.cmd` into the bin directory, and
|
|
108
|
+
that file resolves the launcher as `%~dp0tlc-exec.mjs` — relative to itself, where no launcher sits. The
|
|
109
|
+
uninstall removes the copy correctly; whether the install should have linked it is a separate question.
|
|
110
|
+
- **A `tlc harness reinstall`.** The one-liner already does it, and a verb that means "uninstall then install"
|
|
111
|
+
would need to decide what happens when the first half fails.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-067 — A reserved file that cannot be retired is rendered, and the gate holds it there"
|
|
4
|
+
description: "docs/log.md is a reserved file of the OKF v0.1 bundle, so it cannot be deleted as a duplicate. Hand-maintained, it had drifted to 19 of 66 decision records. It is now rendered from the same files the changelog reads, with a gate step that fails on drift."
|
|
5
|
+
tags: [decision, docs, gates]
|
|
6
|
+
timestamp: "2026-08-12"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-067 — A reserved file that cannot be retired is rendered, and the gate holds it there
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-12
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`docs/log.md` is generated**, from the same `docs/decisions/` files the changelog reads.
|
|
17
|
+
2. **A fourteenth gate step**, `render-log.ts --check`, fails on drift.
|
|
18
|
+
3. **A record with no `timestamp` is dropped, never filed under a guessed date.**
|
|
19
|
+
4. **Days newest first, records within a day in id order** — because AD-064 was taken before AD-065, and
|
|
20
|
+
reading them the other way round inverts the reasoning.
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
**It had drifted to 19 of 66 and nobody noticed.** The log stopped at AD-017 with AD-051 and AD-052 grafted on
|
|
25
|
+
afterwards. Forty-seven records were missing, so a document describing itself as "the chronological record"
|
|
26
|
+
was 71% incomplete — worse than absent, because it reads as complete.
|
|
27
|
+
|
|
28
|
+
**Retiring it was the first answer and it was wrong.** The log duplicates `CHANGELOG.md`, which is already
|
|
29
|
+
rendered from the same records precisely so nobody maintains it by hand
|
|
30
|
+
([/decisions/ad-055.md](/decisions/ad-055.md)) — so deletion looked obvious. Reading
|
|
31
|
+
`tools/check-docs-bundle.ts` before acting found `RESERVED_FILES = new Set(["index.md", "log.md"])`:
|
|
32
|
+
`log.md` is a reserved file of the OKF v0.1 format this bundle adopted
|
|
33
|
+
([/decisions/ad-013.md](/decisions/ad-013.md)), with its own validation rule requiring ISO 8601 headings.
|
|
34
|
+
Deleting it would have left the bundle claiming a format it no longer follows.
|
|
35
|
+
|
|
36
|
+
**So the duplication is the constraint, not the defect.** The format requires two views of the same records:
|
|
37
|
+
`log.md` by date, `decisions/index.md` by number, and `CHANGELOG.md` by release. Three hand-maintained views
|
|
38
|
+
is three things to forget. Three rendered views is one source and no memory.
|
|
39
|
+
|
|
40
|
+
**The gate step is what makes it true tomorrow.** `render-capabilities --check` and `render-changelog --check`
|
|
41
|
+
already exist for exactly this, and both have caught staleness in this repository. A generated file with no
|
|
42
|
+
check is a hand-maintained file with extra steps.
|
|
43
|
+
|
|
44
|
+
## Trade-offs
|
|
45
|
+
|
|
46
|
+
**The log lost its prose.** The old entries carried narrative — what was measured, what was rejected — that a
|
|
47
|
+
title cannot. That narrative is in the decision record itself, which the entry links to, and the alternative
|
|
48
|
+
was 47 entries of narrative invented by whoever noticed the gap.
|
|
49
|
+
|
|
50
|
+
**`DecisionSummary` grew a field.** `timestamp` is now read alongside `title` and `migration`. It is optional,
|
|
51
|
+
so the changelog and the update digest are unaffected, and the reader was already parsing the frontmatter this
|
|
52
|
+
field lives in.
|
|
53
|
+
|
|
54
|
+
**Fourteen gate steps is a lot of steps.** Each one exists because something drifted while nobody was looking,
|
|
55
|
+
and the whole set runs in under thirty seconds. The number is a symptom of the checks being cheap, not of the
|
|
56
|
+
gate being heavy.
|
|
57
|
+
|
|
58
|
+
## Not decided here
|
|
59
|
+
|
|
60
|
+
- **Whether `CHANGELOG.md` should move into the bundle.** It sits at the repository root because npm and GitHub
|
|
61
|
+
both look for it there, and a bundle-relative link cannot reach it. The log now names it in prose.
|
|
62
|
+
- **Rendering `decisions/index.md` as well.** It is the third view of the same records and the only one still
|
|
63
|
+
hand-maintained. It was complete at 66 of 66 when this was written, so there is no evidence of drift to act
|
|
64
|
+
on yet.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-068 — A directory decides what ships, and dist is derived from disk in both directions"
|
|
4
|
+
description: "A hand-maintained array named four development-only checks while ten qualified, so six that validate only this repository were bundled and copied into every install. The exclusion is now a directory, the payload copy honours it, and a bundle whose source is gone is pruned rather than shipped forever."
|
|
5
|
+
tags: [decision, packaging, boundaries, build]
|
|
6
|
+
timestamp: "2026-08-13"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-068 — A directory decides what ships, and dist is derived from disk in both directions
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-13
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`tools/dev/` is the declaration.** Every check that validates this repository's own architecture, docs or
|
|
17
|
+
conventions lives there, and the build reads one directory level, so nothing under it can become a bundle.
|
|
18
|
+
2. **The payload copy honours the same boundary.** `installRuntime` filters `tools/dev` and `tools/__test__`, and
|
|
19
|
+
the published `files` list excludes them.
|
|
20
|
+
3. **The clone route is deliberately different.** A checkout is the repository; a contributor needs these.
|
|
21
|
+
4. **A bundle whose source no longer exists is pruned by the build.**
|
|
22
|
+
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
**The array named four and ten qualified.** `bin/tlc-build` carried
|
|
26
|
+
`DEV_ONLY=(check-boundaries check-docs-bundle check-dist-fresh render-capabilities)`. Measured against the
|
|
27
|
+
repository as it stood: `check-wiring`, `check-screens`, `check-obs-contract`, `check-suppressions`,
|
|
28
|
+
`render-changelog`, `render-log` and `eval-skill-triggers` were all bundled into `dist/` and copied into every
|
|
29
|
+
install. Each validates something a user's project does not have — our module boundaries, our screen contract,
|
|
30
|
+
our observability kinds, our decision records.
|
|
31
|
+
|
|
32
|
+
**And the file says why that shape fails, two lines above it:**
|
|
33
|
+
|
|
34
|
+
> Derived from disk, never hardcoded: a fixed list silently stops building a new entrypoint, and the missing
|
|
35
|
+
> bundle only surfaces when a hook fires in production.
|
|
36
|
+
|
|
37
|
+
The principle was stated and then not applied to the exclusion. This is the same shape as every defect
|
|
38
|
+
[/decisions/ad-065.md](/decisions/ad-065.md) collected — a consumer that does not grow when the producer does —
|
|
39
|
+
with the list playing the consumer.
|
|
40
|
+
|
|
41
|
+
**Source ships too, and that was the larger half.** Excluding a bundle is not enough: `RUNTIME_PAYLOAD` copies
|
|
42
|
+
`tools` wholesale and the npm `files` list published it, so the TypeScript source arrived intact. With Bun
|
|
43
|
+
present the launcher resolves an entry straight from source, so `tlc-exec render-log` on a user's machine would
|
|
44
|
+
have rewritten files inside their runtime home.
|
|
45
|
+
|
|
46
|
+
**A directory cannot be forgotten.** A new checker either sits in `tools/dev/` and never ships, or sits in
|
|
47
|
+
`tools/` and does. There is no third state and nothing to remember, which is what separates this from the array
|
|
48
|
+
it replaces.
|
|
49
|
+
|
|
50
|
+
**dist only ever grew.** The build derived what to *compile* from disk and never what to *remove*, so a bundle
|
|
51
|
+
whose source moved stayed on disk, stayed committed, and stayed shipped — invisible to `check-dist-fresh`,
|
|
52
|
+
because an unchanged file produces no diff. Pruning made the move visible: seven bundles left, 31 to 24.
|
|
53
|
+
|
|
54
|
+
## Trade-offs
|
|
55
|
+
|
|
56
|
+
**Two directories now mean two things by their names.** `tools/` is the product, `tools/dev/` is the repository,
|
|
57
|
+
and the difference is only obvious once you know. The alternative was a marker inside each file, which is a
|
|
58
|
+
convention a reader can violate silently; a path is checked by the build every time.
|
|
59
|
+
|
|
60
|
+
**The tests stay flat in `tools/__test__/`.** They now reach across into `../dev/`, which is slightly worse to
|
|
61
|
+
read than co-location. The gate's glob is flat by design, and a second glob is a second thing to keep in step —
|
|
62
|
+
the exact failure being fixed.
|
|
63
|
+
|
|
64
|
+
**Pruning deletes files the operator did not ask to delete.** It is bounded to `dist/*.mjs`, which the build owns
|
|
65
|
+
entirely and rewrites on every run, and it names each file as it goes.
|
|
66
|
+
|
|
67
|
+
**A contributor's clone still has everything, so the boundary is untested there.** The sensor covers the two
|
|
68
|
+
product routes — the payload copy and the published file list — because those are the ones a user receives.
|
|
69
|
+
|
|
70
|
+
## Not decided here
|
|
71
|
+
|
|
72
|
+
- **Whether `eval-skill-triggers` should exist.** Nothing invokes it: not the gate, not CI, not a document. It
|
|
73
|
+
has a test, so it is exercised but not used. It moved to `tools/dev/` as the conservative placement; whether it
|
|
74
|
+
earns its place is a separate question.
|
|
75
|
+
- **Whether `docs/` should be filtered too.** The runtime payload copies our decision records so
|
|
76
|
+
`tlc harness update` can tell an operator what changed in the harness. That is the product's own release notes,
|
|
77
|
+
not a convention imposed on anyone, and it stays.
|
|
78
|
+
- **A check that fails when a shipped tool imports from `tools/dev`.** `tsc` catches the broken path today
|
|
79
|
+
because the directories differ; a rename could reintroduce the coupling without a type error.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-069 — A decision record declares its shape, and cites by link so a move cannot break it"
|
|
4
|
+
description: "Sixty-seven records, all claiming to be current, with the body shape held only by habit and the citation form 96% consistent. The shape is now checked, the older twenty are a ratchet that may only fall, and a bare parenthesised id fails wherever a link is the convention."
|
|
5
|
+
tags: [decision, docs, gates]
|
|
6
|
+
timestamp: "2026-08-13"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-069 — A decision record declares its shape, and cites by link so a move cannot break it
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
1. **Four headings are required**: `## Decision`, a heading beginning `## Why`, `## Trade-offs`, and
|
|
16
|
+
`## Not decided here`. Bespoke sections between them stay free-form.
|
|
17
|
+
2. **The older twenty are a ratchet**, not an exemption. The count off the current shape starts at 20 and the
|
|
18
|
+
gate fails if it rises **or** if it falls without the recorded budget following.
|
|
19
|
+
3. **A record is cited by link**, in documentation, in comments, and in test names.
|
|
20
|
+
4. **`status` is a closed set**, and it must agree with the folder the record sits in.
|
|
21
|
+
5. **The rule has no exemption list.** Its own examples are written so they do not match.
|
|
22
|
+
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
**The shape was a boundary, not a habit, and nothing knew it.** Measured: AD-021 through AD-067 carry all four
|
|
26
|
+
headings without exception; AD-001 through AD-020 use `## Applies to` and `## Consequences` instead. The format
|
|
27
|
+
changed once and held for forty-seven records. Held by nothing, though — the next record written under time
|
|
28
|
+
pressure would have dropped `## Trade-offs` and started a second boundary nobody noticed.
|
|
29
|
+
|
|
30
|
+
**`## Trade-offs` and `## Not decided here` are the two that get dropped, and the two that pay.** A record
|
|
31
|
+
without them reads as a decision with no cost and no open edges, which is the reading that makes somebody
|
|
32
|
+
re-litigate it six weeks later. This session alone, `## Not decided here` is where the Windows launcher question,
|
|
33
|
+
the per-record token accounting and the unused `eval-skill-triggers` went — each one a thing a reader would
|
|
34
|
+
otherwise assume was settled.
|
|
35
|
+
|
|
36
|
+
**A bare id is a citation only a reader who already knows can check.** The repository stood at 355 bundle-relative
|
|
37
|
+
links against 15 bare `(AD-NNN)` forms. A link survives a file move, `check-docs-bundle` resolves it, and a
|
|
38
|
+
reader can follow it; the bare form does none of that. Fifteen was small enough to fix in one pass and large
|
|
39
|
+
enough to keep growing — and the pass found twenty more inside the records themselves, which the first
|
|
40
|
+
measurement had missed by only looking at `docs/*.md`.
|
|
41
|
+
|
|
42
|
+
**The ratchet fails downward too.** A budget that only refuses to rise is one nobody ever turns: migrating a
|
|
43
|
+
record improves the number and nothing records that it happened. Failing when the count *falls* makes the
|
|
44
|
+
migration a two-line change — the record and the budget — and keeps the number honest.
|
|
45
|
+
|
|
46
|
+
**Backfilling the twenty would have meant inventing them.** Their trade-offs were either never written or are
|
|
47
|
+
now unknowable. A checker that allow-listed them by number would go stale the first time one was migrated, and
|
|
48
|
+
would hide the twenty-first. The count is the honest form.
|
|
49
|
+
|
|
50
|
+
## Trade-offs
|
|
51
|
+
|
|
52
|
+
**A ratchet at twenty is a number in a file that has to be maintained.** It is one integer, the gate names the
|
|
53
|
+
new value when it is wrong, and the alternative — twenty invented rationales — is worse in a way that cannot be
|
|
54
|
+
detected later.
|
|
55
|
+
|
|
56
|
+
**The Why rule matches by prefix, so `## Whyever` would pass.** Requiring the bare word would have forced
|
|
57
|
+
`## Why the runtime home had to change` down to `## Why`, which is worse prose in a record whose whole value is
|
|
58
|
+
prose. The looser rule buys better writing at the cost of a hole nobody has a reason to walk through.
|
|
59
|
+
|
|
60
|
+
**None of this reaches anyone using the harness.** It is a convention of this repository, checked by a tool in
|
|
61
|
+
`tools/dev/` that is never packaged ([/decisions/ad-068.md](/decisions/ad-068.md)). It makes the corpus cheaper
|
|
62
|
+
to read for whoever maintains the product, which reaches a user slowly and indirectly, or not at all.
|
|
63
|
+
|
|
64
|
+
**Two of the four `(AD-NNN)` forms this converted were inside test names**, where the linked form is long and
|
|
65
|
+
reads badly in a test report. The consistency was judged worth more than the line length, and the rule covers
|
|
66
|
+
tests precisely because that is where a convention erodes first.
|
|
67
|
+
|
|
68
|
+
## Not decided here
|
|
69
|
+
|
|
70
|
+
- **Whether the older twenty should be migrated at all.** The ratchet permits it and nothing requires it.
|
|
71
|
+
Whoever has the context for one may lower the number; nobody should do all twenty in one pass.
|
|
72
|
+
- **Whether `## Not decided here` should be required to be non-empty.** A heading with nothing under it
|
|
73
|
+
satisfies the checker today. Judging the content is a semantic question a gate cannot answer.
|
|
74
|
+
- **Applying the citation rule to `.specs/`.** That tree is gitignored and outside the documentation bundle.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-070 — A comment has to read for somebody who was not in the session"
|
|
4
|
+
description: "The comment gate asked whether a comment declares a reason. It could not ask whether the reason means anything to a reader at HEAD, which is the comment a model actually writes — change narration, a decision number only the session saw, an argument aimed at a reviewer. A third mode asks that question, and the rule set was cut on measured false positives before it shipped."
|
|
5
|
+
tags: [decision, comment-policy, rails]
|
|
6
|
+
timestamp: "2026-08-13"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-070 — A comment has to read for somebody who was not in the session
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
1. **A third mode, `resolvable`**, off by default and never implied: `declared` plus one further question — can
|
|
16
|
+
a reader at HEAD, with no transcript of the session, resolve every reference and check every claim?
|
|
17
|
+
2. **Five classes** are refused: change narration, dead citations, review vantage, reviewer-addressed
|
|
18
|
+
justification, and control-flow narration.
|
|
19
|
+
3. **Prose, never syntax.** Nothing here parses a language.
|
|
20
|
+
4. **One comment, one finding.** A block that trips three rules reports once, and a comment already refused for
|
|
21
|
+
having no declared reason is not refused twice.
|
|
22
|
+
5. **The refusal asks for a restatement, not a deletion.**
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
**The rail could not see the comment a model actually writes.** `declared` accepts anything carrying `why:`,
|
|
27
|
+
`hazard:` or `invariant:`, and `strict` accepts nothing. Neither can tell these apart:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
// why: this used to read HEAD, which a committing turn moves past
|
|
31
|
+
// why: without the guard, a bare `*` reads as a block continuation
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Both declare a reason. The first is only checkable by somebody who saw the previous version; the second is
|
|
35
|
+
checkable by anybody, forever. The whole difference is vantage, and no marker carries it.
|
|
36
|
+
|
|
37
|
+
**The distinguishing question is a single one**, which is what makes it implementable: *could a reader at HEAD
|
|
38
|
+
resolve every reference and check every claim?* A counterfactual passes because the reader can construct it. A
|
|
39
|
+
past-tense claim about the code fails because the repository no longer holds the evidence.
|
|
40
|
+
|
|
41
|
+
**The restatement is the fix, not deletion.** Almost every unresolvable comment carries a true, durable fact
|
|
42
|
+
wrapped in the session's vantage. `this used to emit one finding per line, so one failing test arrived as three`
|
|
43
|
+
becomes `one finding per matched line makes a single failing test arrive as three` — same fact, no transcript
|
|
44
|
+
required. Telling the model to delete would throw away the reason along with the framing, so the message names
|
|
45
|
+
the restatement and only then permits deletion.
|
|
46
|
+
|
|
47
|
+
**Prose keeps it language-agnostic.** The syntax catalog exists because a delimiter per language is fragile and
|
|
48
|
+
does not scale ([/decisions/ad-058.md](/decisions/ad-058.md)). Resolvability is a property of the sentence, not
|
|
49
|
+
of the language, so a new language costs nothing.
|
|
50
|
+
|
|
51
|
+
## Trade-offs
|
|
52
|
+
|
|
53
|
+
**One phrase was measured out of the rule set before it shipped.** `no longer` was in the first draft. Run
|
|
54
|
+
against this repository's 1,077 comment blocks it matched four times, and every one described *runtime* state —
|
|
55
|
+
a lock owner that no longer exists, a lesson ref that no longer resolves, a path a future refactor would leave
|
|
56
|
+
behind. Zero described the repository's own history. Four wrong and none right is a phrase that teaches an
|
|
57
|
+
operator to switch the rail off ([/decisions/ad-034.md](/decisions/ad-034.md)), so it went.
|
|
58
|
+
|
|
59
|
+
**This repository would not pass its own new mode.** After that cut, 61 of 1,077 blocks still match — 5.7%, 55
|
|
60
|
+
of them `used to`. Sampling them, they are true positives: each carries a durable fact wrapped in history, and
|
|
61
|
+
each could be restated. The mode is off here, and saying so is more honest than weakening a correct rule until
|
|
62
|
+
the author's own code passes.
|
|
63
|
+
|
|
64
|
+
**Precision was chosen over recall everywhere.** Hedges, version stamps and untranslated fragments are all
|
|
65
|
+
recognisable leakage and none is matched, because none can be matched without also catching prose that is fine.
|
|
66
|
+
The rail catches less than a careful reader would.
|
|
67
|
+
|
|
68
|
+
**A regex is a blunt instrument for a semantic property.** `this is safe` is refused even when the sentence goes
|
|
69
|
+
on to state a real invariant. The refusal names the phrase, so the fix is one edit, and the alternative — a model
|
|
70
|
+
call per comment — is not something a stop hook has the latency budget for
|
|
71
|
+
([/decisions/ad-012.md](/decisions/ad-012.md)).
|
|
72
|
+
|
|
73
|
+
**A fourth mode is a fourth thing to explain.** The catalog's `asks` now offers three words rather than two, and
|
|
74
|
+
the operator prose the model reads is longer in this mode than in the others. The modes are ordered by strictness
|
|
75
|
+
and each is a superset of the last, which is the only arrangement that keeps three explainable.
|
|
76
|
+
|
|
77
|
+
## Not decided here
|
|
78
|
+
|
|
79
|
+
- **Whether `resolvable` should become the default.** It would change behaviour for every project that enabled
|
|
80
|
+
`comments`, and a capability changes nothing by default when it arrives.
|
|
81
|
+
- **Applying the same test to commit messages, plans or handoff prose.** The same leakage appears there and the
|
|
82
|
+
detector is not coupled to comments, but each surface needs its own calibration run before it can block.
|
|
83
|
+
- **Whether this repository should adopt the mode on itself.** It would mean restating 61 passages, and doing
|
|
84
|
+
that well needs the context of whoever wrote each one.
|
|
85
|
+
- **Hedges and planning residue.** `probably fine for now` is real leakage. Every pattern for it also matched
|
|
86
|
+
honest uncertainty, so it stays out until one exists that does not.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-071 — The turn's added lines are checked against the code the project already has"
|
|
4
|
+
description: "Nothing watched for the agent writing something the codebase already contained. A line-run comparison over the turn's diff answers it without parsing a language, and three exclusions — comments, dependency declarations, pure data — were each added because a calibration run said so. It found a real duplication in this repository on its first honest pass."
|
|
5
|
+
tags: [decision, duplication, rails]
|
|
6
|
+
timestamp: "2026-08-13"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-071 — The turn's added lines are checked against the code the project already has
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
1. **A rail, off by default**, that blocks the stop when this turn added a run of `minRun` lines or more that
|
|
16
|
+
already exists elsewhere in the project, naming both sites.
|
|
17
|
+
2. **Diff-scoped** against the sha the turn started from, like the comment gate. A run already duplicated before
|
|
18
|
+
the turn is not this turn's to answer for.
|
|
19
|
+
3. **Lines, never syntax.** Nothing parses a language.
|
|
20
|
+
4. **Three exclusions**, each added because a calibration run demanded it: comments, dependency declarations, and
|
|
21
|
+
runs that are not operational enough to be logic.
|
|
22
|
+
5. **Bounded and honest about it**: 2,000 files and 8 MB, and the scan reports when a bound was reached.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
**Nothing was watching for the copy.** The harness had rails for narration, for evidence, for shell safety and
|
|
27
|
+
for the plan — and none for the failure an operator meets weeks later as two implementations of the same thing
|
|
28
|
+
drifting apart. The second copy is where the drift starts, and the second copy is exactly what a diff-scoped rail
|
|
29
|
+
can see.
|
|
30
|
+
|
|
31
|
+
**Three calibration runs shaped the rule, and each one changed it.**
|
|
32
|
+
|
|
33
|
+
| Run | Reported | What the top of the list was | What changed |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| first | 0 at every window | — | the index kept one site per run, so a run compared equal to itself |
|
|
36
|
+
| second | 137 at six lines | every import block in the repository | dependency declarations excluded |
|
|
37
|
+
| third | 116 | re-export lists, config literals, type bodies, fixtures | a run must be operational in the majority of its lines |
|
|
38
|
+
| fourth | 13 | two entrypoints building the same policy input | shipped |
|
|
39
|
+
|
|
40
|
+
The first run is the one worth naming: **zero findings looked like a clean repository and was a defect.** One
|
|
41
|
+
site per run meant the project index pointed at the very lines the turn had added, so every duplication compared
|
|
42
|
+
equal to itself and vanished. A rail that reports nothing is indistinguishable from a rail that has nothing to
|
|
43
|
+
report, which is why the calibration was run at four window lengths rather than one — four zeroes in a row is
|
|
44
|
+
not a property of a codebase.
|
|
45
|
+
|
|
46
|
+
**It found a real one immediately.** `subagent-start.ts` and `tool-before.ts` each assembled the same twelve-field
|
|
47
|
+
input for `evaluateSubagentSpawn`, identical apart from indentation. A new field in `policy.subagents` had to be
|
|
48
|
+
remembered in both — the shape [/decisions/ad-065.md](/decisions/ad-065.md) collected seven times, a consumer
|
|
49
|
+
that stops growing with its producer. It is now one function.
|
|
50
|
+
|
|
51
|
+
**Data is not logic, and repeated shape is what data is for.** A type's fields, a config literal and a re-export
|
|
52
|
+
list all repeat by design. Requiring a call, an assignment, a branch or a return in the majority of a run's lines
|
|
53
|
+
is what separates "these two do the same thing" from "these two describe the same thing", without asking what
|
|
54
|
+
language it is.
|
|
55
|
+
|
|
56
|
+
**Renaming makes it a different run, deliberately.** Normalisation collapses whitespace and a trailing comma —
|
|
57
|
+
the two things a paste changes without changing the code — and nothing else. Matching through renames would
|
|
58
|
+
report every similarly shaped function, which is the noise that ends a rail's life.
|
|
59
|
+
|
|
60
|
+
## Trade-offs
|
|
61
|
+
|
|
62
|
+
**It reads the repository on every stop.** `git ls-files` plus a read of each file is the honest way to answer
|
|
63
|
+
"does this exist anywhere", and hook latency is a product property ([/decisions/ad-012.md](/decisions/ad-012.md)).
|
|
64
|
+
The bounds are what keep a large repository from paying for it, and the scan says when it stopped early, because
|
|
65
|
+
a partial scan reporting nothing reads exactly like a clean one.
|
|
66
|
+
|
|
67
|
+
**Six lines is a judgement, not a discovery.** At four the matches were punctuation and import blocks; at ten
|
|
68
|
+
almost nothing survived. Six is where a match started being worth reading in *this* repository, and it is a
|
|
69
|
+
config field precisely because another project's answer will differ.
|
|
70
|
+
|
|
71
|
+
**Duplicated logic in tests is reported like any other.** Eleven of the thirteen remaining findings here are test
|
|
72
|
+
setup, and reasonable people call that acceptable. The rail does not know the difference, and inventing a
|
|
73
|
+
test-directory exemption would carve out the place duplication accumulates fastest.
|
|
74
|
+
|
|
75
|
+
**A copy that was renamed is invisible.** The rail catches the paste, not the paraphrase. Catching the paraphrase
|
|
76
|
+
needs a parser per language, which is the cost this design exists to avoid
|
|
77
|
+
([/decisions/ad-058.md](/decisions/ad-058.md)).
|
|
78
|
+
|
|
79
|
+
**Two exclusions are vocabulary, not grammar.** `import`, `require`, `use`, `#include` and their siblings open a
|
|
80
|
+
dependency declaration in most languages but not in all, and a language that spells it differently will have its
|
|
81
|
+
import blocks reported. The failure is loud and the fix is one word in a list.
|
|
82
|
+
|
|
83
|
+
## Not decided here
|
|
84
|
+
|
|
85
|
+
- **Whether to run the rail in observation mode first.** The harness supports recording a rail's findings without
|
|
86
|
+
blocking, and this one has a first-run false-positive history that argues for it. It ships off, so an operator
|
|
87
|
+
who wants the reading before the block can already have it.
|
|
88
|
+
- **Sharing the scan with the comment gate.** Both read the turn's added lines and both walk the project; today
|
|
89
|
+
each does its own. Merging them is a latency win and a coupling, and neither has been measured.
|
|
90
|
+
- **A cache keyed on the tree hash.** The index is rebuilt every stop. `git ls-files` plus a bounded read was
|
|
91
|
+
fast enough here to not need one, and a stale cache would report a duplication against code that has moved.
|
|
92
|
+
- **Whether the eleven test findings in this repository should be fixed.** They are real; whether shared setup is
|
|
93
|
+
worth the coupling is a judgement per test file.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-072 — A record can leave the corpus, and removing is a change worth recording"
|
|
4
|
+
description: "Seventy-one records, every one active and every one about adding something. A record now has a folder it can leave by, value rather than volume decides what leaves, and the doctrine that makes a removal arguable — a test is not the specification, and a decision record is not authority for current behaviour — is written down."
|
|
5
|
+
tags: [decision, docs, process]
|
|
6
|
+
timestamp: "2026-08-13"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-072 — A record can leave the corpus, and removing is a change worth recording
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
1. **`docs/decisions/archived/` exists**, and `- **status**: archived` goes with it. The checker requires the
|
|
16
|
+
folder and the status to agree.
|
|
17
|
+
2. **An archived record keeps its row** in `CHANGELOG.md` and `docs/log.md`. Only the decisions index separates
|
|
18
|
+
active from archived.
|
|
19
|
+
3. **Value decides, never volume.** A record stays active while its alternatives, its ownership boundary, its
|
|
20
|
+
negative guarantee, its security rule, or its condition for reintroduction would still change what somebody
|
|
21
|
+
does. There is no target count and no quota.
|
|
22
|
+
4. **Two doctrines are written down**: a test that pins behaviour nothing load-bearing depends on is evidence
|
|
23
|
+
*for* removal; and a decision record is not authority for current behaviour either.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
**Seventy-one records, seventy-one active, and not one about removing.** The corpus is append-only in both
|
|
28
|
+
senses: nothing leaves, and nothing that arrives is a subtraction. That is not because nothing was removed —
|
|
29
|
+
`format.enabled` went ([/decisions/ad-033.md](/decisions/ad-033.md)), `progressiveHandoff` went — but because a
|
|
30
|
+
removal had no obvious place to be recorded, so it was a footnote inside whatever addition replaced it.
|
|
31
|
+
|
|
32
|
+
**An index that only grows stops being read.** Every record claims to be current, so a reader cannot tell a rule
|
|
33
|
+
that still binds from one whose subject no longer exists. The cost is paid on every read and never recovered.
|
|
34
|
+
[/decisions/ad-067.md](/decisions/ad-067.md) noted the same shape from the other side: fourteen gate steps, each
|
|
35
|
+
added because something drifted, none ever removed. It is fifteen now.
|
|
36
|
+
|
|
37
|
+
**Volume is the wrong criterion, and it is the tempting one.** A rule of "archive anything over N months" or
|
|
38
|
+
"keep the corpus under N records" produces motion without judgement, and the records it removes are the ones
|
|
39
|
+
nobody defended rather than the ones nobody needs. The listed properties are the ones that actually guide a later
|
|
40
|
+
change; a record with none of them has done its work.
|
|
41
|
+
|
|
42
|
+
**An archived record still shipped.** The changelog answers "what landed in this release" and the log answers
|
|
43
|
+
"what was decided when" — neither question changes when a decision stops guiding future work. Only the index
|
|
44
|
+
answers "what binds now", so only the index distinguishes.
|
|
45
|
+
|
|
46
|
+
**Two doctrines, because both were assumed and both are wrong.** A test pinning behaviour is routinely read as
|
|
47
|
+
proof the behaviour is required; it is proof somebody once wrote it down. And this repository has treated its own
|
|
48
|
+
decision records as authority — [/decisions/ad-065.md](/decisions/ad-065.md) assumed the record described the
|
|
49
|
+
code. When code and record disagree, finding out which is wrong is the work, and neither one wins by default.
|
|
50
|
+
|
|
51
|
+
## Trade-offs
|
|
52
|
+
|
|
53
|
+
**Nothing has been archived yet.** The mechanism exists, the rule is written and a test proves an archived record
|
|
54
|
+
keeps its rows and links correctly, but no real record has moved. Judging seventy-one against the rule is work
|
|
55
|
+
that needs the context of whoever wrote each one, and doing it in one pass to prove the folder works would be the
|
|
56
|
+
quota this decision refuses.
|
|
57
|
+
|
|
58
|
+
**The status and the folder are two facts that must agree**, and the checker enforces it rather than deriving one
|
|
59
|
+
from the other. Deriving would have been fewer moving parts; a record whose frontmatter says `active` while it
|
|
60
|
+
sits in `archived/` is a real mistake somebody will make, and a silent derivation would hide it.
|
|
61
|
+
|
|
62
|
+
**`DecisionSummary` grew a `path` field.** The readers previously rebuilt a record's link from its id, which
|
|
63
|
+
stops working the moment a record lives in a subdirectory. Every caller now carries the real path, and four test
|
|
64
|
+
fixtures had to be updated to match.
|
|
65
|
+
|
|
66
|
+
**A removal doctrine in `CONTRIBUTING.md` is prose, not a gate.** Nothing checks that a candidate was judged
|
|
67
|
+
against it. The mechanical half already exists — `check-wiring` and `check-obs-contract` report what nothing
|
|
68
|
+
reads — and those stay reports rather than failures precisely because the judgement is a person's.
|
|
69
|
+
|
|
70
|
+
**None of this reaches anyone using the harness.** It is this repository's own convention, checked from
|
|
71
|
+
`tools/dev/` and never packaged ([/decisions/ad-068.md](/decisions/ad-068.md)).
|
|
72
|
+
|
|
73
|
+
## Not decided here
|
|
74
|
+
|
|
75
|
+
- **Which records should be archived.** Deliberately. The rule is written; applying it is a pass somebody makes
|
|
76
|
+
with the context.
|
|
77
|
+
- **Whether an archived record should be frozen against edits.** Git already holds the history, and a hash
|
|
78
|
+
manifest is machinery for a corpus far larger than seventy-one.
|
|
79
|
+
- **Deleting a record.** Archiving is a `git mv` away from reversible; deletion is not, and nothing needs it.
|
|
80
|
+
- **A `class` on each record** — feature, fix, simplification. It would make "how much of this corpus is
|
|
81
|
+
subtraction" answerable mechanically. It is also seventy-one records to classify, and the question can wait
|
|
82
|
+
until there is more than zero to count.
|