Bundle
dsh-deadend
A refutation ledger for coding agents: remember what did not work, and expire it automatically when the code it depended on changes.
- Source
- liyixuan201211
- License
- MIT
- Updated
- Updated 2 hours ago
Readme
# dsh-deadend
**Remember what did not work — and expire it automatically when the code it
depended on changes.**
A refutation ledger for coding agents, as a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
plugin.
```bash
dsh plugin --profile web add github:liyixuan201211/dsh-deadend
```
No install needed to try it:
```bash
npx --yes github:liyixuan201211/dsh-deadend --help
```
中文:一个**失败账本**。agent 每一轮新会话都会用同样的乐观重试同样的失败,代价重复支付。本插件把"试过且走不通"的方案记在仓库里,并在下一次尝试前先查一遍。关键在于:每条记录都带**锚点**(决定其真伪的文件的哈希)——锚点没变就拦截,锚点一变就自动降级为"待复测",因为当初那个世界已经不在了。
---
## The problem is not forgetting. It is remembering badly.
Every memory system for agents stores **what worked**: facts, preferences,
summaries, embeddings. Almost nothing stores **what failed**. So the failure
loop repeats — the same broken install, the same two-hour detour, once per
session, forever.
The obvious fix is a list of things that did not work. The obvious fix is also
wrong, and worse than nothing:
> A note from six months ago saying "X does not work", written against code that
> has since changed, will confidently block a fix that now works.
A stale block is not a harmless annoyance. It is the tool lying to you, and a
tool that lies gets switched off. **The hard part of negative knowledge is not
recording it — it is invalidating it.**
## The idea: anchors
Every entry carries **anchors**: the files whose content would have to change for
the failure to stop being true.
| Anchors | Meaning | `check` exit |
|---|---|---|
| all unchanged | authoritative — it still fails | **`3`** |
| any changed / deleted | **suspect** — re-test it | **`4`** |
| nothing known | clear | `0` |
| entry refused (no anchors) | `record` declined | `5` |
Validity is judged by content, not by a clock. No `MAX_AGE_DAYS`, no
calibration — and it is self-maintaining, because nobody has to remember to
expire anything.
`record` **refuses to create an entry with no anchors.** That is enforced, not
advised:
```
$ deadend record --title "go build fails on cgo" --cmd "go build ./..."
Refusing to record: no anchors.
A dead end with nothing to watch can never expire, and a claim that can
never expire is indistinguishable from a bug. Name the files whose content
would have to change for this failure to stop being true.
Suggested anchors (they exist here):
--anchor package.json
Or pass --unanchored if this genuinely has no local artifact — it will
block forever, and `deadend status` will keep saying so.
```
The undecayable case exists (an upstream API limit, a vendored dependency) and
`--unanchored` allows it — loudly, and counted in `deadend status`, so you always
know which of your claims are unfalsifiable.
## What it looks like
Before re-running something expensive, ask:
```bash
deadend check --cmd "npm install sharp" --log /tmp/last-install.log
```
```
⛔ BLOCKED — 1 recorded dead end matches, and its anchors are unchanged.
────────────────────────────────────────────────────────────────────────
dd_50e127c7d9c5 npm install sharp fails: no prebuilt binary for darwin-arm64
matched identical command
recorded 2026-09-13 (today)
attempt npm install sharp → exit 1
symptom Error: Cannot find module sharp-darwin-arm64.node
why sharp ships prebuilt binaries and none exists for this platform/arch
instead npm rebuild sharp --build-from-source
anchors 2 of 2 unchanged — still authoritative
✓ package.json
✓ package-lock.json
evidence install.log
────────────────────────────────────────────────────────────────────────
This is not a prohibition — it is a claim that was true, with its evidence.
If you think the world moved, re-test it and re-confirm:
deadend verify dd_50e127c7d9c5 --still-fails --log <new-output>
```
Now change one line of `package-lock.json` and ask again:
```
⚠ SUSPECT — 1 recorded dead end matches, but the world it described has changed.
anchors 1 of 2 changed — no longer authoritative
✓ package.json
✗ package-lock.json f25e7d29 → 55eaf97b
Re-test it: it may now work.
still fails? deadend verify dd_50e127c7d9c5 --still-fails
works now? deadend verify dd_50e127c7d9c5 --now-works
```
That transition — authoritatively blocked, then honestly downgraded the moment
its evidence moved — is the whole plugin.
## Recording a failure
```bash
deadend record \
--title "npm install sharp fails: no prebuilt binary for darwin-arm64" \
--cmd "npm install sharp" --exit 1 --log /tmp/last-install.log \
--why "sharp ships prebuilt binaries and publishes none for this platform/arch" \
--retry "npm rebuild sharp --build-from-source (needs libvips)" \
--anchor package.json --anchor package-lock.json \
--evidence /tmp/last-install.log --tag native
```
`--log` is worth the trouble. The failure is hashed in a **normalised** form —
line and column numbers, absolute paths, temp directories, digests, UUIDs,
timestamps, durations and ANSI colour removed — so the same breakage is
recognised across sessions, machines, and differently-worded commands:
```bash
# Recorded as: python app.py + ModuleNotFoundError: No module named numpy
# Still caught: python3 app.py --verbose, same failure, different command
deadend check --cmd "python3 app.py --verbose" --log ./run.log # -> exit 3
```
## Commands
```
deadend check [--cmd C] [--log F|-] [--symptom T] [--title T] [-q] [--json]
deadend record -t TITLE [--cmd C] [--exit N] [--log F|-] [--why W] [--retry R]
[--anchor PATH]... [--unanchored] [--evidence E]... [--tag T]...
deadend verify <id> (--still-fails | --now-works) [--log F] [--note N]
deadend list [--status active|suspect|retired] [--all] [--json]
deadend show <id> [--json]
deadend status [--json] counts, plus the entries that need attention
deadend merge <ledger.jsonl...> (use - for stdin)
deadend gc [--dry-run] [--drop-retired] [--drop-undecayable]
deadend init
```
Exit codes are the interface, so `check` composes with any shell or CI gate:
```bash
deadend check --cmd "npm install sharp" -q || echo "already ruled out; not retrying"
```
## Matching is layered, so it never cries wolf
| Strength | Match | Decisive? |
|---|---|---|
| 3 | identical failure signature | yes |
| 2 | identical normalised command | yes |
| 1 | same command family, or similar title | **no** — shown as *related* |
Strength-1 matches never change the verdict. `npm install sharp` failing says
nothing about `npm install left-pad`, so installing something else is never
blocked — it is mentioned, with the earlier note attached. A gate that blocks
too much gets disabled, which protects nobody.
Command normalisation strips whitespace and leading wrappers (`sudo`, `time`,
`env`) and nothing else. Flags are part of the command.
## The ledger lives in your repository
```
<repo>/.deadend/ledger.jsonl # append-only event log, one JSON object per line
```
Not in `~/.cache`, not per-machine. That is the design decision with the most
consequences:
- **Reviewable.** It arrives in a pull request as `+ {"v":1,"event":"record",…}` —
a claim you can argue with.
- **Shared.** Your next session, your teammates, their agents, and CI all inherit
it. A per-machine cache protects exactly one machine.
- **Auditable.** Every change keeps its reason: `verify` appends an observation
rather than overwriting, and `gc` folds history into the entry so compaction
never loses one.
- **Concurrency-safe.** Recording appends; there is no read-modify-write to race.
Identity is content-derived (`dd_` + `sha256(title | command | signature | anchor
paths)`), so two clones that record the same refutation produce the same id and
merging ledgers is a set union, not a de-duplication problem.
Full format: [`skills/deadend/reference/schema.md`](skills/deadend/reference/schema.md).
## Merging ledgers
Identity is content-derived, so the same refutation recorded twice — by two
teammates, or by you on two machines — has the same id, and merging is a set
union rather than a de-duplication problem. That is what makes committing the
ledger to a shared repository workable in the first place.
```bash
deadend merge ../other-clone/.deadend/ledger.jsonl # or - for stdin
# added 3
# updated 1
# unchanged 7
# total 11
```
When both sides know an id, the more recently updated observation wins on status
and anchors, and histories and notes are unioned so neither side loses one.
## Use it in CI
`check` reports through its exit code, so the ledger becomes a gate in one line:
```yaml
- name: Do not re-run a known dead end
run: |
npx --yes github:liyixuan201211/dsh-deadend check --cmd "npm install sharp" -q
# exit 3 = still authoritative: fail
# exit 4 = anchors changed: re-test, do not fail
# exit 0 = clear
```
And because an unfalsifiable entry is the failure mode this tool exists to
prevent, it is worth failing the build when the ledger itself drifts:
```yaml
- name: Keep the ledger falsifiable
run: |
npx --yes github:liyixuan201211/dsh-deadend status --json > ledger.json
node -e '
const j = require("./ledger.json");
if (j.attention.length > 0) {
console.error("ledger needs attention:");
for (const a of j.attention) console.error(" " + a.id + " " + a.why);
process.exit(1);
}'
```
## Honest positioning
Recording failures is not a new idea, and this plugin does not claim it is.
The closest prior art is a Claude Code plugin,
[dead-end-registry](https://github.com/karanb192/claude-code-hooks), which mines
transcripts for reverted approaches; there is also academic work on
failure-aware shared memory
([Negative Knowledge](https://github.com/hch-wang/Negative_Knowledge), ICML 2026
AI4Research workshop) and "dead end registries" as a coordination mechanism in
automated-research templates.
| | Prior art | `dsh-deadend` |
|---|---|---|
| Captured | reverted approaches mined from transcripts | refutations you state, with evidence |
| Lives in | `~/.claude/…`, per machine | `.deadend/ledger.jsonl`, in the repo |
| Match | keyword match on the prompt | normalised failure signature + command identity |
| Extraction | heuristics (+ optional model pass) | deterministic, offline, no model |
| **Expiry** | **wall-clock age (e.g. 60 days)** | **content hash of the falsifiers** |
| Undecayable claims | not distinguished | refused by default; loud when allowed |
| Interface | editor hooks | exit codes, composable with any shell or CI |
Time-based expiry is wrong in both directions: too slow (a dependency bump that
invalidates a dead end this morning does not clear it for 60 days) and too fast
(a dead end about a frozen dependency vanishes on day 61 for no reason). The last
three rows are the contribution. Reasoning in full:
[`reference/decay.md`](skills/deadend/reference/decay.md).
## Design notes
**No boot-time code.** `cordis.patch.yml` is an empty patch, deliberately. A
plugin whose subject is "do not repeat mistakes" has no business inserting code
into the DSH process for every profile. The payload is a skill plus a CLI that the
skill runs through the visible shell tool.
**An event log, not a mutable document.** Append-only means no read-modify-write
races, clean diffs, and a preserved reason for every status change.
**`suspect` is never stored.** It is recomputed from anchors on every read. A
stored staleness flag would itself go stale — the exact bug being avoided.
**Refusal over a silent bad entry.** `record` rejects anchors that do not exist,
duplicate entries, and entries without anchors, rather than writing something
that will quietly mislead later.
## Installing as a DSH plugin
```bash
dsh plugin --profile web add github:liyixuan201211/dsh-deadend
```
This installs the skill (`skills/deadend/`), which teaches the agent to check
before it retries and record after it fails. The bundle patch adds nothing to the
boot graph; review `cordis.patch.yml`, `package.json` (no lifecycle scripts) and
`src/` if you want to verify that.
## Development
Requires Node >= 20. The sources are plain ESM JavaScript with JSDoc types, so
there is no build step and no install-time script — and the published `bin`
actually runs when installed. (They cannot be TypeScript: Node refuses to strip
types for files inside `node_modules`, which is precisely where the package lands
when it is installed or run through `npx`. The `installable` CI job guards that.)
```bash
npm test # 74 tests
npm run typecheck # tsc --noEmit over the JSDoc types
npm run check # both
```
```
src/
cli.js exit-code contract and argument parsing
engine.js record / check / verify / gc, and the verdict rules
anchors.js hashing paths, detecting decay, suggesting anchors
fingerprint.js normalising failure output into a stable signature
model.js data model and event-log replay
ledger.js locating and reading/writing .deadend/ledger.jsonl
report.js human-readable rendering
index.js programmatic API
```
## License
MIT.
Install
dsh plugin --profile web add github:liyixuan201211/dsh-deadend
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-deadend from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.