@tech-leads-club/harness-toolkit 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tlc-cli.ts +20 -5
- package/dist/compact-before.mjs +86 -8
- package/dist/doctor.mjs +116 -10
- package/dist/init-project.mjs +13 -7
- package/dist/lessons-cli.mjs +86 -8
- package/dist/obs-cli.mjs +86 -8
- package/dist/prompt-submit.mjs +86 -8
- package/dist/response-after.mjs +86 -8
- package/dist/run.mjs +86 -8
- package/dist/session-end.mjs +86 -8
- package/dist/session-start.mjs +86 -8
- package/dist/shim.mjs +6976 -15
- package/dist/stop.mjs +86 -8
- package/dist/subagent-start.mjs +86 -8
- package/dist/subagent-stop.mjs +86 -8
- package/dist/support.mjs +86 -8
- package/dist/tlc-cli.mjs +106 -12
- package/dist/tool-after.mjs +86 -8
- package/dist/tool-before.mjs +86 -8
- package/dist/tool-failure.mjs +86 -8
- package/docs/log.md +5 -0
- package/package.json +3 -2
- package/src/core/core.facade.ts +11 -0
- package/src/core/index.ts +2 -0
- package/src/core/release/release.version.ts +147 -0
- package/src/core/shim/shim.precedence.ts +72 -0
- package/src/core/skill/skill.link.ts +81 -0
- package/src/entrypoints/shim.ts +41 -9
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/paths.ts +27 -0
- package/tools/doctor.ts +50 -3
- package/tools/init-project.ts +27 -7
- package/CHANGELOG.md +0 -95
- package/docs/decisions/ad-001.md +0 -32
- package/docs/decisions/ad-002.md +0 -51
- package/docs/decisions/ad-003.md +0 -30
- package/docs/decisions/ad-004.md +0 -37
- package/docs/decisions/ad-005.md +0 -36
- package/docs/decisions/ad-006.md +0 -49
- package/docs/decisions/ad-007.md +0 -36
- package/docs/decisions/ad-008.md +0 -54
- package/docs/decisions/ad-009.md +0 -61
- package/docs/decisions/ad-010.md +0 -45
- package/docs/decisions/ad-011.md +0 -59
- package/docs/decisions/ad-012.md +0 -71
- package/docs/decisions/ad-013.md +0 -87
- package/docs/decisions/ad-014.md +0 -56
- package/docs/decisions/ad-015.md +0 -33
- package/docs/decisions/ad-016.md +0 -98
- package/docs/decisions/ad-017.md +0 -65
- package/docs/decisions/ad-018.md +0 -77
- package/docs/decisions/ad-019.md +0 -75
- package/docs/decisions/ad-020.md +0 -88
- package/docs/decisions/ad-021.md +0 -57
- package/docs/decisions/ad-022.md +0 -120
- package/docs/decisions/ad-023.md +0 -87
- package/docs/decisions/ad-024.md +0 -85
- package/docs/decisions/ad-025.md +0 -92
- package/docs/decisions/ad-026.md +0 -131
- package/docs/decisions/ad-027.md +0 -90
- package/docs/decisions/ad-028.md +0 -90
- package/docs/decisions/ad-029.md +0 -73
- package/docs/decisions/ad-030.md +0 -97
- package/docs/decisions/ad-031.md +0 -94
- package/docs/decisions/ad-032.md +0 -83
- package/docs/decisions/ad-033.md +0 -89
- package/docs/decisions/ad-034.md +0 -86
- package/docs/decisions/ad-035.md +0 -86
- package/docs/decisions/ad-036.md +0 -68
- package/docs/decisions/ad-037.md +0 -47
- package/docs/decisions/ad-038.md +0 -52
- package/docs/decisions/ad-039.md +0 -69
- package/docs/decisions/ad-040.md +0 -89
- package/docs/decisions/ad-041.md +0 -98
- package/docs/decisions/ad-042.md +0 -82
- package/docs/decisions/ad-043.md +0 -79
- package/docs/decisions/ad-044.md +0 -61
- package/docs/decisions/ad-045.md +0 -94
- package/docs/decisions/ad-046.md +0 -111
- package/docs/decisions/ad-047.md +0 -96
- package/docs/decisions/ad-048.md +0 -85
- package/docs/decisions/ad-049.md +0 -66
- package/docs/decisions/ad-050.md +0 -94
- package/docs/decisions/ad-051.md +0 -69
- package/docs/decisions/ad-052.md +0 -69
- package/docs/decisions/ad-053.md +0 -78
- package/docs/decisions/ad-054.md +0 -98
- package/docs/decisions/ad-055.md +0 -74
- package/docs/decisions/ad-056.md +0 -85
- package/docs/decisions/ad-057.md +0 -68
- package/docs/decisions/ad-058.md +0 -97
- package/docs/decisions/ad-059.md +0 -82
- package/docs/decisions/ad-060.md +0 -75
- package/docs/decisions/ad-061.md +0 -68
- package/docs/decisions/ad-062.md +0 -72
- package/docs/decisions/ad-063.md +0 -84
- package/docs/decisions/ad-064.md +0 -79
- package/docs/decisions/ad-065.md +0 -81
- package/docs/decisions/ad-066.md +0 -111
- package/docs/decisions/ad-067.md +0 -64
- package/docs/decisions/ad-068.md +0 -79
- package/docs/decisions/ad-069.md +0 -74
- package/docs/decisions/ad-070.md +0 -86
- package/docs/decisions/ad-071.md +0 -93
- package/docs/decisions/ad-072.md +0 -82
- package/docs/decisions/ad-073.md +0 -102
- package/docs/decisions/ad-074.md +0 -91
- package/docs/decisions/ad-075.md +0 -79
- package/docs/decisions/ad-076.md +0 -102
- package/docs/decisions/ad-077.md +0 -94
- package/docs/decisions/ad-078.md +0 -84
- package/docs/decisions/ad-079.md +0 -73
- package/docs/decisions/ad-080.md +0 -86
- package/docs/decisions/ad-081.md +0 -70
- package/docs/decisions/ad-082.md +0 -79
- package/docs/decisions/ad-083.md +0 -88
- package/docs/decisions/index.md +0 -111
- package/tools/test-env.mjs +0 -28
- package/tools/test-env.names.d.mts +0 -1
- package/tools/test-env.names.mjs +0 -14
package/docs/decisions/ad-012.md
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-012 — Prefer Bun at runtime, keep dist/ for the Node fallback, ship no binary"
|
|
4
|
-
description: "tlc-exec tries Bun first, falling back to Node + dist/*.mjs; dist/ stays tracked since the installer is a git clone with no build step, and no compiled single-file binary ships."
|
|
5
|
-
tags: [decision, runtime, bun, performance]
|
|
6
|
-
timestamp: "2026-07-29"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-012 — Prefer Bun at runtime, keep `dist/` for the Node fallback, ship no binary
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-29
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
`tlc-exec` tries **Bun first** when it is on PATH, falling back to Node + `dist/*.mjs`. `dist/` stays
|
|
17
|
-
tracked (the installer is `git clone` with no build step). No compiled single-file binary.
|
|
18
|
-
|
|
19
|
-
## Measured
|
|
20
|
-
|
|
21
|
-
Same handler, 30 warm invocations each, on the development machine:
|
|
22
|
-
|
|
23
|
-
| Path | Per invocation |
|
|
24
|
-
| --- | --- |
|
|
25
|
-
| `bun run src/*.ts` | ~1 ms |
|
|
26
|
-
| `bun run dist/*.mjs` | ~1 ms |
|
|
27
|
-
| `node dist/*.mjs` | 27 ms |
|
|
28
|
-
| `node src/*.ts` | 63 ms |
|
|
29
|
-
|
|
30
|
-
## Why
|
|
31
|
-
|
|
32
|
-
Hooks are inner-loop — they fire on every tool call, shell command, read, and stop. At ~30 fires per agent
|
|
33
|
-
turn that is ~810 ms of pure process startup on Node versus ~30 ms on Bun. Output is byte-identical across
|
|
34
|
-
runtimes — verified on `guard-shell` (`{"permission":"allow"}`).
|
|
35
|
-
|
|
36
|
-
`dist/` still earns its keep only for the Node path, where it is 2.3× faster than stripping types at
|
|
37
|
-
runtime (27 ms vs 63 ms). Under Bun it buys nothing, which is why it is a fallback artifact rather than the
|
|
38
|
-
primary one.
|
|
39
|
-
|
|
40
|
-
No compiled binary because a Bun-compiled executable is 50–100 MB per platform and this repo both installs
|
|
41
|
-
by `git clone` and tracks its build output — four platform binaries in git history is untenable. The gain
|
|
42
|
-
over `bun run` is also ~zero at this scale; the published latency case for binaries is against a *cold Node*
|
|
43
|
-
baseline, which the Bun-first path already removes.
|
|
44
|
-
|
|
45
|
-
Using the harness never requires Bun — Bun is the fast path, Node stays the guaranteed path.
|
|
46
|
-
|
|
47
|
-
**Surface the fallback, never fall back silently.** A user on the 27× slower path must know they are on it
|
|
48
|
-
and how to leave it. `tlc harness doctor` and `tlc harness status` both report the resolved runtime with the
|
|
49
|
-
measured cost of the gap and the one-line fix (install Bun). Absent Bun is a `WARN`, not a `FAIL` — Node is
|
|
50
|
-
supported, just slower.
|
|
51
|
-
|
|
52
|
-
**Detection must not cost a spawn per hook.** `spawnSync(bun, ["--version"])` on every invocation would burn
|
|
53
|
-
more than the 26 ms it saves. Resolve by testing for the executable on `PATH` without executing it, and
|
|
54
|
-
cache the result in `state/` keyed by the resolved path; refresh on `doctor` / `status` / `update`.
|
|
55
|
-
|
|
56
|
-
## Context
|
|
57
|
-
|
|
58
|
-
Claude Code itself is TypeScript on Bun, shipped as a Bun-compiled binary; other CLI agent tools are
|
|
59
|
-
TS/Node or have migrated to a compiled systems language. TS-on-Bun is the incumbent pattern for this exact
|
|
60
|
-
problem domain.
|
|
61
|
-
|
|
62
|
-
## Applies to
|
|
63
|
-
|
|
64
|
-
- `bin/tlc-exec.mjs`
|
|
65
|
-
- `bin/tlc-build`
|
|
66
|
-
- [/architecture.md](/architecture.md)
|
|
67
|
-
|
|
68
|
-
## See also
|
|
69
|
-
|
|
70
|
-
- [/measure.md](/measure.md) — how `doctor`/`status` surface the resolved runtime
|
|
71
|
-
- [/init.md](/init.md) — Step 1b of the init skill, which presents this trade-off to the user
|
package/docs/decisions/ad-013.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-013 — Documentation follows the Open Knowledge Format (OKF v0.1)"
|
|
4
|
-
description: "docs/ becomes an OKF v0.1 bundle — markdown with YAML frontmatter, index.md and log.md as reserved files, absolute bundle-relative cross-links, and a closed type vocabulary."
|
|
5
|
-
tags: [decision, docs, okf]
|
|
6
|
-
timestamp: "2026-07-29"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-013 — Documentation follows the Open Knowledge Format (OKF v0.1)
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-29
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
`docs/` becomes an OKF v0.1 bundle — markdown with YAML frontmatter, `index.md` and `log.md` as reserved
|
|
17
|
-
files, absolute bundle-relative cross-links.
|
|
18
|
-
|
|
19
|
-
## Layout
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
docs/
|
|
23
|
-
├── index.md okf_version: "0.1", entries from concept descriptions
|
|
24
|
-
├── log.md chronological, ISO 8601
|
|
25
|
-
├── architecture.md type: Concept
|
|
26
|
-
├── concepts.md type: Concept
|
|
27
|
-
├── diagnose.md type: Runbook
|
|
28
|
-
├── init.md type: Runbook
|
|
29
|
-
├── lessons.md type: Concept
|
|
30
|
-
├── measure.md type: Runbook
|
|
31
|
-
├── providers/{index,cursor,claude-code}.md type: Aggregate / Provider
|
|
32
|
-
└── decisions/{index,ad-001…ad-016}.md type: Aggregate / Decision
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## `type` vocabulary
|
|
36
|
-
|
|
37
|
-
Closed, small: `Concept`, `Runbook`, `Provider`, `Decision`, `Capability`, `Aggregate`. OKF has no central
|
|
38
|
-
registry, so the constraint is ours to keep.
|
|
39
|
-
|
|
40
|
-
## Frontmatter
|
|
41
|
-
|
|
42
|
-
`type` is the only field the spec requires. This bundle also always sets `title`, `description`, `tags`,
|
|
43
|
-
`timestamp`. Unknown keys must be preserved on round-trip; broken links are valid and mean unrealized
|
|
44
|
-
knowledge.
|
|
45
|
-
|
|
46
|
-
## Links
|
|
47
|
-
|
|
48
|
-
Absolute (`/decisions/ad-010.md`), not relative — they survive a document moving between subdirectories.
|
|
49
|
-
|
|
50
|
-
## Citations
|
|
51
|
-
|
|
52
|
-
Any doc asserting something from an external source carries a numbered `# Citations` section, per the spec.
|
|
53
|
-
|
|
54
|
-
## Why
|
|
55
|
-
|
|
56
|
-
This repo is a harness *for coding agents*; its own documentation should be machine-consumable by the
|
|
57
|
-
agents it steers. OKF is markdown with structured metadata, needs no SDK, no registry and no service, and
|
|
58
|
-
the spec explicitly targets keeping knowledge beside the code so an agent reads both from one clone.
|
|
59
|
-
|
|
60
|
-
## Why this also splits `STATE.md`
|
|
61
|
-
|
|
62
|
-
`AD-001`…`AD-016` were sixteen-plus sections in one growing file. As OKF concepts — one decision per file,
|
|
63
|
-
`type: Decision`, cross-linked — they become addressable: an agent asking "why does `contracts/` exist"
|
|
64
|
-
resolves `/decisions/ad-010.md` directly instead of scanning one long document.
|
|
65
|
-
|
|
66
|
-
## Note on the sibling repo
|
|
67
|
-
|
|
68
|
-
Some projects use OKF for **runtime knowledge** (an agent's own `knowledge/` namespace), not for repo
|
|
69
|
-
docs — its `docs/*.md` carry no frontmatter. Applying it to docs here is a different and additional use, not
|
|
70
|
-
a copy of that setup.
|
|
71
|
-
|
|
72
|
-
## Applies to
|
|
73
|
-
|
|
74
|
-
- `docs/**`
|
|
75
|
-
- `.specs/STATE.md` (split)
|
|
76
|
-
- `tools/check-docs-bundle.ts`
|
|
77
|
-
|
|
78
|
-
## See also
|
|
79
|
-
|
|
80
|
-
- [/index.md](/index.md), [/log.md](/log.md)
|
|
81
|
-
- [/decisions/index.md](/decisions/index.md)
|
|
82
|
-
|
|
83
|
-
# Citations
|
|
84
|
-
|
|
85
|
-
1. Open Knowledge Format annotated specification — https://okf.md/spec/
|
|
86
|
-
2. GitBook, *What is OKF? Understanding Google's Open Knowledge Format* — https://www.gitbook.com/blog/what-is-okf-open-knowledge-format
|
|
87
|
-
3. Reference implementation, `GoogleCloudPlatform/knowledge-catalog` (Apache 2.0)
|
package/docs/decisions/ad-014.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-014 — Claude Code hook payload field paths, pinned"
|
|
4
|
-
description: "The design specified Claude's event names and capability booleans but never its JSON field paths; this decision pins them so a wrong field path cannot silently pass a green test suite."
|
|
5
|
-
tags: [decision, claude, contracts, inbound]
|
|
6
|
-
timestamp: "2026-07-29"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-014 — Claude Code hook payload field paths, pinned
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-29
|
|
13
|
-
|
|
14
|
-
## Why this exists
|
|
15
|
-
|
|
16
|
-
The design specified Claude's event *names* and capability *booleans* but never the JSON field paths.
|
|
17
|
-
Cursor's adapter could be checked against captured goldens from the predecessor handlers; Claude has no
|
|
18
|
-
such upstream, so a wrong field path produces a passing test suite and a silently broken adapter. This
|
|
19
|
-
pins the field paths before later phases build on top.
|
|
20
|
-
|
|
21
|
-
## Field paths
|
|
22
|
-
|
|
23
|
-
| Field | Where | Source |
|
|
24
|
-
| --- | --- | --- |
|
|
25
|
-
| `session_id` | top level, all events | documented |
|
|
26
|
-
| `hook_event_name` | top level, PascalCase | documented |
|
|
27
|
-
| `cwd` | top level | documented |
|
|
28
|
-
| `transcript_path` | top level | documented |
|
|
29
|
-
| `permission_mode` | top level (`default\|plan\|acceptEdits\|auto\|dontAsk\|bypassPermissions`) | documented |
|
|
30
|
-
| `effort.level` | top level object, `low\|medium\|high\|xhigh\|max` | documented |
|
|
31
|
-
| `prompt_id` | top level, absent until first input | documented |
|
|
32
|
-
| `agent_id`, `agent_type` | top level, subagent events only | documented |
|
|
33
|
-
| `tool_name`, `tool_input`, `tool_use_id` | top level, tool events | documented |
|
|
34
|
-
| `tool_input.command` | Bash | documented (official example) |
|
|
35
|
-
| `tool_input.file_path` | Edit / Write / Read | **inferred** from the tool schema, not quoted in the hooks reference — verify against a real payload when one is available |
|
|
36
|
-
| loop counter | **does not exist** on `Stop` | documented by absence; this is why Claude has no native loop counter (see [/providers/claude-code.md](/providers/claude-code.md)) |
|
|
37
|
-
|
|
38
|
-
## Consequence
|
|
39
|
-
|
|
40
|
-
`claude.inbound.ts` reads exactly these paths. Any future divergence is a payload change, not a design
|
|
41
|
-
choice, and must be caught by re-reading the reference rather than by guessing.
|
|
42
|
-
|
|
43
|
-
## Applies to
|
|
44
|
-
|
|
45
|
-
- `src/providers/claude/claude.inbound.ts`
|
|
46
|
-
- its fixtures
|
|
47
|
-
- Claude entrypoints
|
|
48
|
-
|
|
49
|
-
## See also
|
|
50
|
-
|
|
51
|
-
- [/providers/claude-code.md](/providers/claude-code.md)
|
|
52
|
-
|
|
53
|
-
# Citations
|
|
54
|
-
|
|
55
|
-
1. Claude Code hooks reference (Anthropic documentation) — field paths pinned above were cross-checked
|
|
56
|
-
against the documented hook payload shape at the time this decision was recorded.
|
package/docs/decisions/ad-015.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-015 — Wiring handler names are the entrypoint filenames"
|
|
4
|
-
description: "The handler name a provider's wiring registers is the src/entrypoints/<name>.ts filename, so both providers' wiring descriptors reference exactly the same eleven handler names."
|
|
5
|
-
tags: [decision, entrypoints, providers, wiring]
|
|
6
|
-
timestamp: "2026-07-29"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-015 — Wiring handler names are the entrypoint filenames
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-29
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
The handler name a provider's wiring registers is the `src/entrypoints/<name>.ts` filename —
|
|
17
|
-
`session-start`, `session-end`, `prompt-submit`, `tool-before`, `tool-after`, `tool-failure`,
|
|
18
|
-
`subagent-start`, `subagent-stop`, `stop`, `compact-before`, `response-after`. Both providers' wiring
|
|
19
|
-
descriptors already use these names.
|
|
20
|
-
|
|
21
|
-
## Why
|
|
22
|
-
|
|
23
|
-
Building the second provider adapter required picking a naming scheme with no spec anchor. Making the
|
|
24
|
-
entrypoint filename the single source of truth removes the guess for every wiring descriptor that follows.
|
|
25
|
-
|
|
26
|
-
## Applies to
|
|
27
|
-
|
|
28
|
-
- `cursor.wiring.ts`, `claude.wiring.ts`
|
|
29
|
-
- all `src/entrypoints/*.ts` files
|
|
30
|
-
|
|
31
|
-
## See also
|
|
32
|
-
|
|
33
|
-
- [/providers/cursor.md](/providers/cursor.md), [/providers/claude-code.md](/providers/claude-code.md) — the wiring tables built on this naming
|
package/docs/decisions/ad-016.md
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-016 — Field semantics, state writers, and the core export surface"
|
|
4
|
-
description: "Spawn fields are named apart from the running agent's, parent-model state has a writer, core exports what entrypoints need, ObsKind covers the adapter boundary, blockMode is honoured, and shell audit records are signal-level."
|
|
5
|
-
tags: [decision, entrypoints, contracts, observability, correctness]
|
|
6
|
-
timestamp: "2026-07-29"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-016 — Field semantics, state writers, and the core export surface
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-29
|
|
13
|
-
|
|
14
|
-
## Why this exists
|
|
15
|
-
|
|
16
|
-
Seven decisions about contracts that only settle where the layers meet: what a field name promises, who
|
|
17
|
-
writes a piece of state, what core exports, and which records are durable.
|
|
18
|
-
|
|
19
|
-
## 1. `spawn*` names the child of a spawn; unprefixed fields name the running agent
|
|
20
|
-
|
|
21
|
-
Cursor's inbound adapter reads `raw.subagent_type` (the *calling* agent's identity). Claude's inbound
|
|
22
|
-
adapter reads `tool_input.subagent_type` (the *target of a Task spawn*). Same field, inverted semantics.
|
|
23
|
-
`model` had the same clash: on `subagent.start`/`subagent.stop` it means the child being spawned, everywhere
|
|
24
|
-
else the parent's current model. This reproduced as AD-001's `blockParentFast` state being clobbered by
|
|
25
|
-
every subsequent event.
|
|
26
|
-
|
|
27
|
-
Fix — split the fields in `contracts/harness-event.ts`:
|
|
28
|
-
|
|
29
|
-
| Field | Meaning | Cursor | Claude |
|
|
30
|
-
| --- | --- | --- | --- |
|
|
31
|
-
| `subagentType` | the **calling** agent's own identity | `raw.subagent_type` | top-level `agent_type` |
|
|
32
|
-
| `spawnSubagentType` | the **target** of a Task spawn | `tool_input.subagent_type` | `tool_input.subagent_type` |
|
|
33
|
-
| `model` | the current / parent model | `raw.model` | `raw.model` |
|
|
34
|
-
| `spawnModel` | the child model being spawned | `raw.subagent_model` | `tool_input.model` |
|
|
35
|
-
|
|
36
|
-
**Needs verification against a real payload:** AD-014 records `agent_type` as "subagent events only",
|
|
37
|
-
quoting the reference's "present only in subagent hooks". The useful reading is "present whenever a hook
|
|
38
|
-
fires *inside* a subagent's context", which would make it the caller identity on an ordinary `PreToolUse`.
|
|
39
|
-
If that reading is wrong, Claude has no caller-identity signal at tool time and the read-only-subagent rail
|
|
40
|
-
is Cursor-only — a genuine parity gap to be documented rather than hidden.
|
|
41
|
-
|
|
42
|
-
## 2. Parent-model state is written wherever an event carries the running agent's model
|
|
43
|
-
|
|
44
|
-
`upsertParentModelState` was built early and consumed by `evaluateSubagentSpawn`, but no task ever called
|
|
45
|
-
it — `blockParentFast` was dead on the write side for several phases. It is now wired in `run.ts`.
|
|
46
|
-
|
|
47
|
-
## 3. Core must export what entrypoints legitimately need
|
|
48
|
-
|
|
49
|
-
Several constants and helpers (a stagnation follow-up string, default observability config, lessons
|
|
50
|
-
rendering helpers, a gate-lock timeout error) were being duplicated by entrypoints because the core facade
|
|
51
|
-
curated them out. Duplication guarantees drift. They are now exported from core and the copies deleted.
|
|
52
|
-
|
|
53
|
-
## 4. `ObsKind` needs adapter-boundary members
|
|
54
|
-
|
|
55
|
-
`adapter.unrecognized`, `adapter.error` and `adapter.ambiguous` fire before a provider or session is
|
|
56
|
-
resolved. An earlier version bypassed the observability recorder and appended JSONL directly rather than
|
|
57
|
-
widen a core type from outside core — right instinct, wrong resting place. The union is widened inside core
|
|
58
|
-
instead.
|
|
59
|
-
|
|
60
|
-
## 5. `subagents.blockMode` is honoured by the spawn decision
|
|
61
|
-
|
|
62
|
-
A documented option must change behaviour: `deny` refuses the spawn, `ask` escalates it. An option the
|
|
63
|
-
service ignores is removed rather than documented.
|
|
64
|
-
|
|
65
|
-
## 6. `impeccableHook` is out of scope
|
|
66
|
-
|
|
67
|
-
Relaying a third-party script's verdict is the script author's concern, not the harness's. No port,
|
|
68
|
-
timeout or contract for it exists.
|
|
69
|
-
|
|
70
|
-
## 7. Shell audit records must be signal-level, not debug-level
|
|
71
|
-
|
|
72
|
-
`SIGNAL_KINDS` excluded `shell.end` outright, so "shell commands are audited" silently persisted nothing
|
|
73
|
-
unless `debugEnabled` was on. An audit trail that requires opting in is not an audit trail — `shell.end` is
|
|
74
|
-
now signal-level whenever the shell permission was not a plain allow (see
|
|
75
|
-
[/measure.md](/measure.md#observability-planes)).
|
|
76
|
-
|
|
77
|
-
## Also
|
|
78
|
-
|
|
79
|
-
`HarnessEvent` had no generic usage field, so Cursor's `usageInPayload: true` went unread — only Claude's
|
|
80
|
-
transcript path fed cost. `usage?: { inputTokens?, outputTokens?, … }` was added, populated by whichever
|
|
81
|
-
provider reports it.
|
|
82
|
-
|
|
83
|
-
`sandbox` is documented for Cursor only; an earlier version read `raw.sandbox` defensively for Claude with
|
|
84
|
-
no confirmation it exists — verify or drop from the Claude path.
|
|
85
|
-
|
|
86
|
-
## Applies to
|
|
87
|
-
|
|
88
|
-
- `contracts/harness-event.ts`
|
|
89
|
-
- both `*.inbound.ts` adapters
|
|
90
|
-
- `core/observability`, `core/subagent-policy`, `core/gate`, `core/lesson`, `core/stagnation`
|
|
91
|
-
- `core/index.ts` (the facade)
|
|
92
|
-
- entrypoints (`run`, `support`, `tool-before`, `tool-after`)
|
|
93
|
-
|
|
94
|
-
## See also
|
|
95
|
-
|
|
96
|
-
- [/decisions/ad-001.md](/decisions/ad-001.md) — the rail item 2 restores the writer for
|
|
97
|
-
- [/decisions/ad-009.md](/decisions/ad-009.md) — the earlier round of amendments this one follows
|
|
98
|
-
- [/providers/index.md](/providers/index.md) — `subagentType` vs `spawnSubagentType` in the event mapping tables
|
package/docs/decisions/ad-017.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-017 — The docs gate delegates to the project's tool, and the catalog is the only source of capability metadata"
|
|
4
|
-
description: "Path mapping was measured reporting on 82-100% of commits and removed; the docs gate runs the repository's own staleness tool through the grind path; the capability catalog generates the wizard menu and the architecture rails table."
|
|
5
|
-
tags: [decision, docs, capabilities, gates]
|
|
6
|
-
timestamp: "2026-07-30"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-017 — The docs gate delegates to the project's tool, and the catalog is the only source of capability metadata
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-30
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
`docs.command` runs the repository's own documentation staleness tool on stop, through the same lock, artifact
|
|
17
|
-
writer and failure path as the lint and test gates. `docs.severity` is `warn` or `deny`, defaulting to `warn`.
|
|
18
|
-
A repository without such a tool gets no gate.
|
|
19
|
-
|
|
20
|
-
`capabilities/catalog.json` is the only place a capability is described. `tools/render-capabilities.ts`
|
|
21
|
-
generates the wizard's menu and the architecture rails table into marked regions, and `--check` is part of the
|
|
22
|
-
gate.
|
|
23
|
-
|
|
24
|
-
## Why
|
|
25
|
-
|
|
26
|
-
Code changes pass lint, tests and the ship gate; the documents describing them pass nothing.
|
|
27
|
-
|
|
28
|
-
The first implementation inferred staleness from a source-glob to docs-glob map. It was replayed over real
|
|
29
|
-
history with its own evaluator and reported on 100% of applicable commits in one repository and 82–91% in
|
|
30
|
-
another, at every granularity tried — any markdown, co-located markdown, excluding test paths, a specific
|
|
31
|
-
artifact. The denominator is wrong: nearly every commit touches an area and few change documented behaviour.
|
|
32
|
-
A gate that reports almost always detects nothing, so it was removed rather than kept as a fallback.
|
|
33
|
-
|
|
34
|
-
The tools that do discriminate declare rather than infer. `drift` anchors a document to a file or symbol and
|
|
35
|
-
hashes a normalized AST through tree-sitter. `oasdiff` compares generated specs and classifies changes by
|
|
36
|
-
severity. `ast-grep` matches structure.
|
|
37
|
-
|
|
38
|
-
Building an equivalent inside the harness was rejected on availability, not taste. Regex over source is the
|
|
39
|
-
failure mode this project has already removed twice. The TypeScript compiler API is not guaranteed at runtime,
|
|
40
|
-
since the installer runs no package install and a clean runtime has no `node_modules`. `Bun.Transpiler.scan()`
|
|
41
|
-
is precise and free but Bun-only, which would make a gate's verdict depend on which runtime launched the hook.
|
|
42
|
-
|
|
43
|
-
That leaves delegation, which is what `grind` already does: the harness runs the project's lint and test rather
|
|
44
|
-
than implementing either. The docs gate is the same shape, so it inherits stagnation fingerprinting, gap
|
|
45
|
-
feedback and progressive follow-up for free, and adds no second gate mechanism.
|
|
46
|
-
|
|
47
|
-
## Consequences
|
|
48
|
-
|
|
49
|
-
- Prose a catalog entry cannot express stays outside the markers: the floor table, the always-ask section, the
|
|
50
|
-
lessons subsection, and operator mode, which is posture rather than a capability.
|
|
51
|
-
- Adding a capability is one edit to the catalog plus running the generator.
|
|
52
|
-
- No skip token of our own: the tool owns its escape hatch, and `drift` already hardened that path by
|
|
53
|
-
requiring explicit confirmation that the document was read before re-stamping.
|
|
54
|
-
- A repository with no such tool is told so and the capability stays off.
|
|
55
|
-
|
|
56
|
-
## Applies to
|
|
57
|
-
|
|
58
|
-
- `src/entrypoints/stop.ts`, `src/core/policy/`
|
|
59
|
-
- `capabilities/catalog.json`, `tools/render-capabilities.ts`
|
|
60
|
-
- `skills/harness-init/`
|
|
61
|
-
|
|
62
|
-
## See also
|
|
63
|
-
|
|
64
|
-
- [/decisions/ad-013.md](/decisions/ad-013.md) — the documentation bundle this gate protects
|
|
65
|
-
- [/decisions/index.md](/decisions/index.md)
|
package/docs/decisions/ad-018.md
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-018 — Three rails adopted from an external review, each off by default and declared rather than inferred"
|
|
4
|
-
description: "The plan gate, the untrusted-content framing and the global observability spool were adopted after validating each candidate against this codebase; two candidates were refuted and one turned out to be an active defect. All three are opt-in, and both detectors match declared inputs instead of inferring from content."
|
|
5
|
-
tags: [decision, gates, capabilities, security, observability]
|
|
6
|
-
timestamp: "2026-07-30"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-018 — Three rails adopted from an external review, each off by default and declared rather than inferred
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-30
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
Three capabilities were added, all off by default: `planGate.enabled` (declared scope versus the diff),
|
|
17
|
-
`untrustedContent.enabled` (framing for content read from outside the repository) and `obs.globalSpool`
|
|
18
|
-
(cross-repository record spool).
|
|
19
|
-
|
|
20
|
-
Both new detectors match **declared inputs**, never inferred ones. The plan gate reads a `HARNESS_PLAN:`
|
|
21
|
-
protocol line and ignores prose about plans. The untrusted-content rail matches a fixed list of tool names,
|
|
22
|
-
every MCP result, and command patterns anchored at the start of a command segment.
|
|
23
|
-
|
|
24
|
-
## Why
|
|
25
|
-
|
|
26
|
-
The candidates came from reading another agent-facing product. Each was validated against this codebase
|
|
27
|
-
before being accepted, and the record matters more than the list:
|
|
28
|
-
|
|
29
|
-
| Candidate | Outcome |
|
|
30
|
-
| --- | --- |
|
|
31
|
-
| Reclaim a stale grind lock | **Defect found here** — `describeHolder` ignored mtime and the stop short-circuited on it, so the steal path was unreachable |
|
|
32
|
-
| Typed handoff phase machine | Refuted — the routing machine already exists in `last_gate_result`, `fingerprint_hits`, `last_failure_category` and `next_action` |
|
|
33
|
-
| Persist state as it happens | Refuted — `stop.ts` already patches the handoff in every branch |
|
|
34
|
-
| Plan versus diff conformance | Adopted |
|
|
35
|
-
| Untrusted-content framing | Adopted |
|
|
36
|
-
| Cross-repo observability | Adopted |
|
|
37
|
-
| Ship evidence tied to a reproduction | Cut — depends on a `HARNESS_REPRO:` discipline nobody practises, and re-executing agent-authored argv is the riskiest surface proposed even behind the floor |
|
|
38
|
-
|
|
39
|
-
Adopting on reputation would have shipped two mechanisms this product already had and missed the defect.
|
|
40
|
-
|
|
41
|
-
**Off by default** is not timidity. `obs.globalSpool` writes outside the repository, which is the one thing
|
|
42
|
-
an operator cannot undo by editing project policy. The plan gate changes what "done" means for a turn. A
|
|
43
|
-
capability that changes either belongs in the wizard, presented with its trade-off, rather than arriving
|
|
44
|
-
with an update.
|
|
45
|
-
|
|
46
|
-
**Declared over inferred** is the lesson AD-017 already paid for with the docs gate: a check that fires on
|
|
47
|
-
almost everything detects nothing. The untrusted rail proved it again on its first production run — the
|
|
48
|
-
patterns were matched as substrings, so writing the documentation that names them tripped the rail on its
|
|
49
|
-
own prose. Anchoring at the start of a command segment fixed it, and the false positive is now a test.
|
|
50
|
-
|
|
51
|
-
**The plan gate's limit is stated, not hidden.** A turn that declares no plan is not gated. That is the same
|
|
52
|
-
weakness that disqualified the reproduction gate; the difference is that the plan gate executes nothing, so
|
|
53
|
-
the cost of the weakness is friction rather than a privilege-escalation path.
|
|
54
|
-
|
|
55
|
-
## Consequences
|
|
56
|
-
|
|
57
|
-
- `Policy` gained `planGate`, `untrustedContent` and `obs`; the loader merges each like every other section.
|
|
58
|
-
- `ProviderPolicyDefaults` gained `untrustedTools`, so a new adapter cannot forget to declare which of its
|
|
59
|
-
own tools carry outside content — the provider contract test fails if it does.
|
|
60
|
-
- The handoff carries `plan_paths`, `plan_at`, `plan_snippet` and `plan_deviations`.
|
|
61
|
-
- The plan gate runs before the ship gate: a turn whose scope is invalid produced evidence for the wrong
|
|
62
|
-
change.
|
|
63
|
-
- A deviation without a stated reason is refused, otherwise the gate is a formality satisfied by restating
|
|
64
|
-
the file just touched.
|
|
65
|
-
- Catalog version 8, twenty-one capabilities, generated regions rewritten from the catalog per AD-017.
|
|
66
|
-
|
|
67
|
-
## Applies to
|
|
68
|
-
|
|
69
|
-
- `src/core/plan/`, `src/core/untrusted/`, `src/core/observability/`
|
|
70
|
-
- `src/entrypoints/stop.ts`, `src/entrypoints/response-after.ts`, `src/entrypoints/tool-after.ts`
|
|
71
|
-
- `capabilities/catalog.json`, `docs/concepts.md`, `docs/measure.md`
|
|
72
|
-
|
|
73
|
-
## See also
|
|
74
|
-
|
|
75
|
-
- [/decisions/ad-017.md](/decisions/ad-017.md) — declared over inferred, and the catalog as the only source
|
|
76
|
-
- [/decisions/ad-019.md](/decisions/ad-019.md) — the identity bugs found while verifying these rails
|
|
77
|
-
- [/concepts.md](/concepts.md)
|
package/docs/decisions/ad-019.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-019 — A resource is identified by what it resolves to, and a declared capability must be read where it matters"
|
|
4
|
-
description: "Three defects of the same shape: a lock body that parses is not a holder, a launcher path string is not the launcher file, and a capability every adapter declares was consulted by nothing. Each let a gate report a state that was not true."
|
|
5
|
-
tags: [decision, gates, capabilities, providers]
|
|
6
|
-
timestamp: "2026-07-30"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-019 — A resource is identified by what it resolves to, and a declared capability must be read where it matters
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-30
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
Three comparisons were corrected, all of the same shape — the check used a proxy for the thing instead of the
|
|
17
|
-
thing:
|
|
18
|
-
|
|
19
|
-
1. **A lock is a holder only if its body names one.** `readLockBody` returning non-null is not enough:
|
|
20
|
-
`JSON.parse` succeeds on `"a string"`, `42`, `{}` and `{"provider":"p"}`. `isUsableLockBody` checks for
|
|
21
|
-
`provider`, `session` and `pid`. A lock that names no holder is reclaimed after a five-second grace
|
|
22
|
-
window, alongside the existing age-based steal.
|
|
23
|
-
2. **A launcher is the file, not the path string.** Wiring comparison canonicalises any argument naming the
|
|
24
|
-
launcher before deciding whether it changed.
|
|
25
|
-
3. **`contextAtToolBefore` / `contextAtToolAfter` are now read.** `degrade` abstains when a `context`
|
|
26
|
-
decision lands on an event the provider cannot carry.
|
|
27
|
-
|
|
28
|
-
## Why
|
|
29
|
-
|
|
30
|
-
Each of the three let a gate report something untrue, which is worse than a gate that is absent.
|
|
31
|
-
|
|
32
|
-
The lock case cost **120 seconds per stop, indefinitely**. `describeHolder` returned null for an unreadable
|
|
33
|
-
body, so the stop fell through to `withGateLock`, which reclaimed only by mtime — a recent corrupt file was
|
|
34
|
-
never stale, so the loop ran to the deadline and abstained with an `adapter.error`. The escape was deleting
|
|
35
|
-
the file by hand. It was first written off as safe degradation; that was wrong. An unreadable file blocking
|
|
36
|
-
every stop for two minutes is a defect, and the grace window has to outlast `tryAcquire`'s create-then-write
|
|
37
|
-
gap by orders of magnitude without approaching the stale threshold — five seconds is roughly four orders
|
|
38
|
-
above the gap and twenty-four times below the wait it replaces.
|
|
39
|
-
|
|
40
|
-
The launcher case made `doctor` report "detected but not wired" for a provider whose eleven hooks were wired
|
|
41
|
-
and firing, on every from-a-clone install, because `settings.json` holds the symlinked install path while the
|
|
42
|
-
runtime resolves the checkout behind it. `bin/tlc-exec.mjs` already documents this hazard and reads `argv[1]`
|
|
43
|
-
to preserve the caller's path — but `bin/tlc` walks `readlink` before invoking it, so the protection never
|
|
44
|
-
applied to a CLI reached through `~/.local/bin/tlc`. Rather than unpick that resolution, the comparison was
|
|
45
|
-
made canonical, which also stops `update` from rewriting a `settings.json` that was already correct.
|
|
46
|
-
|
|
47
|
-
The capability case was found by verifying an acceptance criterion that assumed a degrade path existed. Both
|
|
48
|
-
adapters declared the field and nothing consulted it, so a `context` returned on an unsupported event was
|
|
49
|
-
rendered into a field the provider ignores and lost silently, while the caller believed it was delivered.
|
|
50
|
-
Abstaining is the honest degradation: `context` is informative, so unlike `ask` there is nothing to escalate
|
|
51
|
-
to.
|
|
52
|
-
|
|
53
|
-
## Consequences
|
|
54
|
-
|
|
55
|
-
- `describeHolder` returns null for a stale lock and for one whose body names no holder, so a caller that
|
|
56
|
-
short-circuits on it falls through to the reclaim path instead of reporting a dead owner.
|
|
57
|
-
- `withGateLock` accepts `unreadableGraceMs`; `GATE_LOCK_UNREADABLE_GRACE_MS` is 5 000 ms.
|
|
58
|
-
- Wiring equality is canonical, so a symlinked install is recognised and not rewritten. Only strings naming
|
|
59
|
-
the launcher are resolved: a foreign hook's own script path is untouched.
|
|
60
|
-
- The untrusted-content rail keeps its own capability check in addition to `degrade`, because reaching
|
|
61
|
-
`degrade` would already have consumed the once-per-turn marker and cost the whole turn its framing.
|
|
62
|
-
- A test that passed for the wrong reason was found the same way: it hardcoded `WebFetch` while
|
|
63
|
-
`providers[0]` is Cursor, whose untrusted list does not contain it. The tool name now comes from the
|
|
64
|
-
provider's own list.
|
|
65
|
-
|
|
66
|
-
## Applies to
|
|
67
|
-
|
|
68
|
-
- `src/core/gate/gate.lock.ts`
|
|
69
|
-
- `src/providers/claude/claude.wiring.ts`, `src/providers/provider.degrade.ts`
|
|
70
|
-
- `src/entrypoints/tool-after.ts`, `tools/doctor.ts`
|
|
71
|
-
|
|
72
|
-
## See also
|
|
73
|
-
|
|
74
|
-
- [/decisions/ad-018.md](/decisions/ad-018.md) — the rails whose verification surfaced these
|
|
75
|
-
- [/diagnose.md](/diagnose.md) — operator-facing symptoms of all three
|
package/docs/decisions/ad-020.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-020 — One resolution for the install path, one source for posture, and a config that only advertises what it reads"
|
|
4
|
-
description: "The wrappers collapsed the install symlink, so generated shims named the checkout; status derived posture separately from the loader and disagreed with it; the observability config section was read by nothing. Each is fixed at the root, and a gate refuses any operator home path in a tracked file."
|
|
5
|
-
tags: [decision, runtime, cli, observability, security]
|
|
6
|
-
timestamp: "2026-07-30"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-020 — One resolution for the install path, one source for posture, and a config that only advertises what it reads
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-07-30
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
1. **`resolveHarnessHome` returns the conventional install path when `realpath` proves it names the same
|
|
17
|
-
runtime as the invoked launcher.** An explicit `TLC_HOME` still wins; a relocated install is untouched.
|
|
18
|
-
2. **`status` reads `loadPolicy`.** It reports the posture a hook resolves and, under `--json` and in the text
|
|
19
|
-
form, where the mode came from (`config`, `file` or `flag`).
|
|
20
|
-
3. **`Policy.obs` carries `globalSpool`, `includePayloads`, `maxAttrChars`, `sessionCostAlertUsd` and
|
|
21
|
-
`retentionDays`.** The `observability` section is removed, not honoured. `debugEnabled` is deliberately
|
|
22
|
-
absent.
|
|
23
|
-
4. **A tracked file may not contain an operator's home path**, enforced by a test over `git ls-files` with a
|
|
24
|
-
per-line `leak-gate-allow` marker for fixtures.
|
|
25
|
-
5. **Project shim hooks stay untracked**, and that is a decision rather than a deferral — see below.
|
|
26
|
-
|
|
27
|
-
## Why
|
|
28
|
-
|
|
29
|
-
**One resolution.** Both bash wrappers walk `readlink` before invoking the launcher, so `argv[1]` and `binDir`
|
|
30
|
-
name the checkout. `resolveHarnessHome` documented that exact hazard and still returned the collapsed path.
|
|
31
|
-
Everything derived from it is written to disk or compared: `init` wrote shim hooks pointing at a checkout that
|
|
32
|
-
exists only on the machine that ran it, and `doctor` compared a checkout-shaped path against the install-shaped
|
|
33
|
-
one recorded in the provider's config, reporting healthy wiring as broken. Preferring the conventional path
|
|
34
|
-
only when `realpath` agrees fixes the source instead of each symptom, and refuses to guess: a deliberately
|
|
35
|
-
relocated install still wins.
|
|
36
|
-
|
|
37
|
-
**One source for posture.** `status` recomputed mode and grind from flag files and defaulted to `solo`. This
|
|
38
|
-
repository, whose policy sets `heads-down`, therefore reported `mode: solo, grind: OFF` while every hook
|
|
39
|
-
resolved focus with grind forced on. A second derivation of the same fact is the whole reason they could
|
|
40
|
-
disagree. Reporting the origin matters as much as the value: the question after a surprising posture is always
|
|
41
|
-
*why*, and a leftover flag is the usual answer.
|
|
42
|
-
|
|
43
|
-
An earlier reading of this — that `tlc harness grind on` and `mode` were inert — was **wrong**, and is recorded
|
|
44
|
-
as refuted: `policy.loader.ts` has always read those flags. Only the reporting was broken.
|
|
45
|
-
|
|
46
|
-
**A config that only advertises what it reads.** The `observability` block sat in the example config and in
|
|
47
|
-
this repository's policy, was documented in `concepts.md` with an instruction to set `debugEnabled: true`, and
|
|
48
|
-
was read by nothing — the planes came from a module constant. Values happened to match the defaults, so it
|
|
49
|
-
never diverged visibly; it simply promised configuration that did not exist. Honouring the key would have been
|
|
50
|
-
back-compat this project refuses ([/decisions/ad-003.md](/decisions/ad-003.md)), so the fields that have a real effect moved to `obs` and the key was
|
|
51
|
-
deleted.
|
|
52
|
-
|
|
53
|
-
`debugEnabled` was implemented, then removed before shipping: every event that resolves to debug level is
|
|
54
|
-
emitted with the audit configuration, which forces debug on for the audit trail (AD-016 item 7). Exposing a
|
|
55
|
-
field with nothing to switch would have repeated the mistake being corrected. A mutation test confirmed the
|
|
56
|
-
same danger elsewhere — `retentionDays` initially reached the runtime with no test covering it, and the
|
|
57
|
-
mutation that ignored it survived until one was added.
|
|
58
|
-
|
|
59
|
-
**The leak gate.** This product installs with a shell one-liner and tracks its own bundles, so a generated file
|
|
60
|
-
is exactly where an account name slips in unnoticed. A denylist of one name would pass on every other machine,
|
|
61
|
-
so the rule is the shape of the path. An audit before adding it found the tree clean.
|
|
62
|
-
|
|
63
|
-
## Consequences
|
|
64
|
-
|
|
65
|
-
- `bin/tlc-exec.d.mts` declares the four-argument resolver; the `.mjs` and its declarations are separate files
|
|
66
|
-
and both must move together.
|
|
67
|
-
- `StatusReport` gained `modeOrigin`. Any consumer of `status --json` sees one more field.
|
|
68
|
-
- `tlc harness obs prune` reads retention from policy, so two projects on one machine can differ.
|
|
69
|
-
- The example config and this repository's policy no longer carry `observability`; a project that copied it
|
|
70
|
-
loses nothing, because nothing read it.
|
|
71
|
-
- Fixtures that need a realistic account name carry `leak-gate-allow` on that line.
|
|
72
|
-
|
|
73
|
-
**Project shims are not version-controllable, by decision.** A portable form needs shell expansion, and `args`
|
|
74
|
-
arrays carry none — so every hook of every provider would need `sh -c` (or `cmd /c` on Windows), paying
|
|
75
|
-
permanent per-hook latency for a file `tlc harness init` regenerates in under a second. They are ignored and
|
|
76
|
-
documented instead.
|
|
77
|
-
|
|
78
|
-
## Applies to
|
|
79
|
-
|
|
80
|
-
- `bin/tlc-exec.mjs`, `bin/tlc-exec.d.mts`, `bin/tlc-cli.ts`
|
|
81
|
-
- `src/core/policy/`, `src/entrypoints/support.ts`, `tools/obs-cli.ts`
|
|
82
|
-
- `config.example.json`, `.gitignore`
|
|
83
|
-
|
|
84
|
-
## See also
|
|
85
|
-
|
|
86
|
-
- [/decisions/ad-019.md](/decisions/ad-019.md) — the same identity-versus-proxy mistake in three other places
|
|
87
|
-
- [/decisions/ad-003.md](/decisions/ad-003.md) — no backward compatibility
|
|
88
|
-
- [/diagnose.md](/diagnose.md), [/concepts.md](/concepts.md)
|