diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 1ab6931696..aad9821d04 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -13,7 +13,9 @@ packages/// # you use Config, + ../..// for each dsh dep) src/index.ts # service default export or plugin (name/inject/apply/Config) tests/.spec.ts - README.md # service API, events, extension points, design notes + README.md # service API, events, extension points, design notes, + # + the gated "Known Limitations and Deferred Work" section + # (or a whitelist entry in scripts/verify-readme-limitations.ts) ``` Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it. diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index 654b68ffb5..9b13bfe557 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -161,6 +161,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Raise the Node LTS engine floor to 22.19](implemented/process/2026-07-06-node-engine-floor.md) | 2026-07-06 | | [Parallel GitHub CI gates](implemented/process/2026-07-06-parallel-github-ci-gates.md) | 2026-07-06 | | [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 | +| [A gated Known-Limitations section in every package README](implemented/process/2026-07-10-readme-known-limitations-gate.md) | 2026-07-10 | ### Testing diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md new file mode 100644 index 0000000000..cc25c0efe7 --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md @@ -0,0 +1,28 @@ +# RFC: A gated Known-Limitations section in every package README + +Status: implemented + +## Problem + +The [documentation standard](../../../AGENTS.md) assigns limitations to the package-README tier ("the per-package contract: config, semantics, limitations, extension points"), but nothing enforced that the section exists or shares a shape. Ten READMEs carried the content under five ad-hoc headings — "What is NOT here (TODO)", "What is NOT here", "Deferred (faithful-but-degraded)", "Limitations (MVP, documented deliberately)", "Known limitations (tracked TODOs)" — and the other forty-odd carried nothing, so a reader could not distinguish "this package has no known limitations" from "nobody wrote them down", and no grep could enumerate the repo's known gaps. + +## Decision + +Every `packages///README.md` carries a canonical `## Known Limitations and Deferred Work` section: a condensed bullet list of consumer-visible gaps (unimplemented features, platform caveats, deliberate MVP cuts) and consciously postponed work (TODO markers, RFC deferrals still open). A `doc-sync` gate, `verify-readme-limitations` ([scripts/verify-readme-limitations.ts](../../../../scripts/verify-readme-limitations.ts)), enforces the shape per README: exactly one limitations-like heading, byte-equal to the canonical h2, with at least one top-level bullet. Near-miss headings at any level ("Limitations", "Deferred", "What is NOT here", …) fail the gate, so variant sections cannot creep back beside — or instead of — the canonical one. + +A package with genuinely nothing to declare is whitelisted (`NO_LIMITATIONS` in the script) and must NOT carry the section. The inverted check keeps the whitelist honest in both directions: an empty or boilerplate section cannot satisfy the gate, and giving a whitelisted package real limitations forces the whitelist edit in the same change. Whitelist entries are validated against the scanned package set, so a package rename or removal fails loud instead of silently un-gating a README. + +The gate checks presence, shape, and the whitelist; the bullets' truthfulness and specificity are governed by review under the documentation standard, like the rest of the README tier. The standing rule lives in [packages/AGENTS.md](../../../../packages/AGENTS.md). + +## Alternatives considered + +- **Free-form headings, gate only that "something limitations-like" exists** — keeps the five variants, stays un-greppable, and needs the same near-miss heuristics anyway without buying uniformity. +- **Require the section in ALL READMEs, allowing an empty body or "None."** — boilerplate "None" rots silently as a package gains real limitations; the whitelist inversion turns "nothing declared" into an explicit, lintable claim that review can challenge. +- **A word-count ceiling on the section** — limitation lists are legitimately variable in length; package READMEs are deliberately unbudgeted (per the [budget policy](../../../AGENTS.md)) and review governs their prose. + +## Consequences + +- A new package cannot ship without either declaring its gaps or explicitly claiming it has none; a missing, drifted, or empty section fails `doc-sync` locally (pre-push) and in CI (`readme-limitations` in the run-gates doc-sync leaf set). +- The pre-existing variant sections are normalized to the canonical heading, and every package README now answers the limitations question one way or the other. +- One more fast tsx script in the `doc-sync` chain; no new dependency (plain `node:fs` glob + line scan). +- The canonical heading is enforced verbatim, so renaming it later is a mechanical one-script-plus-all-READMEs change guarded by the same gate. diff --git a/package.json b/package.json index ed0b603feb..2b697070c1 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "verify-type-equiv": "tsx scripts/verify-type-equiv.ts", "verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts", "verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts", + "verify-readme-limitations": "tsx scripts/verify-readme-limitations.ts", "verify-node-next-types": "tsx scripts/verify-node-next-types.ts", "gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts", "gen-rfc-index": "tsx scripts/gen-rfc-index.ts", @@ -63,7 +64,7 @@ "verify-scoped-dispatch": "tsx scripts/verify-scoped-dispatch.ts", "verify-module-graph": "tsx scripts/gen-module-graph.ts --check", "constraints": "tsx scripts/check-workspace-constraints.ts", - "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-dispatch && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets", + "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-dispatch && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-readme-limitations", "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types", "demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml", diff --git a/packages/AGENTS.md b/packages/AGENTS.md index ac26b926f7..6a39b65b1f 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -12,5 +12,6 @@ Naming notes: - `src/types.ts` contains only types — no runtime code. - Tests live at package level under `tests/`, not `src/__tests__/`. - A package's README and JSDoc are part of the change: altered behavior (config keys, defaults, error codes, wire fields) updates them in the same commit. `doc-sync` gates what it can; prose accuracy stays on the author ([the documentation standard](../docs/AGENTS.md)). +- Every package README carries a `## Known Limitations and Deferred Work` section — condensed bullets for consumer-visible gaps and consciously postponed work; `verify-readme-limitations` (in `doc-sync`) gates it. A package with genuinely nothing to declare is instead whitelisted in [scripts/verify-readme-limitations.ts](../scripts/verify-readme-limitations.ts) and must not carry the section ([rationale](../docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md)). -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, known limitations. diff --git a/packages/bash/bash-local/README.md b/packages/bash/bash-local/README.md index 1ac8a7d06b..0a2c2ec5ec 100644 --- a/packages/bash/bash-local/README.md +++ b/packages/bash/bash-local/README.md @@ -25,6 +25,12 @@ Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi; - **Model-friendly env + credential scrub** — `process.env` minus credential-shaped vars (`*KEY*`/`*SECRET*`/`*TOKEN*`), then `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` (Codex's hardcoded set) so pagers and ANSI color don't garble results. This scrub is the security control that keeps the harness's *ambient* credentials out of a spawned command. A spec's `env` is merged LAST (after the scrub), so a caller's explicit entry — a value it already holds — wins even on a credential-shaped name. The spec's `stdin`, when supplied, is written to the child and closed; with none supplied, fd 0 is `/dev/null` — the exact pre-seam default, so a command that probes stdin's file type is unaffected. Both `env`/`stdin` are set by in-process plugins (the hooks bridges); the model-facing tool doesn't expose them. See [the bash-stdin-env RFC](../../../docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md). - **Background tasks** — `start()` returns immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), `readOutput()` is incremental with whole-stream byte offsets, and disposal kills everything. The spec's opaque `owner` token is stored on the tracked task and returned by `ownerOf(id)` — the executor never interprets it (the consumer's access policy does), and because it lives with the task here it survives a `tool-bash` HMR reload. -## Sandboxing +## Known Limitations and Deferred Work -Execution policy does NOT belong in this package: this executor always runs commands unconfined. Confinement is [`dsh-bash-sandbox`](../bash-sandbox/README.md), which extends this executor verbatim and confines commands under the `ctx.sandbox` seam's bwrap/Landlock/Seatbelt backends ([sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md)); per-call allow/deny/ask policy belongs on the `tools/pre-execute` gate. +- **Unconfined by itself** — this executor always runs commands with the harness process's authority; deployments needing confinement compose [`dsh-bash-sandbox`](../bash-sandbox/README.md), while per-call allow/deny/ask policy belongs on `tools/pre-execute`. +- **POSIX-only** — the `bash` binary, detached process groups, group kills, and SIGTERM→SIGKILL escalation are hardcoded; Windows is unsupported. +- **The credential scrub is a name heuristic** — `*KEY*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSWORD*`) pass through, and a whitelist for over-scrubbed vars is noted future work. +- **Spill files are never deleted** — full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them. +- **Finished background tasks are never evicted** — they stay in the task map, retaining their in-memory output tails, until executor disposal. + +The raw process handling lives in `src/run.ts`; `src/index.ts` is the service wiring. diff --git a/packages/bash/bash/README.md b/packages/bash/bash/README.md index ac7e00c3a3..55bf9d0703 100644 --- a/packages/bash/bash/README.md +++ b/packages/bash/bash/README.md @@ -35,3 +35,9 @@ Implementations subclass `BashExecutor`, implement the abstract methods, and cal The seam also owns the per-session mode override vocabulary (the sandbox RFC § Per-session mode switching): the log-only `'bash/sandbox-mode'` session event, the pure fold `effectiveSandboxMode(events)` (last event wins; `undefined` means "apply the executor default"), and THE write path `setSandboxMode(session, mode)` — the session log is the store, so an override survives restart by replay and two sessions can never see each other's mode. Writers must respect turn-enclosure: the ACP bridge anchors an idle switch at the next turn rather than appending between turns. The task id (`BashTaskId`) and the `owner` token (`OwnerToken`) are [branded](../../util/brand) — `OwnerToken` is a DISTINCT brand from `SessionId` (the seam never imports `dsh-session`; the `dsh-tool-bash` consumer is the single boundary that casts its `SessionId` into one). `run()` returns `BashRunResult` (exitCode, signal, timedOut, aborted, timeoutMs, stdout/stderr as `CollectedOutput`) and `start()`/`readOutput()` use `BashTask`/`BashTaskRead` for the background side. A sandboxing executor additionally stamps `sandbox` result facts on results and settled tasks (`BashSandboxInfo`: the mode it executed under, the conservative `denied` classification, and — for confined modes — the backend's `enforcement` completeness); the mode/enforcement vocabulary is owned by the [`dsh-sandbox`](../../sandbox/sandbox/) seam, and the facts are documented in [core-data-structures/bash.md](../../../docs/core-data-structures/bash.md). See `src/types.ts` for the full contracts. `stdin` and `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload on stdin and its `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` env. The model-facing `dsh-tool-bash` tool does not expose them as parameters — a model already has equivalent power through shell syntax (`FOO=bar cmd`, a heredoc), so they would be redundant tool params. This is not a security boundary: the implementation's credential scrub (not these fields) is what keeps the harness's ambient secrets out of a spawned command. They are plain optionals on the resolved spec (unlike `owner`'s required-but-nullable): a missing one means "none", the safe default. See [the bash-stdin-env RFC](../../../docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md). + +## Known Limitations and Deferred Work + +- **No interactive-input vocabulary** — `stdin` is written once at spawn and closed; the seam has no channel to feed a running task and no PTY session concept. +- **Two overlapping surfaces flagged for pruning** — `BashTask.done` duplicates `onTaskDone` (shipped consumers use only the latter), and `get()`/`list()` have test-harness consumers only; both are marked in [the long-running-runtime RFC](../../../docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md). +- **Foreground timeouts are always executor-owned** — a caller-owned-deadline mode on the seam is explicitly deferred by [the tool-call timeout-policy RFC](../../../docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md). diff --git a/packages/bash/tool-bash/README.md b/packages/bash/tool-bash/README.md index 800de0132c..6251b659fb 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -57,3 +57,8 @@ On top of a denial sits the escalation gate ([the sandbox RFC § Escalation](../ ## Per-session mode switching Under a sandboxing executor this plugin makes the session's standing mode override ([the sandbox RFC § Per-session mode switching](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md); the `bash/sandbox-mode` fold owned by [`dsh-bash`](../bash/README.md)) real at EXECUTION: every call is stamped `escalation grant > session override > undefined` onto `BashExecRequest.sandboxMode`; without either, the executor's `resolve()` applies its configured default. Nothing is stamped under a non-sandboxing executor (nothing would honor it) or for an agent-less caller (no session to fold). The prompt deliberately does NOT state the mode and a switch is not narrated: a standing declaration teaches the model to refuse preemptively, while the denial marker already names the mode the command ran under exactly when the boundary is hit — behavior, not belief, carries the state. + +## Known Limitations and Deferred Work + +- **Replay exit pills parse from result text** — output whose final line happens to be exactly `[exit code: N]` / `[killed by signal: …]` shows a wrong pill on session replay; a display-only known residual. +- **The bash tools opt out of `timeout-policy` budgets** — `bash` keeps the executor-owned `BASH_TIMEOUT` path and `bash_output`/`bash_kill` declare no budget, per [the tool-call timeout-policy RFC](../../../docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md). diff --git a/packages/code-runtime/code-runtime-worker/README.md b/packages/code-runtime/code-runtime-worker/README.md index f691904e88..a483b00839 100644 --- a/packages/code-runtime/code-runtime-worker/README.md +++ b/packages/code-runtime/code-runtime-worker/README.md @@ -30,3 +30,11 @@ Every field is validated (positive numbers) and defaulted; there are no other tu ## The worker entry, unbuilt and built `worker.ts` is deliberately erasable-only TypeScript with type-only cross-package imports: unbuilt (vitest/tsx), the host spawns `src/worker.ts` directly and Node's native type stripping loads it; built, the entry ships as the sibling bundle `lib/worker.js` (its own tsdown entry). The built path is pinned by `tests/built-lib.e2e.ts`, the real-load-path guard from [docs/testing.md](../../../docs/testing.md). + +## Known Limitations and Deferred Work + +- **OS processes a program spawns survive termination** — `worker.terminate()` ends the thread only, weaker than bash-local's process-group kill; orphan cleanup is a deployment concern until a container backend exists. +- **Type-strip rides Node's experimental `stripTypeScriptTypes` API** — the relied-on behavior is pinned by unit tests, with amaro/sucrase as named drop-in replacements if it shifts. +- **`computeMs` expiry can overshoot by up to one poll interval** — busy time is sampled every 25 ms (an internal constant, deliberately not config). +- **Programs get a five-method `console` shim** (`log`/`info`/`warn`/`error`/`debug`) — deliberately not Node's full console surface. +- **A non-cloneable or oversize completion value does not cross as a value** — it arrives as a bounded, truncation-marked `util.inspect` rendering in `value`'s place. diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index 20c9274b9c..ac903ac6d4 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -17,3 +17,9 @@ Semantics every implementation must honor (contract details in the class JSDoc): ## Vocabulary `CodeRunRequest` (`program`, `bindings`, `signal?`) carries everything the runtime acts on — defaulting (time budgets, output caps) is the implementation's validated config, never a hidden `??` inside `run()`. `bindings` is a list of `CodeBindingNamespace`s (`global` + `functions`), each exposed to the program as one global object of async callables. `CodeRunResult` reports the completion `value?`, the ordered `logs` (`CodeLogEntry`: `console`/`stdout`/`stderr` source, console `level`, capped text), and the `error?` (`CodeRunFailure`: `kind` + model-feedable `message`). See `src/types.ts` for the full contracts. + +## Known Limitations and Deferred Work + +- **`run()` is one-shot** — `logs` arrive only on the resolved `CodeRunResult`; the seam exposes no streaming-log or progress surface for a live program's output. +- **A persistent REPL-style kernel is recorded future work** — the no-state-between-runs contract stands until a persistent-kernel backend brings its own logging story ([Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md)). +- **Only the worker-thread backend ships** — `'process'`/`'container'` are declared well-known `isolation` values with no implementation; a hard security boundary awaits a container backend. diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index a06e74b818..f68b52c8b5 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -56,3 +56,11 @@ export function apply(ctx: Context): void { ``` Loading the plugin registers `ctx.compact`. With `auto: true` (the default) it compacts automatically under token pressure; a consumer (a future `/compact` tool) can also call `ctx.compact.compactIfNeeded(...)` or `ctx.compact.compactRegion(...)` directly. + +## Known Limitations and Deferred Work + +- **Token estimation is the chars/`charsPerToken` heuristic** — a marked TODO schedules replacing it with an exact count (a real tokenizer, or provider `usage` fed back) so thresholds track the model's actual budget. +- **`estimatePressure()` does not count the request's `tools` field** — pressure is underestimated by the size of the serialized tool schemas the request also carries. +- **`compactRegion` requires an open turn** — a manual call on a fully-closed session throws ("no open turn") rather than compacting. +- **Summarization failure fails closed with full, over-budget history** — including truncation at the summarization `maxTokens`, which hidden reasoning tokens can consume; the auto path logs a warning and proceeds. +- **The summarization call has no transcript-snapshot coverage** — `dsh-llm-replay` derives calls from `assistant/chunk` events, so this chunk-less direct `ctx.llm.stream()` call cannot replay (named deferred replay infrastructure in [the seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)). diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index e98f00899f..3e47f9ca85 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -7,7 +7,7 @@ This package is the interface tier of the compaction capability, split so each c | Package | Role | |---|---| | `@deepseek-ai/dsh-compact` (this) | the interface: abstract service + `compact/*` events + `CompactionResult` + the shared transcript renderer (`renderTranscript`/`renderContentBlocks`) | -| `@deepseek-ai/dsh-compact-basic` (deferred) | a backend: chars-per-token estimation (`charsPerToken`, default 4) + token-budget retention + `llm.stream()` summarization | +| `@deepseek-ai/dsh-compact-basic` | a backend: chars-per-token estimation (`charsPerToken`, default 4) + token-budget retention + `llm.stream()` summarization | | `@deepseek-ai/dsh-tool-compact` (deferred) | the model-facing `/compact` tool over `ctx.compact` | Unlike the bash seam, this interface depends on `@deepseek-ai/dsh-session` and `@deepseek-ai/dsh-llm` — the contract's verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary, so they cannot be expressed without naming those packages. That deviation from the "interface depends only on cordis" guidance is intentional and recorded in the [compaction capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). @@ -48,3 +48,10 @@ The `compact/*` events extend `SessionEventMap` (merge-extensible) via declarati ## Implementing a backend Subclass `CompactService`, implement `compactIfNeeded` and `compactRegion`, and load the subclass as a plugin — it registers as `ctx.compact`. A tokenizer-, template-, or model-backed implementation can live as a sibling package without changing callers. + +## Known Limitations and Deferred Work + +- **No model-facing consumer tier yet** — `@deepseek-ai/dsh-tool-compact` (the `/compact` tool) is deferred; compaction is reachable only via direct `ctx.compact` calls or a backend's auto listener. +- **Single-unit overflow is out of contract** — one retained unit (a closed step or a large pasted `user/message`) alone exceeding the budget cannot be compacted; the call may go out over-budget. +- **A session prefix that alone approaches the window is a configuration error no backend fixes** — compaction shrinks derived history, never the prefix. +- **Request context injected by downstream `agent/request` listeners sits outside pressure accounting** — `compactIfNeeded` counts prefix, derived history, and system prompt only. diff --git a/packages/compact/compact/src/index.ts b/packages/compact/compact/src/index.ts index 7d1138314c..49330f7647 100644 --- a/packages/compact/compact/src/index.ts +++ b/packages/compact/compact/src/index.ts @@ -11,7 +11,7 @@ * touching consumers. * * The split follows the capability-seams RFC — interface (this) / - * implementation (deferred) / consumer (a `/compact` tool, deferred) — modeled + * implementation (`dsh-compact-basic`) / consumer (a `/compact` tool, deferred) — modeled * on the bash trio. Unlike `dsh-bash`, this interface necessarily * depends on `dsh-session` and `dsh-llm`: the contract's verbs are defined over * a `Session` and its output is the `ContentBlock` vocabulary. That deviation diff --git a/packages/cordis/tool-cordis/README.md b/packages/cordis/tool-cordis/README.md index c4f3cf11b9..78ec4ee94b 100644 --- a/packages/cordis/tool-cordis/README.md +++ b/packages/cordis/tool-cordis/README.md @@ -31,3 +31,9 @@ All three tools render `generic` cards (`read` / `execute` / `delete`); `cordis_ ## Export shape Namespace plugin: named exports `name` / `inject` / `Config` / `apply`, no default export ([docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). + +## Known Limitations and Deferred Work + +- **The sandbox is containment for honest code, not a security boundary** — host-realm helpers on the sandbox global are reachable, so mount code can reach Node; load this plugin as deliberately as you would grant a bash tool (see § Trust stance). +- **The `ctx` façade exposes no `effect()`** — mount code cannot register a bespoke disposer; `on`/`provide`/`tools.register` cover every mount seen so far, and a guarded `effect` waits on a real need (`FIXME(sandbox-effect)`). +- **`vmTimeoutMs` bounds only synchronous evaluation** — an async mount body escapes it; there is no async budget on mount code. diff --git a/packages/core/agent-core/README.md b/packages/core/agent-core/README.md index 33cd0faddd..28e75a9d80 100644 --- a/packages/core/agent-core/README.md +++ b/packages/core/agent-core/README.md @@ -48,3 +48,8 @@ The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loo ## Why a code bundle, not a shared YAML include A YAML include can dedupe the config, but it cannot OWN a `bin`, and it can only *describe* the front-door coupling in a comment and trust each leaf to obey. Moving the spine into a package, and the front-door cluster into the app packages, means the default leaf for an ACP server has no logger entry to copy wrong — "the ACP app never logs to stdout" stops being a prose warning a leaf must remember and becomes the app package's default shape (a leaf can still add a sibling logger, so the rule stays documented — but it has nothing to get wrong by default). Services register in the root store keyed by their isolate symbol, so a child loaded here is visible to the bundle's siblings (the leaf's adapter and executor) exactly as a nested `plugin-include` subtree's services were — cordis gates every read on `inject`, never on load order. + +## Known Limitations and Deferred Work + +- **The spine set is fixed in code** — `apply()` mounts every child unconditionally (including `tool-bash`); no config excludes or replaces one, so swapping the loop or dropping a spine member means composing a different bundle. +- **`dsh-invariants` mounts unconditionally** — a dev/test plugin with default `freeze: true` and no reachable toggle in this bundle's `Config`, so every deployment currently pays the assertion/freeze cost. diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 217ba0a643..1004083829 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -93,7 +93,7 @@ Error containment: a throwing plugin ends the **turn**, never the loop. A malfor Cancellation: `agent.cancel()` is the single public stop primitive — it clears the queued + steering FIFOs, aborts the in-flight step, and drives a turn-scoped marker the driver checks at every point a turn could start or continue (right after the idle wait, after the `running` flip, before each step, and at the continuation gate) so a turn about to start is dropped. A cancelled turn ends `aborted`; a queued-but-not-started prompt never runs and cannot be batched into the cancelled turn. The marker is reset once per loop iteration, so a cancel governs exactly one turn and never leaks onto a later prompt. (The loop still aborts its own per-step `AbortController` directly on disposal and from `cancel()`; that controller is loop-internal, not a public verb.) -### What is NOT here +### What belongs to plugins Everything that goes beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy: - Hooks and policy: the relevant `agent/*` checkpoints plus the guarded `tools/pre-execute` → `tools/execute` → `tools/post-execute` → `tools/result` pipeline; exact signatures and modes live in the [generated event catalog](../../../docs/cordis-catalog/events.md) @@ -102,3 +102,11 @@ Everything that goes beyond "call the model, run the tools, repeat" belongs to p - Sub-agents: implemented outside the loop as `ctx.subagents` providers; in-process providers use `ctx.agents.create()` and owned `AgentHandle` teardown, while child streaming/progress and background/poll collection remain deferred. - Persistence: `session/event` + `session/flush` - UI: `session/event` (assistant token stream, boundaries, tool activity) + `agent/*` control events (`agent/status`, `agent/created`/`agent/disposed`) + +## Known Limitations and Deferred Work + +- **Tool calls within a step execute sequentially** — parallel execution waits on concurrency-safety metadata in the tool contract (see `dsh-tools`). +- **No resume-or-create policy on the config path** — config-driven `create()` starts a fresh `${id}-session-` every run (`TODO(demo)`), and a config `resumeSessionId` whose resume fails logs a warning and creates no agent. +- **Config agents carry no session `cwd`** — `Config` has no cwd field, so a persona referencing `{{cwd}}` fails prompt assembly for config-created agents. +- **No built-in turn budget** — the default continuation is `continue` whenever a step had tool calls or steering; bounding a runaway turn requires an `agent/turn-continuation` force-stop plugin. +- **`runLoop`/`Inbox`/`InboxMessage` stay exported with no outside consumer** — [removal is proposed](../../../docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md). diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 71e2be279d..c31eb3680c 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -52,6 +52,12 @@ The handle every plugin programs against: - Event listeners: all `agent/*` events are declared here — no dependency on the loop package needed. - Subagent delegation: implemented by `@deepseek-ai/dsh-subagent`, not by a method on `Agent`; providers create or drive ordinary `Agent` handles through the factory seam, so spawn/fork/ACP transports stay outside the core agent interface. -### What is NOT here (TODO) +## Known Limitations and Deferred Work - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam. +- **`agent/session-start` cannot gate startup** — a synchronous, veto-less emit, so an async listener's injection is best-effort before turn 1; startup gating is a deferred loop-level change. +- **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface RFC](../../../docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). +- **`AgentRegistry` enforces unique agent ids, not session ids** — two live agents sharing a session id mis-route bash owner-token notices; [id unification stays proposed](../../../docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.md). +- **`HookContext` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable. +- **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`). +- **`agent/pre-step`'s `fullSystemPrompt`/`sessionPrefix` parameters are a flagged smell** — compaction is their only consumer; a lazy prompt provider or a compaction-specific pressure seam is the marked revisit. diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 094c3073df..fe15d4ab97 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -73,6 +73,9 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata) - Replay/fork: `ctx.sessions.create(id, { seed })` seeds a new session with an existing event log. The surface rebuilds deterministically from `surfaceOp` markers in the seeded events. The seed is validated to the SAME always-on invariants `append` enforces — contiguous seqs, JSON-serializable data, and required `surfaceOp` markers on surface-eligible events — so marker-less message events are rejected at construction rather than silently vanishing from `deriveMessages()`. Broader turn-enclosure checks stay in `dsh-invariants` and persistence repair. Ordinary live-session forks use `ctx.sessions.fork(source, boundary?, childSessionId?)`, where `boundary` is the inclusive source event seq to fork through. - Compaction: the `dsh-compact-basic` plugin appends a `user/message` with `surfaceOp: { op: 'replace', start, end }` to shadow old surface nodes behind a summary checkpoint. -### What is NOT here (TODO) +## Known Limitations and Deferred Work - **Session branching/tree** (pi-style entry tree) — deferred unless needed beyond boundary-based `fork()`. +- **`fork()` cuts only at closed-turn boundaries of live sessions** — the boundary must be a `turn/end` event and the source must be in the store; forking a persisted-but-unloaded session is excluded from the [fork API](../../../docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md). +- **`SESSION_FORMAT_VERSION` stays pinned at `0`** — pre-release, no compatibility implied: a backend rejects any other version, and no migration path exists until the first release ([policy](../../../AGENTS.md)). +- **`TurnEndReasonMap` omits the ACP-named `refusal` / `max_turn_requests` variants** — producer-gated: they land when an adapter or the loop first emits them. diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index d712cd4b5b..f55df539d3 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -41,9 +41,13 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse - The `system-prompt/assemble` waterfall: mutate or replace the assembly per caller (dynamic tool filtering, extra variables). - `systemPrompt.protect()`: reserve canonical section/tool contributions for invariants that ordinary waterfall listeners must not be able to remove or replace. -### What is NOT here - -- Any deployment-authored prompt text outside config — the persona is this plugin's `persona` config, and every other section comes from the plugin that owns the fact. (The `harness:identity` line is deliberately a code literal: a harness fact, not a deployment choice; the `system-prompt/assemble` waterfall is the escape valve for a deployment that must drop it.) -- Prompt compaction (belongs on the `agent/pre-step` seam in `dsh-agent`). - Design rationale: [the prompt-variables RFC](../../../docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md). + +## Known Limitations and Deferred Work + +- **No deployment-authored prompt text outside config** — the persona is this plugin's `persona` config, and every other section comes from the plugin that owns the fact. (The `harness:identity` line is deliberately a code literal: a harness fact, not a deployment choice; the `system-prompt/assemble` waterfall is the escape valve for a deployment that must drop it.) +- **No prompt compaction here** — it belongs on the `agent/pre-step` seam in `dsh-agent` (implemented by `dsh-compact-basic`). +- **No escape syntax for literal `{{…}}` braces** — every complete group is interpolated against registered variables; an escape is deferred until a real prompt needs one. +- **`persona` is context-global** — one deployment fragment shared by every agent, subagents included; per-agent personas need a `system-prompt/assemble` listener. +- **`toolOrder` misconfiguration surfaces at prompt assembly (the first turn), not at boot** — only shape violations throw at config load. +- **Sections sharing an `order` value tie-break by registration order** — a plugin-load artifact; determinism relies on the distinct-order band convention, unlike the canonicalized tool order. diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 2a678408e0..b46a109679 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -140,7 +140,12 @@ Under `mode: code` (or `both`) the registry turns the tool surface into a progra The wire collapse is the registry's own contribution (`systemPrompt.tools()` is mode-aware), so the logged `request/header` records it for free. With no deliberate schema-adding assembly listener, `code` assembles exactly `[run_code]`, pinned by tests and the snapshot goldens; protection guarantees that `run_code` and `tools:sdk` remain present, not that unrelated listener additions are erased. Try it: `pnpm run demo:code-mode` ([the coding-agent example's Code Mode overlay](../../../examples/coding-agent/README.md#code-mode)); `pnpm run demo:code-mode acp` serves the same mode over ACP instead of the REPL. -### What is NOT here (TODO) +## Known Limitations and Deferred Work -- **Concurrency metadata** — tool definitions do not declare whether executions are safe to overlap. -- **Parallel execution** — the loop and Code Mode bridge execute tool calls sequentially until that metadata exists. +- **Native tool calls execute sequentially** — `ToolDefinition` carries no concurrency-safety metadata; adding it (and parallel execution in the loop) waits on the deferred tool-shapes review (`TODO(review)`). +- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md). +- **`defineTool`'s schema DSL is a deliberate subset** — string/number/boolean/object/array with string-only `enum`; `validateArgs` tolerates extra keys and never applies `default` (`XXX(unused-default)` flags removing that field); raw-registered JSON-Schema tools validate their own input. +- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper. +- **Code Mode is TypeScript-only and registry-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`, and per-tool visibility tiers (one tool native, another code-only) are knowingly deferred. +- **Code Mode bindings return text only** — non-text content blocks in a sub-call result collapse to `[ content]` placeholders. +- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md). diff --git a/packages/fs/fs-local/README.md b/packages/fs/fs-local/README.md index 26524fad24..21a057b101 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -19,8 +19,13 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) - **`writeText`** — atomic: writes to a temp file opened exclusively (`wx`, `0o600`) inside a randomly-named private staging dir (`0o700`) next to the target, fsyncs, then renames over the target. An existing file's mode is preserved, while new files default to `0o600`. The `expected` guard is OPTIONAL: omitting it unconditionally creates-or-overwrites; `createIfAbsent` creates a missing target and rejects an existing one (`FS_NOT_OBSERVED`); `replaceIfVersion` replaces only at the observed version (a missing target or mismatch is `FS_STALE_VERSION`). - **`editText`** — atomic literal read-modify-write over the same primitive, serialized per target by a mutation lock. The `expected` guard is OPTIONAL: when supplied it verifies the version BEFORE literal matching (a stale edit reports `FS_STALE_VERSION`, never `FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT` against newer content); omitting it edits the current content unconditionally. A missing target reports `FS_STALE_VERSION` either way. LF-normalizes for matching, restores the file's dominant CRLF/LF style, and rejects empty `oldString` / zero matches (`FS_EDIT_NOT_FOUND`) or ambiguous multi-matches without `replace_all` (`FS_AMBIGUOUS_EDIT`). -## `cwd` is not a sandbox - -`config.cwd` is a resolution default, not a containment boundary — absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall. See [the filesystem capability-seam RFC's Consequences section](../../../docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md#consequences). - The raw I/O lives in `src/fsio.ts` (Cordis-free, independently unit-tested); `src/index.ts` is the thin service wiring. + +## Known Limitations and Deferred Work + +- **`config.cwd` is not a sandbox** — it is a resolution default, not containment: absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall ([capability-seam RFC](../../../docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md#consequences)). +- **An overwrite reads the whole prior file into memory** — solely as the UI diff basis; bounding that pre-read above a size threshold is deferred (`TODO(overwrite-diff-bound)`). +- **Version tokens are `mtimeMs:size`** — an external change that preserves both within the filesystem's timestamp granularity defeats the stale guard. +- **`editText` holds the whole file (plus the edited copy) in memory** — streaming exists only on the read path. +- **Binary detection is asymmetric** — reads NUL-sample only the first 8192 bytes while edits scan the whole buffer, so a file with a late NUL reads fine but rejects edits. +- **The per-target mutation lock is in-process only** — a writer in another process is caught only by the optional version guard, never serialized. diff --git a/packages/fs/fs-policy/README.md b/packages/fs/fs-policy/README.md index ad912bfc95..f3741d6ed9 100644 --- a/packages/fs/fs-policy/README.md +++ b/packages/fs/fs-policy/README.md @@ -46,3 +46,10 @@ The `fs/write-intent`/`fs/edit-intent` slots hold exactly one decider — this p ## No method coupling Because the plugin influences the world only through events, removing it does not break `@deepseek-ai/dsh-tool-fs` at a service-injection boundary: the tool falls through to the bare `ctx.fs` provider (unconditional write/edit, no observed-state). Loading it back layers the policy on. That graceful add/remove is the whole point of the event gate over a mandatory method service. + +## Known Limitations and Deferred Work + +- **Observed state does not survive a session resume** — persistence of the `WeakMap` record is deferred, so a resumed session must re-read files before guarded writes/edits. +- **Actors without an agent session can never satisfy the policy** — their edits throw `FS_NOT_OBSERVED` and their writes always resolve `createIfAbsent`, so a non-agent caller cannot overwrite an existing file through the gate. +- **Direct `ctx.fs` reads emit no `fs/observed`** — a file read outside the `read` tool stays unobserved, and a later guarded edit rejects with `FS_NOT_OBSERVED` until the tool reads it. +- **Authorization is version freshness, not view completeness** — any windowed read authorizes a full-file overwrite of an unchanged file, deliberately weaker than a full-view rule ([seam-split RFC](../../../docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md)). diff --git a/packages/fs/fs/README.md b/packages/fs/fs/README.md index ac2866802b..9f5ce3d950 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -42,3 +42,11 @@ This package declares three events (see the generated [events catalog](../../../ ## Vocabulary `FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids RFC](../../../docs/rfc/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsWriteIntent` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy RFC](../../../docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_DIRECTORY`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_PERMISSION_DENIED`, `FS_IO_ERROR`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry surfaces `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts. + +## Known Limitations and Deferred Work + +- **Text-only by contract** — backends reject binary/non-UTF-8 content with `FS_NOT_TEXT`; binary-safe operations are a deliberate deferral of [the tool-schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md). +- **Seven primitives only** — no delete, rename/move, copy, or watch; `listDir` is single-level, with recursion, globbing, pagination, and search out of scope per [the directory-listing RFC](../../../docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md). +- **`listDir` has no in-repo consumer yet** — skill discovery and a model-facing `ls` tool are the named follow-ups; sessions list directories via `bash` meanwhile. +- **No IO deadline** — the seam arms no timeout; cancellation is a best-effort optional `AbortSignal` per primitive (the deliberate [fs-family stance](../README.md)). +- **Resolve-then-operate costs a remote backend two round-trips per tool call** — folding or caching resolution is left to such a backend. diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index fedd1ff7a2..88a0e9663a 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -47,3 +47,9 @@ The tool passes `exec` (the tool-execution context) as the opaque `actor` on eve `fs/observed` fires AFTER the read/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event. The read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them. + +## Known Limitations and Deferred Work + +- **No directory-listing, glob, grep, or search tools ship** — a deferral of [the tool-schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md) (and `ctx.fs.listDir` has no tool consumer yet); models fall back to `bash`. +- **`read` handles UTF-8 text files only** — binary-safe reads and PDF/image/multimodal content are deferred; a directory target is `FS_NOT_REGULAR_FILE`. +- **No timeout surface** — `read`/`write`/`edit` take no timeout argument and declare no `timeout-policy` budget; cancellation rides `exec.signal` only (the deliberate [fs-family stance](../README.md)). diff --git a/packages/guard/repeat-tool-guard/README.md b/packages/guard/repeat-tool-guard/README.md index dc385bc033..566c2bc576 100644 --- a/packages/guard/repeat-tool-guard/README.md +++ b/packages/guard/repeat-tool-guard/README.md @@ -35,3 +35,12 @@ Reminders ride the post-execute decision's `additionalContext` (source `{kind: ' ## Testing Unit suites drive a real agent loop against a mock adapter (no network) and cover the chain semantics above to per-file 100%. The snapshot tier owns the transcript surface: a scripted-replay scenario repeats a call five times and pins both reminder tiers (gentle at 3, detailed at 5) as `context/message`s in the ACP transcript. + +## Known Limitations and Deferred Work + +- **Exact-match detection only** — canonicalization is a deep key-sort, so near-identical variants (a tweaked path, extra whitespace inside a value) evade the chain; fuzzy matching is rejected pending evidence of need. +- **Compaction does not reset chains** — a chain spanning a compaction checkpoint keeps counting. +- **Advisory only** — escalating to `block` at a high threshold is not implemented, though `PostToolDecision` already supports blocking. +- **No subagent chain-sharing** — chains stay isolated per agent; a parent and its subagent repeating the same call never combine. +- **Legitimate idempotent polling still draws nudges** past the thresholds — the pressure valves are `thresholds`/`exclude` config. +- **Past the highest threshold a chain goes silent** — reminders fire only at exact configured counts, never beyond them. diff --git a/packages/hooks/hook-protocol/README.md b/packages/hooks/hook-protocol/README.md index 8296821503..457351d8bb 100644 --- a/packages/hooks/hook-protocol/README.md +++ b/packages/hooks/hook-protocol/README.md @@ -29,6 +29,7 @@ Declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT a Like every event they must sit inside an open turn. The mid-turn points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn by construction; `SessionStart` gets no `hook/*` record (its injected `context/message` is the durable evidence) — see the hooks RFC. -## Input rewrite is parsed but not honored +## Known Limitations and Deferred Work -`HookOutput.updatedInput` carries a hook's requested tool-input rewrite (CC `updatedInput`), but the harness does not honor it yet — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)). A bridge logs + warns when a hook sets it. See `src/types.ts` for the full contracts. +- **`HookOutput.updatedInput` is parsed but not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)); a bridge logs + warns when a hook sets it. See `src/types.ts` for the full contracts. +- **An invalid matcher regex matches nothing, silently** — `matchesMatcher` never throws; surfacing the error needs a diagnostic-returning variant or parse-time validation (`TODO(matcher-diagnostics)`). diff --git a/packages/hooks/hooks-claude/README.md b/packages/hooks/hooks-claude/README.md index f97cdb5cfc..7a738849f4 100644 --- a/packages/hooks/hooks-claude/README.md +++ b/packages/hooks/hooks-claude/README.md @@ -50,8 +50,11 @@ The matcher subject is the tool name (`PreToolUse`/`PostToolUse`), the session s Injected context carries an explicit `{ kind: 'plugin', plugin: 'hooks-claude' }` source. `agent.inject()` defaults a missing source to `{ kind: 'user' }`, which would mislabel plugin context as a user prompt — so the bridge always names itself. -## Deferred (faithful-but-degraded) +## Known Limitations and Deferred Work - **`updatedInput` (tool-input rewrite)** is logged + warned, **not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)). - **`systemMessage`** (a hook's user-facing warning) is logged + warned, **not surfaced** — there is no user-message channel on these seams yet (only model-facing `additionalContext`). The shared merge collects it; the bridge does not yet render it. - **Stop loop-guard.** CC breaks an infinite force-continue with `stop_hook_active` (true once a Stop hook has fired this run) plus a max-consecutive cap; both are deferred (`TODO(stop-loop-guard)`). Today `stop_hook_active` is always `false`, so a Stop hook that unconditionally blocks would force-continue every step — a hook author must self-limit until the guard lands. +- **`{"continue": false}` is recorded, not enforced** — the merge computes the `stop`/`stopReason` outcome and the `hook/result` event records decision `stop`, but the run is not halted (`TODO(hook-continue-false)`). +- **`SessionStart` cannot gate the first turn** — `agent/session-start` is a synchronous emit, so a hook's injected context lands best-effort before turn 1 (`TODO(session-start-gating)`). +- **Hook config is process-level** — one `configPath` parsed at load for the whole process; per-session discovery of a project-local config is deferred (`TODO(per-session-hook-config)`). diff --git a/packages/hooks/hooks-codex/README.md b/packages/hooks/hooks-codex/README.md index b3fbc39928..aa5975eeb7 100644 --- a/packages/hooks/hooks-codex/README.md +++ b/packages/hooks/hooks-codex/README.md @@ -54,8 +54,10 @@ A tool call's payload carries the real `tool_name` (the same value the matcher t Injected context carries an explicit `{ kind: 'plugin', plugin: 'hooks-codex' }` source (`agent.inject()` would otherwise default it to `{ kind: 'user' }`). -## Deferred +## Known Limitations and Deferred Work -**Stop loop-guard** (`TODO(stop-loop-guard)`): as in CC, a Stop hook that unconditionally blocks would force-continue every step (`stop_hook_active` is always `false` here); the loop-guard is deferred. A hook author must self-limit until it lands. - -**`systemMessage`**: a hook's user-facing warning is logged + warned, not surfaced — there is no user-message channel on these seams yet (only model-facing `additionalContext`). +- **Stop loop-guard** (`TODO(stop-loop-guard)`) — as in CC, a Stop hook that unconditionally blocks would force-continue every step (`stop_hook_active` is always `false` here); the loop-guard is deferred, and a hook author must self-limit until it lands. +- **`systemMessage`** — a hook's user-facing warning is logged + warned, not surfaced; there is no user-message channel on these seams yet (only model-facing `additionalContext`). +- **`{"continue": false}` is recorded, not enforced** — the `hook/result` event records decision `stop`, but the run is not halted (`TODO(hook-continue-false)`). +- **`SessionStart` cannot gate the first turn** — `agent/session-start` is a synchronous emit with a detached continuation, so a hook's injected context lands best-effort before turn 1 (`TODO(session-start-gating)`). +- **Hook config is process-level** — one `configPath` parsed at load for the whole process; per-session discovery of a project-local config is deferred (`TODO(per-session-hook-config)`). diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 15fad9f881..63711926f7 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -34,10 +34,6 @@ Every request carries the shared attribution header from dsh-llm's `attributionH - **Reasoning passback rule**: on assistant turns that carried tool calls, `reasoning_content` is serialized back in history (required by the API in thinking mode); on tool-call-free turns it is dropped (ignored anyway — saves tokens). - Cache accounting: `cacheReadTokens` ← `prompt_cache_hit_tokens` / `prompt_tokens_details.cached_tokens`; DeepSeek reports no cache-write metric. -## Limitations (MVP, documented deliberately) - -- `tool_choice` is not mapped (not part of the core vocabulary). - ## Errors Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `RATE_LIMIT` (429), `INVALID_REQUEST` (400), `SERVER` (5xx), `HTTP_` otherwise. Protocol violations throw `STREAM_CLOSED` (no `[DONE]`) or `MALFORMED_RESPONSE` (bad JSON payload). Unknown wire `finish_reason`s (e.g. `content_filter`, `insufficient_system_resource`) become `finish {kind: 'error', code: }` chunks. @@ -45,3 +41,9 @@ Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `RATE_LI ## Testing 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. + +## Known Limitations and Deferred Work + +- **`tool_choice` is not mapped** — not part of the core vocabulary (MVP cut, shared with the pi-ai twin). +- **Requests use raw `fetch`, not `@cordisjs/plugin-http`** — no shared proxy/interception configuration; adoption is deferred until a second adapter wants it (`TODO(http)`). +- **Serialization flattens user and tool-result content to text blocks** — plugin-added block types are skipped, and empty tool output crosses the wire as the literal `(no output)`. diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index f74ccd2246..4af00a48ae 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -33,10 +33,14 @@ Every request carries the shared attribution header from dsh-llm's `attributionH pi-ai declares the openai/anthropic/google/mistral/AWS SDKs as install-time dependencies. They are lazy-loaded — only the openai SDK actually loads for this adapter — but they do land in `node_modules`. Accepted for a package whose purpose is design verification. -## Limitations - -Same MVP contract as llm-deepseek: `tool_choice` is not mapped. - ## 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` (`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. + +## Known Limitations and Deferred Work + +- **`tool_choice` is not mapped** — same MVP contract as llm-deepseek. +- **In-history `system`-role messages fold into `user`-role wire messages** — pi-ai exposes a single `systemPrompt` slot, diverging from the hand-rolled twin's `role: 'system'` passthrough. +- **`LlmError.status` is never set** — pi-ai reports failures as in-stream events with no HTTP status, so error codes are regex-classified from the error text. +- **`buildModel` hardcodes descriptor metadata** — `contextWindow: 128000`, `maxTokens: 64000`, zero cost, identically for every registered model name; not configurable. +- **pi-ai's built-in retries are disabled (`maxRetries: 0`)** — failures surface immediately; retry policy belongs to `llm/stream` listeners. diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 42694ba5ea..20db4064f6 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -47,3 +47,12 @@ Every product adapter must identify the application on every provider HTTP reque ### Real adapters Two adapters implement `LlmAdapter` against this vocabulary, deliberately built on different internals to keep the contract honest (see [the twin LLM adapters](../../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md)): [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) (hand-rolled fetch/SSE) and [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) (via `@earendil-works/pi-ai`). The pair pinned down the `StreamChunk` conventions now documented in `types.ts` (usage before finish, raw-string tool arguments, the two sanctioned error paths). + +## Known Limitations and Deferred Work + +- **No retry/caching/rate-limit layer ships** — `llm/stream` is the intended wrap seam and has no production listener, so provider 429/5xx failures surface immediately. +- **`GenerateOptions` sampling is `temperature`/`maxTokens`/`stop` only** — no `tool_choice`, `top_p`, or penalty fields; the vocabulary grows when a producer lands ([dropped inert knobs](../../../docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md)). +- **Producer-gated variants stay out until produced** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant were pruned as producerless ([RFC](../../../docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)). +- **`BlockAssembler` handles core block kinds only** — a plugin-added block type whose stream is never closed by `block-end` makes `blocks()` throw. +- **`APP_IDENTITY.url` names a repository that does not exist yet** — `FIXME`: creating the public `deepseek-ai/deepseek-harness-sdk` repo gates the first release. +- **`GenerateOptions.sessionId` is a locally-declared brand** — importing dsh-session's `SessionId` would cycle; a future ids-owning package would dissolve the workaround. diff --git a/packages/session-persistence/session-persistence-jsonl/README.md b/packages/session-persistence/session-persistence-jsonl/README.md index 9a76381614..9c81c3be5e 100644 --- a/packages/session-persistence/session-persistence-jsonl/README.md +++ b/packages/session-persistence/session-persistence-jsonl/README.md @@ -25,8 +25,13 @@ The JSONL durable session-persistence backend — a concrete `SessionPersistence - **Append-only.** Committed events (at or below a flushed `turn/end`) are never rewritten. Subsequent appends are line appends at EOF + `fsync`. - **Crash recovery — close, don't truncate.** A crash can leave a log whose final turn never closed (real events after the last `turn/end`). `load` PRESERVES those events (a turn can be huge — they are real work) and closes the orphaned turn by durably appending synthetic boundary events: an error `tool/result` for every `tool-call` the crash left unanswered (the loop logs the assistant message before running the tools, so a mid-tool crash leaves dangling calls — and `deriveMessages()` would replay an assistant tool-call with no result, which providers reject), then a `step/end` if a step was open, then `turn/end {kind:'interrupted'}`, returning a balanced log. Only a never-fully-written **torn tail fragment** (a final line with no newline / unparseable) is `ftruncate`d away before the closers are written. See [session persistence](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md). - **Contiguous-seq.** `load` rejects a mid-log parse error or `seq` gap (unloadable); `append` rejects a batch whose first `seq` does not continue the stored log, and rejects non-JSON-serializable `event.data` naming the offending event type. -- **Format version.** Only the current `SESSION_FORMAT_VERSION` (v0) is supported; `load` rejects any other version. While the harness is unreleased the on-disk format is pre-release/unstable: a breaking format change is absorbed at v0 (no bump until the first tagged release) and non-current logs are rejected — there is no migration (no persisted user data to preserve). ## Write path The plugin generalizes the example `session-jsonl.ts`: it subscribes to `session/created` (capture the header; persist a fork's seed once), `session/event` (snapshot each event when buffering — the live `session.events` object is mutable), and `session/flush`/dispose (drain the write-behind buffer through `append`). A per-session write cursor means a resumed session never re-appends already-stored events. Existing live sessions are seeded on plugin apply (HMR does not replay `session/created`). All backend operations for one session are serialized, and disposal awaits quiescence (every init + final drain) before returning, so no write lands after teardown. + +## Known Limitations and Deferred Work + +- **Only the current `SESSION_FORMAT_VERSION` (v0) loads** — the on-disk format is pre-release/unstable: a breaking format change is absorbed at v0 and non-current logs are rejected; there is no migration. +- **Nothing deletes session files** — logs accumulate under `root` until removed externally (the seam has no deletion surface). +- **Single-process assumption** — per-session serialization and the write cursor live in this process; two processes appending to the same `root` are not coordinated. diff --git a/packages/session-persistence/session-persistence-sqlite/README.md b/packages/session-persistence/session-persistence-sqlite/README.md index c14d3a70ea..9b445a335a 100644 --- a/packages/session-persistence/session-persistence-sqlite/README.md +++ b/packages/session-persistence/session-persistence-sqlite/README.md @@ -2,8 +2,6 @@ A SQLite durable session-persistence backend — a second `SessionPersistence` implementation ([session persistence](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md)), built to validate that the abstract seam and the shared `runPersistenceContract` suite are genuinely backend-agnostic. It satisfies the SAME contract as `dsh-session-persistence-jsonl` (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over `node:sqlite` rows instead of file bytes. -> **TODO:** this backend talks to `node:sqlite` directly. If a cordis database service (`cordis/db` / a `@cordisjs` SQL driver plugin) is adopted, route through that instead of holding a raw `DatabaseSync` here — the contract surface (`SessionPersistence`) would not change, only the storage driver. - ## Storage model Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../docs/rfc/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row), so no separate column is needed. @@ -28,3 +26,10 @@ interface Config { ## Write path Like the JSONL backend, the plugin also installs the `session/event` → buffer → `session/flush` drain: it snapshots each event when buffered (the live `session.events` object is mutable), persists a fork's seed once on `session/created`, keeps a per-session write cursor so a resumed session never re-appends stored events, and seeds existing live sessions on apply (HMR does not replay `session/created`). Dispose awaits every in-flight init + final drain and then closes the database, so no write lands after teardown. + +## Known Limitations and Deferred Work + +- **Raw `node:sqlite`, pending a cordis database service** — the backend holds a `DatabaseSync` directly; if a `cordis/db` / `@cordisjs` SQL driver is adopted, the storage driver routes through it (the `SessionPersistence` contract would not change) — a marked TODO. +- **`DatabaseSync` is synchronous** — every append transaction blocks the event loop for its duration; acceptable for local stores, a throughput ceiling for busy multi-session servers. +- **Only the current `SCHEMA_VERSION` opens** — a database written by any other build is rejected rather than migrated (unreleased software; no persisted user data to preserve). +- **Nothing deletes stored sessions** — rows accumulate until removed externally (the seam has no deletion surface; `ON DELETE CASCADE` is wired for such out-of-band cleanup). diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session-persistence/session-persistence/README.md index 8bd3fed568..9a96515082 100644 --- a/packages/session-persistence/session-persistence/README.md +++ b/packages/session-persistence/session-persistence/README.md @@ -49,3 +49,9 @@ Three backends run these suites: an in-memory reference (in `tests/`), `dsh-sess ## Metadata types Re-exported from `dsh-session`: `SessionHeader` (immutable session metadata: `version`, `id`, `createdAt`, `cwd?`, `parentSession?`, `seedLength?`). + +## Known Limitations and Deferred Work + +- **No deletion or retention surface** — the seam is `create`/`append`/`load`/`list` only; pruning stored sessions is out-of-band backend maintenance. +- **`list()` is unpaginated and unfiltered** — it returns every stored session's header; fine for local stores, unindexed at scale. +- **Repair-time synthetic closers are the only crash story** — a backend must synthesize `tool/result`/`step/end`/`turn/end` closers on load; there is no partial-turn resume that continues an interrupted turn instead of closing it. diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index cb51986915..db2b59d412 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -69,3 +69,11 @@ The child env is built by [`buildChildEnv` from `@deepseek-ai/dsh-subagent-subpr ## Plugin export shape Named `name` / `inject` / `Config` / `apply`, with **no default export**: the cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray default would collapse the module to the bare function and drop the `inject` namespace (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). + +## Known Limitations and Deferred Work + +- **A fresh process per run** — persistent-process pooling is a future optimization ([the seam RFC](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)). +- **No start-time capability enforcement** — an out-of-process child cannot honor the parent's `outputSchema`/`depthLimit`/`toolFilter`, so the provider advertises none and `request.parent` is ignored. +- **Only `agent_message_chunk` text is collected** — the child's tool-call activity, thought chunks, and plan updates are not surfaced to the parent. +- **Permission prompts are auto-answered** (`permission: allow | reject`) — no human is surfaced a child's `session/request_permission` in this cut. +- **No snapshot-tier replay coverage** (`TODO(acp-subagent-replay)`) — an ACP child is its own process with its own replay shape, deferred. diff --git a/packages/subagent/subagent-fork/README.md b/packages/subagent/subagent-fork/README.md index 1434601c8e..a6e2785795 100644 --- a/packages/subagent/subagent-fork/README.md +++ b/packages/subagent/subagent-fork/README.md @@ -21,3 +21,9 @@ The seam this rides on: `CreateAgentOptions.seed` (added on `dsh-agent`, threade | `providerName` | Registry name on `ctx.subagents` (default `fork`). | See [`dsh-subagent-spawn`](../subagent-spawn/README.md) for the run lifecycle, model inheritance, and depth tracking — all shared. + +## Known Limitations and Deferred Work + +- **Tool-scoping (`toolFilter`) is not supported** — declared `false`, so the service rejects a request needing it before `start` runs. +- **Runs expose no `sendMessage`/`resume`** — the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. +- **The seed is a one-time snapshot** — the child sees the parent's completed turns as of the fork and nothing the parent logs afterwards; there is no live context sharing. diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index caa7b9b161..6084fe569c 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -37,3 +37,9 @@ Delegation depth rides on a merge-extensible `AgentOptions.subagentDepth` field ### `SubagentDepthError` Thrown by `startInProcessRun` when a spawn would exceed the request's `maxDepth` cap; carries `attemptedDepth` and `maxDepth`. + +## Known Limitations and Deferred Work + +- **The structured-output runtime is context-global** — the tool registry and prompt assembly are context-wide while schemas differ per concurrent child, hence the final-assembly enforcement dance; per-agent/per-session scoping would dissolve it (the module-doc `FIXME`). +- **Runs expose no `sendMessage`/`resume`** — the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. +- **`toolFilter` is unimplemented in this driver** — both in-process backends declare it `false`; scoping a child's tool set is deferred. diff --git a/packages/subagent/subagent-spawn/README.md b/packages/subagent/subagent-spawn/README.md index e411d44ab0..0a3a17f087 100644 --- a/packages/subagent/subagent-spawn/README.md +++ b/packages/subagent/subagent-spawn/README.md @@ -17,3 +17,8 @@ The run mechanics live in the shared [`@deepseek-ai/dsh-subagent-inprocess`](../ | Key | Meaning | |---|---| | `providerName` | Registry name on `ctx.subagents` (default `spawn`). | + +## Known Limitations and Deferred Work + +- **Tool-scoping (`toolFilter`) is not supported** — the capability is declared `false`, so the service rejects a request needing it before `start` runs; scoping a child's tool set is deferred. +- **Runs expose no `sendMessage`/`resume`** — the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index e214ba58c6..2d11d874f7 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -36,8 +36,9 @@ Beside `capabilities` sits one DESCRIPTIVE fact, not validated by the service: ` The service also announces provider lifecycle: `subagent/provider-added` (the frozen registry snapshot) fires after a registration and `subagent/provider-removed` (the accepted name) after an unregistration, so a consumer deriving state from a named provider (the model-facing tool wording) mirrors registry membership instead of assuming load order — the cordis Loader starts sibling plugins concurrently, so "listed earlier" does not mean "registered earlier". Run lifecycle is gated by provider readiness: `subagent/start` (payload `SubagentRunInfo`) fires only after `run.started` fulfills, and `subagent/end` (payload `SubagentRunEndInfo`) fires only for that announced run; readiness rejection emits neither. For spawn/fork, the start listener can resolve the published child via `ctx.agents.get(info.id)`; a remote provider need not have a local registry entry. Both events are **observe-only** plain emits. The service observes `result` immediately even while readiness is pending, clones its output before the caller can mutate it, and buffers that end payload until start has fired; a rejecting result cannot become an unhandled detached promise, start always precedes end, and a listener cannot corrupt the caller's result. `subagent/end` carries the cloned output as `lastAssistantMessage` on the settle path and omits it on infrastructure rejection. Any run-affecting decision is out of scope for this observe-only surface. -## Scope (first cut) +## Known Limitations and Deferred Work -The consumer collects **synchronously**: it starts a run and awaits `result`. Steering (`sendMessage`) is part of the contract but intentionally unused. Background / poll / spill semantics are deferred to a future redesign unifying long-running-tool handling across subagents and bash. See the RFC: [docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md). +- **The consumer collects synchronously** — it starts a run and awaits `result`; steering (`sendMessage`) is part of the contract but intentionally unused, and background / poll / spill semantics are deferred to a future redesign unifying long-running-tool handling across subagents and bash ([the seam RFC](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)). +- **The lifecycle events are observe-only** — a run-affecting `subagent/end` (an awaited continuation/decision surface) is deferred until a consumer needs one (`FIXME(subagent-continuation)`). See `src/types.ts` for the full contracts. diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 715fb0b9db..18ebea6ada 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -24,3 +24,9 @@ The tool description and the `prompt` parameter description are DERIVED from the `execute` starts a run on the configured provider and **awaits `run.result` inside a `try/finally` that always `dispose()`s the run** — the owned child agent/session is torn down on every path (success, error, abort), never leaked. The tool's abort signal (`exec.signal`) is bridged to `run.cancel()`. A non-`completed` stop reason (aborted/error/max-tokens/refusal) maps to an `isError` tool result rather than returning partial output as success. Background / poll collection is deferred (see the [RFC](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)); this cut blocks the parent turn until the child finishes. + +## Known Limitations and Deferred Work + +- **Delegation blocks the parent turn** — synchronous collect only; background start + poll collection is deferred to the long-running-runtime redesign. +- **Duplicate `toolName` across waiting loads is detected late** (`TODO(subagent-dup-toolname)`) — two loads waiting on providers collide only when a provider arrives, and the throw rolls back the provider's fiber rather than the misconfigured tool's; config-time detection needs a cross-fiber registry of intended names. +- **No per-child persona or prompt shaping** — `agentOptions` carries only `model`; the deployment persona is context-wide, and per-child system-prompt variation has no seam yet. diff --git a/packages/support/acp-snapshot/README.md b/packages/support/acp-snapshot/README.md index 002dbf8a19..89f4ed0f8a 100644 --- a/packages/support/acp-snapshot/README.md +++ b/packages/support/acp-snapshot/README.md @@ -40,3 +40,8 @@ A scenario booting a differently-composed tree sets its own `configPath` (an ove The example also ships a `cordis.snapshot.yml` replay overlay next to its `cordis.yml` (the bin swaps them under `DSH_SNAPSHOT=replay` — [single-source replay config RFC](../../../docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md)); replay fixtures are served by [`dsh-llm-replay`](../llm-replay/README.md), which this package points at via the `DSH_SNAPSHOT_*` env vars it sets on the child. `pnpm run test:snapshot:record` calls the live LLM and rewrites the recorded scenarios' model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay overlay, and rewrites stdout plus comparable session-log goldens from the committed model scripts. Fixture roles, record/replay/refresh semantics, and scenario-table fields are documented on `Scenario` and in the [snapshot RFC](../../../docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md). Constraints: `suite.ts` imports vitest, so the package is importable only inside a vitest run (the harness and normalizers have no such dependency but ship from the same entry). ACP-specific by design — the harness speaks the SDK's `ClientSideConnection`. Permission round-trips are scriptable: `InputScript.permissionAnswers` is a FIFO queue of option-kind selections (`allow_once`, `reject_once`, …) the client maps to the agent-issued `optionId` at answer time; an absent or exhausted queue answers `cancelled`, and a kind the request never offered rejects the run (the agent is answered `cancelled`, so a tolerant agent cannot absorb the scenario bug). Session config options are scriptable too: the `setConfigOption` step switches a knob over `session/set_config_option`, and `setConfigOptionExpectError` asserts the bridge rejects an unknown id or out-of-vocabulary value (the error frame stays in the transcript). + +## Known Limitations and Deferred Work + +- **Session harvest is JSONL-only** — `runScenario` collects persisted `.jsonl` logs, so an example composed over the SQLite persistence backend has no snapshot path. +- **The subprocess boots the unbuilt tsx/Loader path only** — the built-bin artifact is guarded by the separate `built-bin` e2e smokes, never by this tier. diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index 94c2682f9d..acc0fa2a79 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -53,3 +53,8 @@ A `DeepReadonly` is high type-noise across every log consumer, and ## Seeded sessions A seeded/forked session arrives with events already in its log (the `Session` constructor copies the seed without emitting `session/event`). On `session/created` the plugin replays the existing log through the checker and freezes those entries, so seeded history is held to the same contract. + +## Known Limitations and Deferred Work + +- **The request-reconstructability assertion covers loop-built requests only** — hand-built one-shots (e.g. compaction's summarize call) carry no live `sessionId` marker and are skipped. +- **Merge-extended event families get no family-specific assertions** — `compact/*` lock pairing and `hook/*` invoked/result pairing are not checked here; only the core turn/step/chunk/tool-result contract is. diff --git a/packages/support/llm-replay/README.md b/packages/support/llm-replay/README.md index 78b411d54c..91a5a89dd8 100644 --- a/packages/support/llm-replay/README.md +++ b/packages/support/llm-replay/README.md @@ -43,3 +43,8 @@ Replay keys every call by its calling session id (`GenerateOptions.sessionId`, s ## Plugin export shape Named `name` / `inject` / `Config` / `apply`, with **no default export**: the cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray default would collapse the module to the bare function and drop the `inject` namespace (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). + +## Known Limitations and Deferred Work + +- **First-call-order script binding assumes sequential delegation** — a cut that runs sibling subagents concurrently (or a compaction summarize call landing mid-run) would bind live sessions to recorded scripts non-deterministically; a stronger keying is deferred until such a scenario exists (`XXX(concurrent-subagents)`). +- **Only chunk-producing calls are derivable** — a pure pre-chunk throw or a cancel/hang scenario needs the `replay.override.json` sidecar; the override replaces the PRIMARY session's script only. diff --git a/packages/timeout/timeout-policy/README.md b/packages/timeout/timeout-policy/README.md index e637a658bf..8400af5776 100644 --- a/packages/timeout/timeout-policy/README.md +++ b/packages/timeout/timeout-policy/README.md @@ -32,3 +32,8 @@ The derived signal only **notifies**; termination stays with the tool and the ca ### Composing with other `tools/execute` wrappers Multiple `tools/execute` listeners compose by cordis registration order. Combined with a future retry/sandbox/metrics wrapper, registration order chooses the semantics — "timeout covers the whole retry operation" (timeout registered outer) versus "timeout covers each attempt" (timeout registered inner). + +## Known Limitations and Deferred Work + +- **Cooperative, never a hard kill** — the deadline only notifies via `exec.signal`; a tool that ignores the signal does not stop on timeout (see § Cooperative, not a hard kill). +- **No blanket budget** — only tools that declare `timeoutMs` on their `ToolDefinition` get a deadline; there is no registry-wide default for undeclared tools (the shipped `bash`/`read`/`write`/`edit` deliberately declare none). diff --git a/packages/todo/tool-todo/README.md b/packages/todo/tool-todo/README.md index b27cc65227..2766fd0eb1 100644 --- a/packages/todo/tool-todo/README.md +++ b/packages/todo/tool-todo/README.md @@ -23,3 +23,9 @@ The tool writes only the session event; it does not render. UIs subscribe to `se ## Export shape A function/namespace plugin: it exports `name` / `inject` / `apply` and NO default. A stray `export default` would collapse the module via the Loader's `unwrapExports` and drop `inject` (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). + +## Known Limitations and Deferred Work + +- **Single-owner scope only** — the list belongs to the one calling agent session; subagent/shared/swarm scopes are a deliberate cut (see § Single owner), and a non-agent caller is rejected. +- **The item shape is deliberately minimal** — `content` plus three-state `status`; no id, priority, or active-form fields, and the ACP bridge synthesizes the `priority` ACP requires. +- **Whole-list replacement is the only operation** — no partial updates, no read-back tool; the model must resend the entire list each call. diff --git a/packages/ui/acp-agent/README.md b/packages/ui/acp-agent/README.md index d81c9cc091..ed83bfaf4d 100644 --- a/packages/ui/acp-agent/README.md +++ b/packages/ui/acp-agent/README.md @@ -42,3 +42,7 @@ The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the Run it under `node --expose-internals`: the cordis Loader resolves the config's bare plugin specifiers through its internal module loader, active only under that flag. (`demo:acp` runs under tsx, whose tsconfig `paths` map resolves them instead.) All diagnostics go to **stderr** — stdout is the protocol. + +## Known Limitations and Deferred Work + +- **The front-door cluster is fixed in code** — only `model`/`persona`/`toolOrder`/`persistenceRoot` are config; swapping the baked JSONL persistence for another backend, or adding the deliberately-omitted `tool-ask-user`, means a leaf-level sibling entry or a differently-composed app package. diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 2f1f35adc4..465f0a42e6 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -81,9 +81,12 @@ The bridge registers an `approval/request` waterfall listener — the ACP answer Teardown reaches quiescence: for EVERY live session settle any pending prompt as `cancelled`, then run that session's [`AgentHandle`](../../core/agent/README.md) `dispose()` — which stops the loop (sets `disposed` + aborts the in-flight step), `await`s the loop's exit (the final `turn/end` + `session/flush` are captured while the session is still attached), unregisters the agent, and removes its session from the store. A turn cut off mid-flight by teardown ends with reason `disposed` (not `aborted` — `dispose()` uses the disposed path, not `session/cancel`'s queue-aware `cancel()`). The per-session disposes run in parallel. The same teardown runs on a **client disconnect** (`conn.closed` resolves when the editor quits / the transport EOFs), so a vanished client never leaves an orphaned running — or idled-but-still-registered — agent whose `session/update` writes are silently swallowed. The two paths are idempotent and memoized (the first clears the `sessions` map; a second caller awaits the same teardown promise). -## Known limitations (tracked TODOs) +## Known Limitations and Deferred Work - **`additionalDirectories`** — rejected. A session operates in its single `cwd` (see Per-session cwd); widening the tool/filesystem scope to extra roots is a separate sandbox concern, not yet implemented. +- **Prompt content is `text` + `resource_link` only** — image, audio, and embedded-resource blocks are rejected, as is a non-empty `mcpServers` list at `session/new`. +- **One configured `model` for every created session** — per-session model selection has no config or protocol surface here yet. +- **Terminal cards render completed output** — live incremental streaming and command classification are named follow-ups of [the terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md). ## stdout is the protocol diff --git a/packages/ui/stdio-agent/README.md b/packages/ui/stdio-agent/README.md index 27f0d20be7..496aea08fa 100644 --- a/packages/ui/stdio-agent/README.md +++ b/packages/ui/stdio-agent/README.md @@ -63,3 +63,8 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s ``` Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — "swap the backend, keep the app". + +## Known Limitations and Deferred Work + +- **One pre-created `main` agent drives the readline UI** — there is no multi-session or concurrent-agent surface in this app; a run is one conversation. +- **The front-door cluster is fixed in code** — the JSONL persistence backend and the ask-user tooling are baked; a different composition is a leaf-level sibling entry or another app package. diff --git a/packages/ui/tool-ask-user/README.md b/packages/ui/tool-ask-user/README.md index 10d4a082ba..b42192f91a 100644 --- a/packages/ui/tool-ask-user/README.md +++ b/packages/ui/tool-ask-user/README.md @@ -18,3 +18,8 @@ The tool calls `ctx.userInteraction.ask()` and returns JSON text shaped as `{ "a ## Role This is the consumer package for the user-interaction seam. It does not render UI and does not know how input is collected; it only translates model arguments into `AskUserQuestionRequest` and returns the human answer to the agent loop. + +## Known Limitations and Deferred Work + +- **A pending question blocks the tool call until the human answers** — the tool declares no `timeout-policy` budget; cancellation rides the turn's `exec.signal` only. +- **Answers return as JSON text** — the seam's structured `AskUserQuestionAnswer` is serialized into the tool result rather than carried as typed content blocks. diff --git a/packages/ui/user-interaction/README.md b/packages/ui/user-interaction/README.md index 6377c2b7ef..b3113b032c 100644 --- a/packages/ui/user-interaction/README.md +++ b/packages/ui/user-interaction/README.md @@ -22,3 +22,8 @@ When an answer includes `custom`, `selected` is empty; custom text is an overrid ## Role This is the interface package. Model-facing consumers such as `@deepseek-ai/dsh-tool-ask-user` depend on this seam; UI front doors such as the `stdio-agent` readline module and the `acp` bridge provide the provider. The loop stays unchanged: a tool call simply awaits a promise, and the tool result resumes the normal agent loop. + +## Known Limitations and Deferred Work + +- **One provider per context** — there is no routing or fan-out to multiple UIs; a second registration throws `DUPLICATE_PROVIDER`, and with none registered `ask()` throws `NO_PROVIDER` rather than degrading. +- **The vocabulary is the question-form shape only** — selectable options plus optional custom text; richer interaction shapes (file pickers, diff-preview confirmations) have no seam vocabulary yet. diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index ab1326a21e..9b0ea33d86 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -33,3 +33,9 @@ Each tool is registered independently; a product that wants only one disables th Tool registration follows product **enablement**, not backend availability. A tool stays visible even when its selected provider is missing, misconfigured, ambiguous, or temporarily unavailable; the seam resolves the provider at execution time and execution fails with a structured `WebError` (e.g. `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_AMBIGUOUS`), which `ToolRegistry.execute()` turns into an error tool result the model can read and hooks/UI can route on. This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. To remove a web tool entirely, disable it here in config. The tool never calls a provider's `status()` and never enumerates providers — its only execution path is `ctx.web.search()` / `ctx.web.fetch()`, and provider unavailability reaches it as the structured `WebError` codes selection throws at execution time. Provider selection stays entirely inside the seam, with one owner. + +## Known Limitations and Deferred Work + +- **`htmlToMarkdown` is a minimal regex converter, not an HTML parser** — it strips script/style/noscript, keeps headings/bullets/links, and decodes about a dozen named entities; tables, images, and nested formatting are lost. +- **The model-facing surface is minimal by design, with promotions deferred** — `max_results` stays a config bound (not a model argument), and `web_fetch` takes only `url` (no `format`/`prompt`/LLM-summarization mode); both are named later steps in [the seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md). +- **No permission gating** — both tools execute without any permission prompt or policy; integration awaits the deferred permission system, with the owner (a `tools/execute` plugin, provider config, or both) undecided. diff --git a/packages/web/web-fetch-local/README.md b/packages/web/web-fetch-local/README.md index 9c2ef0030f..bada10d7c3 100644 --- a/packages/web/web-fetch-local/README.md +++ b/packages/web/web-fetch-local/README.md @@ -33,6 +33,8 @@ The provider's `timeoutMs`/`maxTimeoutMs` is a **resource backstop** for direct The numeric limits are validated at plugin construction: every cap except `maxRedirects` must be a positive finite number, and `maxRedirects` must be a non-negative integer. An invalid value throws rather than silently constructing a provider with nonsensical limits. -## Security note +## Known Limitations and Deferred Work -SSRF / private-network protection (blocking private, loopback, link-local, multicast, and otherwise non-public destinations, with DNS-resolve-then-validate and per-hop re-validation) is **deferred** — see the [web capability seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets. +- **SSRF / private-network protection is deferred** — no blocking of private, loopback, link-local, multicast, or otherwise non-public destinations, no DNS-resolve-then-validate, no per-hop re-validation (see [the web capability seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets. +- **Only textual content decodes** — html/xhtml and `text/*`-plus-JSON/XML families; a missing `Content-Type` or any binary type throws `WEB_UNSUPPORTED_CONTENT_TYPE`, and text-extractable PDF decoding is named deferred work. +- **Charset comes only from the `Content-Type` header** (UTF-8 default) — an HTML `` declaration is ignored, and a declared-but-unrecognized charset label throws rather than falling back. diff --git a/packages/web/web-search-deepseek/README.md b/packages/web/web-search-deepseek/README.md index 41b000d26a..95e5934806 100644 --- a/packages/web/web-search-deepseek/README.md +++ b/packages/web/web-search-deepseek/README.md @@ -34,3 +34,10 @@ It reuses `$DEEPSEEK_API_KEY` (no new secret) but **not** `$DEEPSEEK_BASE_URL`: ## Mapping DeepSeek returns no provider-generated answer surface this provider trusts as `content`, so `content` is omitted. `sources[]` is built from the `web_search_result` items inside `web_search_tool_result` blocks: `url` ← `url`, `title` ← `title`, `publishedAt` ← `page_age`. The per-source `snippet` lives separately in a `text` block's `citations[]` (a `cited_text` keyed by `url`), so the provider joins the two — a result with no citation excerpt simply has no `snippet`. Results are deduped by `url` (a `maxUses > 1` request can surface the same URL across searches). DeepSeek's `web_search` has no result-count knob (only `maxUses`), so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`). Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. + +## Known Limitations and Deferred Work + +- **One search costs a full Messages model turn** — latency plus generated tokens, with up to `maxUses` server-side searches; DeepSeek exposes no dedicated retrieval endpoint. +- **Over-returned sources still cost tokens** — with no result-count knob on the wire, `maxResults` is enforced only post-hoc by seam truncation. +- **Uncited results carry no `snippet`** — a source gains one only when a `text` block citation (`cited_text`) matches its URL. +- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`. diff --git a/packages/web/web-search-exa/README.md b/packages/web/web-search-exa/README.md index 0bc58d6559..660de43c60 100644 --- a/packages/web/web-search-exa/README.md +++ b/packages/web/web-search-exa/README.md @@ -24,3 +24,9 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i ## Mapping Exa returns a flat `results[]` and no generated answer, so `content` is omitted. Each result maps to a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← the first non-empty `highlights[]` entry (a result with no highlight has no portable snippet and is dropped), `publishedAt` ← `publishedDate`. A request's `maxResults` wins over the configured `numResults` default and is sent as Exa's `numResults` for a cost/latency optimization; the final bound is enforced by the seam. Provider failures (HTTP errors, network failure, unparseable or wrong-shape bodies) surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. + +## Known Limitations and Deferred Work + +- **A result with no non-blank highlight is dropped entirely** — no portable snippet to map, so fewer sources than the requested count can return. +- **Only `searchType`/`numResults`/`highlightsPerResult` are exposed** — Exa's other controls (livecrawl, category, domain/date filters, full-text contents) wait on provider-neutral seam fields ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). +- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`. diff --git a/packages/web/web-search-perplexity/README.md b/packages/web/web-search-perplexity/README.md index f944413c96..ad64518f2d 100644 --- a/packages/web/web-search-perplexity/README.md +++ b/packages/web/web-search-perplexity/README.md @@ -24,3 +24,10 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i ## Mapping `content` ← `choices[0].message.content` (the generated answer). `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` ← `date`), falling back to the URL-only `citations[]` array only when `search_results` is absent — those sources carry just a `url`, which is why `title`/`snippet`/`publishedAt` are optional on the seam. Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. Perplexity has no result-count control, so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`). + +## Known Limitations and Deferred Work + +- **Citation-fallback sources are URL-only** — when Perplexity omits structured `search_results[]`, sources carry no `title`/`snippet`/`publishedAt`, so the tool renders bare hostname labels. +- **Over-returned sources still cost tokens and latency** — with no result-count control on the wire, `maxResults` is enforced only post-hoc by seam truncation. +- **Only `model`/`maxTokens`/`searchRecency` are exposed** — Perplexity's other search controls (domain filters, `web_search_options` context size, images) wait on provider-neutral seam fields ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). +- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`. diff --git a/packages/web/web/README.md b/packages/web/web/README.md index fe5f1e650e..2f5d6508f1 100644 --- a/packages/web/web/README.md +++ b/packages/web/web/README.md @@ -42,3 +42,10 @@ The failure branches throw `WebError`, whose structured code (plus message detai ## Vocabulary `WebSearchRequest` (`query`, `maxResults?`) → `WebSearchResult` (`providerId`, `query`, `content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`, `timeoutMs?`) → `WebFetchResult` (`providerId`, final `url`, `statusCode`, `body`, `truncated`); `WebFetchBody` is a CLOSED discriminated union (`html` | `text`) owned here — consumers `switch` to exhaustiveness so a new kind breaks their compilation until handled. See `src/types.ts` for the full contracts and the `WebError` code taxonomy. + +## Known Limitations and Deferred Work + +- **No observation surface** — no provider-change event and no capability-status query; availability is observed only by executing `search()`/`fetch()` and routing the thrown `WebError` codes, and the no-provider failure is the generic `WEB_PROVIDER_UNAVAILABLE` with no per-provider reason enumeration ([RFC](../../../docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)). +- **`WebSearchRequest` carries only `query` + `maxResults`** — provider-neutral controls (recency, domain filters, regional hints, search depth) are deferred until Exa and Perplexity can both honor them honestly ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). +- **`WebFetchBody` has no `pdf` arm** — text-extractable PDF support is named deferred work; the closed union makes adding it a compile-enforced change across the three web packages. +- **Provider-backed page extraction is out of scope of `fetch()`** — a Firecrawl/Tavily-style `web_extract` capability is deferred rather than widening the fetch seam. diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 2f98c74eea..da238cf7b1 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -273,6 +273,7 @@ function docSyncLeafGates(): Gate[] { pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }), pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }), + pnpmScript('readme-limitations', 'verify-readme-limitations', { label: 'readme limitations' }), ] } diff --git a/scripts/verify-readme-limitations.ts b/scripts/verify-readme-limitations.ts new file mode 100644 index 0000000000..5a215a53fd --- /dev/null +++ b/scripts/verify-readme-limitations.ts @@ -0,0 +1,134 @@ +/** + * Doc-sync gate: every package README carries the standard + * `## Known Limitations and Deferred Work` section — the per-package home for + * consumer-visible gaps and consciously postponed work that the + * [documentation standard](../docs/AGENTS.md) assigns to the package-README + * tier. One canonical heading instead of per-package variants ("Limitations", + * "What is NOT here", …) keeps the section greppable across the repo and makes + * its absence a gate failure rather than an oversight. + * + * A package with genuinely nothing to declare is listed in NO_LIMITATIONS + * below and must NOT carry the section — an empty section invites boilerplate, + * and a whitelisted package that gains real limitations leaves the whitelist + * in the same change. Whitelist entries are validated against the scanned + * package set, so a rename or removal fails loud instead of silently + * un-gating a README. + * + * Checks, per packages///README.md (fenced code excluded): + * 1. Non-whitelisted: exactly one limitations-like heading, byte-equal to the + * canonical h2, with at least one top-level `- ` bullet before the next + * heading. + * 2. Whitelisted: no limitations-like heading at all. + * 3. Every whitelist entry names a scanned package. + * + * "Limitations-like" also matches near-miss headings at any level ("known + * limitations", "deferred work", "what is not here", a heading starting with + * "limitations"/"deferred") so a drifted heading cannot impersonate the + * canonical section and a second competing section cannot coexist with it. + * + * Checker, not fixer: it reports and never rewrites. + * Run: `tsx scripts/verify-readme-limitations.ts`. + */ + +import { globSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const root = resolve(import.meta.dirname, '..') + +/** The one canonical section heading, required verbatim as an h2. */ +const CANONICAL = '## Known Limitations and Deferred Work' + +/** + * Packages with genuinely no known limitations or deferred work (keyed by + * package directory relative to the repo root). Their READMEs must NOT carry + * the section; adding one moves the package off this list in the same change. + */ +const NO_LIMITATIONS: readonly string[] = [ + 'packages/support/subagent-mock', + 'packages/ui/app-boot', + 'packages/util/brand', + 'packages/util/timeout', +] + +/** A heading that reads as a limitations section — canonical or drifted. */ +function isLimitationsLike(headingText: string): boolean { + return ( + /known limitation/i.test(headingText) + || /deferred work/i.test(headingText) + || /what is not here/i.test(headingText) + || /^limitations?\b/i.test(headingText) + || /^deferred\b/i.test(headingText) + ) +} + +interface Line { + index: number + raw: string +} + +/** Split a README into prose lines (fenced code dropped), keeping 1-based line numbers. */ +function proseLines(text: string): Line[] { + let inFence = false + const kept: Line[] = [] + text.split('\n').forEach((raw, i) => { + if (raw.startsWith('```')) { + inFence = !inFence + return + } + if (!inFence) kept.push({ index: i + 1, raw }) + }) + return kept +} + +const readmes = globSync('packages/*/*/README.md', { cwd: root }).sort() +const scannedPackages = new Set(readmes.map(path => path.slice(0, -'/README.md'.length))) +const failures: string[] = [] + +for (const entry of NO_LIMITATIONS) { + if (!scannedPackages.has(entry)) { + failures.push(`whitelist entry ${entry} does not name a scanned package — renamed or removed? update NO_LIMITATIONS in scripts/verify-readme-limitations.ts in the same change`) + } +} + +for (const readme of readmes) { + const pkg = readme.slice(0, -'/README.md'.length) + const lines = proseLines(readFileSync(resolve(root, readme), 'utf8')) + const headings = lines.filter(line => /^#{1,6} /.test(line.raw)) + const limitations = headings.filter(line => isLimitationsLike(line.raw.replace(/^#{1,6}\s+/, ''))) + + if (NO_LIMITATIONS.includes(pkg)) { + for (const heading of limitations) { + failures.push(`${readme}:${heading.index}: whitelisted as having no known limitations, but carries ${JSON.stringify(heading.raw)} — drop the section or remove the package from NO_LIMITATIONS`) + } + continue + } + + const heading = limitations.at(0) + if (heading === undefined) { + failures.push(`${readme}: missing the \`${CANONICAL}\` section (a package with genuinely nothing to declare joins NO_LIMITATIONS in scripts/verify-readme-limitations.ts instead)`) + continue + } + if (limitations.length > 1) { + failures.push(`${readme}: ${limitations.length} limitations-like headings (lines ${limitations.map(line => line.index).join(', ')}) — keep exactly one \`${CANONICAL}\` section`) + continue + } + if (heading.raw.trimEnd() !== CANONICAL) { + failures.push(`${readme}:${heading.index}: non-canonical heading ${JSON.stringify(heading.raw)} — use \`${CANONICAL}\``) + continue + } + const headingAt = lines.indexOf(heading) + const body = lines.slice(headingAt + 1) + const end = body.findIndex(line => /^#{1,6} /.test(line.raw)) + const section = end === -1 ? body : body.slice(0, end) + if (!section.some(line => /^- /.test(line.raw))) { + failures.push(`${readme}:${heading.index}: the \`${CANONICAL}\` section has no top-level \`- \` bullet — state the limitations, or whitelist the package if there are genuinely none`) + } +} + +if (failures.length > 0) { + console.error('verify-readme-limitations: violations found:') + for (const failure of failures) console.error(` ${failure}`) + process.exit(1) +} + +console.log(`verify-readme-limitations: ${readmes.length} package READMEs checked (${NO_LIMITATIONS.length} whitelisted), all conform.`)