feat: migrate to pnpm

This commit is contained in:
07akioni
2026-06-16 14:55:37 +08:00
parent 30765bd6d5
commit dabc2ff411
46 changed files with 4697 additions and 5064 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ This is a where-to-look map, not a rules list. The rules live in the docs below
## Where to look first (review-specific, not in the docs) ## Where to look first (review-specific, not in the docs)
1. **Docs in sync?** If the PR changes a config key, default, error code, wire field, or event name, did it update the package README + module/JSDoc in the same diff? Stale docs are the most common miss — `yarn doc-sync` only gates compilable `ts` blocks and the event-taxonomy table, so prose drift (config keys, defaults, error codes, wire fields) has no gate and is on the reviewer to catch. 1. **Docs in sync?** If the PR changes a config key, default, error code, wire field, or event name, did it update the package README + module/JSDoc in the same diff? Stale docs are the most common miss — `pnpm run doc-sync` only gates compilable `ts` blocks and the event-taxonomy table, so prose drift (config keys, defaults, error codes, wire fields) has no gate and is on the reviewer to catch.
2. **HMR-safety test present?** Any new registry/registration needs a test that disposes the contributing fiber and asserts cleanup. Its absence is a blocking gap. 2. **HMR-safety test present?** Any new registry/registration needs a test that disposes the contributing fiber and asserts cleanup. Its absence is a blocking gap.
3. **Gates green?** typecheck, lint, test, test:coverage (100% per-file on `packages/*/src`), knip, build, publint, constraints. Don't re-review what a gate already enforces — trust the gate, spend attention on what gates can't check (intent, contracts, doc sync). 3. **Gates green?** typecheck, lint, test, test:coverage (100% per-file on `packages/*/src`), knip, build, publint, constraints. Don't re-review what a gate already enforces — trust the gate, spend attention on what gates can't check (intent, contracts, doc sync).
4. **e2e verifies the world, not the agent's self-report.** For real-API tests, confirm the assertion re-runs the command/checks the file externally — a keyword probe lets a cheating agent pass (see AGENTS.md e2e bullet). 4. **e2e verifies the world, not the agent's self-report.** For real-API tests, confirm the assertion re-runs the command/checks the file externally — a keyword probe lets a cheating agent pass (see AGENTS.md e2e bullet).
+10 -10
View File
@@ -24,24 +24,24 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Enable corepack (yarn 4) - name: Enable corepack (pnpm)
run: corepack enable run: corepack enable
- name: Install (immutable) - name: Install (immutable)
run: yarn install --immutable run: pnpm install --frozen-lockfile
- name: Constraints - name: Constraints
run: yarn constraints run: pnpm run constraints
# Before lint: the type-aware ESLint config resolves vendor packages via # Before lint: the type-aware ESLint config resolves vendor packages via
# their built declarations (tsconfig.typecheck.json -> vendor/*/lib), # their built declarations (tsconfig.typecheck.json -> vendor/*/lib),
# which `yarn typecheck` emits. Lint on a fresh checkout would otherwise # which `pnpm run typecheck` emits. Lint on a fresh checkout would otherwise
# see unresolved types and erupt with no-unsafe-* errors. # see unresolved types and erupt with no-unsafe-* errors.
- name: Typecheck (src + tests + examples) - name: Typecheck (src + tests + examples)
run: yarn typecheck run: pnpm run typecheck
- name: Lint - name: Lint
run: yarn lint run: pnpm run lint
# Doc-sync gates (RFC 006). doc-typecheck compiles the fenced ts blocks in # Doc-sync gates (RFC 006). doc-typecheck compiles the fenced ts blocks in
# the docs and resolves vendor packages via their built declarations, which # the docs and resolves vendor packages via their built declarations, which
@@ -49,18 +49,18 @@ jobs:
# taxonomy check only reads source. Same `doc-sync` script the pre-push # taxonomy check only reads source. Same `doc-sync` script the pre-push
# hook runs (ADR 0007: one source of truth). # hook runs (ADR 0007: one source of truth).
- name: Doc-sync gates (doc code blocks + event taxonomy) - name: Doc-sync gates (doc code blocks + event taxonomy)
run: yarn doc-sync run: pnpm run doc-sync
- name: Tests with coverage gate (per-file 100%) - name: Tests with coverage gate (per-file 100%)
run: yarn test:coverage run: pnpm run test:coverage
# Before hygiene: publint validates the packed artifacts (lib/index.js), # Before hygiene: publint validates the packed artifacts (lib/index.js),
# which only the tsdown bundling step emits. # which only the tsdown bundling step emits.
- name: Build (tsc -b + tsdown bundles) - name: Build (tsc -b + tsdown bundles)
run: yarn build run: pnpm run build
- name: Hygiene (knip + publint) - name: Hygiene (knip + publint)
run: yarn knip && yarn publint run: pnpm run knip && pnpm run publint
- name: Demo smoke test - name: Demo smoke test
run: | run: |
+2 -2
View File
@@ -3,8 +3,8 @@ CLAUDE.local.md
node_modules/ node_modules/
lib/ lib/
*.tsbuildinfo *.tsbuildinfo
.yarn/ pnpm-debug.log
yarn-error.log .pnpm-store/
examples/*/*.jsonl examples/*/*.jsonl
coverage/ coverage/
.doc-typecheck-*/ .doc-typecheck-*/
-1
View File
@@ -1 +0,0 @@
nodeLinker: node-modules
+21 -21
View File
@@ -36,7 +36,7 @@ packages/ Harness packages, all named @deepseek-ai/dsh-<name>:
examples/ Runnable demos (not workspaces). echo-agent = mock model + echo examples/ Runnable demos (not workspaces). echo-agent = mock model + echo
tool + stdio UI + JSONL persistence, wired via cordis.yml. tool + stdio UI + JSONL persistence, wired via cordis.yml.
coding-agent = the real thing: DeepSeek V4 + bash tools coding-agent = the real thing: DeepSeek V4 + bash tools
(yarn demo:coding, needs DEEPSEEK_API_KEY). (pnpm run demo:coding, needs DEEPSEEK_API_KEY).
docs/ architecture.md — the design doc. adr/ — decision records (the docs/ architecture.md — the design doc. adr/ — decision records (the
why behind vendoring, event-sourcing, the schema DSL, …). why behind vendoring, event-sourcing, the schema DSL, …).
rfc/ — proposals for substantial future work. rfc/ — proposals for substantial future work.
@@ -50,33 +50,33 @@ scripts/ repo maintenance scripts (vendor-manifest guard, publint runner).
## Commands ## Commands
```sh ```sh
yarn install # Yarn 4 workspaces (node-modules linker), node >= 24 pnpm install # pnpm workspaces, node >= 24
yarn test # vitest run (packages/*/tests/**/*.spec.ts) pnpm run test # vitest run (packages/*/tests/**/*.spec.ts)
yarn test:coverage # vitest run --coverage (per-file 100% gate on packages/*/src) pnpm run test:coverage # vitest run --coverage (per-file 100% gate on packages/*/src)
yarn test:e2e # real-API tests (packages|examples/*/tests/**/*.e2e.ts); pnpm run test:e2e # real-API tests (packages|examples/*/tests/**/*.e2e.ts);
# self-skips without DEEPSEEK_API_KEY — see Secrets below # self-skips without DEEPSEEK_API_KEY — see Secrets below
yarn typecheck # tsc -b tsconfig.build.json (declarations) + tsc -p pnpm run typecheck # tsc -b tsconfig.build.json (declarations) + tsc -p
# tsconfig.typecheck.json (tests/examples typecheck too) # tsconfig.typecheck.json (tests/examples typecheck too)
yarn lint # eslint . pnpm run lint # eslint .
yarn lint:fix # eslint . --fix pnpm run lint:fix # eslint . --fix
yarn build # tsc -b tsconfig.build.json && tsdown (JS bundles into lib/) pnpm run build # tsc -b tsconfig.build.json && tsdown (JS bundles into lib/)
yarn knip # dead-code / unused-dependency check pnpm run knip # dead-code / unused-dependency check
yarn publint # package.json publish-correctness check (publishable packages/*) pnpm run publint # package.json publish-correctness check (publishable packages/*)
yarn hygiene # knip + publint + yarn constraints pnpm run hygiene # knip + publint + workspace constraints
yarn doc-typecheck # typecheck every ```ts block in README.md, docs/**/*.md, pnpm run doc-typecheck # typecheck every ```ts block in README.md, docs/**/*.md,
# packages/*/README.md (doc/code drift gate) # packages/*/README.md (doc/code drift gate)
yarn verify-event-taxonomy # assert the event-taxonomy table in docs/architecture.md pnpm run verify-event-taxonomy # assert the event-taxonomy table in docs/architecture.md
# matches the interface Events declarations in source # matches the interface Events declarations in source
yarn doc-sync # doc-typecheck + verify-event-taxonomy (CI runs this) pnpm run doc-sync # doc-typecheck + verify-event-taxonomy (CI runs this)
yarn demo:echo # run examples/echo-agent (no API key; type "echo hi" to pnpm run demo:echo # run examples/echo-agent (no API key; type "echo hi" to
# see a tool call) — the mock skeleton # see a tool call) — the mock skeleton
yarn demo:coding # run examples/coding-agent — the real agent (needs pnpm run demo:coding # run examples/coding-agent — the real agent (needs
# DEEPSEEK_API_KEY; give it a coding task) # DEEPSEEK_API_KEY; give it a coding task)
``` ```
## Secrets / .env ## 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 (`pnpm run 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_API_KEY=sk-…
@@ -85,7 +85,7 @@ 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: `pnpm run lint`'s type-aware rules resolve vendor packages through their built declarations (`tsconfig.typecheck.json``vendor/*/lib`), so run `pnpm run typecheck` once after a fresh clone (CI does the same) or lint reports unresolved-type `no-unsafe-*` errors.
## Conventions ## Conventions
@@ -123,7 +123,7 @@ This codebase aims to be **very type-safe and well documented** for maintainabil
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 runs `yarn doc-sync` (`doc-typecheck` + `verify-event-taxonomy`), which typechecks every fenced `ts` block in `README.md`, `docs/**/*.md`, and `packages/*/README.md` and verifies the event-taxonomy table against source — but that scope does NOT cover `AGENTS.md`, `packages/AGENTS.md`, or `packages/README.md`, nor does it catch prose drift (config keys, defaults, error codes), so keeping those in sync remains 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 runs `pnpm run doc-sync` (`doc-typecheck` + `verify-event-taxonomy`), which typechecks every fenced `ts` block in `README.md`, `docs/**/*.md`, and `packages/*/README.md` and verifies the event-taxonomy table against source — but that scope does NOT cover `AGENTS.md`, `packages/AGENTS.md`, or `packages/README.md`, nor does it catch prose drift (config keys, defaults, error codes), so keeping those in sync remains 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.
**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. **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.
@@ -131,4 +131,4 @@ Verbose documentation is fine **as long as docs and code stay strictly in sync**
## Vendoring Policy ## 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 `pnpm run test && pnpm run build`.
+4 -4
View File
@@ -11,10 +11,10 @@ Monorepo for the DeepSeek Harness group.
This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) framework (vendored as source under `vendor/`), microkernel-style: everything is a plugin. This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) framework (vendored as source under `vendor/`), microkernel-style: everything is a plugin.
```sh ```sh
yarn install pnpm install
yarn test # vitest pnpm run test # vitest
yarn demo:echo # runnable echo-agent example (no API key needed) pnpm run demo:echo # runnable echo-agent example (no API key needed)
yarn demo:coding # the real DeepSeek coding agent (needs DEEPSEEK_API_KEY) pnpm run demo:coding # the real DeepSeek coding agent (needs DEEPSEEK_API_KEY)
``` ```
For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/). For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/).
+1 -1
View File
@@ -13,7 +13,7 @@ Every AGENTS.md promise gets a command that exits non-zero, wired into git hooks
- Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); tests and examples typecheck in CI via `tsconfig.typecheck.json` (vendored packages resolve as built declarations). - Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); tests and examples typecheck in CI via `tsconfig.typecheck.json` (vendored packages resolve as built declarations).
- ESLint strict-type-checked + @stylistic (the house style, enforced); vendored code excluded. - ESLint strict-type-checked + @stylistic (the house style, enforced); vendored code excluded.
- Per-file 100% coverage on `packages/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion. - Per-file 100% coverage on `packages/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion.
- knip (dead code/deps), publint (package correctness), yarn constraints (workspace rules: private, cordis peer+dev, uniform version, ESM). - knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM).
- lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and pre-push (tests, hygiene); CI runs the full matrix on node 24/26 plus a demo smoke test driving the echo-agent end to end. - lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and pre-push (tests, hygiene); CI runs the full matrix on node 24/26 plus a demo smoke test driving the echo-agent end to end.
## Consequences ## Consequences
+3 -3
View File
@@ -6,16 +6,16 @@ Status: accepted (2026-06-11)
The initial build used **dumble**, the cordiverse zero-config esbuild wrapper that upstream Cordis itself builds with — maximum alignment with the vendored packages' conventions (it reads each package.json and infers entries/formats from the `exports` field). But dumble is a liability as a load-bearing tool in this repo: v0.2.x, ~530 npm downloads/week, effectively one maintainer, and we were invoking it through a custom orchestration script (`scripts/build.ts`) because it has no workspace mode. The initial build used **dumble**, the cordiverse zero-config esbuild wrapper that upstream Cordis itself builds with — maximum alignment with the vendored packages' conventions (it reads each package.json and infers entries/formats from the `exports` field). But dumble is a liability as a load-bearing tool in this repo: v0.2.x, ~530 npm downloads/week, effectively one maintainer, and we were invoking it through a custom orchestration script (`scripts/build.ts`) because it has no workspace mode.
Build output currently matters only for `yarn build` + publint (nothing publishes yet; dev/test/demo run unbuilt via tsx), so the switching cost is at its lowest now and only grows once packages publish. Build output currently matters only for `pnpm run build` + publint (nothing publishes yet; dev/test/demo run unbuilt via tsx), so the switching cost is at its lowest now and only grows once packages publish.
## Decision ## Decision
Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-backed, actively released): Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-backed, actively released):
- Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*']` (explicit globs, not `workspace: true`, which would also pick up `examples/*` — they have package.json files but are not yarn workspaces). - Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*']` (explicit globs, not `workspace: true`, which would also pick up `examples/*` — they have package.json files but are not pnpm workspaces).
- Shared shape: entry `src/index.ts`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ holds tsc's .d.ts output). - Shared shape: entry `src/index.ts`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ holds tsc's .d.ts output).
- Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape). - Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape).
- `scripts/build.ts` deleted; `yarn build` = `tsc -b && tsdown`. - `scripts/build.ts` deleted; `pnpm run build` = `tsc -b && tsdown`.
Alternatives considered: **direct esbuild script** (most established engine, zero wrapper risk, but hand-maintains the per-package spec table tsdown's workspace mode gives us); **pkgroll** (closest drop-in philosophically, but 78k dl/wk and Rollup-based — strictly weaker maintenance story than tsdown); **keep dumble** (perfect upstream alignment, unacceptable bus factor). Alternatives considered: **direct esbuild script** (most established engine, zero wrapper risk, but hand-maintains the per-package spec table tsdown's workspace mode gives us); **pkgroll** (closest drop-in philosophically, but 78k dl/wk and Rollup-based — strictly weaker maintenance story than tsdown); **keep dumble** (perfect upstream alignment, unacceptable bus factor).
+1 -1
View File
@@ -13,7 +13,7 @@ Two gates, mirroring the existing `scripts/` style (tsx ESM, one job each):
1. **`doc-typecheck`** extracts every fenced ` ```ts ` block from `README.md`, `docs/**`, and `packages/*/README.md`, writes them to a temp project, and compiles with `tsc --noEmit`. The temp tsconfig copies only resolution-relevant options and the workspace `paths` map from `tsconfig.typecheck.json` (vendor → built `lib`, harness → `src`) — resolving vendor to `lib` is essential, or tsc type-checks raw vendor source and floods the run. A block that is a deliberate sketch opts out with an explicit ` ```ts ignore-check ` info string; the script reports the opt-out ratio and fails if it exceeds half, so the escape hatch can't quietly become the norm. 1. **`doc-typecheck`** extracts every fenced ` ```ts ` block from `README.md`, `docs/**`, and `packages/*/README.md`, writes them to a temp project, and compiles with `tsc --noEmit`. The temp tsconfig copies only resolution-relevant options and the workspace `paths` map from `tsconfig.typecheck.json` (vendor → built `lib`, harness → `src`) — resolving vendor to `lib` is essential, or tsc type-checks raw vendor source and floods the run. A block that is a deliberate sketch opts out with an explicit ` ```ts ignore-check ` info string; the script reports the opt-out ratio and fails if it exceeds half, so the escape hatch can't quietly become the norm.
2. **`verify-event-taxonomy`** extracts the event names from the `interface Events` blocks across `packages/*/src` and from the taxonomy table in `docs/architecture.md`, and asserts the two sets match exactly. Verify, don't generate: the table keeps its hand-written Mode/Purpose columns; only the set of names is checked. (Landing this surfaced three events the table had been missing — `tools/change`, `llm/adapter-change`, `system-prompt/change`.) 2. **`verify-event-taxonomy`** extracts the event names from the `interface Events` blocks across `packages/*/src` and from the taxonomy table in `docs/architecture.md`, and asserts the two sets match exactly. Verify, don't generate: the table keeps its hand-written Mode/Purpose columns; only the set of names is checked. (Landing this surfaced three events the table had been missing — `tools/change`, `llm/adapter-change`, `system-prompt/change`.)
Both run via a shared `doc-sync` package.json script that the lefthook pre-push hook and CI both invoke (ADR 0007: hooks and CI call the same scripts, so the gate fires locally before a push — not only after it). They run after `yarn typecheck` (which emits the vendor `lib/` that doc-typecheck resolves against). API-extractor golden reports (RFC 006 part 3) were deliberately **deferred** — low value for an internal monorepo where reviewers already see the source diff, and a heavy, finicky dependency. Both run via a shared `doc-sync` package.json script that the lefthook pre-push hook and CI both invoke (ADR 0007: hooks and CI call the same scripts, so the gate fires locally before a push — not only after it). They run after `pnpm run typecheck` (which emits the vendor `lib/` that doc-typecheck resolves against). API-extractor golden reports (RFC 006 part 3) were deliberately **deferred** — low value for an internal monorepo where reviewers already see the source diff, and a heavy, finicky dependency.
## Consequences ## Consequences
+5 -5
View File
@@ -15,7 +15,7 @@ packages/<name>/
README.md # service API, events, extension points, design notes README.md # service API, events, extension points, design notes
``` ```
package.json invariants (enforced by `yarn constraints` / yarn.config.cjs): `private: true`, `version: 0.0.1`, `type: module`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop.
## 2. Register it in the root configs ## 2. Register it in the root configs
@@ -36,10 +36,10 @@ For a swappable capability, split interface / implementation / consumer into sep
## 4. Verify ## 4. Verify
```sh ```sh
yarn install # registers the workspace pnpm install # registers the workspace
yarn constraints && yarn typecheck && yarn lint pnpm run constraints && pnpm run typecheck && pnpm run lint
yarn test:coverage # 100% per-file over src (types.ts exempt) pnpm run test:coverage # 100% per-file over src (types.ts exempt)
yarn build && yarn knip && yarn publint pnpm run build && pnpm run knip && pnpm run publint
``` ```
Test expectations: every registry/registration needs an HMR-safety test (register from a child fiber, dispose it, assert cleanup). Excessive tests are welcome — see AGENTS.md. Test expectations: every registry/registration needs an HMR-safety test (register from a child fiber, dispose it, assert cleanup). Excessive tests are welcome — see AGENTS.md.
+4 -4
View File
@@ -48,9 +48,9 @@ Covered automatically by globs — no edits needed: root `package.json` workspac
## 4. Verify ## 4. Verify
```sh ```sh
yarn install # registers the workspace pnpm install # registers the workspace
yarn typecheck # the base→lib path split means: run once after a fresh add pnpm run typecheck # the base→lib path split means: run once after a fresh add
yarn build && yarn test && yarn constraints pnpm run build && pnpm run test && pnpm run constraints
``` ```
Note the `tsconfig` two-map split (called out in [AGENTS.md](../../AGENTS.md) § Secrets/.env): `lint`'s type-aware rules resolve vendored packages through their built `lib/` declarations, so run `yarn typecheck` (which builds them) once after adding the package or lint reports unresolved-type errors. Note the `tsconfig` two-map split (called out in [AGENTS.md](../../AGENTS.md) § Secrets/.env): `lint`'s type-aware rules resolve vendored packages through their built `lib/` declarations, so run `pnpm run typecheck` (which builds them) once after adding the package or lint reports unresolved-type errors.
+1 -1
View File
@@ -39,5 +39,5 @@ Split the adapter into testable stages (llm-deepseek's layout): wire types (`typ
- **Unit: mock the provider, not the harness.** A scripted `node:http` server speaking the provider's wire format covers happy paths, every error status, malformed payloads, premature closes, and aborts — no network, and it drives the 100% per-file coverage gate. Works for SDK-backed adapters too (point the SDK's baseURL at the mock). - **Unit: mock the provider, not the harness.** A scripted `node:http` server speaking the provider's wire format covers happy paths, every error status, malformed payloads, premature closes, and aborts — no network, and it drives the 100% per-file coverage gate. Works for SDK-backed adapters too (point the SDK's baseURL at the mock).
- **Hostile framing tests.** Split stream payloads at arbitrary byte positions (including mid-UTF-8) — real networks do. - **Hostile framing tests.** Split stream payloads at arbitrary byte positions (including mid-UTF-8) — real networks do.
- **E2E: `tests/*.e2e.ts`** under `yarn test:e2e`, gated with `describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green. Cover each model × each provider mode you map (thinking on/off, effort levels), a tool-call round trip INCLUDING the follow-up turn with results in history, and loose assertions only (substring/structure, bounded maxTokens — real models are nondeterministic). - **E2E: `tests/*.e2e.ts`** under `pnpm run test:e2e`, gated with `describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green. Cover each model × each provider mode you map (thinking on/off, effort levels), a tool-call round trip INCLUDING the follow-up turn with results in history, and loose assertions only (substring/structure, bounded maxTokens — real models are nondeterministic).
- Register the e2e file pattern in `knip.json` (per-workspace `entry` override) or knip flags it unused. - Register the e2e file pattern in `knip.json` (per-workspace `entry` override) or knip flags it unused.
+1 -1
View File
@@ -55,4 +55,4 @@ export function apply(ctx: Context) {
## Runnable wirings ## Runnable wirings
Two complete examples load their plugin trees from `cordis.yml` with HMR: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `yarn demo:echo`) and [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite — the real thing, `yarn demo:coding`). Two complete examples load their plugin trees from `cordis.yml` with HMR: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`) and [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite — the real thing, `pnpm run demo:coding`).
+34 -34
View File
@@ -5,7 +5,7 @@ This guide covers the local setup needed to work on DeepSeek Harness and underst
## Prerequisites ## Prerequisites
- Node.js 24 or newer. The repo declares `node >=24`; CI runs the matrix on Node 24 and 26. - Node.js 24 or newer. The repo declares `node >=24`; CI runs the matrix on Node 24 and 26.
- Corepack-enabled Yarn. The repo pins `yarn@4.14.1` in `package.json`; run `corepack enable` if `yarn --version` does not resolve through Corepack. - Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.
- Git. - Git.
- Optional: a DeepSeek API key for the coding-agent demo and real-API e2e tests. - Optional: a DeepSeek API key for the coding-agent demo and real-API e2e tests.
@@ -14,32 +14,32 @@ This guide covers the local setup needed to work on DeepSeek Harness and underst
Install dependencies from the repo root: Install dependencies from the repo root:
```sh ```sh
yarn install pnpm install
``` ```
Yarn uses the `node-modules` linker in this repo. The install also runs the root `postinstall` script, which installs lefthook from the repo dev dependency. The install also runs the root `postinstall` script, which installs lefthook from the repo dev dependency.
If hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually: If hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:
```sh ```sh
yarn lefthook install pnpm exec lefthook install
``` ```
Run typecheck once after a fresh clone: Run typecheck once after a fresh clone:
```sh ```sh
yarn typecheck pnpm run typecheck
``` ```
That first typecheck builds declaration output used by type-aware linting for vendored packages. Without it, `yarn lint` can report unresolved-type `no-unsafe-*` errors even when source code is fine. That first typecheck builds declaration output used by type-aware linting for vendored packages. Without it, `pnpm run lint` can report unresolved-type `no-unsafe-*` errors even when source code is fine.
If you are preparing to push from a fresh clone or worktree, also build once: If you are preparing to push from a fresh clone or worktree, also build once:
```sh ```sh
yarn build pnpm run build
``` ```
`yarn hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files. A fresh worktree has no bundled JS until `yarn build` runs. `pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files. A fresh worktree has no bundled JS until `pnpm run build` runs.
## Environment variables ## Environment variables
@@ -56,61 +56,61 @@ DEEPSEEK_BASE_URL=https://... # optional
lefthook is configured in `lefthook.yml` as an early local checkpoint before review: lefthook is configured in `lefthook.yml` as an early local checkpoint before review:
- `pre-commit` runs staged-file ESLint fixes, `yarn typecheck`, and the vendor manifest guard. - `pre-commit` runs staged-file ESLint fixes, `pnpm run typecheck`, and the vendor manifest guard.
- `pre-push` runs `yarn test`, `yarn hygiene`, and `yarn doc-sync`. - `pre-push` runs `pnpm run test`, `pnpm run hygiene`, and `pnpm run doc-sync`.
The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code. The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.
These hooks do not exactly mirror CI. Notably, `pre-push` runs unit tests without coverage, while CI runs `yarn test:coverage`; CI also runs an echo-agent smoke test and exercises the matrix on Node 24 and 26. These hooks do not exactly mirror CI. Notably, `pre-push` runs unit tests without coverage, while CI runs `pnpm run test:coverage`; CI also runs an echo-agent smoke test and exercises the matrix on Node 24 and 26.
## CI gates ## CI gates
The GitHub workflow runs these gates on each pull request: The GitHub workflow runs these gates on each pull request:
- `yarn install --immutable` - `pnpm install --frozen-lockfile`
- `yarn constraints` - `pnpm run constraints`
- `yarn typecheck` - `pnpm run typecheck`
- `yarn lint` - `pnpm run lint`
- `yarn doc-sync` - `pnpm run doc-sync`
- `yarn test:coverage` - `pnpm run test:coverage`
- `yarn build` - `pnpm run build`
- `yarn knip && yarn publint` - `pnpm run knip && pnpm run publint`
- an echo-agent smoke test that checks the demo's tool call, tool result, and JSONL output - an echo-agent smoke test that checks the demo's tool call, tool result, and JSONL output
`yarn hygiene` is the local shorthand for `yarn knip && yarn publint && yarn constraints`; CI splits `yarn constraints` into its own earlier step, then runs `yarn knip && yarn publint` after `yarn build`. `pnpm run hygiene` is the local shorthand for `pnpm run knip && pnpm run publint && pnpm run constraints`; CI splits `pnpm run constraints` into its own earlier step, then runs `pnpm run knip && pnpm run publint` after `pnpm run build`.
## Daily commands ## Daily commands
Use these from the repo root: Use these from the repo root:
```sh ```sh
yarn test # unit tests pnpm run test # unit tests
yarn test:coverage # unit tests with per-file coverage gates pnpm run test:coverage # unit tests with per-file coverage gates
yarn test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY
yarn typecheck # build declarations, then typecheck source, tests, and examples pnpm run typecheck # build declarations, then typecheck source, tests, and examples
yarn lint # eslint . pnpm run lint # eslint .
yarn lint:fix # eslint . --fix pnpm run lint:fix # eslint . --fix
yarn doc-typecheck # compile checked TypeScript snippets in Markdown docs pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
yarn verify-event-taxonomy # compare docs/architecture.md event names with source pnpm run verify-event-taxonomy # compare docs/architecture.md event names with source
yarn doc-sync # doc-typecheck plus event taxonomy verification pnpm run doc-sync # doc-typecheck plus event taxonomy verification
yarn build # build declarations and JS bundles pnpm run build # build declarations and JS bundles
yarn hygiene # knip, publint, and yarn constraints pnpm run hygiene # knip, publint, and workspace constraints
``` ```
When changing package public behavior, update the relevant README or JSDoc in the same change. `yarn doc-sync` catches checked TypeScript snippets and event-taxonomy drift, but broader prose/API sync still needs review. When changing package public behavior, update the relevant README or JSDoc in the same change. `pnpm run doc-sync` catches checked TypeScript snippets and event-taxonomy drift, but broader prose/API sync still needs review.
## Demos ## Demos
The echo demo does not need API credentials: The echo demo does not need API credentials:
```sh ```sh
yarn demo:echo pnpm run demo:echo
``` ```
The coding-agent demo uses the real DeepSeek adapter and needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`: The coding-agent demo uses the real DeepSeek adapter and needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:
```sh ```sh
yarn demo:coding pnpm run demo:coding
``` ```
## Architecture context ## Architecture context
@@ -9,7 +9,7 @@ The vendor manifest (ADR 0001) is enforced at commit time in the *forward* direc
## Proposal ## Proposal
1. **Vendor drift check** (nightly CI): clone the upstream repos at the manifest SHAs (shallow), copy the corresponding package sources, and diff against `vendor/*/src`. The job fails unless the diff matches the logged local modifications (kept as a checked-in patch file per modification — the log entries become verifiable artifacts rather than prose). 1. **Vendor drift check** (nightly CI): clone the upstream repos at the manifest SHAs (shallow), copy the corresponding package sources, and diff against `vendor/*/src`. The job fails unless the diff matches the logged local modifications (kept as a checked-in patch file per modification — the log entries become verifiable artifacts rather than prose).
2. **Dependency advisories**: osv-scanner (or `yarn npm audit`) job on the lockfile, scheduled + on lockfile-touching PRs. 2. **Dependency advisories**: osv-scanner (or `pnpm audit`) job on the lockfile, scheduled + on lockfile-touching PRs.
3. **License inventory**: a script asserting every vendored package carries its LICENSE and that package.json `license` fields match the inventory in vendor/README.md (we mix vendored MIT with our BSD-3) — CI step. 3. **License inventory**: a script asserting every vendored package carries its LICENSE and that package.json `license` fields match the inventory in vendor/README.md (we mix vendored MIT with our BSD-3) — CI step.
4. **Renovate** (or a scheduled agent task) proposing npm dependency updates in small PRs that ride the full gate suite; vendored packages are excluded (their updates follow the manifest sync procedure, ideally as a semi-automated agent workflow: fetch upstream, re-apply patches, run gates, open PR with the manifest table updated). 4. **Renovate** (or a scheduled agent task) proposing npm dependency updates in small PRs that ride the full gate suite; vendored packages are excluded (their updates follow the manifest sync procedure, ideally as a semi-automated agent workflow: fetch upstream, re-apply patches, run gates, open PR with the manifest table updated).
+1 -1
View File
@@ -45,7 +45,7 @@ Lifecycle and disposal: the connection, listeners, and in-flight permission prom
3. Internal edit — turn-end reason fidelity (sanctioned: edit internals to fit ACP). Extend `TurnEndReasonMap` in the proper places: (a) declaration-merge a `max-tokens` variant in the owning package (`packages/session/src/types.ts`, alongside `completed|aborted|error|disposed`) — add `max-tokens` because `FinishReasonMap` produces it (DeepSeek maps `length``max-tokens`); do not add `refusal`, since no current adapter produces it (unknown DeepSeek finish reasons collapse to `error`), but leave a comment in `TurnEndReasonMap` noting `refusal` should be added when an adapter first emits it (`FinishReasonMap` is merge-extensible); (b) make `agent-loop`'s `loop.ts` populate the reason from the model `finish` chunk — `assembler.finish` lives inside `runStep`, so `runStep` must return it up to `runTurn`, and the rule is "the last step's finish reason wins, but any `max-tokens` in the turn surfaces as `max-tokens`"; (c) no consumer exhaustively switches over `TurnEndReason` today (the invariants plugin switches on `SessionEventType`, and `deriveMessages` ignores `turn/end`), so adding `max-tokens` is a non-breaking extension — but recheck before landing; (d) update [docs/architecture.md](../architecture.md) (the CI-verified loop-lifecycle/event-taxonomy doc) and the affected package READMEs/JSDoc (`dsh-session`, `dsh-agent`, `dsh-agent-loop`) per the repo doc-sync policy. This replaces a fragile "observe the finish chunk in the bridge" hack with a real, documented contract. 3. Internal edit — turn-end reason fidelity (sanctioned: edit internals to fit ACP). Extend `TurnEndReasonMap` in the proper places: (a) declaration-merge a `max-tokens` variant in the owning package (`packages/session/src/types.ts`, alongside `completed|aborted|error|disposed`) — add `max-tokens` because `FinishReasonMap` produces it (DeepSeek maps `length``max-tokens`); do not add `refusal`, since no current adapter produces it (unknown DeepSeek finish reasons collapse to `error`), but leave a comment in `TurnEndReasonMap` noting `refusal` should be added when an adapter first emits it (`FinishReasonMap` is merge-extensible); (b) make `agent-loop`'s `loop.ts` populate the reason from the model `finish` chunk — `assembler.finish` lives inside `runStep`, so `runStep` must return it up to `runTurn`, and the rule is "the last step's finish reason wins, but any `max-tokens` in the turn surfaces as `max-tokens`"; (c) no consumer exhaustively switches over `TurnEndReason` today (the invariants plugin switches on `SessionEventType`, and `deriveMessages` ignores `turn/end`), so adding `max-tokens` is a non-breaking extension — but recheck before landing; (d) update [docs/architecture.md](../architecture.md) (the CI-verified loop-lifecycle/event-taxonomy doc) and the affected package READMEs/JSDoc (`dsh-session`, `dsh-agent`, `dsh-agent-loop`) per the repo doc-sync policy. This replaces a fragile "observe the finish chunk in the bridge" hack with a real, documented contract.
4. Prompt-turn streaming plus load: translate `agent/stream-chunk` and `session/event` into `session/update`; resolve `session/prompt` on settle, mapping the harness `TurnEndReason` to the ACP `StopReason` wire enum (`completed``end_turn`, `max-tokens``max_tokens`, `aborted``cancelled`) — a small total function with a test asserting the exact wire strings, since the SDK rejects an unknown `stopReason`. Concrete correlation, since the loop batches queued messages into one turn and `send()` does not synchronously flip to running: install listeners before `send()`; gate on an observed `agent/turn-start` (confirms work was accepted) then resolve on the next `agent/turn-end`; reject an empty/whitespace prompt up front rather than calling `send()` (no turn would ever start, so the RPC would hang). Implement `session/load` on RFC 009's resume seam. 4. Prompt-turn streaming plus load: translate `agent/stream-chunk` and `session/event` into `session/update`; resolve `session/prompt` on settle, mapping the harness `TurnEndReason` to the ACP `StopReason` wire enum (`completed``end_turn`, `max-tokens``max_tokens`, `aborted``cancelled`) — a small total function with a test asserting the exact wire strings, since the SDK rejects an unknown `stopReason`. Concrete correlation, since the loop batches queued messages into one turn and `send()` does not synchronously flip to running: install listeners before `send()`; gate on an observed `agent/turn-start` (confirms work was accepted) then resolve on the next `agent/turn-end`; reject an empty/whitespace prompt up front rather than calling `send()` (no turn would ever start, so the RPC would hang). Implement `session/load` on RFC 009's resume seam.
5. Permission gate: a single `tools/execute` listener registered with `prepend: true`, owning a `WeakMap<Agent, sessionId>` of bridge-created agents; no-op (`next()`) for unowned/no-agent calls; for owned calls → `session/request_permission` → allow (`next()`) / veto; settle the stored resolver exactly once on outcome, cancel, or connection close. 5. Permission gate: a single `tools/execute` listener registered with `prepend: true`, owning a `WeakMap<Agent, sessionId>` of bridge-created agents; no-op (`next()`) for unowned/no-agent calls; for owned calls → `session/request_permission` → allow (`next()`) / veto; settle the stored resolver exactly once on outcome, cancel, or connection close.
6. Example wiring (extract a shared base). `@cordisjs/plugin-include` is itself a plugin entry that resets `ctx.baseUrl` and loads a path, so a child `cordis.yml` can nest-include a shared base; the extraction is safe because every dependent plugin declares `inject` (loader groups initialize via `Promise.all`, so YAML order is NOT the dependency mechanism — never rely on it). Extract the provider/tool core (`llm, sessions, system-prompt, tools, agents, invariants, llm-deepseek, bash-local, tool-bash`) into `examples/base.yml`; have both `coding-agent` and a new `examples/acp-agent/` include it and add their own UI plugin plus logger. Keep `agent-loop` per-example (NOT in the base): `AgentLoop` creates its configured agents in its constructor, and the two examples disagree — `coding-agent` needs a pre-created `main` (its `stdio-chat` calls `ctx.agents.get('main')`), while `acp-agent` must pre-create none (ACP `session/new` creates agents). So `coding-agent` declares `agent-loop` with `agents: [{ id: main, … }]` and `acp-agent` with `agents: []`. `acp-agent` loads `dsh-session-persistence-jsonl` (from RFC 009 — required for `session/load`), omits the stdout logger (see Risks), and adds `yarn demo:acp` plus the Zed `agent_servers` snippet. 6. Example wiring (extract a shared base). `@cordisjs/plugin-include` is itself a plugin entry that resets `ctx.baseUrl` and loads a path, so a child `cordis.yml` can nest-include a shared base; the extraction is safe because every dependent plugin declares `inject` (loader groups initialize via `Promise.all`, so YAML order is NOT the dependency mechanism — never rely on it). Extract the provider/tool core (`llm, sessions, system-prompt, tools, agents, invariants, llm-deepseek, bash-local, tool-bash`) into `examples/base.yml`; have both `coding-agent` and a new `examples/acp-agent/` include it and add their own UI plugin plus logger. Keep `agent-loop` per-example (NOT in the base): `AgentLoop` creates its configured agents in its constructor, and the two examples disagree — `coding-agent` needs a pre-created `main` (its `stdio-chat` calls `ctx.agents.get('main')`), while `acp-agent` must pre-create none (ACP `session/new` creates agents). So `coding-agent` declares `agent-loop` with `agents: [{ id: main, … }]` and `acp-agent` with `agents: []`. `acp-agent` loads `dsh-session-persistence-jsonl` (from RFC 009 — required for `session/load`), omits the stdout logger (see Risks), and adds `pnpm run demo:acp` plus the Zed `agent_servers` snippet.
7. Tests (the repo cares a lot here): a property-based test for the protocol shape (precedent: RFC 001 / [ADR 0013](../adr/0013-property-based-testing.md)) — fuzz arbitrary harness event sequences and assert ACP-stream invariants (never a `tool_call_update` before its `tool_call`; exactly one `session/prompt` resolution per prompt; monotonic, well-formed ordering; `stopReason` in the legal set); codec unit tests over an in-memory `Duplex` pair (drive `AgentSideConnection` without a subprocess; assert exact frames for `initialize`, `session/new`, a full prompt turn); the mandatory HMR-safety test (dispose the fiber; assert the connection closed, all `ctx.on` listeners gone, any in-flight `request_permission` settled); failure-path tests (connection closes mid-stream; closes with a permission pending; a notification `send()` rejects but the turn survives; `finish{kind:'error'|'aborted'}`; a `tools/execute` throw with no `tool/result`; a second `session/new` rejected; a `session/prompt` while one is in flight; an empty prompt rejected without hanging; a `session/load` re-derives identical history and replays it); and an e2e (`*.e2e.ts`, self-skips without `DEEPSEEK_API_KEY`) that boots `examples/acp-agent`, connects a `ClientSideConnection`, sends a real prompt, owns and disposes the harness in `afterEach`, and verifies the world (files on disk), not the agent's self-report. 7. Tests (the repo cares a lot here): a property-based test for the protocol shape (precedent: RFC 001 / [ADR 0013](../adr/0013-property-based-testing.md)) — fuzz arbitrary harness event sequences and assert ACP-stream invariants (never a `tool_call_update` before its `tool_call`; exactly one `session/prompt` resolution per prompt; monotonic, well-formed ordering; `stopReason` in the legal set); codec unit tests over an in-memory `Duplex` pair (drive `AgentSideConnection` without a subprocess; assert exact frames for `initialize`, `session/new`, a full prompt turn); the mandatory HMR-safety test (dispose the fiber; assert the connection closed, all `ctx.on` listeners gone, any in-flight `request_permission` settled); failure-path tests (connection closes mid-stream; closes with a permission pending; a notification `send()` rejects but the turn survives; `finish{kind:'error'|'aborted'}`; a `tools/execute` throw with no `tool/result`; a second `session/new` rejected; a `session/prompt` while one is in flight; an empty prompt rejected without hanging; a `session/load` re-derives identical history and replays it); and an e2e (`*.e2e.ts`, self-skips without `DEEPSEEK_API_KEY`) that boots `examples/acp-agent`, connects a `ClientSideConnection`, sends a real prompt, owns and disposes the harness in `afterEach`, and verifies the world (files on disk), not the agent's self-report.
8. Docs: module/JSDoc plus a package README; extend [the extension cookbook](../cookbook/extension-cookbook.md) with the client-driver pattern. Flip Status to `implemented` on landing; write an ADR only if a decision proves durable, contested, and surprising (candidates: the `tools/execute` permission-ownership rule, the npm-dependency choice) — not auto-required. 8. Docs: module/JSDoc plus a package README; extend [the extension cookbook](../cookbook/extension-cookbook.md) with the client-driver pattern. Flip Status to `implemented` on landing; write an ADR only if a decision proves durable, contested, and surprising (candidates: the `tools/execute` permission-ownership rule, the npm-dependency choice) — not auto-required.
+1 -1
View File
@@ -85,7 +85,7 @@ It is insufficient for the **composition / round-trip** half, which is the decis
2. Scaffold the implementation package `packages/code-runtime-vm/`: the node:vm stub — `safe = false`, a constructor that **throws unless given `{ unsafe: true }`**, transpile/type-erase, async-IIFE wrap, capturing `console`, SDK globals, return-value/logs/error capture, output cap, signal-tied timeout. Tests for output capture, return value, error-as-field, abort, the constructor refusal without `unsafe`, and a README documenting the "not a sandbox, trusted-only" caveat prominently. 2. Scaffold the implementation package `packages/code-runtime-vm/`: the node:vm stub — `safe = false`, a constructor that **throws unless given `{ unsafe: true }`**, transpile/type-erase, async-IIFE wrap, capturing `console`, SDK globals, return-value/logs/error capture, output cap, signal-tied timeout. Tests for output capture, return value, error-as-field, abort, the constructor refusal without `unsafe`, and a README documenting the "not a sandbox, trusted-only" caveat prominently.
3. Scaffold the consumer plugin `packages/code-mode/`: `jsonSchemaToTs` codegen with namespace/quoted-access + alias handling (unit tests, including non-identifier MCP names and unsupported-shape → `unknown`); the registered lazy `ctx.systemPrompt.section()` carrying the SDK `.d.ts`; the `agent/request` listener (`prepend: true`) collapsing `request.tools` to `[run_code]` after `await next()`; the **unsafe-runtime gate** (refuse to register `run_code` when `ctx.codeRuntime.safe === false` unless `allowUnsafeRuntime` is set); the `run_code` tool with the dispatch bridge (per-run serialization queue, deterministic sub-call ids, before/after abort checks, `CodeRunError` on error results); and the `code/dispatch` event declared here via `SessionEventMap` merge. Declare `inject = ['tools', 'systemPrompt', 'codeRuntime']`. 3. Scaffold the consumer plugin `packages/code-mode/`: `jsonSchemaToTs` codegen with namespace/quoted-access + alias handling (unit tests, including non-identifier MCP names and unsupported-shape → `unknown`); the registered lazy `ctx.systemPrompt.section()` carrying the SDK `.d.ts`; the `agent/request` listener (`prepend: true`) collapsing `request.tools` to `[run_code]` after `await next()`; the **unsafe-runtime gate** (refuse to register `run_code` when `ctx.codeRuntime.safe === false` unless `allowUnsafeRuntime` is set); the `run_code` tool with the dispatch bridge (per-run serialization queue, deterministic sub-call ids, before/after abort checks, `CodeRunError` on error results); and the `code/dispatch` event declared here via `SessionEventMap` merge. Declare `inject = ['tools', 'systemPrompt', 'codeRuntime']`.
4. Tests: HMR-safety (dispose removes the tool, the section, and the listener); a waterfall test that the wire tool list is exactly `[run_code]` (spy adapter, asserting via `agent/request` and optionally `llm/stream`); an integration test that a program calling two tools returns only its printed/returned output (verify the world, not the self-report); a **serialization test** that `Promise.all([...])` over SDK calls does not overlap the underlying `ctx.tools.execute` invocations (a probe tool records enter/exit; assert no interleaving); `deriveMessages()` ignores `code/dispatch`; abort mid-program stops further dispatches; `CodeRunError` surfaces as `isError: true`; and the **unsafe-runtime refusal test** (§3, the VM-guard): with the unsafe flag unset, a non-mock agent's `run_code` is refused; with it set, the program runs. 4. Tests: HMR-safety (dispose removes the tool, the section, and the listener); a waterfall test that the wire tool list is exactly `[run_code]` (spy adapter, asserting via `agent/request` and optionally `llm/stream`); an integration test that a program calling two tools returns only its printed/returned output (verify the world, not the self-report); a **serialization test** that `Promise.all([...])` over SDK calls does not overlap the underlying `ctx.tools.execute` invocations (a probe tool records enter/exit; assert no interleaving); `deriveMessages()` ignores `code/dispatch`; abort mid-program stops further dispatches; `CodeRunError` surfaces as `isError: true`; and the **unsafe-runtime refusal test** (§3, the VM-guard): with the unsafe flag unset, a non-mock agent's `run_code` is refused; with it set, the program runs.
5. Wire an example: `examples/coding-agent-code-mode` (or a config flag on the existing example) loading the trio. Running it against the node:vm stub requires both opt-ins (`VmCodeRuntime({ unsafe: true })` and `code-mode`'s `allowUnsafeRuntime`); the example sets them explicitly and comments why, or uses a mock model — a real model never reaches the unsandboxed stub without those deliberate flags. Add a `yarn demo:*` entry. 5. Wire an example: `examples/coding-agent-code-mode` (or a config flag on the existing example) loading the trio. Running it against the node:vm stub requires both opt-ins (`VmCodeRuntime({ unsafe: true })` and `code-mode`'s `allowUnsafeRuntime`); the example sets them explicitly and comments why, or uses a mock model — a real model never reaches the unsandboxed stub without those deliberate flags. Add a `pnpm run demo:*` entry.
6. Docs: update [docs/architecture.md](../architecture.md) (a `ctx.codeRuntime` row in the service map, a Code Mode note under the tool pipeline / capability seams sections); add a [cookbook](../cookbook/) note on writing a `CodeRuntime` backend; and **file the follow-up RFC for the hardened execution substrate** (the isolate/sandboxed-process design, the additional-language backends sketched in §1 — AssemblyScript/WASM, Python — with their per-language SDK generators, plus the tool-visibility-tier design skipped here). Append the `| 012 | … | proposed |` row to [the RFC index](README.md). 6. Docs: update [docs/architecture.md](../architecture.md) (a `ctx.codeRuntime` row in the service map, a Code Mode note under the tool pipeline / capability seams sections); add a [cookbook](../cookbook/) note on writing a `CodeRuntime` backend; and **file the follow-up RFC for the hardened execution substrate** (the isolate/sandboxed-process design, the additional-language backends sketched in §1 — AssemblyScript/WASM, Python — with their per-language SDK generators, plus the tool-visibility-tier design skipped here). Append the `| 012 | … | proposed |` row to [the RFC index](README.md).
## Risks ## Risks
+2 -2
View File
@@ -12,7 +12,7 @@ A mock model + echo tool + stdio UI + JSONL persistence demo. Demonstrates:
- Persisting session events to JSONL via the `session/event` + `session/flush` pattern - Persisting session events to JSONL via the `session/event` + `session/flush` pattern
- A minimal stdio UI consuming `agent/stream-chunk` and session events - A minimal stdio UI consuming `agent/stream-chunk` and session events
Run with: `yarn demo:echo` Run with: `pnpm run demo:echo`
When prompted, type "echo <something>" to trigger a tool call round-trip. When prompted, type "echo <something>" to trigger a tool call round-trip.
@@ -20,4 +20,4 @@ When prompted, type "echo <something>" to trigger a tool call round-trip.
The real thing: DeepSeek V4 + the bash tool suite + stdio chat + JSONL persistence, wired from `cordis.yml`. Where echo-agent proves the skeleton with mocks, this is a usable coding assistant. The real thing: DeepSeek V4 + the bash tool suite + stdio chat + JSONL persistence, wired from `cordis.yml`. Where echo-agent proves the skeleton with mocks, this is a usable coding assistant.
Run with: `yarn demo:coding` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details. Run with: `pnpm run demo:coding` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details.
+2 -2
View File
@@ -9,7 +9,7 @@ The first REAL agent wiring: DeepSeek V4 + the bash tool suite + stdio chat
# repo root .env (gitignored) or exported env: # repo root .env (gitignored) or exported env:
# DEEPSEEK_API_KEY=sk-… # DEEPSEEK_API_KEY=sk-…
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API # DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
yarn demo:coding pnpm run demo:coding
``` ```
Type a coding task. The agent's only tools are `bash` (+ `bash_output` / `bash_kill` for background tasks): file reads, writes, searches, and test runs all happen through shell commands, each in a fresh `bash -c` (the system prompt tells the model to pass `workdir` instead of `cd`). Reasoning streams dimmed; tool calls/results render inline. Type a coding task. The agent's only tools are `bash` (+ `bash_output` / `bash_kill` for background tasks): file reads, writes, searches, and test runs all happen through shell commands, each in a fresh `bash -c` (the system prompt tells the model to pass `workdir` instead of `cd`). Reasoning streams dimmed; tool calls/results render inline.
@@ -32,7 +32,7 @@ Type a coding task. The agent's only tools are `bash` (+ `bash_output` / `bash_k
| `src/session-jsonl.ts` | write-behind persistence on `session/event` + `session/flush` (copied from echo-agent) | | `src/session-jsonl.ts` | write-behind persistence on `session/event` + `session/flush` (copied from echo-agent) |
| `src/stdio-chat.ts` | UI as a plugin; copied from echo-agent with reasoning-dimming and an exit-on-idle close handler for piped stdin. Example-local on purpose — extract a shared UI package when a third example needs it | | `src/stdio-chat.ts` | UI as a plugin; copied from echo-agent with reasoning-dimming and an exit-on-idle close handler for piped stdin. Example-local on purpose — extract a shared UI package when a third example needs it |
## End-to-end tests (`yarn test:e2e`, key-gated) ## End-to-end tests (`pnpm run test:e2e`, key-gated)
- `tests/full-loop.e2e.ts` — the canary: real model runs `echo e2e-ok` through the real bash tool; asserts `tool/call`/`tool/result` session events and the final answer. - `tests/full-loop.e2e.ts` — the canary: real model runs `echo e2e-ok` through the real bash tool; asserts `tool/call`/`tool/result` session events and the final answer.
- `tests/coding-task.e2e.ts` — the swebench-style smoke: a temp dir holds `add.js` (with `a - b` where `a + b` belongs) and a failing `add.test.js`; the agent must fix the bug and verify. The test re-runs `node add.test.js` ITSELF and inspects the files — agent claims are not trusted. - `tests/coding-task.e2e.ts` — the swebench-style smoke: a temp dir holds `add.js` (with `a - b` where `a + b` belongs) and a failing `add.test.js`; the agent must fix the bug and verify. The test re-runs `node add.test.js` ITSELF and inspects the files — agent claims are not trusted.
+1 -1
View File
@@ -23,7 +23,7 @@ Runnable demo: stdin chat with a scripted mock model and an echo tool.
## Run ## Run
```sh ```sh
yarn demo:echo pnpm run demo:echo
# or: # or:
node --expose-internals --import tsx examples/echo-agent/start.ts node --expose-internals --import tsx examples/echo-agent/start.ts
``` ```
+5 -5
View File
@@ -1,6 +1,6 @@
# Git hooks (lefthook). Hooks call the same package.json scripts CI runs — # Git hooks (lefthook). Hooks call the same package.json scripts CI runs —
# one source of truth; the hook is just an earlier, faster checkpoint. # one source of truth; the hook is just an earlier, faster checkpoint.
# Install: `yarn lefthook install` (runs automatically via postinstall). # Install: `pnpm exec lefthook install` (runs automatically via postinstall).
pre-commit: pre-commit:
parallel: true parallel: true
@@ -14,7 +14,7 @@ pre-commit:
- name: typecheck - name: typecheck
glob: '*.ts' glob: '*.ts'
run: yarn typecheck run: pnpm run typecheck
- name: vendor manifest guard - name: vendor manifest guard
run: scripts/check-vendor-manifest.sh run: scripts/check-vendor-manifest.sh
@@ -23,10 +23,10 @@ pre-push:
parallel: true parallel: true
jobs: jobs:
- name: test - name: test
run: yarn test run: pnpm run test
- name: hygiene - name: hygiene
run: yarn hygiene run: pnpm run hygiene
- name: doc-sync - name: doc-sync
run: yarn doc-sync run: pnpm run doc-sync
+4 -4
View File
@@ -3,7 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "yarn@4.14.1", "packageManager": "pnpm@11.7.0",
"engines": { "engines": {
"node": ">=24" "node": ">=24"
}, },
@@ -23,8 +23,9 @@
"publint": "tsx scripts/publint-all.ts", "publint": "tsx scripts/publint-all.ts",
"doc-typecheck": "tsx scripts/doc-typecheck.ts", "doc-typecheck": "tsx scripts/doc-typecheck.ts",
"verify-event-taxonomy": "tsx scripts/verify-event-taxonomy.ts", "verify-event-taxonomy": "tsx scripts/verify-event-taxonomy.ts",
"doc-sync": "yarn doc-typecheck && yarn verify-event-taxonomy", "constraints": "tsx scripts/check-workspace-constraints.ts",
"hygiene": "yarn knip && yarn publint && yarn constraints", "doc-sync": "pnpm run doc-typecheck && pnpm run verify-event-taxonomy",
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints",
"demo:echo": "node --expose-internals --import tsx examples/echo-agent/start.ts", "demo:echo": "node --expose-internals --import tsx examples/echo-agent/start.ts",
"demo:coding": "node --expose-internals --import tsx examples/coding-agent/start.ts", "demo:coding": "node --expose-internals --import tsx examples/coding-agent/start.ts",
"postinstall": "lefthook install" "postinstall": "lefthook install"
@@ -33,7 +34,6 @@
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^25.3.5", "@types/node": "^25.3.5",
"@vitest/coverage-v8": "^4.1.8", "@vitest/coverage-v8": "^4.1.8",
"@yarnpkg/types": "^4.0.1",
"eslint": "^10.4.1", "eslint": "^10.4.1",
"fast-check": "^4.8.0", "fast-check": "^4.8.0",
"knip": "^6.16.1", "knip": "^6.16.1",
+1 -1
View File
@@ -11,6 +11,6 @@ Naming notes:
- Files `src/index.ts` export the service default + all public types - Files `src/index.ts` export the service default + all public types
- `src/types.ts` contain only types — no runtime code - `src/types.ts` contain only types — no runtime code
- Tests live at package level under `tests/`, not `src/__tests__/` - Tests live at package level under `tests/`, not `src/__tests__/`
- A package's README and module/JSDoc comments are part of the change: when you alter behavior (config keys, defaults, error codes, wire fields), update them in the same commit. CI runs `yarn doc-sync`, which typechecks fenced `ts` blocks in `packages/*/README.md` and verifies the event-taxonomy table — but it does NOT cover this file or prose drift (config keys, defaults, error codes), so those stay on the author. - A package's README and module/JSDoc comments are part of the change: when you alter behavior (config keys, defaults, error codes, wire fields), update them in the same commit. CI runs `pnpm run doc-sync`, which typechecks fenced `ts` blocks in `packages/*/README.md` and verifies the event-taxonomy table — but it does NOT cover this file or prose drift (config keys, defaults, error codes), so those stay on the author.
Read the per-package README.md for package-specific details: service API, events, extension points, TODOs. Read the per-package README.md for package-specific details: service API, events, extension points, TODOs.
+6 -6
View File
@@ -31,12 +31,12 @@
"schemastery": "^3.18.0" "schemastery": "^3.18.0"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+2 -2
View File
@@ -25,8 +25,8 @@
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@
"schemastery": "^3.18.0" "schemastery": "^3.18.0"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-bash": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+3 -3
View File
@@ -26,9 +26,9 @@
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+1 -1
View File
@@ -43,4 +43,4 @@ Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `RATE_LI
## Testing ## Testing
Unit suites run against a local `node:http` mock SSE server (no network). Real-API coverage lives in `tests/adapter.e2e.ts` (`yarn test:e2e`, key-gated): V4 Flash + V4 Pro across thinking enabled/disabled and both official effort levels, including the thinking+tools round trip with reasoning passback. Unit suites run against a local `node:http` mock SSE server (no network). Real-API coverage lives in `tests/adapter.e2e.ts` (`pnpm run test:e2e`, key-gated): V4 Flash + V4 Pro across thinking enabled/disabled and both official effort levels, including the thinking+tools round trip with reasoning passback.
+1 -1
View File
@@ -27,7 +27,7 @@
"schemastery": "^3.18.0" "schemastery": "^3.18.0"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+1 -1
View File
@@ -35,4 +35,4 @@ Same MVP contract as llm-deepseek: `prefill` throws `UNSUPPORTED`, images are no
## Testing ## Testing
Unit suites run against a local `node:http` mock SSE server (pi-ai's openai SDK happily talks to any base URL). Real-API coverage in `tests/adapter.e2e.ts` (`yarn test:e2e`, key-gated): V4 Flash + V4 Pro across all exposed reasoning levels (off/high/xhigh), the thinking+tools round trip, and a cross-adapter structural-equivalence check against llm-deepseek. Unit suites run against a local `node:http` mock SSE server (pi-ai's openai SDK happily talks to any base URL). Real-API coverage in `tests/adapter.e2e.ts` (`pnpm run test:e2e`, key-gated): V4 Flash + V4 Pro across all exposed reasoning levels (off/high/xhigh), the thinking+tools round trip, and a cross-adapter structural-equivalence check against llm-deepseek.
+2 -2
View File
@@ -28,8 +28,8 @@
"schemastery": "^3.18.0" "schemastery": "^3.18.0"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "^0.0.1", "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+1 -1
View File
@@ -24,7 +24,7 @@
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+1 -1
View File
@@ -24,7 +24,7 @@
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+8 -8
View File
@@ -27,14 +27,14 @@
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-local": "^0.0.1", "@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+3 -3
View File
@@ -26,9 +26,9 @@
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
}, },
"devDependencies": { "devDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "workspace:^",
"cordis": "^4.0.0-rc.6" "cordis": "^4.0.0-rc.6"
} }
} }
+4440
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
packages:
- vendor/*
- packages/*
peerDependencyRules:
allowedVersions:
typescript: '>=5 <7'
allowBuilds:
'@google/genai': true
esbuild: true
lefthook: true
protobufjs: true
+97
View File
@@ -0,0 +1,97 @@
/**
* Workspace package invariant checks for package-manager-independent quality
* gates.
*
* Run: `tsx scripts/check-workspace-constraints.ts`.
*/
import { readdirSync, readFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const workspaceGlobs = ['vendor', 'packages'] as const
const vendoredPackages = new Set([
'cordis',
'cosmokit',
'schemastery',
'@cordisjs/plugin-loader',
'@cordisjs/plugin-include',
'@cordisjs/plugin-group',
'@cordisjs/plugin-timer',
'@cordisjs/plugin-hmr',
'@cordisjs/plugin-logger-console',
])
/** The subset of package.json fields this constraint check cares about. */
interface PackageManifest {
name?: string
version?: string
private?: boolean
type?: string
peerDependencies?: Record<string, string>
devDependencies?: Record<string, string>
}
/** One workspace manifest and its repo-relative path. */
interface WorkspaceManifest {
dir: string
manifest: PackageManifest
}
function readJson(path: string): PackageManifest {
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
}
function workspaceManifests(): WorkspaceManifest[] {
const manifests: WorkspaceManifest[] = [
{ dir: '.', manifest: readJson(join(root, 'package.json')) },
]
for (const workspaceDir of workspaceGlobs) {
for (const entry of readdirSync(join(root, workspaceDir), { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const dir = join(workspaceDir, entry.name)
manifests.push({ dir, manifest: readJson(join(root, dir, 'package.json')) })
}
}
return manifests
}
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
const errors: string[] = []
const label = manifest.name ?? dir
if (manifest.private !== true) {
errors.push(`${label}: package.json must set "private": true`)
}
if (manifest.name && vendoredPackages.has(manifest.name)) {
return errors
}
if (manifest.name?.startsWith('@deepseek-ai/dsh-') && manifest.name !== '@deepseek-ai/dsh-root') {
const peer = manifest.peerDependencies?.cordis
const dev = manifest.devDependencies?.cordis
if (!peer) errors.push(`${label}: cordis must be a peerDependency`)
if (!dev) errors.push(`${label}: cordis must also be a devDependency`)
if (peer && dev && peer !== dev) {
errors.push(`${label}: cordis peer (${peer}) and dev (${dev}) ranges must match`)
}
if (manifest.version !== '0.0.1') {
errors.push(`${label}: package.json must set "version": "0.0.1"`)
}
if (manifest.type !== 'module') {
errors.push(`${label}: package.json must set "type": "module"`)
}
}
return errors.map(error => `${relative(root, join(root, dir, 'package.json'))}: ${error}`)
}
const errors = workspaceManifests().flatMap(checkWorkspace)
if (errors.length > 0) {
console.error(errors.join('\n'))
process.exitCode = 1
}
+2 -2
View File
@@ -62,10 +62,10 @@ function extractBlocks(absPath: string): Block[] {
/** /**
* Read the workspace `paths` map from tsconfig.typecheck.json (JSONC). This map * Read the workspace `paths` map from tsconfig.typecheck.json (JSONC). This map
* resolves vendored packages to their BUILT declarations (`lib`) and harness * resolves vendored packages to their BUILT declarations (`lib`) and harness
* packages to source (`src`) — the same resolution `yarn lint`/`typecheck` use. * packages to source (`src`) — the same resolution `pnpm run lint`/`typecheck` use.
* Resolving vendor to `lib` (not `src`) is essential: otherwise tsc type-checks * Resolving vendor to `lib` (not `src`) is essential: otherwise tsc type-checks
* raw vendor source and floods the run with unrelated errors. Requires the * raw vendor source and floods the run with unrelated errors. Requires the
* vendor `lib/` to exist (a fresh clone runs `yarn build` first; CI does too). * vendor `lib/` to exist (a fresh clone runs `pnpm run build` first; CI does too).
*/ */
function workspacePaths(): Record<string, string[]> { function workspacePaths(): Record<string, string[]> {
const raw = readFileSync(join(root, 'tsconfig.typecheck.json'), 'utf8') const raw = readFileSync(join(root, 'tsconfig.typecheck.json'), 'utf8')
+1 -1
View File
@@ -11,7 +11,7 @@ import { defineConfig } from 'tsdown'
*/ */
export default defineConfig({ export default defineConfig({
// Explicit globs: `workspace: true` would also discover examples/* (any // Explicit globs: `workspace: true` would also discover examples/* (any
// package.json), but only vendor/* and packages/* are yarn workspaces. // package.json), but only vendor/* and packages/* are pnpm workspaces.
workspace: ['vendor/*', 'packages/*'], workspace: ['vendor/*', 'packages/*'],
entry: ['src/index.ts'], entry: ['src/index.ts'],
outDir: 'lib', outDir: 'lib',
+3 -3
View File
@@ -2,7 +2,7 @@
This directory contains source-vendored copies of the Cordis framework and its foundation libraries. They are copied into this monorepo instead of being depended on via npm, so that the harness fully owns its framework layer (auditable, patchable, pinned). This directory contains source-vendored copies of the Cordis framework and its foundation libraries. They are copied into this monorepo instead of being depended on via npm, so that the harness fully owns its framework layer (auditable, patchable, pinned).
All vendored packages keep their **original npm names** (they are resolved through Yarn workspaces) and are marked `private: true` — they are never published from this repo. Upstream MIT `LICENSE` files are preserved in each package directory. All vendored packages keep their **original npm names** (they are resolved through pnpm workspaces) and are marked `private: true` — they are never published from this repo. Upstream MIT `LICENSE` files are preserved in each package directory.
This file covers the manifest, the local-modification log, and the procedure for **updating** an existing vendored package. To **add a new** one, see the cookbook guide: [docs/cookbook/adding-a-vendored-package.md](../docs/cookbook/adding-a-vendored-package.md). This file covers the manifest, the local-modification log, and the procedure for **updating** an existing vendored package. To **add a new** one, see the cookbook guide: [docs/cookbook/adding-a-vendored-package.md](../docs/cookbook/adding-a-vendored-package.md).
@@ -31,7 +31,7 @@ Intentionally **not** vendored (verified unused by this set): `reggol`, `@cordis
Keep this log exhaustive — every divergence from upstream must be listed. Keep this log exhaustive — every divergence from upstream must be listed.
1. **`hmr/src/index.ts`**: removed the `./locales/en-US.yml` / `./locales/zh-CN.yml` imports, the `.i18n({...})` call on the `Config` schema, and the `src/locales/` directory. Rationale: those imports require a runtime YAML loader hook (`@cordisjs/unyaml`) that we do not vendor; the i18n texts only localize config descriptions. 1. **`hmr/src/index.ts`**: removed the `./locales/en-US.yml` / `./locales/zh-CN.yml` imports, the `.i18n({...})` call on the `Config` schema, and the `src/locales/` directory. Rationale: those imports require a runtime YAML loader hook (`@cordisjs/unyaml`) that we do not vendor; the i18n texts only localize config descriptions.
2. **All `package.json` files**: regenerated — added `private: true`, added `src` to `files` and a `./src/*` export where missing, removed upstream `devDependencies`/`scripts`/`repository` fields. Dependency and peer-dependency ranges preserved. 2. **All `package.json` files**: regenerated — added `private: true`, added `src` to `files` and a `./src/*` export where missing, removed upstream `devDependencies`/`scripts`/`repository` fields. Dependency and peer-dependency ranges preserved, except `hmr` declares `esbuild` as a direct dev dependency because its source imports the `BuildFailure` type and pnpm's strict workspace resolution requires the owner package to name that dependency.
3. **All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json` and declare project references. 3. **All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json` and declare project references.
4. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. Like the regenerated tsconfigs, they are not part of the upstream sync surface. 4. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. Like the regenerated tsconfigs, they are not part of the upstream sync surface.
@@ -43,4 +43,4 @@ To update a vendored package from upstream:
2. Copy the package's `src/` (and `bin.js`, `README.md`, `LICENSE` if changed) over the vendored directory. 2. Copy the package's `src/` (and `bin.js`, `README.md`, `LICENSE` if changed) over the vendored directory.
3. Re-apply the local modifications listed above (or drop them if upstream made them unnecessary — update the log either way). 3. Re-apply the local modifications listed above (or drop them if upstream made them unnecessary — update the log either way).
4. Update the version and commit hash in the manifest table. 4. Update the version and commit hash in the manifest table.
5. Run `yarn install && yarn test && yarn build` at the repo root. 5. Run `pnpm install && pnpm run test && pnpm run build` at the repo root.
+2 -1
View File
@@ -44,6 +44,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/babel__code-frame": "^7.27.0", "@types/babel__code-frame": "^7.27.0",
"@types/picomatch": "^3.0.2" "@types/picomatch": "^3.0.2",
"esbuild": "^0.28.1"
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ export default defineConfig({
// importing file it discovers the NEAREST tsconfig.json and applies that // importing file it discovers the NEAREST tsconfig.json and applies that
// file's own `paths`. Every workspace under packages/* and vendor/* has its // file's own `paths`. Every workspace under packages/* and vendor/* has its
// own tsconfig.json without `paths`, so native resolution maps nothing, // own tsconfig.json without `paths`, so native resolution maps nothing,
// falls through to package.json exports (lib/, absent until `yarn build`), // falls through to package.json exports (lib/, absent until `pnpm run build`),
// and every test file fails to import (verified on vite 8.0.16 / // and every test file fails to import (verified on vite 8.0.16 /
// vitest 4.1.8). Making it work would mean copying the paths map into all // vitest 4.1.8). Making it work would mean copying the paths map into all
// 15 workspace tsconfigs — including vendor/* ones, which are pinned // 15 workspace tsconfigs — including vendor/* ones, which are pinned
+2 -2
View File
@@ -1,8 +1,8 @@
import tsconfigPaths from 'vite-tsconfig-paths' import tsconfigPaths from 'vite-tsconfig-paths'
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
// Real-API end-to-end tests: `yarn test:e2e`, file pattern *.e2e.ts. // Real-API end-to-end tests: `pnpm run test:e2e`, file pattern *.e2e.ts.
// Separate from the default suite (`yarn test`, *.spec.ts) on purpose — // Separate from the default suite (`pnpm run test`, *.spec.ts) on purpose —
// these hit the live DeepSeek API, spend tokens, and need a key. // these hit the live DeepSeek API, spend tokens, and need a key.
// //
// Secrets: tests gate themselves with // Secrets: tests gate themselves with
-53
View File
@@ -1,53 +0,0 @@
// Yarn constraints: mechanize the package rules from AGENTS.md.
// Run with `yarn constraints`; CI fails on violations.
//
// 1. Every package is private (nothing publishes from this repo yet).
// 2. Harness packages (@deepseek-ai/dsh-*) declare `cordis` as BOTH a
// peerDependency and a devDependency (upstream Cordis convention).
// 3. Harness packages are versioned 0.0.1 (bumped together, later via tooling).
// 4. Vendored packages keep their upstream versions — constraints don't touch
// them beyond privacy.
// 5. All packages are ESM (`"type": "module"`) — except vendored packages
// whose upstream isn't (schemastery ships dual cjs/mjs without type).
/** @type {import('@yarnpkg/types')} */
const { defineConfig } = require('@yarnpkg/types')
const VENDORED = new Set([
'cordis', 'cosmokit', 'schemastery',
'@cordisjs/plugin-loader', '@cordisjs/plugin-include', '@cordisjs/plugin-group',
'@cordisjs/plugin-timer', '@cordisjs/plugin-hmr', '@cordisjs/plugin-logger-console',
])
module.exports = defineConfig({
async constraints({ Yarn }) {
for (const workspace of Yarn.workspaces()) {
const name = workspace.manifest.name
// (1) everything stays private
workspace.set('private', true)
if (name?.startsWith('@deepseek-ai/dsh-') && name !== '@deepseek-ai/dsh-root') {
// (2) cordis as peer + dev
const peer = Yarn.dependency({ workspace, ident: 'cordis', type: 'peerDependencies' })
const dev = Yarn.dependency({ workspace, ident: 'cordis', type: 'devDependencies' })
if (!peer) workspace.error(`${name}: cordis must be a peerDependency`)
if (!dev) workspace.error(`${name}: cordis must also be a devDependency`)
if (peer && dev && peer.range !== dev.range) {
workspace.error(`${name}: cordis peer (${peer.range}) and dev (${dev.range}) ranges must match`)
}
// (3) uniform version
workspace.set('version', '0.0.1')
// (5) ESM
workspace.set('type', 'module')
}
if (name && VENDORED.has(name)) {
// (4) vendored: privacy only; versions/fields follow upstream
continue
}
}
},
})
-4864
View File
File diff suppressed because it is too large Load Diff