@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,56 @@
|
|
|
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.
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
|
@@ -0,0 +1,65 @@
|
|
|
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)
|
|
@@ -0,0 +1,77 @@
|
|
|
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)
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
|
@@ -0,0 +1,88 @@
|
|
|
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)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-021 — A gate command that never resolved is a config fault, and a recipe runner does not receive file arguments"
|
|
4
|
+
description: "The grind gate appended changed files to every lint/test argv. Against a recipe runner that reads the first path as a second target, the gate failed on code it never executed, and the failure was classified as verification — a follow-up that instructs the agent to fix findings without deleting tests, aimed at healthy code."
|
|
5
|
+
tags: [decision, grind, gate, config]
|
|
6
|
+
timestamp: "2026-07-30"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-021 — A gate command that never resolved is a config fault, and a recipe runner does not receive file arguments
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-07-30
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`grind.appendFiles` decides whether changed files reach the lint/test argv**, with modes `auto`
|
|
17
|
+
(default), `always` and `never`. `auto` appends, except to a recipe runner.
|
|
18
|
+
2. **A recipe runner never receives file arguments.** The set is `just`, `make`, `task`, `mise` and `rake`,
|
|
19
|
+
matched on the executable name after stripping the directory and a Windows `.exe`/`.cmd`/`.bat` suffix.
|
|
20
|
+
3. **A gate whose command never resolved is classified `config`, not `verification`.** The signal is exit
|
|
21
|
+
127, or a runner message that names an unresolved target: `does not contain recipe`, `No rule to make
|
|
22
|
+
target`, `unknown recipe`, `Missing script:`, `task "…" does not exist`, `Don't know how to build task`.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
A project configured `testCommand` as a recipe runner invocation. The gate ran
|
|
27
|
+
`just <recipe> <path>.test.ts`; `just` read the path as a second recipe and exited 1 in 53ms with
|
|
28
|
+
`justfile does not contain recipe`. Nothing about the code caused it, and no edit to the code could clear it.
|
|
29
|
+
|
|
30
|
+
The second half is the damaging one. `classifyGateFailure` maps the `test` gate to `verification`, whose
|
|
31
|
+
follow-up reads *"Fix the test findings without suppressions or deleted tests"*, reinforced by a lesson that
|
|
32
|
+
forbids deleting or skipping tests. Pointed at a malformed command, that text is an instruction to go edit a
|
|
33
|
+
test file that passes — in the observed case a file being edited concurrently by another session, on another
|
|
34
|
+
branch. The harness was steering an agent into damaging healthy work, and the loop counter plus stagnation
|
|
35
|
+
detection kept it there.
|
|
36
|
+
|
|
37
|
+
`config` already existed as a category, with the suggestion *"Check .tlc/harness/config.json commands/paths;
|
|
38
|
+
run harness doctor."* The fix routes to it rather than inventing a concept.
|
|
39
|
+
|
|
40
|
+
## Trade-offs
|
|
41
|
+
|
|
42
|
+
Detection is a closed set of runner names and messages, not inference. A runner outside the set that also
|
|
43
|
+
rejects file arguments still needs `appendFiles: "never"`; that is deliberate, because guessing at argv
|
|
44
|
+
semantics is how the original bug reached a user. The message patterns are specific strings rather than a
|
|
45
|
+
generic `command not found`, so a test whose own output mentions a missing command is not misfiled — exit 127
|
|
46
|
+
covers the real missing-binary case.
|
|
47
|
+
|
|
48
|
+
`auto` changes behaviour for an existing config that pairs a recipe runner with a file-scoped expectation:
|
|
49
|
+
the gate now runs the recipe as written, over the whole suite, instead of failing. That is the intended
|
|
50
|
+
correction.
|
|
51
|
+
|
|
52
|
+
## Not decided here
|
|
53
|
+
|
|
54
|
+
The gate scopes files from `listChangedRepoFiles`, which reads the whole dirty worktree. When two sessions
|
|
55
|
+
share a checkout, one session's gate sees the other's edits. The observed incident surfaced that, and it is
|
|
56
|
+
not addressed: there is no per-session record of edited paths to intersect against, and adding one is a
|
|
57
|
+
separate change to the tool-after path.
|