Files
deepseek-harness/docs/defensive-patterns.md
Tianyi Cui 7702a33531 docs(AGENTS): rewrite the root standing orders to the 1,500-word budget
Applies the documentation standard to its biggest offender. Every rule
survives as one to three lines plus a link to its durable home; the
stories, duplicate statements, and re-narrations go:

- Situational clusters evict to new homes: docs/testing.md (tiers,
  with-key policy, real-over-mock, world-verification, real-entry-path
  guards), docs/defensive-patterns.md (the bug-class rules), and
  docs/cookbook/responding-to-pr-review-on-a-stack.md (the stacked-PR
  review procedure).
- Doc-authoring rules consolidate into docs/AGENTS.md § Writing rules
  (current-state-never-history, md-wrap, ts-block compilation, @mode,
  catalog same-change, pair same-change).
- packages/README.md drops to the group table + the extension-vs-bundle
  dependency rule; the hand ASCII graph yields to the generated
  module-graph.md; group READMEs are the canonical per-package map.
- packages/AGENTS.md keeps only its packages-specific rules (export
  shape, ctx.get, real-Loader coverage); examples/AGENTS.md repoints
  its with-key-policy link; rfc/README.md loses a narrated-history
  aside; dsh-code-review / dsh-find-simplifications / verify-md-wrap
  references follow the moved content.
- Budget manifest ratchets: AGENTS.md 8200 -> 1500 (now 1,495 words),
  packages/README.md 1900 -> 600, packages/AGENTS.md 600 -> 450; the
  two new eviction docs join the budget set (testing 800, defensive
  550); docs/AGENTS.md raises 1000 -> 1250 for the absorbed writing
  rules (the one justified increase). The doc-tiers RFC's deferred list
  prunes the two items this change ships.
2026-07-04 14:22:47 +08:00

3.0 KiB

Defensive patterns

Hard-won bug-class rules: each pattern below is a class of defect that actually shipped or nearly shipped here, stated as the rule that prevents its recurrence. Read this before writing lifecycle, concurrency, subprocess, or teardown code. Test-tier counterparts (real entry path, world-verification, resource ownership) are in testing.md.

Report orthogonal outcomes independently

A result can be several things at once — a process can time out AND exit 0 because it trapped the signal. Surface each independent fact (timedOut, signal, exitCode) on its own; never nest one flag's report inside another's branch, or a caller reads a cut-short run as a clean success.

Honor cross-seam contracts on BOTH sides

When an interface documents two valid ways to signal something — an adapter may report failure by THROWING from stream() or by ending the stream with a finish {kind:'error'|'aborted'} chunk — the consumer handles both, not just the one the first implementation used. A library-backed adapter that can't throw mid-stream relies on the in-band path; a loop that only catches throws turns a provider 401 into a normal completed turn. Document the contract where the type is defined; exercise every branch through the real consumer.

Async state is not synchronous state

agent.send() does not flip status before returning; a background task's completion races turn boundaries; reader.close() fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (agent/status, task.done), and observe the transition (saw running THEN idle) rather than counting actions you assume map 1:1 to turns (the loop batches queued messages). The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never running), the wait hangs — handle the "nothing to wait for" branch explicitly.

Dispose must reach quiescence, not just request it

A teardown that issues kills/aborts but returns before the work stops leaves orphans. Make cleanup async and await the children's exit (kill → await done), and close listener/notification registries BEFORE killing so late completions stay silent. Tests prove disposal waited (pid gone right after await fiber.dispose()), not merely that the process eventually dies.

Contain callback exceptions at the boundary

A user-supplied listener that throws must not reject the promise it runs inside or starve the listeners after it. Wrap the dispatch loop in try/catch and log; one bad subscriber never breaks core lifecycle.

Never hand untrusted output the ambient environment or predictable paths

Spawned commands get a scrubbed env (drop *KEY*/*SECRET*/*TOKEN*) so harness credentials cannot leak into output, env, or spill files. Temp/spill files use a private (0700) dir, random names, and exclusive owner-only opens ('wx', 0o600) — predictable world-readable paths invite symlink races and disclosure.