|
|
@@ -63,200 +63,59 @@ yarn demo:coding # run examples/coding-agent — the real agent (needs
|
|
|
|
|
|
## Secrets / .env
|
|
|
|
|
|
-Real-API e2e tests (`yarn test:e2e`) read `DEEPSEEK_API_KEY` (and optionally
|
|
|
-`DEEPSEEK_BASE_URL`) from the environment, or from a gitignored `.env` at the
|
|
|
-repo root loaded via Node's native `process.loadEnvFile()`:
|
|
|
+Real-API e2e tests (`yarn test:e2e`) read `DEEPSEEK_API_KEY` (and optionally `DEEPSEEK_BASE_URL`) from the environment, or from a gitignored `.env` at the repo root loaded via Node's native `process.loadEnvFile()`:
|
|
|
|
|
|
```
|
|
|
DEEPSEEK_API_KEY=sk-…
|
|
|
DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
|
|
|
```
|
|
|
|
|
|
-cordis.yml configs reference env vars with the `!!js` tag:
|
|
|
-`apiKey: !!js process.env.DEEPSEEK_API_KEY`. Never commit real credentials;
|
|
|
-CI has no secrets and e2e suites must self-skip without them.
|
|
|
+cordis.yml configs reference env vars with the `!!js` tag: `apiKey: !!js process.env.DEEPSEEK_API_KEY`. Never commit real credentials; CI has no secrets and e2e suites must self-skip without them.
|
|
|
|
|
|
-Dev/test/demo run **unbuilt** via tsx + the `paths` map in the root
|
|
|
-`tsconfig.json` (`vitest` resolves through `tsconfig.test.json`). Building is
|
|
|
-only needed for publishing/consumption outside the repo — with one exception:
|
|
|
-`yarn lint`'s type-aware rules resolve vendor packages through their built
|
|
|
-declarations (`tsconfig.typecheck.json` → `vendor/*/lib`), so run
|
|
|
-`yarn typecheck` once after a fresh clone (CI does the same) or lint reports
|
|
|
-unresolved-type `no-unsafe-*` errors.
|
|
|
+Dev/test/demo run **unbuilt** via tsx + the `paths` map in the root `tsconfig.json` (`vitest` resolves through `tsconfig.test.json`). Building is only needed for publishing/consumption outside the repo — with one exception: `yarn lint`'s type-aware rules resolve vendor packages through their built declarations (`tsconfig.typecheck.json` → `vendor/*/lib`), so run `yarn typecheck` once after a fresh clone (CI does the same) or lint reports unresolved-type `no-unsafe-*` errors.
|
|
|
|
|
|
## Conventions
|
|
|
|
|
|
-- **Package naming**: every npm package in this repo is `@deepseek-ai/dsh-<name>`
|
|
|
- (vendored packages keep their upstream names and are `private: true`).
|
|
|
-- **ESM everywhere** (`"type": "module"`); imports between workspace packages
|
|
|
- use package names, never relative paths across package boundaries.
|
|
|
- In-package imports use explicit `.ts` extensions (allowImportingTsExtensions).
|
|
|
-- **`cordis` is a peerDependency** (+ devDependency) of every harness package,
|
|
|
- mirroring upstream convention.
|
|
|
-- **Registrations are effects**: anything a plugin contributes (adapter, tool,
|
|
|
- section, agent, event listener) goes through `ctx.effect()` / `ctx.on()` so
|
|
|
- disposal and HMR work. If you write a registry, `register()` must return the
|
|
|
- disposer.
|
|
|
-- **Typed events via declaration merging**: services declare their events in
|
|
|
- `declare module 'cordis' { interface Events { … } }`, and their ctx key in
|
|
|
- `interface Context`. Extensible unions use the merge-extensible-map pattern
|
|
|
- (see `ContentBlockMap`, `MessageSourceMap`).
|
|
|
-- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)`
|
|
|
- and MUST call `next()` to delegate; returning without it short-circuits.
|
|
|
- This is the veto mechanism — use deliberately.
|
|
|
-- **Discriminated unions: match, don't chain**: branch on a tagged union
|
|
|
- (`StreamChunk`, `FinishReason`, `SessionEvent`, …) with a `switch` on the
|
|
|
- tag, not a chain of `if (x.kind === '…')`. The switch narrows each arm so
|
|
|
- member-only fields (`finish.message`, `finish.code`) are reachable in the
|
|
|
- right case and a typo'd tag fails to compile. Prefer extracting a small
|
|
|
- typed helper (`finishError(finish: FinishReason)`) over inlining the
|
|
|
- branches at the call site.
|
|
|
-- **Switch exhaustiveness**: switches over CLOSED unions (e.g. `StreamChunk`)
|
|
|
- end with `default: assertNever(value, 'context')` (from dsh-llm) so adding a
|
|
|
- variant breaks compilation at every switch that must handle it. Switches
|
|
|
- over MERGE-EXTENSIBLE unions (`SessionEventMap`, `ContentBlockMap`,
|
|
|
- `FinishReason`, …) must NOT use assertNever — plugin-added variants are
|
|
|
- valid unknown values; handle known cases and fall through `default` with a
|
|
|
- comment (the lint rule `switch-exhaustiveness-check` makes the choice
|
|
|
- explicit either way; a redundant disable directive is itself a lint error).
|
|
|
-- **Plugins, not loop changes**: new behavior goes into a plugin on the
|
|
|
- documented extension seams (see the plugin sanity checklist in
|
|
|
- docs/architecture.md). Changing `agent-loop` requires updating that doc.
|
|
|
-- **Capability seams are three packages**: when adding a swappable capability
|
|
|
- (an execution backend, a provider integration, …), split it into
|
|
|
- *interface* (abstract service + vocabulary types, e.g. `bash/`),
|
|
|
- *implementation* (a concrete subclass, e.g. `bash-local/`), and
|
|
|
- *consumer* (what the model/plugins see, e.g. `tool-bash/`). Implementations
|
|
|
- and consumers then evolve independently — a sandboxed executor replaces
|
|
|
- `bash-local` without touching tool schemas. The LLM seam follows the same
|
|
|
- shape (`llm/` is interface + consumer surface; adapters are implementations).
|
|
|
- See docs/architecture.md § "Capability seams" for when NOT to split.
|
|
|
-- **Explicit > implicit at package seams**: interface/vocabulary types spell
|
|
|
- out every field a consumer must supply — no optional field that the
|
|
|
- implementation silently fills with a hidden `?? default`. Put defaulting in
|
|
|
- the owning implementation as an explicit step (a `resolve(request): Spec`
|
|
|
- method that turns the optional-field request into the required-field spec),
|
|
|
- not smuggled inside `run()`/`start()`. Example: `dsh-bash` splits
|
|
|
- `BashExecRequest` (optional `workdir`/`timeoutMs`, model-facing) from
|
|
|
- `BashExecSpec` (required, what `run`/`start` act on); the tool layer calls
|
|
|
- `ctx.bash.resolve()` between them. The reader of a `BashExecSpec` never has
|
|
|
- to wonder where the working directory came from.
|
|
|
-- **An empty `catch` must name what it swallows and why nothing else can hit
|
|
|
- it**: a bare `catch {}` hides bugs. When you deliberately ignore a throw, the
|
|
|
- comment must (a) name the single expected failure, (b) say why ignoring it is
|
|
|
- correct — usually because the useful state was already captured *before* the
|
|
|
- `try` — and (c) make clear nothing else of consequence can reach the catch
|
|
|
- (ideally the `try` wraps a single statement). Example: the error-body
|
|
|
- `response.json()` parse in `dsh-llm-deepseek`'s adapter sets `code` + HTTP
|
|
|
- `status` from the status line before the `try`, so a malformed provider body
|
|
|
- can only cost a richer message, never the real error.
|
|
|
-- **Symmetry is usually more correct**: when two related values play parallel
|
|
|
- roles (a test fixture and its expected output, a request shape and its
|
|
|
- response shape, a buggy input and the test that checks the fix), give them
|
|
|
- parallel form — both named consts, or both inline, not one each way. Asymmetry
|
|
|
- is a smell that usually points at a missed extraction.
|
|
|
-- **Tests**: vitest, colocated under `packages/<name>/tests/*.spec.ts`. Every
|
|
|
- registry needs an HMR-safety test (dispose the contributing fiber, assert
|
|
|
- cleanup). **Excessive tests are welcome** — when in doubt, write the test;
|
|
|
- err on the side of covering edge cases, error paths, event ordering, and
|
|
|
- concurrency races even if they seem unlikely. Review findings get regression
|
|
|
- tests (see `packages/agent-loop/tests/review-fixes.spec.ts`).
|
|
|
+- **Package naming**: every npm package in this repo is `@deepseek-ai/dsh-<name>` (vendored packages keep their upstream names and are `private: true`).
|
|
|
+- **ESM everywhere** (`"type": "module"`); imports between workspace packages use package names, never relative paths across package boundaries. In-package imports use explicit `.ts` extensions (allowImportingTsExtensions).
|
|
|
+- **`cordis` is a peerDependency** (+ devDependency) of every harness package, mirroring upstream convention.
|
|
|
+- **Registrations are effects**: anything a plugin contributes (adapter, tool, section, agent, event listener) goes through `ctx.effect()` / `ctx.on()` so disposal and HMR work. If you write a registry, `register()` must return the disposer.
|
|
|
+- **Typed events via declaration merging**: services declare their events in `declare module 'cordis' { interface Events { … } }`, and their ctx key in `interface Context`. Extensible unions use the merge-extensible-map pattern (see `ContentBlockMap`, `MessageSourceMap`).
|
|
|
+- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)` and MUST call `next()` to delegate; returning without it short-circuits. This is the veto mechanism — use deliberately.
|
|
|
+- **Discriminated unions: match, don't chain**: branch on a tagged union (`StreamChunk`, `FinishReason`, `SessionEvent`, …) with a `switch` on the tag, not a chain of `if (x.kind === '…')`. The switch narrows each arm so member-only fields (`finish.message`, `finish.code`) are reachable in the right case and a typo'd tag fails to compile. Prefer extracting a small typed helper (`finishError(finish: FinishReason)`) over inlining the branches at the call site.
|
|
|
+- **Switch exhaustiveness**: switches over CLOSED unions (e.g. `StreamChunk`) end with `default: assertNever(value, 'context')` (from dsh-llm) so adding a variant breaks compilation at every switch that must handle it. Switches over MERGE-EXTENSIBLE unions (`SessionEventMap`, `ContentBlockMap`, `FinishReason`, …) must NOT use assertNever — plugin-added variants are valid unknown values; handle known cases and fall through `default` with a comment (the lint rule `switch-exhaustiveness-check` makes the choice explicit either way; a redundant disable directive is itself a lint error).
|
|
|
+- **Plugins, not loop changes**: new behavior goes into a plugin on the documented extension seams (see the plugin sanity checklist in docs/architecture.md). Changing `agent-loop` requires updating that doc.
|
|
|
+- **Capability seams are three packages**: when adding a swappable capability (an execution backend, a provider integration, …), split it into *interface* (abstract service + vocabulary types, e.g. `bash/`), *implementation* (a concrete subclass, e.g. `bash-local/`), and *consumer* (what the model/plugins see, e.g. `tool-bash/`). Implementations and consumers then evolve independently — a sandboxed executor replaces `bash-local` without touching tool schemas. The LLM seam follows the same shape (`llm/` is interface + consumer surface; adapters are implementations). See docs/architecture.md § "Capability seams" for when NOT to split.
|
|
|
+- **Explicit > implicit at package seams**: interface/vocabulary types spell out every field a consumer must supply — no optional field that the implementation silently fills with a hidden `?? default`. Put defaulting in the owning implementation as an explicit step (a `resolve(request): Spec` method that turns the optional-field request into the required-field spec), not smuggled inside `run()`/`start()`. Example: `dsh-bash` splits `BashExecRequest` (optional `workdir`/`timeoutMs`, model-facing) from `BashExecSpec` (required, what `run`/`start` act on); the tool layer calls `ctx.bash.resolve()` between them. The reader of a `BashExecSpec` never has to wonder where the working directory came from.
|
|
|
+- **An empty `catch` must name what it swallows and why nothing else can hit it**: a bare `catch {}` hides bugs. When you deliberately ignore a throw, the comment must (a) name the single expected failure, (b) say why ignoring it is correct — usually because the useful state was already captured *before* the `try` — and (c) make clear nothing else of consequence can reach the catch (ideally the `try` wraps a single statement). Example: the error-body `response.json()` parse in `dsh-llm-deepseek`'s adapter sets `code` + HTTP `status` from the status line before the `try`, so a malformed provider body can only cost a richer message, never the real error.
|
|
|
+- **Symmetry is usually more correct**: when two related values play parallel roles (a test fixture and its expected output, a request shape and its response shape, a buggy input and the test that checks the fix), give them parallel form — both named consts, or both inline, not one each way. Asymmetry is a smell that usually points at a missed extraction.
|
|
|
+- **Tests**: vitest, colocated under `packages/<name>/tests/*.spec.ts`. Every registry needs an HMR-safety test (dispose the contributing fiber, assert cleanup). **Excessive tests are welcome** — when in doubt, write the test; err on the side of covering edge cases, error paths, event ordering, and concurrency races even if they seem unlikely. Review findings get regression tests (see `packages/agent-loop/tests/review-fixes.spec.ts`).
|
|
|
|
|
|
## Defensive patterns (hard-won)
|
|
|
|
|
|
Each bullet is a bug class that bit us; the rule prevents the reoccurrence.
|
|
|
|
|
|
-- **Report orthogonal outcomes independently.** A result can be several
|
|
|
- things at once (a process can both time out AND exit 0 because it trapped
|
|
|
- the signal). Don't nest the report of one flag inside the branch of
|
|
|
- another. Surface each independent fact (`timedOut`, `signal`, `exitCode`)
|
|
|
- on its own so a caller never 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 (e.g. an adapter may report a model
|
|
|
- failure by THROWING from `stream()` *or* by ending the stream with a
|
|
|
- `finish {kind:'error'|'aborted'}` chunk), the consumer must handle both —
|
|
|
- not just the one the first implementation happened to use. A library-backed
|
|
|
- adapter that can't throw mid-stream relies on the finish-chunk path; if the
|
|
|
- loop only catches throws, a provider 401 becomes a normal completed turn.
|
|
|
- Document the contract where the type is defined and exercise every branch
|
|
|
- through the real consumer in tests.
|
|
|
-- **Async state is not synchronous state.** `agent.send()` does not flip
|
|
|
- status to `running` before it returns; 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
|
|
|
- when "done" needs a settle signal, observe the transition (saw `running`
|
|
|
- THEN `idle`) rather than counting actions you assume map 1:1 to turns —
|
|
|
- the loop batches queued messages into one turn. But a settle-signal guard
|
|
|
- cuts both ways: if the awaited transition can *never* occur (EOF with no
|
|
|
- work submitted → no turn ever starts → never `running`), it hangs forever.
|
|
|
- Always handle the "nothing to wait for" branch explicitly alongside the
|
|
|
- "wait for the work" branch.
|
|
|
-- **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 must prove disposal *waited* (pid already gone right
|
|
|
- after `await fiber.dispose()`), not merely that the process eventually dies.
|
|
|
-- **Contain callback exceptions at the boundary.** A user-supplied listener
|
|
|
- (`onTaskDone`, event handlers) 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; never let one bad subscriber break core lifecycle.
|
|
|
-- **Never hand untrusted/model output the ambient environment or predictable
|
|
|
- paths.** Spawned commands get a scrubbed env (drop `*KEY*`/`*SECRET*`/
|
|
|
- `*TOKEN*`) so the harness's own credentials can't leak into output, `env`,
|
|
|
- or spill files. Temp/spill files use a private (0700) dir, random names,
|
|
|
- and exclusive owner-only (`'wx'`, `0o600`) opens — predictable
|
|
|
- world-readable paths invite symlink races and disclosure.
|
|
|
-- **e2e tests own their resources.** Real-API/integration tests must create
|
|
|
- the harness in the test and dispose it in `afterEach` (even on
|
|
|
- failure/retry/timeout), so a flaky run doesn't leak processes or contexts.
|
|
|
- Shared fixtures live in a plain `tests/harness.ts` module, NOT another
|
|
|
- `*.e2e.ts` file — importing a spec file re-registers its `describe` and
|
|
|
- duplicates real API calls. Verify the WORLD, not the agent's self-report:
|
|
|
- re-run the command/check externally and assert files are byte-identical
|
|
|
- where they should be unchanged (a keyword probe lets a cheating agent pass).
|
|
|
-- **Tag spelling and EOF hygiene.** cordis.yml interpolates env via the
|
|
|
- `!!js` tag (js-yaml resolves custom tags under `tag:yaml.org,2002:js`), not
|
|
|
- `!js` — keep code, comments, and docs consistent. Files end with exactly
|
|
|
- one trailing newline; `git diff --check` (a pre-push gate) rejects new
|
|
|
- blank lines at EOF.
|
|
|
+- **Report orthogonal outcomes independently.** A result can be several things at once (a process can both time out AND exit 0 because it trapped the signal). Don't nest the report of one flag inside the branch of another. Surface each independent fact (`timedOut`, `signal`, `exitCode`) on its own so a caller never 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 (e.g. an adapter may report a model failure by THROWING from `stream()` *or* by ending the stream with a `finish {kind:'error'|'aborted'}` chunk), the consumer must handle both — not just the one the first implementation happened to use. A library-backed adapter that can't throw mid-stream relies on the finish-chunk path; if the loop only catches throws, a provider 401 becomes a normal completed turn. Document the contract where the type is defined and exercise every branch through the real consumer in tests.
|
|
|
+- **Async state is not synchronous state.** `agent.send()` does not flip status to `running` before it returns; 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 when "done" needs a settle signal, observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns — the loop batches queued messages into one turn. But a settle-signal guard cuts both ways: if the awaited transition can *never* occur (EOF with no work submitted → no turn ever starts → never `running`), it hangs forever. Always handle the "nothing to wait for" branch explicitly alongside the "wait for the work" branch.
|
|
|
+- **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 must prove disposal *waited* (pid already gone right after `await fiber.dispose()`), not merely that the process eventually dies.
|
|
|
+- **Contain callback exceptions at the boundary.** A user-supplied listener (`onTaskDone`, event handlers) 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; never let one bad subscriber break core lifecycle.
|
|
|
+- **Never hand untrusted/model output the ambient environment or predictable paths.** Spawned commands get a scrubbed env (drop `*KEY*`/`*SECRET*`/ `*TOKEN*`) so the harness's own credentials can't leak into output, `env`, or spill files. Temp/spill files use a private (0700) dir, random names, and exclusive owner-only (`'wx'`, `0o600`) opens — predictable world-readable paths invite symlink races and disclosure.
|
|
|
+- **e2e tests own their resources.** Real-API/integration tests must create the harness in the test and dispose it in `afterEach` (even on failure/retry/timeout), so a flaky run doesn't leak processes or contexts. Shared fixtures live in a plain `tests/harness.ts` module, NOT another `*.e2e.ts` file — importing a spec file re-registers its `describe` and duplicates real API calls. Verify the WORLD, not the agent's self-report: re-run the command/check externally and assert files are byte-identical where they should be unchanged (a keyword probe lets a cheating agent pass).
|
|
|
+- **Tag spelling and EOF hygiene.** cordis.yml interpolates env via the `!!js` tag (js-yaml resolves custom tags under `tag:yaml.org,2002:js`), not `!js` — keep code, comments, and docs consistent. Files end with exactly one trailing newline; `git diff --check` (a pre-push gate) rejects new blank lines at EOF.
|
|
|
|
|
|
## Type Safety and Documentation
|
|
|
|
|
|
-This codebase aims to be **very type-safe and well documented** for
|
|
|
-maintainability. Code that fails to compile under `strict: true` (with
|
|
|
-`noImplicitAny` enabled for all `packages/*` source) is not acceptable. Every
|
|
|
-`any` that remains must have a specific justification (a comment explaining why
|
|
|
-a narrower type is infeasible).
|
|
|
+This codebase aims to be **very type-safe and well documented** for maintainability. Code that fails to compile under `strict: true` (with `noImplicitAny` enabled for all `packages/*` source) is not acceptable. Every `any` that remains must have a specific justification (a comment explaining why a narrower type is infeasible).
|
|
|
|
|
|
-In the **core** packages (`packages/llm`, `packages/tools`, `packages/agent`,
|
|
|
-`packages/agent-loop`, `packages/session`, `packages/system-prompt`), **type
|
|
|
-gymnastics are acceptable when they improve the DX of plugin authors** for
|
|
|
-common plugin types. The `defineTool` typed schema DSL in `dsh-tools` is the
|
|
|
-canonical example: the `SchemaSpec` to `InferArgs<S>` type-level mapping gives
|
|
|
-tool authors zero-cast typed `execute` args, and the cost of the conditional
|
|
|
-types stays inside the core package.
|
|
|
+In the **core** packages (`packages/llm`, `packages/tools`, `packages/agent`, `packages/agent-loop`, `packages/session`, `packages/system-prompt`), **type gymnastics are acceptable when they improve the DX of plugin authors** for common plugin types. The `defineTool` typed schema DSL in `dsh-tools` is the canonical example: the `SchemaSpec` to `InferArgs<S>` type-level mapping gives tool authors zero-cast typed `execute` args, and the cost of the conditional types stays inside the core package.
|
|
|
|
|
|
-Verbose documentation is fine **as long as docs and code stay strictly in
|
|
|
-sync**. Out-of-sync docs are worse than no docs. **When you change code, update
|
|
|
-its docs in the SAME change** — grep the package README and the module/JSDoc
|
|
|
-comments for the old behavior (config keys, defaults, error codes, wire field
|
|
|
-names, event names) and fix every hit. CI has no doc-sync gate, so this is on
|
|
|
-the author. Every module has a module-level doc comment explaining its role.
|
|
|
-Every exported class, interface, type, function, and non-obvious method has a
|
|
|
-JSDoc that explains semantics (not just the name) — contracts (what events fire
|
|
|
-when), disposal behavior, error behavior, and extension intent. Internal
|
|
|
-helpers get docs only where non-obvious. Prefer one-liners when one line
|
|
|
-suffices.
|
|
|
+Verbose documentation is fine **as long as docs and code stay strictly in sync**. Out-of-sync docs are worse than no docs. **When you change code, update its docs in the SAME change** — grep the package README and the module/JSDoc comments for the old behavior (config keys, defaults, error codes, wire field names, event names) and fix every hit. CI has no doc-sync gate, so this is on the author. Every module has a module-level doc comment explaining its role. Every exported class, interface, type, function, and non-obvious method has a JSDoc that explains semantics (not just the name) — contracts (what events fire when), disposal behavior, error behavior, and extension intent. Internal helpers get docs only where non-obvious. Prefer one-liners when one line suffices.
|
|
|
|
|
|
-**Editing these instructions**: `AGENTS.md` is the real file; `CLAUDE.md` is a
|
|
|
-symlink to it (at the repo root and in `packages/`). Always edit `AGENTS.md` —
|
|
|
-never write through the `CLAUDE.md` symlink or replace it with a regular file.
|
|
|
+**Markdown is not hard-wrapped**: write one line per paragraph and let the editor soft-wrap. Hard line breaks mid-paragraph make docs harder to edit and diff — a one-word change reflows and re-diffs the whole paragraph. This applies to prose only: leave fenced code blocks, tables, and list structure intact (a wrapped list item folds to one line per bullet). Code comments / JSDoc are exempt — they stay under the linter's column limit.
|
|
|
+
|
|
|
+**Editing these instructions**: `AGENTS.md` is the real file; `CLAUDE.md` is a symlink to it (at the repo root and in `packages/`). Always edit `AGENTS.md` — never write through the `CLAUDE.md` symlink or replace it with a regular file.
|
|
|
|
|
|
## Vendoring Policy
|
|
|
|
|
|
-`vendor/` packages are pinned source copies (manifest with upstream commit
|
|
|
-SHAs in [vendor/README.md](vendor/README.md)). To update one, follow the sync
|
|
|
-procedure there; re-apply (or retire) the logged local modifications and rerun
|
|
|
-`yarn test && yarn build`.
|
|
|
+`vendor/` packages are pinned source copies (manifest with upstream commit SHAs in [vendor/README.md](vendor/README.md)). To update one, follow the sync procedure there; re-apply (or retire) the logged local modifications and rerun `yarn test && yarn build`.
|