Merge remote-tracking branch 'origin/master' into codex/rfc-subagent-background-tasks

# Conflicts:
#	docs/architecture.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/bash.md
#	docs/event-producer-consumer.md
#	docs/rfc/implemented/feature/2026-07-06-sandbox.md
#	docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md
#	examples/AGENTS.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/permission-switching/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/system-prompt.golden.md
#	examples/coding-agent/cordis.yml
#	examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl
#	packages/bash/bash-local/README.md
#	packages/bash/bash-local/src/index.ts
#	packages/bash/bash-local/tests/executor.spec.ts
#	packages/bash/bash-sandbox/README.md
#	packages/bash/bash-sandbox/src/index.ts
#	packages/bash/bash/README.md
#	packages/bash/bash/src/index.ts
#	packages/bash/bash/src/types.ts
#	packages/bash/tool-bash/README.md
#	packages/bash/tool-bash/src/index.ts
#	packages/bash/tool-bash/tests/tools.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-core/README.md
#	packages/core/agent-core/src/index.ts
#	packages/subagent/subagent/README.md
#	packages/subagent/tool-subagent/README.md
#	packages/subagent/tool-subagent/src/index.ts
#	packages/ui/acp-agent/README.md
#	packages/ui/acp/README.md
#	packages/ui/stdio-agent/README.md
#	packages/util/brand/src/index.ts
#	scripts/doc-budgets.manifest.json
This commit is contained in:
Yichen Jiang
2026-07-14 18:05:46 +08:00
669 changed files with 10081 additions and 13745 deletions

View File

@@ -12,7 +12,7 @@ Build output currently matters only for `pnpm run build` + publint (nothing publ
Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-backed, actively released):
- Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*']` (explicit globs, not `workspace: true`, which would also pick up `examples/*` — they have package.json files but are not pnpm workspaces).
- Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*/*']` (explicit globs keep bundling to vendored Cordis and the TypeScript package tree; `workspace: true` would also discover example manifests and non-bundled workspace members).
- Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build RFC](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler.
- Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape).
- `scripts/build.ts` deleted; `pnpm run build` = `tsc -b tsconfig.build.json && tsdown`.

View File

@@ -44,13 +44,11 @@ The durability requirement was specific: the doc should show the **literal** cur
- **A compiled `_Check` assignability assertion** instead of the verbatim source match — rejected because byte-equality, not assignability, is the property we want: a renamed field with the same type would pass assignability.
- **Provenance as directive comments in the prose** — rejected for the central manifest, whose enforced 1:1 correspondence means a block can never be silently unchecked and an entry can never rot.
## Process
## Verification lesson
The design was driven entirely by a one-question-at-a-time grilling that walked the scoping decision tree through concrete examples (`BashExecRequest`, `ToolSchema`, `ToolDefinition`, the schema DSL, the presentation types, the session/persistence split) before committing to the spine-vs-seam rule — the rule was the *output* of the examples, not an a-priori axiom. The implementation landed as four commits mirroring the structure of the work: the gate (`e97f94b`), the catalog (`7e33c7b`), the maintenance-guard updates (`53e01a0`), and a review-fix commit (`6da7a0f`).
The spine-vs-seam rule was tested against `BashExecRequest`, tool schemas and definitions, the schema DSL, presentation types, and the session/persistence split before adoption.
That last commit is why the process is worth recording: an independent Codex review (gpt-5.5:xhigh) found a real **scan-gap bug**`verify-type-equiv` only scanned the docs the manifest named, so a type-equiv block added to an *unmanifested* doc was silently skipped, defeating the 1:1 guarantee in one direction. The fix scans every doc in the markdown scope and reports an unmanifested block as an orphan. The same review corrected a `SessionPersistence` surface-listing prose error (`has`/`delete`) and the `doc-sync` command summary. The bug is the point: a drift gate that silently skips part of its input is worse than no gate, and only an adversarial reader caught it.
This decision shipped in #71 **without** an RFC at the time — the judgment was that the `ts type-equiv` convention was small enough to document in `development.md`. This RFC is the retroactive record: the spine-vs-seam scoping rule and the verbatim-match-over-assignability choice are exactly the kind of "why was it done this way?" decisions a future maintainer would otherwise re-litigate, and its sibling catalog ([generated cordis events + services](2026-06-20-generated-cordis-catalog.md)) does carry an RFC, so the pair should be documented symmetrically.
`verify-type-equiv` must scan the complete Markdown scope, not only manifest-named documents. Otherwise an unmanifested `type-equiv` block escapes the claimed one-to-one check. The gate therefore reports such blocks as orphans. This RFC records that fail-closed scan rule together with the spine-vs-seam and verbatim-match decisions; the generated Cordis catalog has the symmetric design record in [its RFC](2026-06-20-generated-cordis-catalog.md).
## Consequences

View File

@@ -12,7 +12,7 @@ This is the wiring-axis complement to the [core-data-structures catalog](../../.
Generate the catalog from source instead of hand-maintaining a table and verifying a subset.
`scripts/gen-cordis-catalog.ts` walks the `interface Events` and `interface Context` declarations (plus the service classes) with the TypeScript compiler API and emits two sibling pages: `docs/cordis-catalog/events.md` (events grouped by scope, each rendered as signature + mode badge + its source JSDoc, plus the dispatch-mode legend) and `docs/cordis-catalog/services.md` (each `ctx.<key>` with its public method signatures + class JSDoc). The two axes are separate documents — a reader is either finding what to listen to or what to call, and each page scans and deep-links as its own reference instead of one long combined scroll. It mirrors the `gen-module-graph` pattern exactly: `--write` regenerates both, `--check` fails if either committed file is stale, output is deterministic (sorted), and the files are build artifacts that are never hand-edited. `verify-cordis-catalog` (the `--check`) runs inside `doc-sync`, so the freshness gate fires in the same lefthook pre-push and CI paths as every other doc gate.
`scripts/gen-cordis-catalog.ts` uses the TypeScript compiler API to emit separate event and service references from declarations and source JSDoc. Events include dispatch modes; services include public signatures. Deterministic `--write` and `--check` modes make both pages generated artifacts, with freshness enforced by `doc-sync`.
Pure generation is correct here because the codebase is disciplined enough that the AST is the whole truth: every event/service name is a string literal that round-trips to a static declaration — there are no dynamically-named events and no runtime-only services. So a generated doc cannot be wrong, and it closes the undocumented-event gap structurally (generation enumerates source rather than checking a hand-written subset).

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-02-bilingual-docs-and-pairing-gate.md: 8731fef46b16cfa20d223575c70774cff780a6aa
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: ce2589498ab16cf5ca2f5cdb3f58d031aeb8298f
2026-07-02-bilingual-docs-and-pairing-gate.md: 1e96622e7fb5694ab61772d68744394ef1aeb53a
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: c752d76f12f556ce190bf80c4f3a531c0821be8e

View File

@@ -34,5 +34,5 @@ Paired sibling files with locale suffixes are the dominant Chinese big-tech conv
- Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, "who confirmed these consistent, and when" is answerable from git blame on the yaml.
- When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.
- Generated docs (`cordis-catalog/`, `tool-catalog/`, `module-graph.md`) are excluded for now; the planned follow-up is to teach their generators to emit Chinese alongside English, at which point they leave the exclusion list.
- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR.
- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. New documents are the exception — a date-named document dated on/after the manifest's `requiredSince` cutoff merges bilingual or not at all, so the backlog only ever shrinks.
- The recorded hashes double as the update tool (`git cat-file -p <hash>` recovers either side's last-confirmed text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism.

View File

@@ -1,4 +1,4 @@
# RFC: 通过配对兄弟文件与配对门禁实现双语文档
# RFC通过配对兄弟文件与配对门禁实现双语文档
Status: implemented
@@ -6,33 +6,33 @@ Status: implemented
## 问题
本仓库的 README 与 docs 目录树会被公司内外的人和 agent智能体以中英两种语言阅读。没有机制纯靠手工维护第二语言,正是译文腐烂的方式:一侧续演进,另一侧默默地说谎,而没有门禁会注意到。对这类不变式,本仓库一贯的答案是把它编码机械检查(见[质量门禁](2026-06-11-quality-gates.md)与 [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。
本仓库的 README 与 docs 目录树会被公司内外的人和 agent智能体以中英两种语言阅读。没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧续演进,另一侧默默失实,而没有门禁会注意到。对这类不变式,本仓库一贯的做法是将其编码机械检查(见[质量门禁](2026-06-11-quality-gates.md)与 [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。
## 决策
- **配对兄弟文件,两种语言同权。**一对文档三个兄弟文件:英文 `foo.md`、中文 `foo.zh.md`一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典——一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束这对文件的是两侧必须说同样的话,且配对整体合(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../i18n/terminology.md)。
- **旁挂记录两侧 blob hash使一致性可检查。**`foo.i18n.yaml` 保存两侧文件在上一次确认一致状态下各自的完整 git blob hash。此后改了任一侧而重新确认配对,都能被机械检测出来——纯内容比较无需查询历史——而且同一个 PR 改动的文件也能算出 hashcommit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)产生一份可评审的 yaml diff确认一致在 PR 是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。**门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)强制执行required 的配对存在;任何已存在的配对完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)保持不配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单是一个棘轮:每个合的翻译批次自己的文件加进去,覆盖面只增不减。
- **翻译是 agent 的工作,由人评审。**进仓的工作流是 [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md) 同一模式skill 承载工作流,并把真源让给文档
- **配对兄弟文件,两种语言同权。** 一对文档三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是两侧必须表达相同的内容,且配对整体合(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../i18n/terminology.md)。
- **伴随记录保存两侧 blob hash使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致各自的完整 git blob hash。此后改了任一侧而重新确认配对,都能被机械检测出来纯内容比较无需查询历史而且同一个 PR 改动的文件也能算出 hashcommit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`产生一份可评审的 yaml diff确认一致在 PR 是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则required 的配对必须存在;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)保持不配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单只进不退:每个合的翻译批次自己的文件加入其中,覆盖面只增不减。
- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md) 模式相同skill 承载工作流,并将文档作为真源
## 曾考虑的替代方案
- **英文为正典源、指纹放在译文内**——本 RFC 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 RFC再译英文两种语言同权而单向正典模型无法表达这一点。覆盖**两侧**的旁挂记录取代了文件内的单向指纹blob hash 的机制原样保留
- **语言目录(`docs/en/` + `docs/zh/`Kubernetes/ECharts 模式)**——否决本仓库没有 locale 映射到路由的文档站框架,挪动每个英文文件会搅动所有既有交叉引用`verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑而不是原样工作。
- **独立翻译仓库PingCAP `docs`/`docs-cn` 模式)**——否决适合有独立发布节奏的文档产品,对 monorepo 自的文档而言过重;还会把译文置于本仓库门禁不到的地方。
- **中英混排单文件(一个文件、两种语言)**——否决每个 diff 都翻倍,破坏一段一行约定的 diff 工效,且局部不一致不可见。
- **Commit hash 式记录MDN `l10n.sourceCommit` 模式)**——否决,改用 blob hash同一个 PR 内的改动还没有 commit hashMDN 模式无法表达「与本 PR 引入的状态一致」,且校验它需要 git 历史而非文件内容。
- **比较配对两侧的 git 时间戳(无记录)**——否决纯格式化的改动会误报,一次无关改动之后提交的另一侧会漏报;只有内容同一性这个信号与门禁的承诺名实相符。
- **英文为正典源、指纹放在译文内**本 RFC 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 RFC再译英文两种语言同权而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹blob hash 的机制本身保持不变
- **语言目录(`docs/en/` + `docs/zh/`Kubernetes/ECharts 模式)**否决本仓库没有 locale 映射到路由的文档站框架;如果移动所有英文文件所有既有交叉引用都要随之修改;`verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑,而非原样工作。
- **独立翻译仓库PingCAP `docs`/`docs-cn` 模式)**否决适合有独立发布节奏的文档产品,对 monorepo 自的文档而言过重;还会把译文置于本仓库门禁触及不到的地方。
- **中英混排单文件(一个文件、两种语言)**否决每个 diff 都翻倍,破坏一段一行约定的 diff 易读性,且局部不一致不可见。
- **Commit hash 式记录MDN `l10n.sourceCommit` 模式)**否决,改用 blob hash同一个 PR 内的改动还没有 commit hashMDN 模式无法表达「与本 PR 引入的状态一致」,且校验它需要 git 历史而非文件内容。
- **比较配对两侧的 git 时间戳(无记录)**否决纯格式化的改动会误报,一次无关改动之后提交的对侧文件会漏报;只有内容同一性这个信号与门禁的承诺名实相符。
## 业界先例
带语言后缀的配对兄弟文件是中国大厂的主流约定ant-design 的 `index.zh-CN.md`/`index.en-US.md`arco-design 的 `README.zh-CN.md` 加顶部切换行Apache ShardingSphere 的 387 对 `.cn.md`/`.en.md`——但这些仓库都没有在 CI **强制**配对或一致性;约定纯靠评审维系。一致性自动化存在于中国MDN 的 `l10n.sourceCommit` front-matter 指纹、Vue 的 Ryu-Cho action监视上游 commit为陈旧译文自动开 issue/PR、Kubernetes 的本地化漂移脚本、微软 Azure co-op-translatorCI 中由源 hash 驱动的 LLM 重译)。本设计两者结合:中文生态的文件布局,加 hash 对门禁,再加一个仓 agent skill(技能)替代 bot 服务。
带语言后缀的配对兄弟文件是中国大厂的主流约定ant-design 的 `index.zh-CN.md`/`index.en-US.md`arco-design 的 `README.zh-CN.md` 加顶部切换行Apache ShardingSphere 的 387 对 `.cn.md`/`.en.md`但这些仓库都没有在 CI **强制**配对或一致性检查;约定纯靠评审维系。一致性自动化存在于中国MDN 的 `l10n.sourceCommit` front-matter 指纹、Vue 的 Ryu-Cho action监视上游 commit为陈旧译文自动开 issue/PR、Kubernetes 的本地化漂移脚本、微软 Azure co-op-translatorCI 中由源 hash 驱动的 LLM 重译)。本设计两者结合:中文生态的文件布局,加 hash 对门禁,再加一个仓库内置的 agent skill 替代 bot 服务。
## 后果
- 修改已配对文档的任一侧,同一个 PR 就有义务更新另一侧并重新记录配对——门禁 doc-sync 规则双语化,不变式由 CI而非评审者的记忆承载。
- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对一致」可以从 yaml 的 git blame 直接回答。
- 两侧说法冲突时,没有机械规则裁决谁赢——由 PR 评审裁决。这是同权的代价,是有意接受的:另一个选项(正典语言)禁止中文先行撰写。
- 生成文档(`cordis-catalog/``tool-catalog/``module-graph.md`)暂被排除;计划中的后续工作是让它们的生成器在输出英文的同时输出中文,届时移出排除清单。
- 推进天然是渐进的:`required` 之外的文档是可见的 backlog`--list`不是红的 CI因此配对按可评审的批次落地,无需一个巨型 PR。
- 记录的 hash 兼作更新工具(`git cat-file -p <hash>` 能还原任一侧上次确认的文本,用于基于 diff 的最小更新),所以这套机制从不强迫整篇重译。
- 修改已配对文档的任一侧,同一个 PR 就有义务更新侧并重新记录配对门禁 doc-sync 规则双语化,不变式由 CI而非评审者的记忆承载。
- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。
- 两侧说法冲突时,没有机械规则裁决谁赢由 PR 评审裁决。这是同权的代价,是有意接受的:另一个选项(正典语言)禁止中文先行撰写。
- 生成文档(`cordis-catalog/``tool-catalog/``module-graph.md`)暂被排除;计划中的后续工作是让生成器在输出英文的同时输出中文,届时将这些文件移出排除清单。
- 推进天然是渐进的:`required` 之外的文档是可见的 backlog`--list`而非红色的 CI因此配对按可评审的批次落地,无需一个巨型 PR。
- 记录的 hash 兼作更新工具(`git cat-file -p <hash>` 能还原任一侧上次确认的文本,用于基于 diff 的最小更新),因此这套机制从不强迫整篇重译。

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
A reader — a plugin author, a prompt engineer, someone auditing what the agent can do — has no single place that lists the model-facing tools the harness ships. The `name` / `description` / JSON-Schema `parameters` a tool contributes are what the model actually receives (via `ctx.systemPrompt.tools()` off `ctx.tools.schemas()`), but they are scattered across each `defineTool` call in each `packages/*/tool-*` package, buried in string concatenation and runtime spreads. The cordis [events](../../../cordis-catalog/events.md) & [services](../../../cordis-catalog/services.md) catalogs ([their RFC](2026-06-20-generated-cordis-catalog.md)) document the *wiring* a plugin works against and the [core-data-structures catalog](../../../core-data-structures/core.md) documents the *vocabulary* those signatures move — but neither documents the *tools* the agent is offered. This RFC adds that third reference surface, `docs/tool-catalog.md`, and a freshness gate so it cannot drift.
The repository had no single reference for the names, descriptions, and JSON Schemas actually exposed to the model. Source declarations are scattered and runtime-composed, while the existing Cordis and data-structure catalogs cover wiring and vocabulary rather than tools.
## Decision
@@ -27,13 +27,13 @@ Booting has a cost the AST pass did not: there is no source declaration set to e
### A hand-maintained boot manifest is the irreducible policy
The boot manifest (`TOOL_PACKAGES`) is a hand-written list — in tension with the proposed [Discover package inventories instead of maintaining static lists](../../proposed/process/2026-06-20-discover-package-inventory.md). The tension is deliberate and resolved as follows: the *inventory* is discovered (the glob guard means no one maintains "the list of tool packages" — the filesystem is the source of truth, and drift fails the gate), but the *boot recipe* per package — which seams to plug (`bash-local` for `ctx.bash`, `subagent` + `subagent-mock` for `ctx.subagents`) and with what config (`{ provider: 'mock' }`) — is genuine policy that no layout fact encodes. Per that RFC's own "what we give up" ("stay boring: read manifests, filter on explicit fields, print the resolved list, and fail loud"), a recipe closure is the boring, explicit form; inferring seam wiring from injects would be the "too clever" path it warns against. So: discovered inventory, hand-written recipe, gate on completeness.
The filesystem discovers the tool-package inventory and the completeness guard rejects omissions. `TOOL_PACKAGES` still owns an explicit boot recipe for each package because required seam implementations and config are policy, not facts that can be inferred safely from layout or injection names.
### Scope
Shipped product tool packages under `packages/*/tool-*`, each booted with its default config, including `dsh-tool-bash` (`bash`), `dsh-tool-tasks` (`task_output`, `task_list`, `task_kill`), and `dsh-tool-subagent` (`subagent`). Example-only tools are excluded.
The unit is the PACKAGE, not the deployed tool instance. A package's registered tool name can be a load-time config — `tool-subagent`'s `toolName` — so the same package surfaces as `subagent` (spawn backend) AND `subagent_fork` (fork backend) in the shipped `coding-agent` / `acp-agent` configs, with an identical schema. The generator boots each package once at its default and records such shipped aliases in a per-package note, rather than enumerating every deployment permutation. Cataloguing at the package level keeps the source of truth the package (what a plugin author reads) and avoids leaking example-app `cordis.yml` config into a packages-scoped generator; the note keeps the doc honest about the names a reader will actually see the model receive. The design deliberately does not attempt to catalog "every configured tool instance across every leaf config" — that is a deployment inventory, a different (and unbounded) surface.
The catalog unit is a package, not every configured tool instance. Each package boots once with default config; load-time aliases such as `subagent_fork` are noted without enumerating every deployment permutation. A deployment inventory is a separate, unbounded surface.
### A plain `json` fence

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
The [generated cordis catalog](2026-06-20-generated-cordis-catalog.md) already walks every harness `interface Events` member and every `ctx.<key>` service class with the TypeScript compiler API, and already hard-errors on a missing `@mode` tag — a forcing function that made dispatch modes impossible to leave undocumented. Nothing equivalent guarded the rest of the JSDoc: a service method could ship with no doc at all, and no event or method documented its parameters or return value individually. A survey at adoption found 5 public service methods with no JSDoc and roughly 139 missing `@param`/`@returns` entries across 15 files — on the product API spine (`ctx.bash`, `ctx.fs`, `ctx.sessions`, …) and the cross-plugin event payload contracts, exactly the surface where "what does this argument mean" is the question a plugin author asks the IDE.
The generated Cordis catalog enforced event dispatch modes but not complete service and event contracts. Methods could lack descriptions, and parameters or returns could be undocumented on the cross-plugin API surface where IDE guidance matters most.
The AGENTS.md rule ("every export has a JSDoc explaining semantics") is prose-checkable only by review; the repo's stated preference is to encode invariants in mechanical gates. The scope "cordis service functions and events" has a precise machine definition that only the catalog generator knows: events are the `interface Events` members inside `declare module 'cordis'`, and the service surface is the public methods of the class each `interface Context` key names. An ESLint rule cannot see that mapping; the generator computes it on every run.

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
The repo's standing docs accrete. Root `AGENTS.md` reached 8,130 words through 50 commits in two and a half weeks — each PR appending its own lesson, none displacing anything — until the same rule was stated two or three times inside one file (the pushed-branch rewrite ban ~600 words across two sections; the with-key e2e policy ~400 words across two), an incident already recorded in [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md) was retold inline at ~750 words, and the per-package one-liner map existed in five places. [architecture.md](../../../architecture.md) grew the same way: paragraph walls re-narrating RFCs it already links, plus implementation-status annotations that were stale the week after they were written. The writing rules that forbid this (document current state, never history) predate the drift and sat in the very file violating them — prose rules alone do not hold against accretion pressure. The repo's standing answer to an invariant of this kind is a mechanical check ([quality gates](2026-06-11-quality-gates.md), [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md)).
Standing docs accumulated repeated rules, retold incidents, duplicated package maps, and stale RFC summaries despite existing writing guidance. Because review alone did not prevent that growth, the repository needed a mechanical budget alongside its documentation taxonomy.
## Decision
@@ -24,11 +24,3 @@ The repo's standing docs accrete. Root `AGENTS.md` reached 8,130 words through 5
- Adding to a budgeted doc now requires displacement: relocate the addition to its taxonomy home with a pointer, or condense existing prose to pay for it. Growth without pruning fails CI.
- The bring-under-target rewrites land as stacked follow-ups that ratchet the manifest down as they merge; until each lands, its doc's frozen ceiling only prevents further growth.
- Word count is a crude proxy accepted deliberately: it cannot judge quality, but it forces the relocation decision at exactly the moment content is being added, which is when the author has the context to place it correctly.
## Deferred work
The first audit cycle under the standard, in rough priority order (evidence gathered in the survey that motivated this RFC):
- Package README trims where generated catalogs or JSDoc are restated or history is narrated: `packages/ui/acp`, `packages/core/tools`, `packages/bash/tool-bash`, `packages/core/session`, `packages/compact/compact-basic`, `packages/session-persistence/session-persistence`.
- `docs/core-data-structures/core.md`: drop the JSDoc walls from the `Agent`/`GenerateOptions` type-equiv pastes per that page's own stated rule.
- [Postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md): merge the overlapping Executive summary and Summary sections.

View File

@@ -4,13 +4,13 @@ Status: implemented
## Problem
The session event log is the harness's on-disk contract: every `SessionEventMap` member is a record a persistence backend writes verbatim and a replay reconstructs from, and adding one that breaks the durability rules is a breaking change to the on-disk format. Yet the vocabulary had no single reference. The declarations are split across three files — the owning interface in `@deepseek-ai/dsh-session` plus declaration merges in `@deepseek-ai/dsh-compact` and `@deepseek-ai/dsh-hook-protocol` — and the doc surfaces covered it with hand-copies: a `hook/*` payload table in [session.md](../../../core-data-structures/session.md), a `compact/*` payload table in the compact README, payload bullets in the hook-protocol README, and a name-list in the session README. The name-list's merge note had already drifted (it named the compaction merge and omitted the hook merge entirely), and nothing could catch the next merge going undocumented: a hand-copy only checks the names someone already wrote down. This is the same gap the [cordis catalog](2026-06-20-generated-cordis-catalog.md) closed for bus events and the [tool catalog](2026-07-02-tool-schema-catalog.md) closed for model-facing tools — and log events are covered by neither: a `SessionEventMap` member is not a cordis `Events` declaration (it reaches listeners via the single `session/event` emit), so it has no cordis-catalog row by design.
`SessionEventMap` is the on-disk vocabulary, but its declarations are split across the owning session package and declaration merges. The generated persistence catalog is the single reference for every event and payload; hand-maintained tables drift and are removed. These records are not Cordis events—observers receive them through the single `session/event` bus event—so the Cordis catalog cannot cover them. The generator discovers all declarations and the doc-sync freshness gate rejects omissions or stale output.
## Decision
Generate `docs/persistence-catalog.md` from source, with a freshness gate, as the fourth reference surface: the *records* a persisted session log can contain, complementing the cordis catalog (wiring), core-data-structures (vocabulary), and the tool catalog (tools).
`scripts/gen-persistence-catalog.ts` is a pure TypeScript-AST pass, like `gen-cordis-catalog.ts` — log events ARE statically knowable: every member is a string-literal-named property with a static type annotation, so the AST is the whole truth. The walk collects every `interface SessionEventMap` declaration under `packages/*/*/src` — the owning top-level interface and every `declare module '@deepseek-ai/dsh-session'` merge — so a brand-new event, core or merged, appears in the next regenerate and an un-regenerated file fails `--check` (`verify-persistence-catalog`, a `doc-sync` member, so pre-push and CI both run it). Each entry renders the member's JSDoc prose, its payload (printed through the TypeScript printer, so a newline-separated multi-line type literal still yields a valid one-line fragment), a surface badge, cross-links into core-data-structures, and the declaration's source pointer, grouped by scope.
`gen-persistence-catalog.ts` scans every owning and declaration-merged `SessionEventMap` with the TypeScript AST. It renders source JSDoc, payload type, derived surface badge, reference links, and source location. The doc-sync freshness check rejects a vocabulary change whose catalog was not regenerated.
Specific choices:

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
The tree's layout is uniform — [the classification scheme](2026-06-20-rfc-classification.md) path-encodes lifecycle and class and gates both — but the file insides never were. The corpus the format decision faced had two H1 spellings; some twenty-seven `Status:` line spellings once free-text rejection reasons are collapsed — bare enums, dated parentheticals duplicating what the filename and git already carry — plus three English files (and the zh counterpart of one of them) with no status at all; two body genres side by side (ADR-style `Context`/`Decision`/`Consequences` beside proposal-style `Problem`/`Proposal`/`Risks`), so every new RFC guessed its shape from whichever neighbor its author opened; thirty-nine files carrying a debt comment that flagged them as "legacy ADR/RFC body format" awaiting a unified template that was never actually defined; and nineteen implemented RFCs still carrying thirty occurrences of the proposal-era headings (`Acceptance criteria`, `Plan`, `Migration plan`, `Proposal`) that the [documentation standard's slop checklist](../../../AGENTS.md) outlaws for `implemented/` — outlawed, but enforced by nothing, so the `proposed/``implemented/` move could silently skip the rewrite [implemented/AGENTS.md](../AGENTS.md) requires.
RFC paths encoded lifecycle and class, but file contents still mixed headings, status formats, ADR and proposal templates, and proposal-era sections in implemented records. Authors copied whichever neighbor they found, and lifecycle moves could skip the required rewrite because no gate enforced an in-file contract.
## Decision

View File

@@ -22,7 +22,7 @@ The contract by declaration kind:
Three exemption families keep the gate from demanding boilerplate, in the spirit of the cordis gate's `this`/`next` exemptions (documenting an exempt name anyway is allowed; only absence goes unchecked):
- **Heritage members.** A class member whose name exists on an `extends`/`implements` heritage type is exempt: the seam declaration is the doc's one home, and the IDE inherits it on hover — re-documenting every `LocalBashExecutor.run` invites drift. The exemption stops where the override grows surface the base never documented: a protected-only base member does not exempt a public override, parameters the base never names keep their `@param` duty (an underscore-prefixed rename of a base parameter — the deliberately-unused marker — is the same parameter), and a concrete result above a void base return keeps its `@returns` duty (an unannotated override's inferred return is classified by the checker, so a faithful void override needs no boilerplate annotation). Heritage lookups and that one return classification are the walk's only TYPE CHECKER questions (heritage types live across package boundaries, resolved through the repo `paths` map); everything else stays pure AST, and the annotated-return requirement is kept for symmetry with the cordis gate (it bound nothing at adoption — every exported function was already annotated).
- **Heritage members.** Overrides inherit documentation from their base declaration. New public surface still requires docs: added parameters, a public override of a protected member, or a concrete return over a void base. Heritage lookup and inferred return classification are the gate's only type-checker work; other checks use the AST.
- **Plugin-protocol slots.** Top-level `name` / `inject` / `reusable` / `Config` consts and the `apply` entry, plus the same slots as statics on a plugin class, are framework protocol: their shape is fixed by cordis, and the module doc comment plus the `interface Config` carry the plugin's real semantics.
- **Constructors**, mirroring the cordis gate: plugin classes are framework-constructed, and the class doc owns the story.

View File

@@ -4,11 +4,11 @@ Status: implemented
## Problem
The config surface — the exact set of fields a `cordis.yml` entry's `config:` block can set for each plugin, with types, defaults, and semantics — had no reference page. A deployment author assembling a config tree had to open every plugin's source (or trust its README) to learn what is settable. The per-package README `## Config` sections cover parts of it by hand, in formats that diverged package-by-package (a key/default table here, an annotated YAML snippet there) and with no gate tying them to source. Nothing enumerated which packages are loadable at all — plugin vs abstract seam vs plain library — and nothing verified that the runtime schemastery schema and the documented `Config` interface agree, so a schema-validated field could exist with no documentation anywhere.
The repository had no source-backed reference for plugin configuration. Package READMEs documented fields inconsistently, did not enumerate which packages are loadable, and did not verify that runtime schemas agree with declared config types.
## Decision
Generate the catalog from source: `scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../config-catalog.md), one section per configurable package containing the VERBATIM config declaration — the `export interface Config` (or equivalently named type) with its JSDoc, pasted as-is in a ` ```ts config-catalog ` fence — plus a `Requires:` line (the plugin's `inject`), a `Depends on:` line resolving every type name the paste references, and a source pointer. The paste is the plugin's full declared config type: a field the runtime schema deliberately excludes is a runtime-only seam, marked as such by its own JSDoc, not a `cordis.yml`-settable knob. Package-local referenced types are pasted transitively into the same fence; another plugin's config type links to that plugin's section; names in the cordis catalog's shared `LINK_MAP` link to core-data-structures; any other workspace type links to its source; an external type is named with its module. It mirrors the `gen-cordis-catalog` pattern exactly: `--write` regenerates, `--check` (`verify-config-catalog`, inside `doc-sync`) fails if the committed file is stale, output is deterministic, the file is a build artifact never hand-edited.
`scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../config-catalog.md) from each plugin's declared config type and JSDoc, with injection requirements, referenced-type links, and a source pointer. Package-local types are included transitively; workspace and external types are linked or named. Deterministic `--write` and `--check` modes make the committed page a generated artifact.
Pure AST generation is correct here for the same reason it is for the events/services catalog and NOT for the tool catalog: a config type is a static declaration and every schemastery schema in the repo is a static `z.object`/`z.intersect` literal, so the source is the whole truth — nothing about the config surface is runtime-composed.
@@ -17,7 +17,7 @@ Specific choices:
- **The config type is the second-parameter type.** What the catalog documents is the declared type of `apply(ctx, config)` / the service constructor's `(ctx, config)` — the value cordis actually passes — not a `Config` export located by naming convention. This is what makes the walk total: it works for interfaces named `AcpConfig` or `BasicCompactConfig`, for types declared in a sibling file, and for plugins with no validating schema at all.
- **Classification is total.** Every `packages/<group>/<pkg>` entry resolves, mirroring the Loader's `unwrapExports` (`exports.default ?? exports`), to a configurable plugin, a config-free plugin, an abstract seam class, or a library — each rendered in its own section — and an unclassifiable entry hard-errors. A new package cannot be silently undocumented.
- **Per-field JSDoc is enforced.** Every property of a pasted declaration (nested type literals included) needs non-empty JSDoc prose, or generation fails. The paste IS the documentation, so this is the same forcing function the events catalog applies via `@mode`: thin source docs fail the gate rather than yielding a thin catalog.
- **The schema is cross-checked, one-directionally, nested keys included.** When a plugin declares a schemastery schema (`export const Config` / `static Config`), the generator walks it statically — object-literal keys and their nested object/array compositions as key paths (`agents[].id`), chained refinements, and `z.intersect` composition across workspace packages — and every schema-validated key path must be locatable on the declared config type, resolving package-local and workspace-imported types (re-export chains included), intersections, unions, utility wrappers, and indexed access. So the paste cannot hide a loader-accepted field, top-level or nested. The check is presence-only and fails loud only on a definite miss: a path crossing a type the walk cannot enumerate (an external package's type) is skipped rather than mis-reported, and dynamic-key shapes (`z.dict`) or union alternatives contribute no nested paths. The reverse direction is deliberately unchecked: a declared field may be a runtime-only seam the schema excludes (the ACP bridge's test-injected `stream`).
- **Schema keys are checked against the declared type.** The generator resolves nested object and array paths through local and workspace types. Definite missing paths fail; external or dynamic shapes that cannot be enumerated are skipped. The check is intentionally one-way because declared types may contain runtime-only fields excluded from loader config.
- **A dedicated fence.** Pasted declarations use a ` ```ts config-catalog ` info string that `doc-typecheck` skips (a lone declaration referencing imported types is not standalone-compilable), excluded from the opt-out ratio — the same treatment the `cordis-catalog` and `persistence-catalog` fences get.
- **A single file at `docs/config-catalog.md`**, not a one-file directory: the page serves one audience (the `cordis.yml` author) with one axis, unlike `cordis-catalog/`, which holds two sibling pages.

View File

@@ -10,15 +10,15 @@ The hard part is the artifact boundary. `publint`, `verify-node-next-types`, and
## Decision
[CI](../../../../.github/workflows/ci.yml) keeps the keyless workflow to a few broad jobs instead of one job per gate. The Node 24 matrix has five lanes: static gates (`pnpm run check:ci:static`), lint (`pnpm run check:ci:lint`), coverage (`pnpm run check:ci:coverage`), snapshot replay (`pnpm run check:ci:snapshot`), and artifact gates (`pnpm run check:ci:artifacts`). The compatibility matrix has Node 22.19, 24, and 26 jobs; each installs once and runs `pnpm run check:node-compat`.
[CI](../../../../.github/workflows/ci.yml) groups keyless checks into broad primary-runtime lanes plus a compatibility matrix. The workflow file owns the current lane and runtime inventory.
Each lane delegates to [scripts/run-gates.ts](../../../../scripts/run-gates.ts), an in-process scheduler with bounded concurrency (`DSH_GATE_CONCURRENCY`). The static lane fans out constraints, the echo-agent demo smoke, `doc-sync` leaf gates, module-graph freshness, and `knip`; the lint lane runs ESLint with its own Node heap cap and a content-strategy ESLint cache; the coverage lane runs Vitest coverage with bounded file workers (`DSH_COVERAGE_MAX_WORKERS`); the snapshot lane isolates replay; the artifact lane builds once and then fans out the artifact consumers. Every compatibility job runs the TypeScript typecheck and a keyless workflow-workerthread source-launch smoke, which starts a real unbuilt worker and therefore catches Node-version-specific loader/runtime failures that typechecking cannot. The scheduler buffers each gate's output and prints a named result block with duration, so independent failures stay attributable inside each broad job log.
Each lane delegates to [scripts/run-gates.ts](../../../../scripts/run-gates.ts), which schedules independent gates with bounded concurrency and prints an attributable result block for each one. Artifact consumers depend on one build within their lane, while compatibility jobs combine typechecking with a real unbuilt worker launch to cover runtime-specific loader behavior.
Generated `.sessions/` logs and `.doc-typecheck-*` temp directories are ignored by lint. The aggregate local CI mode still runs demo smoke after lint, while the split GitHub static lane can run demo smoke directly because lint is isolated in its own lane.
Build output is produced once inside the Node 24 artifact lane. The artifact consumers (`publint`, `verify-node-next-types`, and built-bin smoke) declare a dependency on `build`, so there is no upload/download handoff and no consumer can race ahead of declarations or bundles. The CI coverage reporter is text-only while local coverage keeps the HTML report.
Both CI workflows cache the pnpm store after enabling Corepack. The real-API e2e workflow also uses the shared `vitest.e2e.config.ts` bounded file pool (`DSH_E2E_MAX_WORKERS=14` in CI), so its speedup comes from dependency-cache reuse plus lower-level test-file fan-out instead of a separate GitHub job split.
Both workflows cache the pnpm store. The real-API workflow uses the shared bounded Vitest file pool rather than a separate job per test group.
## Alternatives considered

View File

@@ -0,0 +1,27 @@
# RFC: A gated Known-Limitations section in every package README
Status: implemented
## Problem
The [documentation standard](../../../AGENTS.md) assigns limitations to package READMEs. Without a shared shape, an omitted section cannot distinguish an audited absence from forgotten documentation, and variant headings prevent a repository-wide search.
## Decision
Every package manifest under `packages/<group>/<pkg>/package.json` has a sibling README with the canonical `## Known Limitations and Deferred Work` section. Its bullets record durable consumer gaps and non-obvious maintainer constraints owned by that package; ordinary cleanup remains in its source TODO or owning RFC. The [`verify-package-readme-limitations` gate](../../../../scripts/verify-package-readme-limitations.ts) derives the package set from manifests, rejects missing READMEs, and requires exactly one canonical h2 with at least one top-level bullet. Near-miss headings such as “Limitations,” “Deferred,” “What is NOT here,” or “Non-goals” fail.
A package with nothing to declare is listed in `NO_LIMITATIONS` and omits the section. Adding a limitation requires removing the entry; renames and removals fail because every entry must name a scanned package.
The gate checks presence, shape, and the allowlist. Review under the documentation and [prose](../../../../.agents/skills/dsh-prose-standard/SKILL.md) standards owns coverage and accuracy. The standing rule lives in [packages/AGENTS.md](../../../../packages/AGENTS.md).
## Alternatives considered
- **Free-form headings** — cannot be searched uniformly and still need near-miss detection.
- **Require an empty section or “None.”** — boilerplate can remain after a package gains a limitation; an allowlist makes absence explicit and reviewable.
- **Impose a word ceiling** — legitimate limitation counts vary, so review governs this unbudgeted README tier.
## Consequences
- New packages declare qualifying limitations or explicitly join the allowlist; missing, drifted, and empty sections fail `doc-sync` locally and in CI.
- The gate adds one dependency-free TypeScript script to `doc-sync`.
- Renaming the enforced heading requires changing the script and every package README together.

View File

@@ -0,0 +1,31 @@
# RFC: Package Model Experience contract
Status: implemented
## Problem
A package README can explain APIs and runtime mechanics without answering the question that dominates an agent harness's behavior and cost: what from this package reaches a model request, under which conditions, and how long those tokens remain. The omission is especially hard to audit in a plugin architecture. A consumer may turn a backend result into a tool message, a policy plugin may replace success with an error, compaction may remove old history, and an agent-scoped registration may change one agent's prompt or schemas while leaving every other agent unchanged. Reading only the nominally model-facing packages therefore misses real context effects, while reading source across every dependency is too expensive for routine review.
## Decision
Every workspace package README with a model-facing or model-adjacent contract ends with the canonical [Model Experience section](../../../cookbook/adding-a-package.md#4-write-the-package-readme), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. An audited model-agnostic generic package omits the section through `NO_MODEL_EXPERIENCE_SECTION`.
Packages with direct, conditional, capped, lifetime, multi-surface, or auxiliary-model effects use one H3 per context surface. Each names what the relevant model receives and when, then classifies the token effect. Stable package-owned text is quoted exactly: system-prompt prose and other long literals use a nested H4 plus `markdown` fence, while short literals stay inline with named interpolation placeholders. Tool-schema surfaces link their anchored section in the generated [tool catalog](../../../tool-catalog.md) and state only composition or configuration deltas; runtime-only definitions explain why the catalog omits them. Data-dependent and provider-owned text is summarized. Agent-scoped visibility is explicit, and prompt and schema surfaces remain separate when scoping can hide one without the other.
A package with no model-context effect, or one path rendered entirely by another package, uses the verifier's audited one-sentence form: `None, as ` or `Indirectly, through `. Pure transport and keyless test-support packages use the none form when they create no model-bound content. Provider backends use the indirect form even when they cap or filter data, and wiring bundles use it when named children own every effect. These sentences locate the contribution without restating the consumer. Structured sections likewise document only package-owned inputs, transformations, and deltas.
`verify-package-readme-model-experience` discovers package manifests and validates the three classifications, canonical final-section order, required fields, concrete literal evidence, nested verbatim blocks, and anchored tool-catalog links. It runs in `doc-sync` and the parallel gate runner. Review still owns coverage, link relevance, and factual accuracy.
## Alternatives considered
- **Document only packages that register prompts or tools** — rejected because backends, policy plugins, adapters, persistence, scoping, and compaction change the content or lifetime of tokens without owning a model-facing schema.
- **Generate one central context-cost catalog from source** — rejected because an AST can find registrations but cannot infer semantic conditions such as history retention, output truncation, parent-versus-child visibility, or an auxiliary model boundary. The package README is the implementation-local contract; a central copy would add another drift surface.
- **Require numeric token counts** — rejected because exact counts depend on the selected model tokenizer, adapter serialization, configuration, and runtime data. The stable contract is the growth shape: fixed per request, conditional per call, retained, replaced, capped, or zero-direct.
- **Use a three-column table** — rejected because exact source text and conditional result shapes make cells dense and difficult to scan. Repeated subsections give each context surface readable vertical space while preserving the same fields.
- **Allow every zero-impact package to omit the section** — rejected because unconstrained absence is ambiguous between an audited zero and forgotten documentation. Omission is reserved for model-agnostic generic packages named with a reason in the verifier; model-adjacent zero-impact packages keep one explicit sentence.
- **Require the full structured form for audited zero or simple indirect packages** — rejected because it repeats labels around one fact. A gated sentence preserves explicit coverage without the ceremony.
- **Convention without a gate** — rejected because a repo-wide contract must also cover every future package; review memory cannot reliably detect an omitted README section.
## Consequences
A reviewer can start at any model-facing or model-adjacent package and see its contribution to the conversation model, child models, and auxiliary calls without reconstructing the full plugin graph. Token-budget work can distinguish repeated request overhead from data-dependent history, and agent-scoped changes have an explicit documentation checkpoint. Package authors maintain one or more compact context-surface blocks or one classified sentence whenever model-visible behavior changes; audited generic packages carry no irrelevant model boilerplate. The structured fields do not promise provider-exact token counts; measurements remain model- and workload-specific, while the documented growth and visibility contract stays stable.