Commit Graph
49 Commits
Author SHA1 Message Date
Tianyi Cui d6a2ab30c8 feat(types): brand bash ids + stop brand erosion; extract Branded to dsh-brand
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.

- Extract the `Branded<B>` primitive into a new standalone type-only package
  `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
  dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
  dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
  dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
  generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
  the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
  the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
  SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
  that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
  agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
  at the config boundary and the inner create()/resume casts disappear (only the
  genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
  Map keys and public params/exports (SessionStore, AgentRegistry + factory
  options, the ACP session-id surface + ToolPresenter CallId map, the
  persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
  the Branded type-equiv at dsh-brand, fix stale param types in the session/
  agent/bash READMEs, regenerate the cordis catalog + module graph.

Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
2026-06-21 07:19:59 +08:00
Tianyi Cui 584349f881 fix review findings: stale service prose + catalog cleanup
Codex review of the PR1 diff surfaced docs/cleanup drift:
- LlmService class JSDoc still advertised "streaming / non-streaming call
  surfaces, both interceptable via waterfall events" — corrected to the single
  streaming surface; regenerated the cordis catalog so its mirror updates.
- Removed GenerateResult from gen-cordis-catalog.ts LINK_MAP (the type is gone).
- The adapter-change RFC's acceptance criterion named the retired
  verify-event-taxonomy gate; updated to verify-cordis-catalog.
- Dropped the now-tautological "streaming and one-shot assembly agree" property
  test (the streaming/one-shot distinction lived in the removed flush API;
  usage/finish remain covered by assembler.spec.ts and the finish property).
2026-06-21 01:41:02 +08:00
Tianyi Cui 30cd67b8a1 simplify(llm): drop unconsumed adapter-change event and assembled call surfaces
The LLM service exposed three call surfaces (stream/streamBlocks/generate) but
the only production consumer — the agent loop — uses stream() exclusively,
feeding raw chunks through its own BlockAssembler for replay fidelity. Drop the
speculative convenience surfaces and the registry-change event that no listener
consumed, leaving stream() as the single model-call contract for both
production and tests.

- Remove LlmService.streamBlocks() and generate(), the llm/generate waterfall,
  and GenerateResult.
- Remove the llm/adapter-change event (declaration + emits) and the
  listener-throw rollback ordering that existed only to protect it; keep the
  HMR rollback disposer.
- Remove BlockAssembler.flushReady()/flushRemaining()/result() and the flushed
  cursor — the streaming-flush slice existed only for streamBlocks().
- Adapter tests drive a stream()+BlockAssembler helper (tests/assemble.ts)
  instead of generate(), exercising the same path production uses.
- Land the AGENTS.md "RFCs are proposals, not golden truth" principle and move
  both RFCs proposed -> implemented.

Implements:
- docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
- docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
2026-06-21 01:27:41 +08:00
Tianyi Cui ca2207e26c Fix doc cross-links for the hierarchy; add package-path + shape gates
Merge brought in the RFC-classification reorg and two new doc gates;
rewrite every drifted packages/<name> cross-link (Markdown link targets,
moved-README relative depths, and .ts comment paths) to the grouped paths.

Add two doc-sync/hygiene gates so the manual checks this restructure
needed become automated:
- verify-package-paths.ts: flags a packages/<path> reference (in Markdown
  or a .ts comment/string) that does not resolve AND names a real package
  in a segment — i.e. a stale path to a MOVED package. A path naming a
  non-existent package (a forward-looking proposal) is left alone, so it
  applies uniformly across proposed/implemented/rejected.
- check-workspace-constraints: assert the packages/<group>/<pkg> depth-2
  shape (group dirs carry no package.json; no flat or over-nested
  packages). Group names stay open; only the shape is fixed.
2026-06-20 23:12:14 +08:00
Tianyi Cui 08f6f17cc1 Merge remote-tracking branch 'origin/master' into worktree-package-hierarchy 2026-06-20 22:55:38 +08:00
Tianyi Cui d02e9f1bd6 Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.

Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.

Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.

WIP: doc cross-links and package/RFC docs still to update.
2026-06-20 22:55:20 +08:00
Tianyi Cui 605587e79c docs(rfc): classify RFCs by kind via path-encoded subdirectories
Add a second axis to every RFC — its class (feature, bug-fix,
simplification, architecture, process, testing) — encoded in the path
as docs/rfc/{lifecycle}/{class}/file.md. The folder is the label, so
the closed set is enforced by structure rather than a parsed field.

Two new doc-sync gates back it:
- verify-rfc-classification: every RFC sits in a valid class folder and
  the README index lists it under the matching lifecycle→class heading.
- verify-doc-refs: every docs/*.md path cited in a packages|examples TS
  comment resolves — closes a drift class verify-md-links can't see, and
  catches the four comment refs this reorg moved.

The README gains a Classification section explaining the taxonomy and
per-class index sub-sections. A self-referential process RFC records why
the scheme is path-encoded and gated.
2026-06-20 22:29:45 +08:00
Tianyi Cui f593f33dea Merge remote-tracking branch 'origin/master' into codex/rfc-simplify-candidates 2026-06-20 21:42:01 +08:00
Tianyi Cui 800e08e930 docs: refine package hierarchy RFC 2026-06-20 21:28:02 +08:00
Tianyi Cui d324b06e74 docs: fix Codex review findings on the cordis catalog
- Exclude protected methods from the generated service interface: a protected
  member (e.g. BashExecutor.notifyTaskDone) is a subclass hook, not part of the
  public ctx.<key> surface a plugin author calls. The method filter now drops
  private, protected, and static.
- Add BashTaskRead to the type cross-link map so readOutput()'s return type
  links to its core-data-structures page.
- Reword the generator module comment and the AGENTS.md @mode rule to state the
  current capability without narrating the retired event-taxonomy verifier
  (that history lives in the RFC).
2026-06-20 20:04:39 +08:00
Tianyi Cui 4e5c08ef82 docs: generated cordis events + services catalog
Add scripts/gen-cordis-catalog.ts: a fully-generated docs/cordis-catalog/
events-and-services.md cataloging every cordis event (exact signature + @mode)
and ctx.<key> service (exact interface), modeled on gen-module-graph's
--write/--check freshness gate. The harness tier renders in full from the
interface Events / interface Context declarations and their JSDoc; the inherited
cordis-core/loader/hmr/timer surface renders tersely from a curated table.

The generator hard-errors on a missing @mode tag and on a tag that contradicts
a conclusive signature shape (a trailing next param is structurally a
waterfall). Signature blocks use a ts cordis-catalog fence that doc-typecheck
skips. Type tokens cross-link to the core-data-structures catalog.

This supersedes the hand-maintained event-taxonomy table: verify-event-taxonomy
is deleted and verify-cordis-catalog joins doc-sync. architecture.md keeps the
Event taxonomy heading (TOC anchor) but points at the catalog; the Service-map
role table stays. RFC, AGENTS.md @mode authoring rule, and dependent doc/skill
references updated. Negative gate tests cover the missing-tag and
tag/shape-contradiction paths.
2026-06-20 19:47:09 +08:00
Tianyi Cui 0e73a45dc9 Merge remote-tracking branch 'origin/master' into worktree-acp-feature-checklist
# Conflicts:
#	AGENTS.md
2026-06-20 18:12:55 +08:00
Tianyi Cui f5e61417ee docs: move ACP checklist into packages/acp
Co-locate the ACP feature support checklist with the bridge package
(packages/acp/acp-feature-support.md) and rewrite its relative links for
the new depth. Broaden the doc-sync globs (doc-typecheck, verify-md-wrap,
verify-md-links) from packages/*/README.md to packages/*/*.md so a
package-level doc beyond the README stays under the drift gates, and
update the AGENTS.md prose describing that scope.
2026-06-20 18:08:17 +08:00
Tianyi Cui 5a6243900d fix(doc-sync): close verify-type-equiv scan gap; correct persistence prose
Review found verify-type-equiv only scanned docs the manifest already named, so
a type-equiv block in an unmanifested doc was silently skipped — defeating the
1:1 guarantee. Scan all docs in the markdown glob scope instead, so an orphan
block in any doc is caught. Also parse `abstract class` in blockSymbol (matches
sourceDeclaration's class support).

persistence.md listed the SessionPersistence surface as create/append/load/list;
the abstract service also exposes has/delete. AGENTS.md's doc-sync command
summary omitted verify-md-links and verify-type-equiv.
2026-06-20 17:29:42 +08:00
Tianyi Cui ea3f138ae9 docs: address simplification RFC review 2026-06-20 17:26:23 +08:00
Tianyi Cui cc47f76cea docs: propose simplification RFCs 2026-06-20 16:33:03 +08:00
Tianyi Cui 07048983e0 build(doc-sync): add verify-type-equiv gate for verbatim type pastes
Introduce a `ts type-equiv` Markdown fence: a verbatim paste of a source type
definition that `scripts/verify-type-equiv.ts` drift-checks against the source
symbol via the TypeScript parser, with provenance in a central
`scripts/type-equiv.manifest.json` kept 1:1 with the blocks. doc-typecheck
recognizes the same fence, skips compiling it (not standalone-compilable), and
excludes it from the opt-out ratio. Wired into the `doc-sync` chain.
2026-06-20 16:24:37 +08:00
Tianyi Cui 072f97c184 refactor(examples): extract reusable logic into tested packages
Logic that lived under examples/ was outside the per-file 100% coverage
gate (examples/ are not workspaces) and, in the stdio-UI case, duplicated
across two examples. Move it into packages/ so it is gated and de-duped.

- packages/ui-stdio (new): unify the two diverged stdio-chat.ts copies into
  one @deepseek-ai/dsh-ui-stdio plugin (welcome/agent Config). A test-only
  I/O seam (createStdioChat(ctx, config, runtime)) keeps process streams out
  of the serializable config and makes every render/EOF/disposal branch
  unit-testable. Per-file 100%. echo/coding cordis.yml now load the package;
  both src/stdio-chat.ts deleted.
- packages/llm-replay (new): move examples/acp-agent/src/llm-replay.ts (+ its
  spec) here so its derive/parse/replay branches fall under the coverage gate.
  cordis.snapshot.yml + README rewired to the package name; added apply/env
  /assertNever/abort tests to reach per-file 100%.
- examples/{echo,coding}-agent: keyless Loader-path e2e smokes that boot the
  real cordis.yml (no key) — the guard a hand-mounted unit test cannot be for
  the unwrapExports/export-shape class (postmortem 0001). examples/AGENTS.md
  codifies the keyless+with-key smoke convention (keyless-by-nature exception
  for echo-agent).
- AGENTS.md: a scoped, removal-triggered pre-release stance (foundation over
  blast radius). packages/README.md: new rows + a FIXME to later regroup ALL
  packages into a hierarchy. Wiring: tsconfig paths/refs, publint, knip,
  module-graph.

Verified: typecheck, lint, test:coverage (887 tests, 100%), build, hygiene,
doc-sync, test:snapshot (10), test:e2e (6 keyless pass, with-key self-skip).
2026-06-19 12:42:28 +08:00
Tianyi Cui 7fa113be0e Merge remote-tracking branch 'origin/master' into codex/pr48-repo-hardening-rfcs
# Conflicts:
#	docs/adr/README.md
#	docs/rfc/009-session-persistence-and-resumability.md
#	docs/rfc/README.md
#	docs/rfc/implemented/2026-06-11-doc-sync-enforcement.md
#	docs/rfc/proposed/2026-06-14-acp-agent-client-protocol.md
#	examples/acp-agent/tests/acp.e2e.ts
#	packages/acp/README.md
#	packages/acp/src/index.ts
#	packages/acp/tests/stream-update.spec.ts
#	packages/agent-loop/src/loop.ts
#	packages/tools/src/index.ts
2026-06-18 23:41:14 +08:00
Tianyi Cui 86ec067bff Merge remote-tracking branch 'origin/master' into feat/acp-2-bridge
# Conflicts:
#	.agents/skills/dsh-code-review/SKILL.md
#	AGENTS.md
#	docs/rfc/proposed/2026-06-14-acp-agent-client-protocol.md
2026-06-18 03:31:04 +08:00
Tianyi Cui 27f5f84e3b docs: address Codex review of the RFC reorg
- Fix two root-AGENTS.md cross-links that the depth bump left pointing at the
  new docs/AGENTS.md instead of the root file they cite (capability-seams,
  optional-code-mode). These resolved on disk so verify-md-links passed — the
  gate checks existence, not which file you meant; corrected to ../../../.
- Broaden verify-md-links scope to .agents/skills/**/*.md: this PR rewrote the
  dsh-code-review skill's links into the RFC tree, but the skill dir was outside
  the gate, so a broken skill link would have passed silently.
- Percent-decode the path component before the existence check, so a valid
  encoded relative target (My%20File.md) is not falsely reported broken; a
  malformed escape (%zz) is reported broken rather than crashing the gate.
- Drop the merged property-testing RFC's "nightly CI job 100x" claim: that line
  came from the original proposal, not the accepted decision, and CI has only
  push/pull_request triggers — note it as possible future work instead.

doc-sync (incl. verify-md-links over 58 files), doc-typecheck, lint pass.
2026-06-18 02:41:19 +08:00
Tianyi Cui 7c400e9c02 docs: unify ADR/RFC trees into one lifecycle-organized RFC tree
Collapse docs/adr/ and docs/rfc/ into a single docs/rfc/ with proposed/,
implemented/, and rejected/ subfolders. Every file is renamed to
yyyy-mm-dd-topic-title.md, where the date is when the topic was first
proposed (from git history). ADRs and RFCs that covered exactly the same
topic are merged (property-based testing, session persistence); the
umbrella RFC 005 stays split across its three implemented decisions, and
RFC 006's deferred part-3 (API extractor reports) splits into its own
proposed RFC. All cross-references become machine-checkable relative
links instead of bare "ADR NNNN" / "RFC NNN" prose.

Add a verify-md-links doc-sync gate (scripts/verify-md-links.ts) that
checks every relative Markdown cross-link resolves, wired into doc-sync
alongside verify-md-wrap. This makes the reorganization self-verifying:
the same change that rewrote ~forty inter-doc links adds the check that
proves none dangle. Document the cross-link convention in a new
docs/AGENTS.md and record the gate as an implemented RFC.

doc-sync, typecheck, lint, and the full test suite (667) all pass.
2026-06-18 02:18:24 +08:00
Tianyi Cui 513e16f9dc fix(dev): make hooks and bash seams safer 2026-06-17 21:26:44 +08:00
Tianyi Cui a9d5a5ba68 Merge branch 'feat/acp-1-max-tokens-turn-end' into feat/acp-2-bridge 2026-06-17 15:20:36 +08:00
Tianyi Cui c870c8fcb1 Merge remote-tracking branch 'origin/split/agent-factory' into split/session-persistence-sqlite 2026-06-17 15:05:30 +08:00
Tianyi Cui edd6eb28dd Merge pull request #33 from deepseek-ai/split/session-persistence
feat(session-persistence): abstract seam + JSONL backend + wiring (split 3/5)
2026-06-17 15:01:52 +08:00
Tianyi Cui c2f8af30da Merge branch 'feat/acp-1-max-tokens-turn-end' into feat/acp-2-bridge
# Conflicts:
#	AGENTS.md
#	docs/cookbook/extension-cookbook.md
#	yarn.lock
2026-06-16 23:40:23 +08:00
Tianyi Cui 63425a2b87 refactor: detect md hard-wraps via mdast AST, not regex
Per review feedback (use a real markdown parser with an AST linked to
source positions), rewrite verify-md-wrap to parse each file with
mdast-util-from-markdown (the CommonMark parser behind remark) + the GFM
extension, then flag any `paragraph` node whose source span covers more
than one line.

Why a parser over the hand-rolled line scanner:
- It is a checker, not a formatter — it reports and never rewrites, so
  zero cosmetic churn (no emphasis-marker or table-delimiter
  normalization, which is why Prettier was rejected for this).
- The AST owns every structural exemption (fenced code of any fence
  length, tables, lists, blockquotes, HTML, headings, reference defs),
  fixing both bugs the regex version had: it now catches wrapped
  list-item / blockquote prose (a `paragraph` inside those nodes) and no
  longer false-positives on a longer ```` fence wrapping an inner ```.

Also unwrap two pre-existing hard-wrapped blockquotes (architecture.md,
adding-a-tool.md) that the stricter AST check correctly surfaced.
2026-06-16 23:35:33 +08:00
Tianyi Cui 9fb12740c7 Merge remote-tracking branch 'origin/split/agent-factory' into split/session-persistence-sqlite 2026-06-16 23:20:29 +08:00
Tianyi Cui 16e75c2631 Merge remote-tracking branch 'origin/master' into split/session-persistence 2026-06-16 23:18:06 +08:00
Tianyi Cui 67447fcdc3 feat: enforce merge-commit policy and markdown wrap convention
- AGENTS.md: require merging PRs with a merge commit (gh pr merge
  --merge), never squash/rebase — the per-PR commit history (review-fix
  and regression-test commits) is intentional record.
- Add scripts/verify-md-wrap.ts: a doc-sync gate that fails on
  hard-wrapped prose paragraphs (one physical line per paragraph), with
  smart exemptions for fenced code, tables, lists, blockquotes, headings,
  HTML comments, hrs, and reference defs. Scope covers README.md,
  docs/**/*.md, packages/*/README.md, plus AGENTS.md / packages/AGENTS.md
  (the files doc-sync did not previously cover). Folded into doc-sync so
  it rides the existing pre-push and CI gates.
- Sync AGENTS.md and docs/development.md doc-sync descriptions and
  command lists to include verify-md-wrap.
2026-06-16 22:14:25 +08:00
Tianyi Cui 8e6fd91e15 docs: document module-graph gate; unwrap generated prose
Codex review follow-up:
- Update docs/development.md: add verify-module-graph to the pre-push and
  CI gate lists and gen/verify-module-graph to the daily commands, so the
  contributor guide matches the new freshness gate.
- Emit the module-graph.md intro paragraph as a single line (repo Markdown
  convention: one line per paragraph), since regenerating would otherwise
  reintroduce hard wrapping.
2026-06-16 21:21:21 +08:00
Tianyi Cui b33668ef05 refactor: order module-graph table topologically (low→high level)
Replace the alphabetical package ordering with a Kahn-style topological
sort (alphabetical tiebreak within each layer to stay deterministic), so
the table and graph list leaf interfaces first and dependents last.
2026-06-16 21:11:09 +08:00
Tianyi Cui 4c8c1da8b3 feat: generate module dependency graph with freshness gate
Add scripts/gen-module-graph.ts, which derives the inter-package
dependency graph from each package's @deepseek-ai/dsh-* peerDependencies
and renders docs/module-graph.md (a GitHub-native Mermaid graph plus a
dependency table). Output is deterministic so a regenerate-and-diff
check is stable.

Wire a freshness gate the same way doc-sync is wired (ADR 0007: hooks
and CI run the same package.json scripts): verify-module-graph runs in
pre-push (lefthook) and as a CI step. It fails if the committed file
drifts from what the generator would produce.
2026-06-16 21:00:24 +08:00
Tianyi Cui fb9636db44 feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio
Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the
harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited
stdio), so Zed and other ACP editors can drive the coding agent — streaming
render, tool-call display, and resumable sessions via `session/load`.

- packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/
  prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a
  fallback chain (agent/turn-end → logged turn/end → idle); single-session
  guard; cwd-must-equal-launch-dir validation; load replays from the persisted
  event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*).
- agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent
  implements it (resolves on the first running→idle/disposed transition). The
  bridge awaits it on disposal so teardown reaches quiescence, not just abort.
- examples: extract the shared provider/tool core into examples/base.yml;
  coding-agent nest-includes it; new examples/acp-agent serves the agent over
  ACP with JSONL persistence and no stdout logger (stdout is the protocol).
- Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the
  executor's full authority; only the Agent→sessionId ownership seam is laid
  down. Cancel is best-effort for a not-yet-started queued turn
  (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`.
- Docs: package README + Zed snippet; client-driver cookbook section; root and
  packages layout/commands; RFC 010 implementation-status note.

48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the
example as a subprocess and verifies a written file on disk (key-gated, with a
no-key stdout-purity check).
2026-06-16 18:44:31 +08:00
Tianyi Cui c27b1bba6e Merge branch 'split/agent-factory' into split/session-persistence-sqlite
# Conflicts:
#	yarn.lock
2026-06-16 17:09:39 +08:00
Tianyi Cui 96331432b8 Merge branch 'split/turn-enclosure' into split/session-persistence
# Conflicts:
#	docs/adr/README.md
#	packages/agent-loop/package.json
#	yarn.lock
2026-06-16 17:01:36 +08:00
07akioni dabc2ff411 feat: migrate to pnpm 2026-06-16 14:55:37 +08:00
Tianyi Cui 9126697d87 feat(session-persistence-sqlite): second backend validating the abstraction
Add a SQLite SessionPersistence backend (node:sqlite), a SECOND
implementation built to prove the abstract seam + the shared
runPersistenceContract suite are genuinely backend-agnostic. Each
SessionEvent maps 1:1 onto an events row (session_id, seq, type, time,
data); append is an INSERT inside a transaction asserting the
contiguous-seq contract; the mutable SessionSummary lives in the
sessions metadata row.

It satisfies the SAME contract semantics as the JSONL backend, expressed
over rows instead of file bytes:
- Lazy materialization: create() records intent in memory; no row until
  the first append (a never-appended session is absent from has()/list()
  via a materialized flag set inside the first append transaction).
- Crash-tail-on-load: load() returns events only through the last
  complete turn/end and deletes the uncommitted tail; a seq gap in the
  committed region makes the session unloadable.
- Transactional append: a mid-batch failure (a UNIQUE seq collision from
  a concurrent writer) rolls back entirely, keeping the cursor truthful.

Like the JSONL backend it is also the write-path plugin (session/event →
buffer → session/flush drain, onCreated seed/adopt/collision handling,
HMR seeding, dispose-to-quiescence). The package runs the shared
runPersistenceContract suite plus SQLite-specific tests (transaction
rollback, crash-tail cut, schema version, HMR adoption).

Docs flip every "SQLite is future/deferred" reference (ADR 0016,
architecture.md, the persistence module doc + README) to "implemented;
the contract holds both backends to identical semantics".
2026-06-15 21:45:21 +08:00
Tianyi Cui df4b7d3d9a feat(session-persistence): abstract seam + JSONL backend + wiring
Add the durable session-persistence capability seam (ADR 0016): an
abstract SessionPersistence service (dsh-session-persistence,
ctx.sessionPersistence) defining create/append/load/list/has/delete/
update over the existing SessionEvent — no parallel persisted type — and
a first implementation (dsh-session-persistence-jsonl): an append-only
JSONL log per session with crash-safe atomic writes, truncation-repair
of a never-committed crash tail, and a read/replay path. SessionMeta
(format version, cwd, lineage) travels out-of-log via session.header.

A shared runPersistenceContract suite holds every backend to the same
append-only / contiguous-seq / lazy-materialization / serializability
semantics.

Config-driven create() now uses a per-run ${id}-session-<uuid> session
id so a fixed name no longer collides with an on-disk log once a durable
backend is loaded; each run is a new session (a demo simplification). The
examples drop their hand-rolled session-jsonl.ts and load the JSONL
backend via cordis.yml; CI smoke-loads it too.

The agent-facing create/resume factory that consumes load() is a
separate seam, deferred to a follow-up; this change stops at the load
primitive and does not reach into the loop.
2026-06-15 21:05:46 +08:00
Tianyi Cui 7a39616a06 fix(scripts): harden event-taxonomy brace walk against JSDoc braces (PR 4)
Strip comments before the interface-Events brace walk so a future {@link} tag
(or a // { line) inside an Events block can't unbalance the depth counter.
Codex review flagged this as a latent risk; event names live in code, never in
comments, so stripping loses nothing.
2026-06-14 00:54:47 +08:00
Tianyi Cui 6a528be569 build: doc-sync gates — typecheck doc code blocks + verify event taxonomy (RFC 006 pts 1-2)
Two tsx CI gates make doc/code drift fail fast:
- doc-typecheck extracts every fenced ts block from README/docs/package READMEs,
  compiles them with tsc --noEmit against a temp project (vendor->lib, harness->src
  paths from tsconfig.typecheck.json), and fails on errors. Deliberate sketches opt
  out with ```ts ignore-check; the opt-out ratio is reported and capped.
- verify-event-taxonomy asserts the docs/architecture.md taxonomy table names
  exactly the events declared in the interface Events blocks. This surfaced three
  events the table had been missing (tools/change, llm/adapter-change,
  system-prompt/change), now added.

Doc snippets made compilable with stub imports/declares (1 genuine sketch ignored).
Wired into CI after typecheck. API reports (RFC 006 pt 3) deferred. Graduates RFC
006 pts 1-2 -> ADR 0014.
2026-06-14 00:47:38 +08:00
Tianyi Cui 11a29fdefe feat(invariants): dev-mode event-contract assertions + session-log freeze (RFC 005 pt 3, RFC 008)
New @deepseek-ai/dsh-invariants plugin (pure listeners, off in prod) asserts
the event taxonomy at runtime — seq monotonicity, turn/step nesting, a
tool/result needs a prior tool/call (NOT the converse), legal agent/status
transitions — and deep-freezes logged event data so mutating history throws.
Seeded sessions are checked + frozen on session/created.

The real RFC 008 fix is always-on: deriveMessages now structured-clones the
content it emits, so the loop's sanctioned request/adapter mutation can no
longer reach back and rewrite the append-only log. The pervasive
DeepReadonly<T> type flip is rejected (compile-only, high-noise, castable) —
recorded in ADR 0012, which folds in RFC 008. Wired into both demos.
2026-06-13 23:25:12 +08:00
Tianyi Cui ab19fed77c Add two DeepSeek LLM adapters: dsh-llm-deepseek and dsh-llm-pi-ai
The first real LlmAdapter implementations, shipped as a deliberate pair:
same models and wire protocol, completely different internals, so the
StreamChunk protocol is verified across independent implementations.

- dsh-llm-deepseek: hand-rolled fetch + SSE parser + chunk-translation
  state machine against the official chat-completions format (thinking
  mode via top-level thinking/reasoning_effort; the empty-string
  reasoning_content first chunk; usage attached to the finish chunk or
  trailing; reasoning_content passback on tool-call turns; disjoint
  cache-token accounting).
- dsh-llm-pi-ai: the same endpoint through @earendil-works/pi-ai,
  mapping its event vocabulary (parsed tool arguments, in-stream error
  events, folded reasoning tokens) onto the same chunks.

The agent loop now honors the in-band error path: an adapter that ends
its stream with finish {kind:error|aborted} (the only option for
adapters that can't throw mid-stream, like pi-ai) is translated into a
step error, so the turn ends error/aborted with a logged error event
instead of a normal completed assistant message. This makes the
StreamChunk error contract real for both adapters; docs/architecture.md
and the StreamChunk doc are updated accordingly.

New yarn test:e2e (vitest.e2e.config.ts, *.e2e.ts) runs key-gated
real-API matrices for both adapters across V4 Flash/Pro and all
thinking/effort levels; it self-skips without DEEPSEEK_API_KEY. Unit
suites run against local node:http mock SSE servers at 100% per-file
coverage.
2026-06-13 18:30:03 +08:00
Tianyi Cui 8b5a3ef730 Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):

- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
  process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
  full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
  runtime arg validation and background completion notices via
  agent.inject(). Non-zero exits are reported, not errored.

Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-13 18:28:10 +08:00
Tianyi Cui 630bbddf9a Replace dumble with tsdown for JS bundling
dumble (0.2.x, ~530 dl/wk, single-maintainer) was a bus-factor risk as
the load-bearing bundler. tsdown (rolldown-based, ~2.5M dl/wk, actively
maintained) replaces it while output stays list-identical, verified by
snapshot diff: 17 JS bundles, externals preserved, schemastery dual
.mjs/.cjs and logger-console node+browser entries intact.

Root tsdown.config.ts uses workspace globs ['vendor/*', 'packages/*']
(explicit, so examples/* stays excluded); two per-package overrides in
vendor/ cover the special shapes and are logged in vendor/README.md as
ours (not upstream sync surface). scripts/build.ts (dumble
orchestration) is deleted; yarn build = tsc -b && tsdown. tsc -b keeps
owning declarations (dts: false, clean: false).

Rationale recorded in ADR 0008 (also covers the direct-esbuild and
pkgroll alternatives).

Gates: lint, typecheck, 134 tests, hygiene (knip/publint/constraints),
demo smoke all green.
2026-06-11 22:34:43 +08:00
Tianyi Cui 9d20a36cc4 Add lefthook git hooks with a vendor-manifest guard
pre-commit: ESLint --fix on staged files (vendored source excluded),
incremental typecheck, and the vendor-manifest guard — any staged
change under vendor/*/src must be accompanied by a vendor/README.md
update in the same commit, mechanizing the local-modification log
discipline. pre-push: tests + hygiene (knip/publint/constraints).
Hooks call the same package.json scripts CI runs (single source of
truth); installed automatically via postinstall.
2026-06-11 15:07:55 +08:00
Tianyi Cui 6796a3922d Add repo hygiene gates: knip, publint, yarn constraints
knip (workspace-aware config) fails on unused files, exports, and
dependencies in our code — vendor/ excluded, duplicate default+named
exports allowed (intentional API shape). publint checks every
packages/* package.json for publishing correctness (scripts/
publint-all.ts). Yarn constraints (yarn.config.cjs) mechanize the
AGENTS.md package rules: everything private, dsh-* packages declare
cordis as matching peer+dev dependency, uniform 0.0.1 version, ESM.

yarn hygiene runs all three.
2026-06-11 15:04:50 +08:00
Tianyi Cui ae2e08b4d6 Set up monorepo infra: Yarn 4 workspaces, tsc -b + dumble build, vitest
Workspaces are vendor/* (Cordis framework vendored as source) and
packages/* (@deepseek-ai/dsh-* harness packages). Dev/test/demo run
unbuilt via tsx + root tsconfig paths; build = tsc -b (declarations)
+ dumble (JS bundles); tests = vitest + vite-tsconfig-paths.
2026-06-11 10:52:45 +08:00