From e56c6234d2f4cdaa517f875fbddbc3b7c36181d3 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 10 Aug 2026 14:49:30 +0800 Subject: [PATCH 1/5] chore: exclude archived Agent Notes from rg --- .../process/2026-07-26-frozen-agent-note-archive.i18n.yaml | 4 ++-- .../process/2026-07-26-frozen-agent-note-archive.md | 6 +++++- .../process/2026-07-26-frozen-agent-note-archive.zh.md | 6 +++++- .rgignore | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 .rgignore diff --git a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml index 1ae66cc1b7..df1a615d35 100644 --- a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md -2026-07-26-frozen-agent-note-archive.md: 52b43088b276c0c8e263fc8a81a2df1408cc8059 -2026-07-26-frozen-agent-note-archive.zh.md: a37e06e7cbc19b5000d4849cc3ee2ddc9b451ee3 +2026-07-26-frozen-agent-note-archive.md: 0c139c4a5d892de5bdace76b4935edf32586c4c1 +2026-07-26-frozen-agent-note-archive.zh.md: e67f981e5d7ae9d800b725e5ecbaf15dd7fef46b diff --git a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md index 52b43088b2..0c139c4a5d 100644 --- a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md +++ b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md @@ -14,6 +14,8 @@ Only implemented Agent Notes can be archived. An implemented note moves when its The archive uses `.agents/notes/archived/{kind}/yyyy-mm-dd-topic.md`; the redundant `implemented` segment is absent. The archival change moves the complete English, Chinese, and consistency-sidecar triplet, leaves `Status: implemented` intact, and inserts `Archived: YYYY-MM-DD` immediately below it in both language files. Relocation, that metadata line, the corresponding sidecar re-record, and mechanical inbound-link repair are the only permitted archival edits. +The root `.rgignore` excludes the archive from searches that traverse a parent directory. Historical queries name the archive directory explicitly, so intentional access remains available without mixing frozen facts into active decision discovery. + After archival, the triplet is permanently frozen and is historical context rather than current authority. It is not updated for renamed packages, changed behavior, translation standards, formatting rules, broken outbound links, or later documentation contracts. Active prose may intentionally link into an archived note, redirect that link to current authority, or delete it. Repository gates therefore validate links into archived files but never treat archived files as link sources. [`verify-archived-agent-notes`](../../../../scripts/verify-archived-agent-notes.ts) owns the frozen boundary. It accepts only the closed set of Agent Note kinds, requires a complete triplet with implemented status and matching valid archive dates, verifies the sidecar against both current Git blob hashes, and seals every artifact by path and SHA-256 content hash in an append-only manifest. Its `--write` mode first proves every existing seal unchanged and then appends only newly archived artifacts. Pull-request CI supplies the trusted base SHA and checks out complete history before running the verifier, so a reused runner's shallow checkout cannot omit the baseline manifest. The ordinary Agent Note format, translation-pairing, wrapping, Markdown-link, package-path, Mermaid, documentation-TypeScript, and type-equivalence gates exclude archive sources; their evolving standards cannot create pressure to edit history. @@ -28,6 +30,8 @@ Supersession is checked while a new Agent Note is being written, not deferred to **Keep every implemented and rejected note active.** Rejected because maintenance effort and search noise grow with records that no longer help a future decision. Rejected notes in particular earn retention only by preventing a plausible fallacy. +**Leave archived notes in default repository search results.** Rejected because archived facts may be stale by design and can outrank current results by lexical match. Historical work can search the archive directory explicitly. + **Defer supersession cleanup to periodic corpus audits.** Rejected because the author of a replacement note has the freshest evidence about ownership and overlap. Postponement leaves redundant active authorities and makes later classification more expensive. **Archive rejected or proposed notes too.** Rejected because archive status means “implemented historical decision.” An obsolete proposal needs an explicit rejection, while a rejection with no guardrail value needs deletion rather than a second low-value holding area. @@ -38,4 +42,4 @@ Supersession is checked while a new Agent Note is being written, not deferred to ## Consequences -The active corpus becomes a set of decisions expected to influence future work, while low-value implemented history remains searchable and linkable without consuming maintenance attention. Writing a new note includes a scoped supersession check, so replacement decisions cannot silently leave redundant active records behind. Rejected clutter can disappear when it no longer protects a meaningful choice, and proposed work cannot quietly evade a verdict through archival. The archive adds a manifest, a dedicated verifier, and an explicit one-time metadata step. Archived facts and outbound links can become stale by design, so readers and agents must treat active code and documentation as authority and cite an archived note only as history. +The active corpus becomes a set of decisions expected to influence future work, while low-value implemented history remains explicitly searchable and linkable without consuming maintenance attention or appearing in parent-directory searches. Writing a new note includes a scoped supersession check, so replacement decisions cannot silently leave redundant active records behind. Rejected clutter can disappear when it no longer protects a meaningful choice, and proposed work cannot quietly evade a verdict through archival. The archive adds a manifest, a dedicated verifier, and an explicit one-time metadata step. Archived facts and outbound links can become stale by design, so readers and agents must treat active code and documentation as authority and cite an archived note only as history. diff --git a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md index a37e06e7cb..e67f981e5d 100644 --- a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md @@ -14,6 +14,8 @@ implemented Agent Note 作为当前决策记录持续维护,因此活跃记录 归档路径为 `.agents/notes/archived/{kind}/yyyy-mm-dd-topic.md`,其中省略了冗余的 `implemented` 层级。归档变更会移动完整的英文、中文和一致性伴随记录三个文件,保留 `Status: implemented`,并在两种语言的文件中紧接该状态行插入 `Archived: YYYY-MM-DD`。归档时只允许做文件迁移、添加该元数据行、相应地重新记录伴随记录,以及机械修复入站链接。 +根目录的 `.rgignore` 会将归档目录排除在从上层目录开始的搜索之外。查找历史内容时会显式指定归档目录,因此仍可按需访问,同时不会把冻结事实混入对活跃决策的检索。 + 归档后,这三个文件永久冻结,只作为历史背景,不再是当前权威依据。不得因为包重命名、行为变化、翻译标准、格式规则、出站链接失效或后续文档约定而更新归档文件。活跃文档可以有意链接到归档 Agent Note,也可以把该链接重定向到当前权威依据,或直接删除。仓库门禁因此会校验指向归档文件的链接,但绝不把归档文件作为链接源来校验。 [`verify-archived-agent-notes`](../../../../scripts/verify-archived-agent-notes.ts) 负责维护冻结边界。它只接受封闭集合中的 Agent Note 类别,要求三个配对文件完整、状态为 implemented,且归档日期有效并互相匹配;它还会用双方当前的 Git blob hash 校验伴随记录,并在仅追加的 manifest(元数据清单) 中按路径和 SHA-256 内容 hash 封存每项产物。其 `--write` 模式会先证明每条现有封存记录对应的内容都未改变,再仅追加新归档的产物。拉取请求 CI 会提供可信的基准 SHA,并在运行校验器前检出完整历史,因此复用运行器上的浅克隆检出无法漏掉基线 manifest。普通的 Agent Note 格式、翻译配对、换行、Markdown 链接、包路径、Mermaid、文档 TypeScript 和类型等价门禁都排除归档源文件,因此这些门禁持续演进的标准不会产生修改历史记录的压力。 @@ -28,6 +30,8 @@ implemented Agent Note 作为当前决策记录持续维护,因此活跃记录 **继续将每一份 implemented 和 rejected Agent Note 作为活跃记录保留。** 不予采纳,因为不再帮助未来决策的记录会不断增加维护成本和搜索噪声。尤其是 rejected Agent Note,只有能避免一种可能发生的谬误时,才值得保留。 +**让归档 Agent Note 继续出现在默认的仓库搜索结果中。** 不予采纳,因为归档事实按设计可能已经陈旧,并可能仅凭字面匹配就排在当前结果之前。需要查找历史内容时,可以显式搜索归档目录。 + **把取代关系清理留到定期审计记录集合时再做。** 不予采纳,因为替代记录的作者掌握着关于归属和重叠的最新证据。推迟处理会留下冗余的活跃权威依据,并增加日后分类的成本。 **同时归档 rejected 或 proposed Agent Note。** 不予采纳,因为归档状态表达的是「已经实施的历史决策」。过时的提案需要明确转为 rejected;无法提供防错价值的 rejected Agent Note 则应删除,而不是再放入第二个低价值存放区。 @@ -38,4 +42,4 @@ implemented Agent Note 作为当前决策记录持续维护,因此活跃记录 ## 后果 -活跃记录集合由预计仍会影响未来工作的决策组成;未来指导价值较低的实施历史仍可搜索和链接,却不再消耗维护精力。编写新记录时会包含一项范围明确的取代关系检查,因此取代既有决策的新决策无法悄然留下冗余的活跃记录。当被否决的记录不再保护有意义的选择时,可以清除这类杂项;提案也无法通过归档悄悄逃避明确结论。归档机制增加一份 manifest、一个专用校验器和一个显式的一次性元数据步骤。归档中的事实和出站链接可以按设计逐渐陈旧,因此读者和 agent 必须以活跃代码与文档为权威依据,并且仅将归档 Agent Note 作为历史引用。 +活跃记录集合由预计仍会影响未来工作的决策组成;未来指导价值较低的实施历史仍可显式搜索和链接,却不再消耗维护精力,也不会出现在从上层目录开始的搜索中。编写新记录时会包含一项范围明确的取代关系检查,因此取代既有决策的新决策无法悄然留下冗余的活跃记录。当被否决的记录不再保护有意义的选择时,可以清除这类杂项;提案也无法通过归档悄悄逃避明确结论。归档机制增加一份 manifest、一个专用校验器和一个显式的一次性元数据步骤。归档中的事实和出站链接可以按设计逐渐陈旧,因此读者和 agent 必须以活跃代码与文档为权威依据,并且仅将归档 Agent Note 作为历史引用。 diff --git a/.rgignore b/.rgignore new file mode 100644 index 0000000000..6bffdc1726 --- /dev/null +++ b/.rgignore @@ -0,0 +1,2 @@ +# Frozen Agent Notes are historical snapshots, not current search authority. +/.agents/notes/archived/ From ef35c7f3b227b0640097e8d8c6ffd37bd5c3e323 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 7 Aug 2026 16:01:41 +0800 Subject: [PATCH 2/5] fix(feedback): include session id in acknowledgement --- packages/feedback/command-feedback/README.i18n.yaml | 4 ++-- packages/feedback/command-feedback/README.md | 2 +- packages/feedback/command-feedback/README.zh.md | 2 +- packages/feedback/command-feedback/src/index.ts | 8 ++++++-- .../command-feedback/tests/command-feedback.spec.ts | 6 +++--- .../command-feedback/tests/loader-composition.spec.ts | 5 ++++- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/feedback/command-feedback/README.i18n.yaml b/packages/feedback/command-feedback/README.i18n.yaml index d919320643..b1b1a8d4d4 100644 --- a/packages/feedback/command-feedback/README.i18n.yaml +++ b/packages/feedback/command-feedback/README.i18n.yaml @@ -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 packages/feedback/command-feedback/README.md -README.md: d7849e25fc62897e4ac6793f40bdc139adf9ba3d -README.zh.md: c3b7b59d90d924de6042aeac1e7eec39457c6c83 +README.md: 96d2825f4b63c95ad6f45ca8b2e05d1fc5ae92aa +README.zh.md: 5220afe68b1f0de50fd1368900758906ee6907c9 diff --git a/packages/feedback/command-feedback/README.md b/packages/feedback/command-feedback/README.md index d7849e25fc..96d2825f4b 100644 --- a/packages/feedback/command-feedback/README.md +++ b/packages/feedback/command-feedback/README.md @@ -8,7 +8,7 @@ Trigger-independent session feedback plus human-facing `/feedback` capture. The | Input | Result | |---|---| -| `/feedback ` | Append `feedback/record` and acknowledge with `Feedback recorded.` | +| `/feedback ` | Append `feedback/record` and acknowledge with `Feedback recorded for session {id}`. | | `/feedback` | Return a direct usage error. Whitespace-only input is treated as empty. | Surrounding whitespace is discarded, but feedback is otherwise unparsed: no truncation, case folding, or control words. Text that looks like another command, such as `/feedback /plan felt slow`, is feedback content. Repeated commands each produce their own event; nothing is replaced or merged. diff --git a/packages/feedback/command-feedback/README.zh.md b/packages/feedback/command-feedback/README.zh.md index c3b7b59d90..5220afe68b 100644 --- a/packages/feedback/command-feedback/README.zh.md +++ b/packages/feedback/command-feedback/README.zh.md @@ -8,7 +8,7 @@ | 输入 | 结果 | |---|---| -| `/feedback ` | 追加 `feedback/record`,并以 `Feedback recorded.` 确认。 | +| `/feedback ` | 追加 `feedback/record`,并以 `Feedback recorded for session {id}` 确认。 | | `/feedback` | 返回一个直接用法错误。仅含空白的输入视为空输入。 | 前后空白会被丢弃,但除此之外,反馈内容不会被解析:没有截断、大小写折叠或控制词。看起来像另一个命令的文本(例如 `/feedback /plan felt slow`)就是反馈内容。重复执行命令时,每次都会产生一个事件;不会发生替换或合并。 diff --git a/packages/feedback/command-feedback/src/index.ts b/packages/feedback/command-feedback/src/index.ts index 037a463104..92ef839415 100644 --- a/packages/feedback/command-feedback/src/index.ts +++ b/packages/feedback/command-feedback/src/index.ts @@ -41,14 +41,18 @@ export function recordFeedback(session: Session, text: string): void { * Validate, record, and acknowledge one feedback entry. Returning an error * leaves no `feedback/record` event. * @param invocation - receiving agent, raw command input, and UI cancellation. - * @returns an acknowledgement, or a usage error when no feedback text was supplied. + * @returns an acknowledgement containing the receiving session id, or a usage error + * when no feedback text was supplied. */ function executeFeedbackCommand(invocation: CommandInvocation): CommandResult { if (invocation.rawInput.trim().length === 0) { return { kind: 'error', text: `Feedback text is required. ${USAGE}` } } recordFeedback(invocation.agent.session, invocation.rawInput) - return { kind: 'success', text: 'Feedback recorded.' } + return { + kind: 'success', + text: `Feedback recorded for session ${invocation.agent.session.id}`, + } } /** Register the global `/feedback` command for every composed command adapter. */ diff --git a/packages/feedback/command-feedback/tests/command-feedback.spec.ts b/packages/feedback/command-feedback/tests/command-feedback.spec.ts index 145b7ccf11..19d886af00 100644 --- a/packages/feedback/command-feedback/tests/command-feedback.spec.ts +++ b/packages/feedback/command-feedback/tests/command-feedback.spec.ts @@ -93,7 +93,7 @@ describe('/feedback human command', () => { const test = await harness() await expect(run(test, ' the diff view is unreadable')).resolves.toEqual({ kind: 'success', - text: 'Feedback recorded.', + text: `Feedback recorded for session ${test.session.id}`, }) expect(feedbackTexts(test.session)).toEqual(['the diff view is unreadable']) const commandRun = test.session.events.find(event => event.type === 'command/run') @@ -141,8 +141,8 @@ describe('/feedback human command', () => { test.ctx.commands.execute(test.agent, '/feedback second', signal), ]) expect(settled.map(item => item?.result)).toEqual([ - { kind: 'success', text: 'Feedback recorded.' }, - { kind: 'success', text: 'Feedback recorded.' }, + { kind: 'success', text: `Feedback recorded for session ${test.session.id}` }, + { kind: 'success', text: `Feedback recorded for session ${test.session.id}` }, ]) expect(feedbackTexts(test.session)).toEqual(['first', 'second']) }) diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts index 227672d167..98609afdea 100644 --- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts +++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts @@ -87,7 +87,10 @@ describe('/feedback real Loader composition through cordis.yml', () => { expect(context.commands.list(owner).map(command => command.name)).toContain('feedback') const accepted = await context.commands.execute(owner, '/feedback the diff view is unreadable', signal) - expect(accepted?.result).toEqual({ kind: 'success', text: 'Feedback recorded.' }) + expect(accepted?.result).toEqual({ + kind: 'success', + text: 'Feedback recorded for session feedback-loader-agent', + }) const rejected = await context.commands.execute(owner, '/feedback', signal) expect(rejected?.result).toEqual({ kind: 'error', From 8dc91d2c00f231d14b962f27b6a6143f36258add Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 7 Aug 2026 16:38:54 +0800 Subject: [PATCH 3/5] fix(feedback): report shared anonymous user id --- ...hared-feedback-telemetry-user-id.i18n.yaml | 6 ++ ...08-07-shared-feedback-telemetry-user-id.md | 33 +++++++++++ ...07-shared-feedback-telemetry-user-id.zh.md | 33 +++++++++++ .../2026-07-28-feedback-command.i18n.yaml | 4 +- .../feature/2026-07-28-feedback-command.md | 4 +- .../feature/2026-07-28-feedback-command.zh.md | 4 +- ...7-31-telemetry-anonymous-user-id.i18n.yaml | 4 +- .../2026-07-31-telemetry-anonymous-user-id.md | 10 ++-- ...26-07-31-telemetry-anonymous-user-id.zh.md | 10 ++-- apps/web/tests/seeded-history.e2e.ts | 55 ++++++++++++++++--- .../seeded-history/feedback-row.expected.md | 53 ++++++++++++++++++ docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 1 + docs/config-catalog.zh.md | 1 + docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 13 +++-- docs/module-graph.zh.md | 13 +++-- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 2 +- docs/persistence-catalog.zh.md | 2 +- .../command-feedback/README.i18n.yaml | 4 +- packages/feedback/command-feedback/README.md | 4 +- .../feedback/command-feedback/README.zh.md | 4 +- .../feedback/command-feedback/package.json | 2 + .../feedback/command-feedback/src/index.ts | 7 ++- .../tests/command-feedback.spec.ts | 20 +++++-- .../tests/loader-composition.spec.ts | 8 ++- .../feedback/command-feedback/tsconfig.json | 3 + .../session-telemetry-otel/package.json | 6 +- .../session-telemetry-otel/src/index.ts | 2 +- .../session-telemetry-otel/tests/otel.spec.ts | 2 +- .../session-telemetry-otel/tsconfig.json | 5 +- packages/session/user-id/README.i18n.yaml | 6 ++ packages/session/user-id/README.md | 29 ++++++++++ packages/session/user-id/README.zh.md | 29 ++++++++++ packages/session/user-id/package.json | 39 +++++++++++++ .../src/user-id.ts => user-id/src/index.ts} | 26 ++++----- packages/session/user-id/src/invariant.ts | 31 +++++++++++ .../session/user-id/tests/invariant.spec.ts | 12 ++++ .../tests/user-id.spec.ts | 2 +- packages/session/user-id/tsconfig.json | 21 +++++++ pnpm-lock.yaml | 27 +++++++-- .../verify-package-readme-model-experience.ts | 1 + tsconfig.host.json | 1 + 44 files changed, 462 insertions(+), 89 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md create mode 100644 .agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md create mode 100644 apps/web/tests/snapshots/seeded-history/feedback-row.expected.md create mode 100644 packages/session/user-id/README.i18n.yaml create mode 100644 packages/session/user-id/README.md create mode 100644 packages/session/user-id/README.zh.md create mode 100644 packages/session/user-id/package.json rename packages/session/{session-telemetry-otel/src/user-id.ts => user-id/src/index.ts} (79%) create mode 100644 packages/session/user-id/src/invariant.ts create mode 100644 packages/session/user-id/tests/invariant.spec.ts rename packages/session/{session-telemetry-otel => user-id}/tests/user-id.spec.ts (99%) create mode 100644 packages/session/user-id/tsconfig.json diff --git a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml new file mode 100644 index 0000000000..226b62f100 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md +2026-08-07-shared-feedback-telemetry-user-id.md: 6d4020828cb1f2ab3de0328c8959a18a0fcfe6c4 +2026-08-07-shared-feedback-telemetry-user-id.zh.md: 892fa0f848d656609885d008ab36e3ebbe09b992 diff --git a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md new file mode 100644 index 0000000000..6d4020828c --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md @@ -0,0 +1,33 @@ +# Agent Note: Shared feedback and telemetry anonymous user id + +Status: implemented + +English | [中文](2026-08-07-shared-feedback-telemetry-user-id.zh.md) + +## Problem + +The OpenTelemetry backend already persisted one anonymous UUID in `$DSH_HOME/.userid`. `/feedback` now needs to report both the receiving session id and a user id so an operator can correlate the acknowledgement with exported records. Duplicating or independently generating that identity would make the reported user meaningless, while importing it from `session-telemetry-otel` would make a direct command depend on an exporter backend and create a dependency cycle when feedback export is mounted by telemetry. + +The earlier [anonymous-user-id decision](../feature/2026-07-31-telemetry-anonymous-user-id.md) deliberately kept the helper inside the OTel backend until a second real consumer existed. Feedback is that consumer. + +## Decision + +`@deepseek-ai/dsh-user-id` owns `getOrCreateAnonymousUserId()` and the `$DSH_HOME/.userid` storage contract. `session-telemetry-otel` uses the returned id as OpenTelemetry Resource `user.id`; the `/feedback` success acknowledgement reports `Feedback recorded for session {sessionId}` followed by `User: {userId}` on a second line, which keeps both identifiers available through the generic command row's expandable body. Invalid feedback is rejected before resolving the id, so an empty command does not create `.userid`. + +The extraction preserves the existing random UUID, home resolution, process memo, exclusive-create concurrency, corruption replacement, and best-effort write semantics. It does not unify the dsh-sdk launcher's separate `telemetry.json` identity. + +## Alternatives considered + +| Rejected | Reason | +|---|---| +| Import the helper from `session-telemetry-otel` | Couples feedback to an optional exporter backend and forms a reverse dependency cycle once telemetry exports feedback | +| Duplicate the persistence helper in feedback | Two implementations of one file contract can drift and race with different validation or failure semantics | +| Generate a separate feedback user id | The acknowledgement could not correlate with the OTel Resource and would not satisfy the reporting purpose | +| Move the launcher telemetry id too | The launcher feed is not a consumer of `.userid`; unifying unrelated stores remains out of scope | + +## Consequences + +- One harness home now has one anonymous id shared by feedback acknowledgements and session telemetry exports. +- The feedback package depends only on the identity capability, not the telemetry seam or OTel SDK. +- The new package is a justified shared seam with two consumers; its empty invariant companion explains why reading the private file is not a useful runtime relationship check. +- The original anonymous-user-id Note remains authoritative for storage and privacy semantics, while this Note supersedes only its OTel-local ownership decision. diff --git a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md new file mode 100644 index 0000000000..892fa0f848 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 反馈与遥测共享匿名用户 id + +Status: implemented + +[English](2026-08-07-shared-feedback-telemetry-user-id.md) | 中文 + +## 问题 + +OpenTelemetry 后端已在 `$DSH_HOME/.userid` 中持久化一个匿名 UUID。`/feedback` 需要同时报告接收反馈的会话 id 与用户 id,以便运维人员将确认文本与导出的记录相关联。复制该身份或单独生成身份会使报告的用户失去意义;从 `session-telemetry-otel` 导入身份则会让直接命令依赖导出后端,并在遥测侧挂载反馈导出时形成依赖环。 + +早先的[匿名用户 id 决策](../feature/2026-07-31-telemetry-anonymous-user-id.md)刻意将辅助函数留在 OTel 后端内,直至出现第二个真实消费方。反馈就是这个消费方。 + +## 决策 + +`@deepseek-ai/dsh-user-id` 负责 `getOrCreateAnonymousUserId()` 和 `$DSH_HOME/.userid` 存储契约。`session-telemetry-otel` 将返回的 id 用作 OpenTelemetry Resource 的 `user.id`;`/feedback` 的成功确认先报告 `Feedback recorded for session {sessionId}`,再在第二行显示 `User: {userId}`,使两个标识符都可通过通用命令行的可展开正文查看。系统在获取 id 前拒绝无效反馈,因此空命令不会创建 `.userid`。 + +此次抽取保留既有的随机 UUID、home 解析、进程内缓存、独占创建并发、损坏文件替换与 best-effort 写入语义。它不会统一 dsh-sdk launcher 独立的 `telemetry.json` 身份。 + +## 考虑过的替代方案 + +| 已否决 | 原因 | +|---|---| +| 从 `session-telemetry-otel` 导入辅助函数 | 使反馈耦合到可选的导出后端,并在遥测导出反馈后形成反向依赖环 | +| 在反馈中复制持久化辅助函数 | 同一文件契约的两份实现可能发生偏差,并因校验或失败语义不同而产生竞态 | +| 生成独立的反馈用户 id | 确认文本无法与 OTel Resource 相关联,因而不能达到报告目的 | +| 同时移动 launcher telemetry id | launcher 回流不是 `.userid` 的消费方;统一无关存储仍不在范围内 | + +## 后果 + +- 一个 harness home 只有一个匿名 id,由反馈确认与会话遥测导出共享。 +- 反馈包只依赖身份能力,不依赖遥测 seam 或 OTel SDK。 +- 新包由两个消费方使用,成为有充分依据的共享 seam;其空不变式伴生插件解释了为何读取私有文件并非有用的运行时关系检查。 +- 原始匿名用户 id Note 仍是存储与隐私语义的权威记录;本 Note 仅取代其中由 OTel 本地拥有身份的决策。 diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml index e0133df46f..809e37044f 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-28-feedback-command.md -2026-07-28-feedback-command.md: 78cc9e89a5811b3f1520bae2bd971cbcf2522ede -2026-07-28-feedback-command.zh.md: b0bba25ec3331123cb86066fc6c89186f2297cc9 +2026-07-28-feedback-command.md: 3edb29283c289d6d006891a4c19087b01fa8166f +2026-07-28-feedback-command.zh.md: c2513d2570474cbbaf8d94f87603d8ce10d40c14 diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md index 78cc9e89a5..3edb29283c 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md @@ -12,7 +12,7 @@ The capture surface has to be usable at the moment of annoyance, which rules out ## Decision -`@deepseek-ai/dsh-command-feedback` in `packages/feedback/command-feedback/` registers one global `feedback` command over `ctx.commands`. `/feedback ` acknowledges; bare or whitespace-only input returns a direct usage error. The handler is synchronous, injects only `commands`, and has no configuration. +`@deepseek-ai/dsh-command-feedback` in `packages/feedback/command-feedback/` registers one global `feedback` command over `ctx.commands`. `/feedback ` acknowledges with the receiving session id and the shared harness-home anonymous user id; bare or whitespace-only input returns a direct usage error. The handler is synchronous, injects only `commands`, and has no configuration. [The shared-id decision](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md) records why feedback and OpenTelemetry use the same `$DSH_HOME/.userid` value. The package declares the log-only `feedback/record { text }` session event and exports `recordFeedback(session, text)` as its command-independent producer. The producer discards surrounding whitespace, rejects an empty result, and appends exactly one event. `/feedback` delegates to it, so another UI, hook, or host integration can record the same domain fact without constructing a slash command. @@ -54,7 +54,7 @@ Surrounding whitespace is discarded, but nothing else is parsed. `/feedback /pla ## Consequences -The shipped `dsh` base mounts the command unconditionally — no configuration, no dependency on the goal stack. The Web client exposes it through its command adapter. Headless mode, ACP, and JSON-RPC do not provide a command adapter, so `/feedback` is unavailable there. +The shipped `dsh` base mounts the command unconditionally — no configuration, no dependency on the goal stack. The Web client exposes it through its command adapter. Headless mode, ACP, and JSON-RPC do not provide a command adapter, so `/feedback` is unavailable there. The first accepted feedback for a harness home can create `$DSH_HOME/.userid`; rejected empty input does not resolve or create an id. The package owns one independent append-only event with no cross-event or mutable-data relation for an invariant companion to check. The event follows the session log's existing replay, fork, persistence, and crash-tail behavior. diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md index b0bba25ec3..c2513d2570 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -位于 `packages/feedback/command-feedback/` 的 `@deepseek-ai/dsh-command-feedback` 通过 `ctx.commands` 注册一个全局 `feedback` 命令。`/feedback ` 给出确认;空输入或仅含空白的输入返回直接用法错误。处理器是同步的,只注入 `commands`,且没有任何配置。 +位于 `packages/feedback/command-feedback/` 的 `@deepseek-ai/dsh-command-feedback` 通过 `ctx.commands` 注册一个全局 `feedback` 命令。`/feedback ` 在确认文本中包含接收反馈的会话 id 与 harness home 的共享匿名用户 id;空输入或仅含空白的输入返回直接用法错误。处理器是同步的,只注入 `commands`,且没有任何配置。[共享 id 决策](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md)说明了反馈与 OpenTelemetry 为何使用同一个 `$DSH_HOME/.userid` 值。 本包声明仅写入日志的 `feedback/record { text }` 会话事件,并导出 `recordFeedback(session, text)`,作为不依赖命令的生产方。该生产方丢弃前后空白,拒绝空结果,并且恰好追加一个事件。`/feedback` 委托给它,因此其他 UI、钩子或 host 集成无需构造斜杠命令也能记录同一个领域事实。 @@ -54,7 +54,7 @@ Status: implemented ## 后果 -随附的 `dsh` 基础组合无条件挂载该命令:没有配置,也不依赖 goal 栈。Web 客户端通过命令适配器暴露该命令。无头模式、ACP 和 JSON-RPC 不提供命令适配器,因此 `/feedback` 在那里不可用。 +随附的 `dsh` 基础组合无条件挂载该命令:没有配置,也不依赖 goal 栈。Web 客户端通过命令适配器暴露该命令。无头模式、ACP 和 JSON-RPC 不提供命令适配器,因此 `/feedback` 在那里不可用。对于某个 harness home,首次接受反馈时可能创建 `$DSH_HOME/.userid`;被拒绝的空输入不会获取或创建 id。 本包拥有一个独立的仅追加事件,不存在跨事件关系或可变数据关系可供不变式伴生插件检查。该事件遵循会话日志现有的回放、fork、持久化和崩溃尾部行为。 diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml index 47e1e0144b..1bec2a758b 100644 --- a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md -2026-07-31-telemetry-anonymous-user-id.md: f53fc9ce7eb3a5aefcb0be0a20c7403b5601e369 -2026-07-31-telemetry-anonymous-user-id.zh.md: 99b2dd88df94810ccfc85d099e74a6f0852153a7 +2026-07-31-telemetry-anonymous-user-id.md: 75b65e9fd477d19afb3a3a25e424a7f7620099a3 +2026-07-31-telemetry-anonymous-user-id.zh.md: 3db5b665f9cbbe6f884a9717afa758f22a419658 diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md index f53fc9ce7e..75b65e9fd4 100644 --- a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md @@ -10,7 +10,7 @@ Session telemetry is mounted by default ([default-mount Note](2026-07-31-web-tel ## Decision -The `session-telemetry-otel` package's own module `src/user-id.ts` owns the OTel feed's user identity: `getOrCreateAnonymousUserId()` returns the bare UUID line in `$DSH_HOME/.userid` (resolved by `resolveDshHome`, `$DSH_HOME` > `~/.dsh`), minting and persisting a random UUID v4 on first use; the backend constructor carries it as the Resource's `user.id` (the OTel semconv user attribute), once per export batch. This identity belongs to the OTel feed alone; the dsh-sdk launcher telemetry keeps its own anonymous-id store (`telemetry.json`), and the two are not shared (the first cut unified both feeds through a shared util package — no shared package before a second real consumer exists, revisit when a feed-correlation need appears). +`getOrCreateAnonymousUserId()` returns the bare UUID line in `$DSH_HOME/.userid` (resolved by `resolveDshHome`, `$DSH_HOME` > `~/.dsh`), minting and persisting a random UUID v4 on first use; the backend constructor carries it as the Resource's `user.id` (the OTel semconv user attribute), once per export batch. The original implementation lived inside `session-telemetry-otel` because no second real consumer existed. `/feedback` later became that consumer, so [the shared-id decision](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md) moves ownership to `@deepseek-ai/dsh-user-id` without changing the storage, anonymity, concurrency, or loss semantics recorded here. The dsh-sdk launcher telemetry keeps its own anonymous-id store (`telemetry.json`) and remains unrelated. | Ruling | Value | Rationale | |---|---|---| @@ -22,8 +22,8 @@ The `session-telemetry-otel` package's own module `src/user-id.ts` owns the OTel | Write failure | Best-effort: return the in-memory id | Telemetry is never blocked by a read-only home | | Report position | Resource attribute, not per-record attributes | Once per batch suffices for Resource-dimension aggregation; per-record injection would touch the seam contract and grow the wire | | semconv dependency | `@opentelemetry/semantic-conventions` is not imported | One string constant does not justify a dependency | -| Home | A module inside `session-telemetry-otel`, not a shared util package | Repo rule: split a package only for a second real consumer; the sdk launcher feed keeps its own store, and no real correlation need exists | -| Separate switch | None | Identity follows the telemetry master switch (`DSH_TELEMETRY_DISABLED`); telemetry off means nothing reports | +| Home | `@deepseek-ai/dsh-user-id`, shared by the OTel backend and `/feedback` | The second real consumer now exists; direct feedback must not depend on an exporter backend | +| Separate switch | None | Either consumer can create the identity; `DSH_TELEMETRY_DISABLED` stops telemetry reporting but does not disable feedback acknowledgement | ## Alternatives considered @@ -31,7 +31,7 @@ The `session-telemetry-otel` package's own module `src/user-id.ts` owns the OTel |---|---| | Hostname/IP-hash-derived id (the prior ruling) | Reversible means not anonymous; the random UUID is semantically clean — the user ruled to supersede | | user.id on every record's attributes (Claude Code's shape) | Touches the session-telemetry seam contract or injects per record, growing the wire; once per batch on the Resource already aggregates | -| A shared util package unifying both feeds (the first cut) | The only real consumer is the OTel backend; switching the sdk launcher onto it was unification for its own sake — the user reconsidered and pulled it back, to be re-extracted when a correlation need appears | +| A shared package before `/feedback` needed the id (the first cut) | At that time the only real consumer was the OTel backend; extraction became justified only when direct feedback needed the same correlation id | | Reusing telemetry.json instead of a new file | The file name/JSON format files the identity under the launcher feed's naming; the OTel feed's identity is a standalone fact | | AppCLIEntry reading the id and injecting via config patch | Every surface entry needs wiring; a runtime fact inside deployment config conflates the two | | Housing it in `@deepseek-ai/dsh-paths` | paths is pure path computation with zero IO; a persisting identity capability would pollute the package boundary | @@ -39,6 +39,6 @@ The `session-telemetry-otel` package's own module `src/user-id.ts` owns the OTel ## Consequences - One `$DSH_HOME` is one stable user in the OTel feed; separate homes are separate users by construction, with no cross-home linking mechanism. -- The OTel feed and the launcher feed each hold their own id (`.userid` vs `telemetry.json`) and cannot be correlated — the direct cost of not extracting a shared package, to be unified when a real correlation need appears. +- The OTel feed and `/feedback` share `.userid`; the launcher feed still uses `telemetry.json` and cannot be correlated with them. - Deleting `.userid` resets the identity (effective next launch); on an unwritable home each process holds its own in-memory id until the home becomes writable. - The [default-mount Note](2026-07-31-web-telemetry-default-mount.md)'s identity follow-up is closed for the anonymous-user-id part by this decision; hostname/surface dimensions, the redaction rule, and the usage-metrics track remain open. diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md index 99b2dd88df..3db5b665f9 100644 --- a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md @@ -10,7 +10,7 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry ## Decision -`session-telemetry-otel` 包内模块 `src/user-id.ts` 是 OTel 回流用户身份的属主:`getOrCreateAnonymousUserId()` 返回 `$DSH_HOME/.userid`(`resolveDshHome` 解析,`$DSH_HOME` > `~/.dsh`)中的裸 UUID 行,首用生成随机 UUID v4 并落盘;backend 构造时把它作为 Resource 的 `user.id`(OTel semconv 标准用户属性)随每批导出携带一次。该身份只属于 OTel 回流;dsh-sdk launcher telemetry 保留自己的匿名 id 存储(`telemetry.json`),两者不共享(初版曾做公用 util 包统一两条回流——在有第二个真实消费者之前不抽公共包,回流关联需求出现时再议)。 +`getOrCreateAnonymousUserId()` 返回 `$DSH_HOME/.userid`(`resolveDshHome` 解析,`$DSH_HOME` > `~/.dsh`)中的裸 UUID 行,首用生成随机 UUID v4 并落盘;后端构造时把它作为 Resource 的 `user.id`(OTel semconv 标准用户属性)随每批导出携带一次。原始实现位于 `session-telemetry-otel`,因为当时不存在第二个真实消费方。`/feedback` 后来成为该消费方,因此[共享 id 决策](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md)将所有权移交给 `@deepseek-ai/dsh-user-id`,但不改变本 Note 记录的存储、匿名、并发与丢失语义。dsh-sdk launcher telemetry 继续使用自己独立的匿名 id 存储(`telemetry.json`),与此身份无关。 | 裁定 | 取值 | 理由 | |---|---|---| @@ -22,8 +22,8 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry | 写失败 | best-effort 返回内存 id | telemetry 永不因 home 只读被阻塞 | | 上报位置 | Resource 属性,非逐条 attributes | 每批一次即够接收端按 Resource 维度聚合;逐条注入要动 seam 约定且涨 wire 体积 | | semconv 依赖 | 不引 `@opentelemetry/semantic-conventions` 包 | 一个字符串常量不值一个依赖 | -| 落点 | `session-telemetry-otel` 包内模块,非公共 util 包 | 仓规「有第二个真实消费者才拆包」;sdk launcher 回流保留自有存储,无现实关联需求 | -| 单独开关 | 无 | 身份跟随 telemetry 整体开关(`DSH_TELEMETRY_DISABLED`);关 telemetry 即整体不报 | +| 落点 | `@deepseek-ai/dsh-user-id`,由 OTel 后端与 `/feedback` 共享 | 第二个真实消费方已经出现;直接反馈不能依赖导出后端 | +| 单独开关 | 无 | 任一消费方都可创建该身份;`DSH_TELEMETRY_DISABLED` 会停止遥测上报,但不会禁用反馈确认 | ## Alternatives considered @@ -31,7 +31,7 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry |---|---| | hostname/IP 哈希派生 id(此前口径) | 可反查即非匿名;随机 UUID 语义干净,用户裁决取代 | | user.id 放每条 record 的 attributes(Claude Code 形态) | 要动 session-telemetry seam 约定或逐条注入,wire 体积涨;Resource 每批一次已满足聚合 | -| 公用 util 包统一两条回流(初版实现) | 唯一现实消费者是 OTel backend;sdk launcher 换用它只是为统一而统一——用户复议收回,回流关联需求出现时再抽包 | +| 在 `/feedback` 需要该 id 之前抽取共享包(初版实现) | 当时唯一的真实消费方是 OTel 后端;只有直接反馈需要同一个关联 id 后,抽取才具备依据 | | 复用 telemetry.json 不新建文件 | 文件名/JSON 格式把身份挂在 launcher 链路命名下;OTel 回流身份是独立事实 | | AppCLIEntry 读好 id 经 config patch 注入 | 每个 surface 入口都要接线;config 里传运行时事实与部署配置混淆 | | 挂进 `@deepseek-ai/dsh-paths` | paths 是纯路径计算零 IO;带持久化的身份能力会污染包边界 | @@ -39,6 +39,6 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry ## Consequences - 一个 `$DSH_HOME` 在 OTel 回流中是一个稳定用户;不同 home 在构造上就是不同用户,无跨 home 关联机制。 -- OTel 回流与 launcher 回流各有各的 id(`.userid` 与 `telemetry.json`),无法互相关联——这是「不抽公共包」的直接代价,等真实关联需求出现再统一。 +- OTel 回流与 `/feedback` 共享 `.userid`;launcher 回流仍使用 `telemetry.json`,无法与前两者关联。 - 删除 `.userid` 即重置身份(下次启动生效);home 不可写时每进程各自持有一个内存 id 直至恢复可写。 - [默认挂载 Note](2026-07-31-web-telemetry-default-mount.md) 的身份 follow-up 中「匿名用户 id」项由本决定关闭;hostname/surface 维度与脱敏规则、usage-metrics track 仍是待办。 diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 80ef25ba39..257da0031a 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -3,12 +3,11 @@ // else covers: sidebar cold listing, the implicit resume/attach inside the // history RPC, history-page tool views, and the client's log-ordered transcript // events — with ZERO model calls in replay (no replay fixture; a stray stream -// fails loud on the open llm seam). The cold session also carries the one -// keyless command-row surfaces: the seeded manual `/compact` lifecycle folds -// into its checkpoint, while an Access-chip pick later runs `/permission` on -// the host. The seed is a recorded -// fixture under the -// same record discipline as every other: DSH_SNAPSHOT=record drives the turn +// fails loud on the open llm seam). The cold session also carries keyless +// command-row surfaces: the seeded manual `/compact` lifecycle folds into its +// checkpoint, an Access-chip pick later runs `/permission` on the host, and +// `/feedback` pins its expandable correlation ids. The seed is a recorded +// fixture under the same record discipline as every other: DSH_SNAPSHOT=record drives the turn // live through the composer (real read tool against seeded workspace files) // and harvests seed.jsonl; replay/refresh seed it cold and only render. import { readFile, writeFile, mkdir } from 'node:fs/promises' @@ -32,9 +31,9 @@ import { newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/seeded-history', import.meta.url)) const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url)) const UI_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/ui.expected.md', import.meta.url)) -// The command-row golden: the same conversation after one /permission switch, -// which is the only surface that shows a settled command row's copy. +// Command-row goldens over the same conversation after direct host commands. const COMMAND_ROW_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/command-row.expected.md', import.meta.url)) +const FEEDBACK_ROW_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/feedback-row.expected.md', import.meta.url)) const MODE = webSnapshotMode() const SEED_ID = 'seeded-history-web-e2e' @@ -446,6 +445,44 @@ describe('web e2e: seeded history renders through cold resume', () => { await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE) }, 60_000) + it.skipIf(MODE === 'record')('reports full feedback correlation ids in an expandable two-line row', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-feedback-row')) + const previousDshHome = process.env.DSH_HOME + process.env.DSH_HOME = scaffold.harnessHome + try { + const input = page.locator('textarea').first() + await input.fill('/feedback the diff view is unreadable') + await input.press('Enter') + const row = page.locator('[data-variant="others"]').filter({ + hasText: `Feedback recorded for session ${SEED_ID}`, + }) + await row.waitFor({ timeout: 10_000 }) + const disclosure = row.locator('[data-expandable]') + expect(await disclosure.getAttribute('aria-expanded')).toBe('false') + await disclosure.click() + await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true') + + const agent = scaffold.ctx.agents.get(SessionId(SEED_ID)) + if (agent === undefined) throw new Error('seeded session did not attach an agent') + const done = agent.session.events.filter(event => event.type === 'command/done').at(-1) + if (done?.type !== 'command/done') throw new Error('feedback command did not settle') + const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? [] + expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`) + expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + expect(extraLine).toBeUndefined() + const userId = userLine?.slice('User: '.length) + if (userId === undefined) throw new Error('feedback command omitted the user id') + + const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) + .split(SEED_ID).join('{{seededId}}') + .split(userId).join('{{userId}}') + await compareOrRefreshGolden(FEEDBACK_ROW_EXPECTED, snapshot, MODE) + } finally { + if (previousDshHome === undefined) delete process.env.DSH_HOME + else process.env.DSH_HOME = previousDshHome + } + }, 60_000) + it.skipIf(MODE === 'record')('fits short logged context without a scrollport', async () => { const agent = scaffold.ctx.agents.get(SessionId(SEED_ID)) if (agent === undefined) throw new Error('seeded session did not attach an agent') @@ -473,6 +510,6 @@ describe('web e2e: seeded history renders through cold resume', () => { // stream would have failed the turn loudly. Cleanliness pins the wire. expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) - await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'seed.jsonl', 'ui.expected.md']) + await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'feedback-row.expected.md', 'seed.jsonl', 'ui.expected.md']) }) }) diff --git a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md new file mode 100644 index 0000000000..87b763d37c --- /dev/null +++ b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md @@ -0,0 +1,53 @@ +- banner: + - navigation "Session hierarchy": + - button "Use the read tool twice" [disabled] + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. 7/25 {{clock}}" +- button "Copy": + - img +- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": + - img + - img + - text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel. +- button "Read a.txt": + - img + - img + - text: Read + - button "a.txt" +- button "Read b.txt": + - img + - img + - text: Read + - button "b.txt" +- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.": + - img + - img + - text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed. +- paragraph: DONE +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s +- button "compact Compacted 5 history items (~{{tokens}} tokens)" +- button "Context injection AGENTS.md": + - img + - img + - text: Context injection AGENTS.md +- img +- text: permission preset read-only +- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" [expanded]': + - img + - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" +- text: "Feedback recorded for session {{seededId}} User: {{uuid}}" +- textbox "Message the agent" +- button "Commands": + - img +- 'button "Access mode, current: Read Only"': Read Only +- button "Select model, current DeepSeek-V4-Flash": + - text: DeepSeek-V4-Flash + - img +- button "Send message" [disabled] +- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 135 tok diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index cc76a6b557..7319ee9070 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 docs/config-catalog.md -config-catalog.md: a785f856f0c5e3b1f99260e365ba5d15641dd5be -config-catalog.zh.md: 913f7d7771aa3f5e86b199121c64d5b9b00e968d +config-catalog.md: bf5bdc275e4611afaa6950078459ea34723a0d53 +config-catalog.zh.md: 0d9711d729364d2b06dbc7859f7c0a255222979f diff --git a/docs/config-catalog.md b/docs/config-catalog.md index a785f856f0..bf5bdc275e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2811,3 +2811,4 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-type-meta` ([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts)) - `@deepseek-ai/dsh-typert-generator` ([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts)) - `@deepseek-ai/dsh-typert-registry` ([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts)) +- `@deepseek-ai/dsh-user-id` ([`packages/session/user-id/src/index.ts`](../packages/session/user-id/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 913f7d7771..0d9711d729 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2811,3 +2811,4 @@ export interface Config { - `@deepseek-ai/dsh-type-meta`([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts)) - `@deepseek-ai/dsh-typert-generator`([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts)) - `@deepseek-ai/dsh-typert-registry`([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts)) +- `@deepseek-ai/dsh-user-id`([`packages/session/user-id/src/index.ts`](../packages/session/user-id/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index c8cd127319..66486e97b7 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -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 docs/module-graph.md -module-graph.md: 9df5eabe1c39fd8bb3e14b635e43b782c816d369 -module-graph.zh.md: ee187222fa1ed727941e7820ac8c33ed532497e3 +module-graph.md: a2407f5d394020834172288e3d03518d1e8045db +module-graph.zh.md: 364033c29d773a764ce3f8f0036edeac7c9e0b21 diff --git a/docs/module-graph.md b/docs/module-graph.md index 9df5eabe1c..a2407f5d39 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -269,6 +269,7 @@ flowchart TD pkg_session_title_all_messages_llm["session-title-all-messages-llm"] pkg_session_title_first_message_llm["session-title-first-message-llm"] pkg_session_title_llm["session-title-llm"] + pkg_user_id["user-id"] end subgraph group_settings["packages/settings"] pkg_settings["settings"] @@ -355,6 +356,9 @@ flowchart TD pkg_telemetry --> pkg_brand pkg_telemetry --> pkg_invariants pkg_telemetry --> pkg_paths + pkg_user_id --> pkg_brand + pkg_user_id --> pkg_invariants + pkg_user_id --> pkg_paths pkg_settings --> pkg_brand pkg_settings --> pkg_invariants pkg_storage_domain --> pkg_invariants @@ -711,6 +715,7 @@ flowchart TD pkg_command_feedback --> pkg_commands pkg_command_feedback --> pkg_invariants pkg_command_feedback --> pkg_session + pkg_command_feedback --> pkg_user_id pkg_permission --> pkg_bash pkg_permission --> pkg_commands pkg_permission --> pkg_invariants @@ -937,13 +942,12 @@ flowchart TD pkg_session_checkpoint_policy --> pkg_session pkg_session_checkpoint_policy --> pkg_session_persistence pkg_session_checkpoint_policy --> pkg_tools - pkg_session_telemetry_otel --> pkg_brand pkg_session_telemetry_otel --> pkg_command_feedback pkg_session_telemetry_otel --> pkg_invariants pkg_session_telemetry_otel --> pkg_llm - pkg_session_telemetry_otel --> pkg_paths pkg_session_telemetry_otel --> pkg_session pkg_session_telemetry_otel --> pkg_session_telemetry + pkg_session_telemetry_otel --> pkg_user_id pkg_session_title_all_messages_llm --> pkg_invariants pkg_session_title_all_messages_llm --> pkg_llm pkg_session_title_all_messages_llm --> pkg_session @@ -1274,6 +1278,7 @@ flowchart TD | [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) | | [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | +| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) | | [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) | @@ -1362,7 +1367,7 @@ flowchart TD | [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | -| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) | | [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) | @@ -1402,7 +1407,7 @@ flowchart TD | [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | -| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | +| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) | | [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index ee187222fa..364033c29d 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -271,6 +271,7 @@ flowchart TD pkg_session_title_all_messages_llm["session-title-all-messages-llm"] pkg_session_title_first_message_llm["session-title-first-message-llm"] pkg_session_title_llm["session-title-llm"] + pkg_user_id["user-id"] end subgraph group_settings["packages/settings"] pkg_settings["settings"] @@ -357,6 +358,9 @@ flowchart TD pkg_telemetry --> pkg_brand pkg_telemetry --> pkg_invariants pkg_telemetry --> pkg_paths + pkg_user_id --> pkg_brand + pkg_user_id --> pkg_invariants + pkg_user_id --> pkg_paths pkg_settings --> pkg_brand pkg_settings --> pkg_invariants pkg_storage_domain --> pkg_invariants @@ -713,6 +717,7 @@ flowchart TD pkg_command_feedback --> pkg_commands pkg_command_feedback --> pkg_invariants pkg_command_feedback --> pkg_session + pkg_command_feedback --> pkg_user_id pkg_permission --> pkg_bash pkg_permission --> pkg_commands pkg_permission --> pkg_invariants @@ -939,13 +944,12 @@ flowchart TD pkg_session_checkpoint_policy --> pkg_session pkg_session_checkpoint_policy --> pkg_session_persistence pkg_session_checkpoint_policy --> pkg_tools - pkg_session_telemetry_otel --> pkg_brand pkg_session_telemetry_otel --> pkg_command_feedback pkg_session_telemetry_otel --> pkg_invariants pkg_session_telemetry_otel --> pkg_llm - pkg_session_telemetry_otel --> pkg_paths pkg_session_telemetry_otel --> pkg_session pkg_session_telemetry_otel --> pkg_session_telemetry + pkg_session_telemetry_otel --> pkg_user_id pkg_session_title_all_messages_llm --> pkg_invariants pkg_session_title_all_messages_llm --> pkg_llm pkg_session_title_all_messages_llm --> pkg_session @@ -1276,6 +1280,7 @@ flowchart TD | [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) | | [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | +| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) | | [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) | @@ -1364,7 +1369,7 @@ flowchart TD | [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | -| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) | | [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) | @@ -1404,7 +1409,7 @@ flowchart TD | [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | -| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | +| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) | | [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 7ca14e31fb..7a8f73a488 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -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 docs/persistence-catalog.md -persistence-catalog.md: 9953214182521ac2c1aac8b4589bad7ad45e3094 -persistence-catalog.zh.md: 730513ea259dde274c8c63948dd21fdc0b70417f +persistence-catalog.md: f1dd0f6635bbb2ed2bbf679fdab2664cef08906d +persistence-catalog.zh.md: 7a0f66b5622fbc9527947019da442b21a1b67b9a diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 9953214182..f1dd0f6635 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -364,7 +364,7 @@ Source: [`packages/compact/compact/src/types.ts:33`](../packages/compact/compact 'feedback/record': { text: string } ``` -Source: [`packages/feedback/command-feedback/src/index.ts:24`](../packages/feedback/command-feedback/src/index.ts) +Source: [`packages/feedback/command-feedback/src/index.ts:25`](../packages/feedback/command-feedback/src/index.ts) ### `goal/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 730513ea25..7a0f66b562 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -366,7 +366,7 @@ export type SessionEvent = { 'feedback/record': { text: string } ``` -来源:[`packages/feedback/command-feedback/src/index.ts:24`](../packages/feedback/command-feedback/src/index.ts) +来源:[`packages/feedback/command-feedback/src/index.ts:25`](../packages/feedback/command-feedback/src/index.ts) ### `goal/*` diff --git a/packages/feedback/command-feedback/README.i18n.yaml b/packages/feedback/command-feedback/README.i18n.yaml index b1b1a8d4d4..ea0c591ae2 100644 --- a/packages/feedback/command-feedback/README.i18n.yaml +++ b/packages/feedback/command-feedback/README.i18n.yaml @@ -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 packages/feedback/command-feedback/README.md -README.md: 96d2825f4b63c95ad6f45ca8b2e05d1fc5ae92aa -README.zh.md: 5220afe68b1f0de50fd1368900758906ee6907c9 +README.md: 52b8fb6a423fca69f76397deec36ecd22a6a6023 +README.zh.md: ca74d53f2531a46c2c16aa1423cee52e89c8256f diff --git a/packages/feedback/command-feedback/README.md b/packages/feedback/command-feedback/README.md index 96d2825f4b..52b8fb6a42 100644 --- a/packages/feedback/command-feedback/README.md +++ b/packages/feedback/command-feedback/README.md @@ -8,7 +8,7 @@ Trigger-independent session feedback plus human-facing `/feedback` capture. The | Input | Result | |---|---| -| `/feedback ` | Append `feedback/record` and acknowledge with `Feedback recorded for session {id}`. | +| `/feedback ` | Append `feedback/record` and acknowledge with `Feedback recorded for session {sessionId}` followed by `User: {userId}`. | | `/feedback` | Return a direct usage error. Whitespace-only input is treated as empty. | Surrounding whitespace is discarded, but feedback is otherwise unparsed: no truncation, case folding, or control words. Text that looks like another command, such as `/feedback /plan felt slow`, is feedback content. Repeated commands each produce their own event; nothing is replaced or merged. @@ -17,7 +17,7 @@ Surrounding whitespace is discarded, but feedback is otherwise unparsed: no trun `recordFeedback(session, text)` is the command-independent write path. It rejects empty normalized text and appends `feedback/record { text }`; a different UI, hook, or host integration can call it without constructing a slash command. The `/feedback` handler uses that producer and starts no model work. The optional [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) consumer observes the event without changing its capture contract. -The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record`. +The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. The acknowledgement identifies both the receiving session and the [shared anonymous user](../../session/user-id/); the first accepted feedback for a harness home can create `$DSH_HOME/.userid`. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record` and no user-id lookup. The event is authoritative rather than the command record because feedback may arrive through a trigger other than `/feedback`. Keeping the payload out of `command/run` avoids two records carrying the same text. diff --git a/packages/feedback/command-feedback/README.zh.md b/packages/feedback/command-feedback/README.zh.md index 5220afe68b..ca74d53f25 100644 --- a/packages/feedback/command-feedback/README.zh.md +++ b/packages/feedback/command-feedback/README.zh.md @@ -8,7 +8,7 @@ | 输入 | 结果 | |---|---| -| `/feedback ` | 追加 `feedback/record`,并以 `Feedback recorded for session {id}` 确认。 | +| `/feedback ` | 追加 `feedback/record`,并以 `Feedback recorded for session {sessionId}` 确认,随后显示 `User: {userId}`。 | | `/feedback` | 返回一个直接用法错误。仅含空白的输入视为空输入。 | 前后空白会被丢弃,但除此之外,反馈内容不会被解析:没有截断、大小写折叠或控制词。看起来像另一个命令的文本(例如 `/feedback /plan felt slow`)就是反馈内容。重复执行命令时,每次都会产生一个事件;不会发生替换或合并。 @@ -17,7 +17,7 @@ `recordFeedback(session, text)` 是不依赖命令的写入路径。它拒绝规范化后为空的文本,并追加 `feedback/record { text }`;其他 UI、钩子或 host 集成无需构造斜杠命令即可调用它。`/feedback` 处理器通过该生产方写入,且不启动任何模型工作。可选的 [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) 消费方会观察该事件,但不改变它的采集约定。 -反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`。 +反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。确认文本同时标明接收反馈的会话和[共享匿名用户](../../session/user-id/);对于某个 harness home,首次接受反馈时可能创建 `$DSH_HOME/.userid`。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`,也不会查找用户 id。 权威记录是该事件,而不是命令记录,因为反馈可能来自 `/feedback` 之外的触发方式。让载荷不进入 `command/run`,可避免两条记录携带相同文本。 diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 535c438a63..433087eff3 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -28,6 +28,7 @@ "@deepseek-ai/dsh-commands": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", + "@deepseek-ai/dsh-user-id": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { @@ -38,6 +39,7 @@ "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-user-id": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/feedback/command-feedback/src/index.ts b/packages/feedback/command-feedback/src/index.ts index 92ef839415..7f0bb3a59f 100644 --- a/packages/feedback/command-feedback/src/index.ts +++ b/packages/feedback/command-feedback/src/index.ts @@ -9,6 +9,7 @@ import type { Context } from 'cordis' import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands' import type { Session } from '@deepseek-ai/dsh-session' +import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' export const name = 'command-feedback' export const inject = ['commands'] @@ -41,8 +42,8 @@ export function recordFeedback(session: Session, text: string): void { * Validate, record, and acknowledge one feedback entry. Returning an error * leaves no `feedback/record` event. * @param invocation - receiving agent, raw command input, and UI cancellation. - * @returns an acknowledgement containing the receiving session id, or a usage error - * when no feedback text was supplied. + * @returns an acknowledgement containing the receiving session and anonymous + * user ids, or a usage error when no feedback text was supplied. */ function executeFeedbackCommand(invocation: CommandInvocation): CommandResult { if (invocation.rawInput.trim().length === 0) { @@ -51,7 +52,7 @@ function executeFeedbackCommand(invocation: CommandInvocation): CommandResult { recordFeedback(invocation.agent.session, invocation.rawInput) return { kind: 'success', - text: `Feedback recorded for session ${invocation.agent.session.id}`, + text: `Feedback recorded for session ${invocation.agent.session.id}\nUser: ${getOrCreateAnonymousUserId()}`, } } diff --git a/packages/feedback/command-feedback/tests/command-feedback.spec.ts b/packages/feedback/command-feedback/tests/command-feedback.spec.ts index 19d886af00..6f93ff854e 100644 --- a/packages/feedback/command-feedback/tests/command-feedback.spec.ts +++ b/packages/feedback/command-feedback/tests/command-feedback.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' @@ -7,6 +7,17 @@ import CommandService from '@deepseek-ai/dsh-commands' import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session' import * as commandFeedback from '@deepseek-ai/dsh-command-feedback' +const { USER_ID, getOrCreateAnonymousUserId } = vi.hoisted(() => { + const USER_ID = '01234567-89ab-4cde-8f01-23456789abcd' + return { USER_ID, getOrCreateAnonymousUserId: vi.fn(() => USER_ID) } +}) + +vi.mock('@deepseek-ai/dsh-user-id', () => ({ + getOrCreateAnonymousUserId, +})) + +beforeEach(() => getOrCreateAnonymousUserId.mockClear()) + interface Harness { readonly ctx: Context readonly agent: Agent @@ -93,7 +104,7 @@ describe('/feedback human command', () => { const test = await harness() await expect(run(test, ' the diff view is unreadable')).resolves.toEqual({ kind: 'success', - text: `Feedback recorded for session ${test.session.id}`, + text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}`, }) expect(feedbackTexts(test.session)).toEqual(['the diff view is unreadable']) const commandRun = test.session.events.find(event => event.type === 'command/run') @@ -141,8 +152,8 @@ describe('/feedback human command', () => { test.ctx.commands.execute(test.agent, '/feedback second', signal), ]) expect(settled.map(item => item?.result)).toEqual([ - { kind: 'success', text: `Feedback recorded for session ${test.session.id}` }, - { kind: 'success', text: `Feedback recorded for session ${test.session.id}` }, + { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` }, + { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}` }, ]) expect(feedbackTexts(test.session)).toEqual(['first', 'second']) }) @@ -167,6 +178,7 @@ describe('/feedback human command', () => { } await expect(run(test)).resolves.toEqual(expected) await expect(run(test, ' \n\t ')).resolves.toEqual(expected) + expect(getOrCreateAnonymousUserId).not.toHaveBeenCalled() expect(feedbackTexts(test.session)).toEqual([]) const done = test.session.events.filter(event => event.type === 'command/done') expect(done.map(event => event.data.kind)).toEqual(['error', 'error']) diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts index 98609afdea..958b23736f 100644 --- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts +++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import Include from '@cordisjs/plugin-include' @@ -11,6 +11,7 @@ import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandService from '@deepseek-ai/dsh-commands' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import * as CommandFeedback from '@deepseek-ai/dsh-command-feedback' +import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' let root: string | undefined let context: Context | undefined @@ -20,6 +21,7 @@ afterEach(async () => { context = undefined if (root !== undefined) await rm(root, { recursive: true, force: true }) root = undefined + vi.unstubAllEnvs() }) /** Register one idle agent over a store-owned session, as an app's spine does. */ @@ -51,6 +53,7 @@ function agent(ctx: Context): Agent { describe('/feedback real Loader composition through cordis.yml', () => { it('boots cordis.yml and records feedback without model-visible output', async () => { root = await mkdtemp(join(tmpdir(), 'dsh-command-feedback-loader-')) + vi.stubEnv('DSH_HOME', root) const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ "- name: '@deepseek-ai/dsh-agent'", @@ -87,9 +90,10 @@ describe('/feedback real Loader composition through cordis.yml', () => { expect(context.commands.list(owner).map(command => command.name)).toContain('feedback') const accepted = await context.commands.execute(owner, '/feedback the diff view is unreadable', signal) + const userId = getOrCreateAnonymousUserId({ env: { DSH_HOME: root } }) expect(accepted?.result).toEqual({ kind: 'success', - text: 'Feedback recorded for session feedback-loader-agent', + text: `Feedback recorded for session feedback-loader-agent\nUser: ${userId}`, }) const rejected = await context.commands.execute(owner, '/feedback', signal) expect(rejected?.result).toEqual({ diff --git a/packages/feedback/command-feedback/tsconfig.json b/packages/feedback/command-feedback/tsconfig.json index f59431af9a..c39f55f60f 100644 --- a/packages/feedback/command-feedback/tsconfig.json +++ b/packages/feedback/command-feedback/tsconfig.json @@ -20,6 +20,9 @@ { "path": "../../core/session" }, + { + "path": "../../session/user-id" + }, { "path": "../../support/invariants" } diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index 5d941e3fe1..2af0b5294e 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -35,23 +35,21 @@ }, "peerDependencies": { "@deepseek-ai/dsh-command-feedback": "^0.0.1", - "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", - "@deepseek-ai/dsh-paths": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-telemetry": "^0.0.1", + "@deepseek-ai/dsh-user-id": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@cordisjs/plugin-loader": "workspace:^", "@deepseek-ai/dsh-command-feedback": "workspace:^", - "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-paths": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-telemetry": "workspace:^", + "@deepseek-ai/dsh-user-id": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/session/session-telemetry-otel/src/index.ts b/packages/session/session-telemetry-otel/src/index.ts index b128f65978..b66d641750 100644 --- a/packages/session/session-telemetry-otel/src/index.ts +++ b/packages/session/session-telemetry-otel/src/index.ts @@ -25,7 +25,7 @@ import { type TelemetrySeverity, } from '@deepseek-ai/dsh-session-telemetry' import { APP_IDENTITY } from '@deepseek-ai/dsh-llm' -import { getOrCreateAnonymousUserId } from './user-id.ts' +import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' import { BatchLogRecordProcessor, LoggerProvider, diff --git a/packages/session/session-telemetry-otel/tests/otel.spec.ts b/packages/session/session-telemetry-otel/tests/otel.spec.ts index 6139b5c505..511c95c0d8 100644 --- a/packages/session/session-telemetry-otel/tests/otel.spec.ts +++ b/packages/session/session-telemetry-otel/tests/otel.spec.ts @@ -13,7 +13,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { gunzipSync } from 'node:zlib' import { Context } from 'cordis' -import { getOrCreateAnonymousUserId } from '../src/user-id.ts' +import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' import Loader from '@cordisjs/plugin-loader' import { recordFeedback } from '@deepseek-ai/dsh-command-feedback' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' diff --git a/packages/session/session-telemetry-otel/tsconfig.json b/packages/session/session-telemetry-otel/tsconfig.json index 60aee08eda..421742a62d 100644 --- a/packages/session/session-telemetry-otel/tsconfig.json +++ b/packages/session/session-telemetry-otel/tsconfig.json @@ -30,10 +30,7 @@ "path": "../session-telemetry" }, { - "path": "../../util/brand" - }, - { - "path": "../../util/paths" + "path": "../user-id" }, { "path": "../../support/invariants" diff --git a/packages/session/user-id/README.i18n.yaml b/packages/session/user-id/README.i18n.yaml new file mode 100644 index 0000000000..5d58bba70e --- /dev/null +++ b/packages/session/user-id/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 packages/session/user-id/README.md +README.md: 31a72f5e7b58b90b165b16374c2301389cbe2ca0 +README.zh.md: 013097b3038c43ff740660ef9159ca2b13f7b743 diff --git a/packages/session/user-id/README.md b/packages/session/user-id/README.md new file mode 100644 index 0000000000..31a72f5e7b --- /dev/null +++ b/packages/session/user-id/README.md @@ -0,0 +1,29 @@ +# @deepseek-ai/dsh-user-id + +English | [中文](README.zh.md) + +Shared anonymous identity for session telemetry and direct feedback acknowledgement. `getOrCreateAnonymousUserId()` returns a random UUID v4 scoped to one harness home, persisted as the bare line `$DSH_HOME/.userid` (`~/.dsh/.userid` when `DSH_HOME` is unset). The OpenTelemetry backend reports it as Resource `user.id`; `/feedback` includes the same value in its acknowledgement so an operator can correlate a submitted session and user with exported telemetry. + +The identity is never derived from the hostname, network address, git remote, or another identifying source. Deleting `.userid` resets the identity on the next process launch. Separate harness homes have separate identities, and the dsh-sdk launcher telemetry intentionally keeps its own unrelated store. + +## Storage contract + +Reads and writes are synchronous because both boot-time telemetry construction and direct command execution need one API. The result is memoized per resolved file path for the process lifetime. A first writer uses exclusive creation and a concurrent loser adopts the persisted winner; a corrupt file is replaced. Persistence is best-effort, so an unwritable home still receives a process-local UUID rather than blocking telemetry or feedback. + +## Composition + +This package is a shared library, not a Cordis plugin. Consumers import `getOrCreateAnonymousUserId()` directly. Its invariant companion is intentionally empty because the package owns no event stream or public mutable relation that can be checked without creating the identity as a side effect. + +## Model Experience + +None, as the identifier is used only in telemetry metadata and a direct human command response; it never enters a model request. + +#### KV Cache effect + +None; this package never contributes to a model request. + +## Known Limitations and Deferred Work + +- **No recovery after deletion** — loss mints a new anonymous identity by design; recovery would require stable derivation material that weakens anonymity. +- **Best-effort concurrency** — a reader landing in the narrow interval between a concurrent process's exclusive create and completed write can use a different in-memory UUID for that run; later launches converge on the persisted value. +- **No cross-home identity** — different `$DSH_HOME` values cannot be correlated, and this package does not unify the separate dsh-sdk launcher telemetry identity. diff --git a/packages/session/user-id/README.zh.md b/packages/session/user-id/README.zh.md new file mode 100644 index 0000000000..013097b303 --- /dev/null +++ b/packages/session/user-id/README.zh.md @@ -0,0 +1,29 @@ +# @deepseek-ai/dsh-user-id + +[English](README.md) | 中文 + +会话遥测与直接反馈确认共用的匿名身份。`getOrCreateAnonymousUserId()` 返回一个限定于单个 harness home 的随机 UUID v4,并以裸行形式持久化到 `$DSH_HOME/.userid`(未设置 `DSH_HOME` 时为 `~/.dsh/.userid`)。OpenTelemetry 后端将其作为 Resource 的 `user.id` 上报;`/feedback` 在确认文本中包含同一个值,以便运维人员将所报告的会话和用户与导出的遥测相关联。 + +该身份绝不从 hostname、网络地址、git remote 或其他可用于识别身份的来源派生。删除 `.userid` 后,下次启动进程时会重置身份。不同 harness home 拥有不同身份,dsh-sdk launcher telemetry 则刻意使用与此无关的独立存储。 + +## 存储契约 + +读写采用同步方式,因为启动时构造遥测和直接执行命令都需要使用同一个 API。结果在进程生命周期内按解析后的文件路径缓存。首个写入方采用独占创建;并发竞争中失败的一方会采用已持久化的胜出值。损坏的文件会被替换。持久化采用 best-effort,因此即使 home 不可写,系统仍会返回进程本地 UUID,而不会阻塞遥测或反馈。 + +## 组合 + +本包是共享库,并非 Cordis 插件。消费方直接导入 `getOrCreateAnonymousUserId()`。其不变式伴生插件刻意留空,因为本包既不拥有事件流,也不拥有任何可以在不触发创建身份这一副作用的情况下检查的公开可变关系。 + +## 模型体验 + +无,因为该标识符只用于遥测元数据和面向用户的直接命令响应;它绝不会进入模型请求。 + +#### KV Cache 影响 + +无;本包绝不会向模型请求贡献任何内容。 + +## 已知限制与暂缓工作 + +- **删除后无法恢复**:身份丢失后会按设计生成新的匿名身份;若要恢复身份,就需要稳定的派生材料,这会削弱匿名性。 +- **Best-effort 并发**:如果读取方恰好落在并发进程完成独占创建但尚未写完的狭窄时间窗内,本次运行可能使用不同的内存 UUID;后续启动会收敛到已持久化的值。 +- **没有跨 home 身份**:不同 `$DSH_HOME` 值之间无法关联,本包也不会统一 dsh-sdk launcher telemetry 的独立身份。 diff --git a/packages/session/user-id/package.json b/packages/session/user-id/package.json new file mode 100644 index 0000000000..2a09c73b0e --- /dev/null +++ b/packages/session/user-id/package.json @@ -0,0 +1,39 @@ +{ + "name": "@deepseek-ai/dsh-user-id", + "description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/types/**/*.d.ts" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", + "@deepseek-ai/dsh-paths": "^0.0.1", + "cordis": "^4.0.0-rc.7" + }, + "devDependencies": { + "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-paths": "workspace:^", + "cordis": "^4.0.0-rc.7" + } +} diff --git a/packages/session/session-telemetry-otel/src/user-id.ts b/packages/session/user-id/src/index.ts similarity index 79% rename from packages/session/session-telemetry-otel/src/user-id.ts rename to packages/session/user-id/src/index.ts index 0a2cf95a6a..ca314e945a 100644 --- a/packages/session/session-telemetry-otel/src/user-id.ts +++ b/packages/session/user-id/src/index.ts @@ -1,22 +1,20 @@ /** - * Per-harness-home anonymous user id for the OTel Resource. + * Per-harness-home anonymous user id shared by telemetry and feedback. * * The id is a random UUID persisted as a bare line in `.userid` inside the * harness home resolved by {@link resolveDshHome} (`$DSH_HOME` > `~/.dsh`), * and never derived from the hostname, network address, git remote, or any - * other identifying source — a derived id would make "anonymous" a fiction. - * The id is scoped to the harness home, not the machine: every process - * sharing one `$DSH_HOME` reports the same id, and deleting the file simply - * mints a fresh identity on the next launch (loss is accepted by design). - * This identity belongs to the OTel feed alone; the dsh-sdk launcher - * telemetry keeps its own separate store. + * other identifying source. It is scoped to the harness home, not the + * machine: every process sharing one `$DSH_HOME` reports the same id, and + * deleting the file mints a fresh identity on the next launch. The dsh-sdk + * launcher telemetry keeps its own separate store. * - * Reads and writes are synchronous so the backend constructor can call this - * on its boot path, and the result is memoized per resolved file path: one - * process touches the disk once, and a file deleted mid-run keeps the - * process's id until the next launch. + * Reads and writes are synchronous so boot-time and command consumers can + * use one API. The result is memoized per resolved file path: one process + * touches the disk once, and a file deleted mid-run keeps the process's id + * until the next launch. * - * @module @deepseek-ai/dsh-session-telemetry-otel/user-id + * @module @deepseek-ai/dsh-user-id */ import { randomUUID } from 'node:crypto' @@ -64,8 +62,8 @@ function readPersistedId(file: string): AnonymousUserId | undefined { * narrow create-to-write window can still yield two per-process ids for that * run; the next launch converges on the persisted one.) Persistence is * best-effort — a write failure (read-only home) still returns a usable id - * for the current run so telemetry is never blocked. - * @param options - Home-location and UUID-generation hooks. + * for the current run so feedback and telemetry are never blocked. + * @param options - home-location and UUID-generation seams. * @returns the stable per-harness-home anonymous user id. */ export function getOrCreateAnonymousUserId(options: AnonymousUserIdOptions = {}): AnonymousUserId { diff --git a/packages/session/user-id/src/invariant.ts b/packages/session/user-id/src/invariant.ts new file mode 100644 index 0000000000..b649e23619 --- /dev/null +++ b/packages/session/user-id/src/invariant.ts @@ -0,0 +1,31 @@ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-user-id`. + * @module @deepseek-ai/dsh-user-id/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-user-id' + +/** Cordis companion plugin name. */ +export const name = 'user-id-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** + * No runtime invariant: the API owns one private memo and one best-effort + * file, with no independent event stream or public mutable relation for a + * companion to compare without creating the identity as a side effect. + */ +const install: InvariantInstaller = () => {} + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/session/user-id/tests/invariant.spec.ts b/packages/session/user-id/tests/invariant.spec.ts new file mode 100644 index 0000000000..abffc89621 --- /dev/null +++ b/packages/session/user-id/tests/invariant.spec.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import InvariantService from '@deepseek-ai/dsh-invariants' +import * as UserIdInvariant from '@deepseek-ai/dsh-user-id/invariant' + +describe('invariant companion', () => { + it('registers the package ownership with an empty installer', async () => { + const ctx = new Context() + await ctx.plugin(InvariantService, { enabled: true }) + await expect(ctx.plugin(UserIdInvariant).await()).resolves.toBeDefined() + }) +}) diff --git a/packages/session/session-telemetry-otel/tests/user-id.spec.ts b/packages/session/user-id/tests/user-id.spec.ts similarity index 99% rename from packages/session/session-telemetry-otel/tests/user-id.spec.ts rename to packages/session/user-id/tests/user-id.spec.ts index f7abf45f0b..0f21cb8204 100644 --- a/packages/session/session-telemetry-otel/tests/user-id.spec.ts +++ b/packages/session/user-id/tests/user-id.spec.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { USER_ID_FILE_NAME, getOrCreateAnonymousUserId, -} from '../src/user-id.ts' +} from '../src/index.ts' const dirs: string[] = [] diff --git a/packages/session/user-id/tsconfig.json b/packages/session/user-id/tsconfig.json new file mode 100644 index 0000000000..52e417d5ba --- /dev/null +++ b/packages/session/user-id/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../util/brand" + }, + { + "path": "../../util/paths" + }, + { + "path": "../../support/invariants" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7de565e0c9..2507f83973 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3686,6 +3686,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-user-id': + specifier: workspace:^ + version: link:../../session/user-id cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -5845,9 +5848,6 @@ importers: '@cordisjs/plugin-loader': specifier: workspace:^ version: link:../../../vendor/loader - '@deepseek-ai/dsh-brand': - specifier: workspace:^ - version: link:../../util/brand '@deepseek-ai/dsh-command-feedback': specifier: workspace:^ version: link:../../feedback/command-feedback @@ -5857,15 +5857,15 @@ importers: '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm - '@deepseek-ai/dsh-paths': - specifier: workspace:^ - version: link:../../util/paths '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session '@deepseek-ai/dsh-session-telemetry': specifier: workspace:^ version: link:../session-telemetry + '@deepseek-ai/dsh-user-id': + specifier: workspace:^ + version: link:../user-id cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -5988,6 +5988,21 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis + packages/session/user-id: + devDependencies: + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-paths': + specifier: workspace:^ + version: link:../../util/paths + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + packages/settings/settings: devDependencies: '@deepseek-ai/dsh-brand': diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 1b39447215..1a1de87d09 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -127,6 +127,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers no model surface.' }, 'packages/session/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' }, 'packages/session/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' }, + 'packages/session/user-id': { kind: 'none', reason: 'The shared identifier appears only in telemetry metadata and a direct human command response; it registers no model surface.' }, 'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' }, 'packages/skill/skill-badge': { kind: 'indirect', reason: 'The bundled provider delegates model rendering to dsh-tool-skill.' }, 'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' }, diff --git a/tsconfig.host.json b/tsconfig.host.json index 750b586f5f..d9bf1c29e4 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -139,6 +139,7 @@ { "path": "./packages/session/session-title-first-message-llm" }, { "path": "./packages/session/session-title-all-messages-llm" }, { "path": "./packages/session/session-telemetry" }, + { "path": "./packages/session/user-id" }, { "path": "./packages/session/session-telemetry-otel" }, { "path": "./packages/core/system-prompt" }, { "path": "./packages/core/agent" }, From a27efdef363de392d675d4ce517db58b78826b44 Mon Sep 17 00:00:00 2001 From: Turtle Date: Sun, 9 Aug 2026 15:27:21 +0800 Subject: [PATCH 4/5] docs: make technical prose concrete --- .agents/notes/AGENTS.md | 2 +- .agents/notes/README.i18n.yaml | 4 +- .agents/notes/README.md | 12 +-- .agents/notes/README.zh.md | 8 +- .agents/notes/implemented/AGENTS.md | 2 +- ...19-gui-layering-and-rpc-protocol.i18n.yaml | 4 +- ...026-07-19-gui-layering-and-rpc-protocol.md | 40 +++++----- ...-07-19-gui-layering-and-rpc-protocol.zh.md | 36 ++++----- ...4-configuration-source-ownership.i18n.yaml | 4 +- ...26-08-04-configuration-source-ownership.md | 2 +- ...08-04-configuration-source-ownership.zh.md | 2 +- .../feature/2026-06-15-code-mode.i18n.yaml | 4 +- .../feature/2026-06-15-code-mode.md | 6 +- .../feature/2026-06-15-code-mode.zh.md | 6 +- ...6-06-21-subagent-capability-seam.i18n.yaml | 4 +- .../2026-06-21-subagent-capability-seam.md | 4 +- .../2026-06-21-subagent-capability-seam.zh.md | 4 +- .../feature/2026-06-30-hook-bridges.i18n.yaml | 4 +- .../feature/2026-06-30-hook-bridges.md | 2 +- .../feature/2026-06-30-hook-bridges.zh.md | 2 +- ...16-durable-per-step-time-context.i18n.yaml | 4 +- ...026-07-16-durable-per-step-time-context.md | 2 +- ...-07-16-durable-per-step-time-context.zh.md | 2 +- ...26-07-26-code-dispatch-log-spill.i18n.yaml | 4 +- .../2026-07-26-code-dispatch-log-spill.md | 20 ++--- .../2026-07-26-code-dispatch-log-spill.zh.md | 20 ++--- ...-plan-review-presentation-intent.i18n.yaml | 4 +- ...6-07-30-plan-review-presentation-intent.md | 6 +- ...7-30-plan-review-presentation-intent.zh.md | 6 +- ...3-translation-prompt-v4-contract.i18n.yaml | 4 +- ...26-07-23-translation-prompt-v4-contract.md | 6 +- ...07-23-translation-prompt-v4-contract.zh.md | 6 +- ...staller-adopts-existing-checkout.i18n.yaml | 4 +- ...7-31-installer-adopts-existing-checkout.md | 10 +-- ...1-installer-adopts-existing-checkout.zh.md | 10 +-- ...08-unified-github-label-taxonomy.i18n.yaml | 4 +- ...026-08-08-unified-github-label-taxonomy.md | 10 +-- ...-08-08-unified-github-label-taxonomy.zh.md | 10 +-- ...-09-chinese-contract-terminology.i18n.yaml | 4 +- ...2026-08-09-chinese-contract-terminology.md | 2 +- ...6-08-09-chinese-contract-terminology.zh.md | 2 +- ...-09-committed-artifact-citations.i18n.yaml | 4 +- ...2026-08-09-committed-artifact-citations.md | 4 +- ...6-08-09-committed-artifact-citations.zh.md | 4 +- ...-names-actors-and-recorded-facts.i18n.yaml | 4 +- ...e-prose-names-actors-and-recorded-facts.md | 4 +- ...rose-names-actors-and-recorded-facts.zh.md | 4 +- ...7-29-shared-base-config-overlays.i18n.yaml | 4 +- .../2026-07-29-shared-base-config-overlays.md | 2 +- ...26-07-29-shared-base-config-overlays.zh.md | 2 +- ...root-and-derived-medium-recovery.i18n.yaml | 4 +- ...torage-root-and-derived-medium-recovery.md | 2 +- ...age-root-and-derived-medium-recovery.zh.md | 2 +- .../skills/dsh-archive-agent-notes/SKILL.md | 12 +-- .agents/skills/dsh-code-review/SKILL.md | 20 ++--- .agents/skills/dsh-doc-site-sync/SKILL.md | 2 +- .agents/skills/dsh-doc-standards/SKILL.md | 12 +-- .../skills/dsh-find-simplifications/SKILL.md | 26 +++--- .agents/skills/dsh-prose-standard/SKILL.md | 14 ++-- .../dsh-prose-standard/references/examples.md | 16 ++-- .agents/skills/dsh-translate-docs/SKILL.md | 2 +- .agents/skills/dsh-trim-cot-leakage/SKILL.md | 4 +- .../references/examples.md | 2 +- .../references/recall-batteries.md | 6 +- .agents/skills/record-browser-gif/SKILL.md | 8 +- .github/issue-management/policy.mjs | 4 +- AGENTS.md | 12 +-- README.i18n.yaml | 4 +- README.md | 2 +- README.zh.md | 2 +- .../agent-presets/code/agent.cordis.yml | 2 +- .../agent-presets/cordis/agent.cordis.yml | 2 +- .../editing-cordis-compositions/SKILL.md | 4 +- .../agent-presets/standard/agent.cordis.yml | 2 +- apps/cli/reference/README.i18n.yaml | 2 +- apps/cli/reference/README.md | 2 +- apps/cli/src/args.ts | 2 +- apps/cli/src/process-shutdown.ts | 4 +- apps/cli/tests/memory-mcp-configs.spec.ts | 4 +- apps/cli/tests/source-launch.compat.spec.ts | 2 +- apps/web/tests/approval-composer.e2e.ts | 6 +- apps/web/tests/chat-long-interactions.e2e.ts | 2 +- apps/web/tests/complex-history.perf.ts | 4 +- apps/web/tests/composer-draft-scroll.e2e.ts | 10 +-- .../tests/details-session-lifecycle.e2e.ts | 2 +- apps/web/tests/navigation-panes.e2e.ts | 4 +- apps/web/tests/pwsh-terminal.e2e.ts | 6 +- apps/web/tests/question-composer.e2e.ts | 4 +- apps/web/tests/scaffold.ts | 6 +- apps/web/tests/search-card.snapshot.ts | 8 +- apps/web/tests/seeded-history.e2e.ts | 4 +- apps/web/tests/shipped-composition.e2e.ts | 2 +- apps/web/tests/startup-auto-selection.e2e.ts | 9 +-- apps/web/tests/todo-row.snapshot.ts | 2 +- apps/web/tests/turn-tail-actions.e2e.ts | 4 +- apps/web/tests/workspace-management.e2e.ts | 4 +- apps/web/vite.config.ts | 6 +- docs/AGENTS.md | 18 ++--- docs/agent-lifecycle.i18n.yaml | 4 +- docs/agent-lifecycle.md | 4 +- docs/agent-lifecycle.zh.md | 4 +- docs/api-gateway.i18n.yaml | 4 +- docs/api-gateway.md | 6 +- docs/api-gateway.zh.md | 6 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 6 +- docs/architecture.zh.md | 8 +- docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 2 +- docs/capability-seams.zh.md | 2 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 20 ++--- docs/config-catalog.zh.md | 20 ++--- docs/cookbook/adding-a-package.i18n.yaml | 4 +- docs/cookbook/adding-a-package.md | 6 +- docs/cookbook/adding-a-package.zh.md | 6 +- docs/cookbook/adding-a-tool.i18n.yaml | 4 +- docs/cookbook/adding-a-tool.md | 4 +- docs/cookbook/adding-a-tool.zh.md | 4 +- .../adding-a-vendored-package.i18n.yaml | 4 +- docs/cookbook/adding-a-vendored-package.md | 6 +- docs/cookbook/adding-a-vendored-package.zh.md | 6 +- docs/cookbook/extension-cookbook.i18n.yaml | 4 +- docs/cookbook/extension-cookbook.md | 6 +- docs/cookbook/extension-cookbook.zh.md | 6 +- .../maintaining-dsh-code-review.i18n.yaml | 4 +- docs/cookbook/maintaining-dsh-code-review.md | 2 +- .../maintaining-dsh-code-review.zh.md | 2 +- docs/cordis-primer.i18n.yaml | 2 +- docs/cordis-primer.md | 2 +- .../cordis-tutorial/01-first-plugin.i18n.yaml | 4 +- docs/cordis-tutorial/01-first-plugin.md | 2 +- docs/cordis-tutorial/01-first-plugin.zh.md | 2 +- docs/cordis-tutorial/index.i18n.yaml | 4 +- docs/cordis-tutorial/index.md | 2 +- docs/cordis-tutorial/index.zh.md | 2 +- docs/defensive-patterns.i18n.yaml | 4 +- docs/defensive-patterns.md | 4 +- docs/defensive-patterns.zh.md | 4 +- docs/development.i18n.yaml | 4 +- docs/development.md | 14 ++-- docs/development.zh.md | 14 ++-- docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 6 +- docs/event-producer-consumer.zh.md | 6 +- docs/graph-atlas.i18n.yaml | 4 +- docs/graph-atlas.md | 2 +- docs/graph-atlas.zh.md | 2 +- docs/i18n/README.i18n.yaml | 4 +- docs/i18n/README.md | 6 +- docs/i18n/README.zh.md | 6 +- docs/i18n/style-samples.md | 14 ++-- docs/i18n/translation-prompt.md | 16 ++-- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 2 +- docs/persistence-catalog.zh.md | 2 +- ...-acp-default-export-drops-inject.i18n.yaml | 4 +- .../0001-acp-default-export-drops-inject.md | 4 +- ...0001-acp-default-export-drops-inject.zh.md | 4 +- ...ession-disabled-filesystem-tools.i18n.yaml | 4 +- ...js-expression-disabled-filesystem-tools.md | 4 +- ...expression-disabled-filesystem-tools.zh.md | 4 +- ...0003-web-agent-gui-feedback-loop.i18n.yaml | 4 +- .../0003-web-agent-gui-feedback-loop.md | 2 +- .../0003-web-agent-gui-feedback-loop.zh.md | 2 +- docs/subsystems/README.i18n.yaml | 4 +- docs/subsystems/README.md | 10 +-- docs/subsystems/README.zh.md | 10 +-- docs/subsystems/bash.i18n.yaml | 4 +- docs/subsystems/bash.md | 2 +- docs/subsystems/bash.zh.md | 2 +- docs/subsystems/code-runtime.i18n.yaml | 2 +- docs/subsystems/code-runtime.md | 2 +- docs/subsystems/compaction.i18n.yaml | 4 +- docs/subsystems/compaction.md | 4 +- docs/subsystems/compaction.zh.md | 4 +- docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 20 ++--- docs/subsystems/core.zh.md | 22 +++--- docs/subsystems/credentials.i18n.yaml | 4 +- docs/subsystems/credentials.md | 2 +- docs/subsystems/credentials.zh.md | 2 +- docs/subsystems/filesystem.i18n.yaml | 4 +- docs/subsystems/filesystem.md | 16 ++-- docs/subsystems/filesystem.zh.md | 16 ++-- docs/subsystems/goal.i18n.yaml | 4 +- docs/subsystems/goal.md | 4 +- docs/subsystems/goal.zh.md | 4 +- docs/subsystems/http-server.i18n.yaml | 4 +- docs/subsystems/http-server.md | 8 +- docs/subsystems/http-server.zh.md | 8 +- docs/subsystems/llm-streaming.i18n.yaml | 4 +- docs/subsystems/llm-streaming.md | 30 +++---- docs/subsystems/llm-streaming.zh.md | 30 +++---- docs/subsystems/persistence.i18n.yaml | 4 +- docs/subsystems/persistence.md | 2 +- docs/subsystems/persistence.zh.md | 2 +- docs/subsystems/plan.i18n.yaml | 4 +- docs/subsystems/plan.md | 43 +++++----- docs/subsystems/plan.zh.md | 43 +++++----- docs/subsystems/session-projection.i18n.yaml | 4 +- docs/subsystems/session-projection.md | 6 +- docs/subsystems/session-projection.zh.md | 6 +- docs/subsystems/session-query.i18n.yaml | 4 +- docs/subsystems/session-query.md | 4 +- docs/subsystems/session-query.zh.md | 4 +- docs/subsystems/session-reference.i18n.yaml | 4 +- docs/subsystems/session-reference.md | 2 +- docs/subsystems/session-reference.zh.md | 2 +- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 6 +- docs/subsystems/session.zh.md | 8 +- docs/subsystems/settings.i18n.yaml | 4 +- docs/subsystems/settings.md | 6 +- docs/subsystems/settings.zh.md | 6 +- docs/subsystems/storage.i18n.yaml | 4 +- docs/subsystems/storage.md | 6 +- docs/subsystems/storage.zh.md | 6 +- docs/subsystems/subagent.i18n.yaml | 4 +- docs/subsystems/subagent.md | 9 ++- docs/subsystems/subagent.zh.md | 9 ++- docs/subsystems/system-prompt.i18n.yaml | 4 +- docs/subsystems/system-prompt.md | 2 +- docs/subsystems/system-prompt.zh.md | 2 +- docs/subsystems/tasks.i18n.yaml | 4 +- docs/subsystems/tasks.md | 6 +- docs/subsystems/tasks.zh.md | 6 +- docs/subsystems/telemetry.i18n.yaml | 4 +- docs/subsystems/telemetry.md | 17 ++-- docs/subsystems/telemetry.zh.md | 17 ++-- docs/subsystems/tools.i18n.yaml | 4 +- docs/subsystems/tools.md | 27 ++++--- docs/subsystems/tools.zh.md | 27 ++++--- docs/subsystems/user-interaction.i18n.yaml | 4 +- docs/subsystems/user-interaction.md | 10 +-- docs/subsystems/user-interaction.zh.md | 10 +-- docs/subsystems/web.i18n.yaml | 4 +- docs/subsystems/web.md | 4 +- docs/subsystems/web.zh.md | 4 +- docs/subsystems/workflow.i18n.yaml | 4 +- docs/subsystems/workflow.md | 10 +-- docs/subsystems/workflow.zh.md | 10 +-- docs/testing.i18n.yaml | 4 +- docs/testing.md | 8 +- docs/testing.zh.md | 8 +- docs/tool-execution-pipeline.i18n.yaml | 4 +- docs/tool-execution-pipeline.md | 2 +- docs/tool-execution-pipeline.zh.md | 2 +- docs/user/develop/basic/index.i18n.yaml | 4 +- docs/user/develop/basic/index.md | 2 +- docs/user/develop/basic/index.zh.md | 2 +- docs/user/develop/basic/publish.i18n.yaml | 4 +- docs/user/develop/basic/publish.md | 4 +- docs/user/develop/basic/publish.zh.md | 4 +- docs/user/develop/practice/index.i18n.yaml | 4 +- docs/user/develop/practice/index.md | 6 +- docs/user/develop/practice/index.zh.md | 8 +- docs/web-styling.i18n.yaml | 2 +- docs/web-styling.zh.md | 2 +- examples/AGENTS.md | 2 +- examples/acp-agent/tests/acp.snapshot.ts | 2 +- examples/acp-agent/tests/escalation.e2e.ts | 8 +- .../headless-agent/tests/keyless-smoke.e2e.ts | 2 +- examples/mcp-memory/README.i18n.yaml | 4 +- examples/mcp-memory/README.md | 4 +- examples/mcp-memory/README.zh.md | 4 +- examples/web-cordis/README.i18n.yaml | 4 +- examples/web-cordis/README.md | 2 +- examples/web-cordis/README.zh.md | 2 +- native/landlock-run/AGENTS.md | 6 +- native/landlock-run/docs/architecture.md | 2 +- native/landlock-run/docs/packaging.md | 2 +- packages/AGENTS.md | 12 +-- packages/bash/bash-sandbox/src/index.ts | 2 +- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 6 +- packages/boot/app-boot/README.zh.md | 6 +- packages/boot/app-boot/src/index.ts | 8 +- packages/boot/app-boot/src/profile.ts | 2 +- packages/client/AGENTS.md | 18 ++--- packages/client/hmr/README.i18n.yaml | 2 +- packages/client/hmr/README.md | 2 +- packages/client/modules/README.i18n.yaml | 4 +- packages/client/modules/README.md | 2 +- packages/client/modules/README.zh.md | 2 +- packages/client/modules/src/index.ts | 4 +- packages/client/tsdown.client.ts | 8 +- packages/client/ui-command/README.i18n.yaml | 4 +- packages/client/ui-command/README.md | 6 +- packages/client/ui-command/README.zh.md | 6 +- .../client/ui-command/src/client/directory.ts | 2 +- .../src/client/skeleton/InputBar.tsx | 2 +- packages/client/ui-goal/src/client/index.ts | 2 +- .../client/ui-model/src/client/service.ts | 2 +- packages/client/ui-models/README.i18n.yaml | 4 +- packages/client/ui-models/README.md | 4 +- packages/client/ui-models/README.zh.md | 4 +- .../ui-primitives/src/markdown/highlight.ts | 2 +- .../ui-question/src/client/contract/slots.ts | 4 +- .../client/tool/models/search-card-model.ts | 11 +-- .../tool/toolviews/ask-question-row.tsx | 4 +- .../src/client/tool/toolviews/todo-row.tsx | 2 +- .../code-runtime-worker/src/bootstrap.ts | 2 +- .../context/time-context/src/invariant.ts | 4 +- .../time-context/tests/invariant.spec.ts | 10 +-- packages/core/session/src/index.ts | 4 +- packages/core/system-prompt/src/index.ts | 2 +- packages/core/tools/README.i18n.yaml | 4 +- packages/core/tools/README.md | 6 +- packages/core/tools/README.zh.md | 6 +- packages/core/tools/src/code-mode.ts | 42 +++++----- packages/core/tools/src/index.ts | 13 +-- packages/core/tools/src/json-schema.ts | 4 +- packages/core/tools/src/py-types.ts | 2 +- packages/core/tools/tests/code-mode.spec.ts | 12 +-- .../credentials-local/src/index.ts | 2 +- packages/e2b/subprocess-e2b/README.i18n.yaml | 4 +- packages/e2b/subprocess-e2b/README.md | 2 +- packages/e2b/subprocess-e2b/README.zh.md | 2 +- packages/experimental/AGENTS.md | 4 +- packages/fs/fs-policy/src/types.ts | 12 +-- packages/fs/tool-fs-search/src/grep.ts | 2 +- packages/fs/tool-fs/src/edit.ts | 2 +- packages/fs/tool-fs/src/write.ts | 2 +- packages/goal/goal/src/fold.ts | 10 +-- packages/goal/goal/tests/goal.spec.ts | 10 +-- packages/host/apiproxy/README.i18n.yaml | 4 +- packages/host/apiproxy/README.md | 8 +- packages/host/apiproxy/README.zh.md | 10 +-- .../host/apiproxy/src/api/approvals.schema.ts | 2 +- .../host/apiproxy/src/api/commands.schema.ts | 2 +- .../host/apiproxy/src/api/events.schema.ts | 2 +- packages/host/apiproxy/src/api/rpc.schema.ts | 4 +- .../host/apiproxy/src/api/sessions.schema.ts | 2 +- .../directory-picker-auto/README.i18n.yaml | 4 +- packages/host/directory-picker-auto/README.md | 2 +- .../host/directory-picker-auto/README.zh.md | 2 +- .../host/directory-picker/README.i18n.yaml | 4 +- packages/host/directory-picker/README.md | 4 +- packages/host/directory-picker/README.zh.md | 4 +- packages/host/webserver/README.i18n.yaml | 4 +- packages/host/webserver/README.md | 4 +- packages/host/webserver/README.zh.md | 4 +- packages/host/webserver/src/index.ts | 15 ++-- .../interaction/permission/src/invariant.ts | 2 +- .../user-interaction/README.i18n.yaml | 4 +- .../interaction/user-interaction/README.md | 2 +- .../interaction/user-interaction/README.zh.md | 2 +- .../interaction/user-interaction/src/types.ts | 8 +- packages/llm/llm-pi-ai/README.i18n.yaml | 4 +- packages/llm/llm-pi-ai/README.md | 2 +- packages/llm/llm-pi-ai/README.zh.md | 2 +- packages/llm/llm-pi-ai/src/catalog.ts | 6 +- packages/llm/llm-pi-ai/src/config.ts | 8 +- packages/llm/llm/src/index.ts | 4 +- packages/llm/llm/src/message.ts | 10 +-- packages/llm/llm/src/types.ts | 8 +- packages/llm/token-meter/src/index.ts | 4 +- packages/plan/plan-mode/README.i18n.yaml | 4 +- packages/plan/plan-mode/README.md | 10 +-- packages/plan/plan-mode/README.zh.md | 12 +-- packages/plan/plan-mode/src/index.ts | 79 ++++++++++--------- packages/plan/plan-mode/src/types.ts | 4 +- .../plan/plan-mode/tests/projection.spec.ts | 2 +- .../sandbox/sandbox-local/README.i18n.yaml | 4 +- packages/sandbox/sandbox-local/README.md | 2 +- packages/sandbox/sandbox-local/README.zh.md | 2 +- packages/sandbox/sandbox-local/src/index.ts | 2 +- .../sandbox/sandbox-policy/src/invariant.ts | 2 +- packages/scaffold/client/src/api.ts | 2 +- .../helper/src/documents/tsconfig-file.ts | 2 +- .../helper/src/features/define-feature.ts | 2 +- .../scaffold/helper/src/features/feature.ts | 2 +- .../repository-plugin/src/index.ts | 2 +- .../tool-cordis/src/api-catalog.ts | 22 +++--- .../tool-cordis/src/sandbox.ts | 4 +- .../session-query/session-query/src/index.ts | 4 +- .../session-persistence-jsonl/src/format.ts | 4 +- .../session-persistence/README.i18n.yaml | 4 +- .../session/session-persistence/README.md | 8 +- .../session/session-persistence/README.zh.md | 8 +- .../session/session-projection/src/index.ts | 4 +- .../session-telemetry-otel/src/index.ts | 16 ++-- .../session-telemetry/README.i18n.yaml | 4 +- packages/session/session-telemetry/README.md | 4 +- .../session/session-telemetry/README.zh.md | 4 +- .../session/session-telemetry/src/index.ts | 11 ++- packages/settings/settings/README.i18n.yaml | 4 +- packages/settings/settings/README.md | 2 +- packages/settings/settings/README.zh.md | 2 +- packages/settings/settings/src/index.ts | 24 +++--- .../settings/settings/tests/settings.spec.ts | 10 +-- packages/storage/storage-domain/src/spec.ts | 2 +- packages/storage/storage/src/backend.ts | 12 +-- .../subagent/subagent-acp/README.i18n.yaml | 4 +- packages/subagent/subagent-acp/README.md | 2 +- packages/subagent/subagent-acp/README.zh.md | 2 +- packages/subagent/subagent-acp/src/index.ts | 2 +- packages/subagent/subagent-acp/src/run.ts | 4 +- packages/subagent/subagent/src/types.ts | 5 +- packages/support/acp-snapshot/src/suite.ts | 2 +- packages/support/invariants/README.i18n.yaml | 4 +- packages/support/invariants/README.md | 4 +- packages/support/invariants/README.zh.md | 4 +- packages/support/llm-replay/src/index.ts | 4 +- packages/tasks/tasks-local/src/index.ts | 4 +- .../tasks/tasks-local/tests/tasks.spec.ts | 2 +- packages/todo/tool-todo/src/invariant.ts | 2 +- packages/typert/loader/src/index.ts | 6 +- packages/typert/loader/tests/loader.spec.ts | 2 +- packages/util/retention/src/index.ts | 6 +- packages/web/tool-web/src/index.ts | 2 +- packages/web/web-fetch-local/src/index.ts | 2 +- packages/web/web-fetch-local/src/provider.ts | 2 +- packages/web/web/src/index.ts | 2 +- packages/web/web/src/types.ts | 2 +- .../workflow/tool-workflow/README.i18n.yaml | 4 +- packages/workflow/tool-workflow/README.md | 2 +- packages/workflow/tool-workflow/README.zh.md | 2 +- packages/workflow/tool-workflow/src/index.ts | 2 +- .../workflow-workerthread/src/meta.ts | 4 +- .../workflow-workerthread/src/realm.ts | 14 ++-- .../workflow-workerthread/src/runtime.ts | 2 +- packages/workflow/workflow/src/types.ts | 8 +- python/README.i18n.yaml | 4 +- python/README.md | 2 +- python/README.zh.md | 2 +- scripts/AGENTS.md | 2 +- scripts/archived-agent-notes.ts | 2 +- scripts/client-bundle-purity.spec.ts | 2 +- scripts/cordis-walk.ts | 2 +- scripts/coverage-exempt.ts | 2 +- scripts/doc-typecheck-paths.ts | 2 +- scripts/doc-typecheck.ts | 2 +- scripts/gen-config-catalog.ts | 12 +-- scripts/gen-cordis-catalog.ts | 16 ++-- scripts/gen-doc-graphs.ts | 18 ++--- scripts/gen-third-party-notices.spec.ts | 4 +- scripts/gen-third-party-notices.ts | 4 +- scripts/gen-translation-brief.ts | 2 +- scripts/lint-rule-fingerprint.spec.ts | 4 +- scripts/package-invariants.ts | 4 +- scripts/run-gates.ts | 4 +- .../request-response.expected.json | 14 ++-- scripts/test-invariants.spec.ts | 2 +- scripts/test-invariants.ts | 2 +- scripts/translation-pairing-git.ts | 2 +- scripts/translation-pairing.ts | 4 +- scripts/translation-prompt.spec.ts | 4 +- scripts/translation-prompt.ts | 4 +- scripts/verify-agent-note-classification.ts | 2 +- scripts/verify-agent-note-format.ts | 2 +- scripts/verify-archived-agent-notes.ts | 2 +- scripts/verify-config-source-ownership.ts | 2 +- scripts/verify-export-jsdoc.ts | 6 +- scripts/verify-package-invariants.ts | 2 +- .../verify-package-readme-model-experience.ts | 4 +- scripts/verify-translation-pairing.ts | 2 +- skills/dsh-upgrade/SKILL.md | 2 +- 459 files changed, 1342 insertions(+), 1329 deletions(-) diff --git a/.agents/notes/AGENTS.md b/.agents/notes/AGENTS.md index e8e1a0ef66..66997859a8 100644 --- a/.agents/notes/AGENTS.md +++ b/.agents/notes/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — Agent Notes -Agent Notes are effectively RFCs written by agents: durable proposals and decision records that preserve rationale, alternatives, consequences, and verification contracts. Follow the [documentation standard](../../docs/AGENTS.md) and the [Agent Note contract](README.md). +Agent Notes are effectively RFCs written by agents: durable proposals and decision records that preserve rationale, alternatives, consequences, and required verification. Follow the [documentation standard](../../docs/AGENTS.md) and the [Agent Note rules](README.md). **Every new Agent Note triggers a supersession check.** Search the active tree for older notes covering the same decision or mechanism, classify any full or partial supersession with [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md), and archive every qualifying implemented triplet in the same PR. Keep partial supersessions active and cross-linked. diff --git a/.agents/notes/README.i18n.yaml b/.agents/notes/README.i18n.yaml index 56eeb36b4d..cd767c17bc 100644 --- a/.agents/notes/README.i18n.yaml +++ b/.agents/notes/README.i18n.yaml @@ -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 .agents/notes/README.md -README.md: d3a8943a78238d974d54028e38b773e932429b0e -README.zh.md: 4b3a1ee57ea61a8e8ba4d01cf7c719bbf8440e30 +README.md: ae8e4724d610c97d74910d7dec5c95af69e93281 +README.zh.md: d9989dd1d722185145099b706169dfce19559b52 diff --git a/.agents/notes/README.md b/.agents/notes/README.md index d3a8943a78..ae8e4724d6 100644 --- a/.agents/notes/README.md +++ b/.agents/notes/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the entry point and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format). +One kind of design doc lives here. An **Agent Note** records a decision or proposal that affects this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file defines where Agent Notes live, when to write one, and [the in-file format](#the-file-format). ## Layout and naming @@ -43,13 +43,13 @@ Once sealed, every archived triplet is permanently frozen. Do not edit, translat ## When to write one -Every non-trivial change MUST add or update at least one Agent Note in the same PR. A change is non-trivial when it alters behavior, architecture, a cross-file or cross-package contract, process or tooling, testing strategy, an on-disk, wire, or configuration format, or another decision a maintainer may reasonably revisit. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)). +Every non-trivial change MUST add or update at least one Agent Note in the same PR. A change is non-trivial when it alters behavior, architecture, a contract shared across files or packages, process or tooling, testing strategy, an on-disk, wire, or configuration format, or another decision a maintainer may reasonably revisit. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)). -Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Only a purely mechanical or local edit with no behavioral, contractual, structural, process, or rationale change is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one, and keep both notes cross-linked unless the old note is later fully consolidated under the rule below. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md). +Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Only a purely mechanical or local edit with no change to behavior, contracts, structure, process, or rationale is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one, and keep both notes cross-linked unless the old note is later fully consolidated under the rule below. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md). -An implemented Agent Note that is fully superseded may be consolidated into the current owning note and deleted. Before deletion, the owner must preserve every unique rationale, alternative, consequence, verification contract, and named coverage gap; repair every inbound link; and delete the Chinese counterpart and consistency record in the same change. Partial supersession does not qualify: keep both notes cross-linked and update every fact that remains current. Consolidation must not rewrite the old file into its opposite or rely on git history as the only copy of rationale. +An implemented Agent Note that is fully superseded may be consolidated into the current owning note and deleted. Before deletion, the owner must preserve every unique rationale, alternative, consequence, required verification, and named coverage gap; repair every inbound link; and delete the Chinese counterpart and consistency record in the same change. Partial supersession does not qualify: keep both notes cross-linked and update every fact that remains current. Consolidation must not rewrite the old file into its opposite or rely on git history as the only copy of rationale. -A feature-addition note may be consolidated into the later removal note only when the feature is absent from production code, configuration, schemas, durable or wire formats, migration, and compatibility behavior; no current documentation presents it as available; and no test exercises it as supported behavior. Removal rationale and tests that verify absence may remain. The removal owner preserves the original motivation, why it no longer justified the feature, alternatives to full removal, the capability given up, conditions for reintroduction, and verification of complete absence. Obsolete implementation inventories and tests that only verified the deleted behavior are not current verification contracts. Removing one transport, default, implementation, or presentation is partial supersession, as is any surviving durable data or compatibility handling. +A feature-addition note may be consolidated into the later removal note only when the feature is absent from production code, configuration, schemas, durable or wire formats, migration, and compatibility behavior; no current documentation presents it as available; and no test exercises it as supported behavior. Removal rationale and tests that verify absence may remain. The removal owner preserves the original motivation, why it no longer justified the feature, alternatives to full removal, the capability given up, conditions for reintroduction, and verification of complete absence. Obsolete implementation inventories and tests that only verified the deleted behavior are not current verification evidence. Removing one transport, default, implementation, or presentation is partial supersession, as is any surviving durable data or compatibility handling. ## The file format @@ -122,4 +122,4 @@ Moving a file between lifecycle folders means updating the `Status:` line and re ### Chinese counterparts -A `.zh.md` counterpart mirrors its English sibling's structure section-for-section under the [i18n contract](../../docs/i18n/README.md); the machine-checked header tokens (`# Agent Note: ` and the `Status:` line) stay in English verbatim. The format gate skips `.zh.md` files — the pairing gate owns their consistency. +A `.zh.md` counterpart mirrors its English sibling's structure section-for-section under the [i18n contract](../../docs/i18n/README.md); the machine-checked header tokens (`# Agent Note: ` and the `Status:` line) stay in English verbatim. The format gate skips `.zh.md` files — the pairing gate checks their consistency. diff --git a/.agents/notes/README.zh.md b/.agents/notes/README.zh.md index 4b3a1ee57e..d9989dd1d7 100644 --- a/.agents/notes/README.zh.md +++ b/.agents/notes/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -这里存放一类设计文档。**Agent Note** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件是入口和约定:Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。 +这里存放一类设计文档。**Agent Note** 记录影响本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件规定 Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。 ## 布局与命名 @@ -49,9 +49,9 @@ 更新已经拥有该决策的 Agent Note 即可满足规则;不要创建重复记录。只有不涉及行为、约定、结构、流程或理由变化的纯机械性或局部编辑才可豁免。Agent Note 永远不会被编辑为一个*不同的决策*:用新 Agent Note 取代旧记录,并让两个记录保持互相链接,除非后续依据下方规则完全合并旧记录。编辑 `implemented/` Agent Note 以跟踪其现有决策的所在位置是必需的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。 -被完全取代的 implemented Agent Note 可以合并到当前持有该决策的记录中,并删除原文件。删除前,当前记录必须保存所有独有的决策依据、备选方案、影响、验证约定和明确指出的覆盖缺口;修复所有入站链接;并在同一变更中删除中文对侧文件和一致性记录。仅部分被取代的记录不符合此条件:保留两个记录并让它们互相链接,同时更新所有仍然适用的事实。合并不得将旧文件改写成与其相反的决策,也不得让 git 历史成为决策依据的唯一副本。 +被完全取代的 implemented Agent Note 可以合并到当前持有该决策的记录中,并删除原文件。删除前,当前记录必须保存所有独有的决策依据、备选方案、影响、必需的验证和明确指出的覆盖缺口;修复所有入站链接;并在同一变更中删除中文对侧文件和一致性记录。仅部分被取代的记录不符合此条件:保留两个记录并让它们互相链接,同时更新所有仍然适用的事实。合并不得将旧文件改写成与其相反的决策,也不得让 git 历史成为决策依据的唯一副本。 -只有当一项功能已从生产代码、配置、schema、持久化格式或协议格式、迁移和兼容行为中完全消失,当前文档不再将其描述为可用,且没有测试把它作为受支持行为来执行时,新增该功能的 Agent Note 才可合并进后续的移除记录。移除决策的依据和验证该功能已不存在的测试可以保留。移除决策的持有记录必须保留最初动机、为什么该动机已不足以证明保留该功能的合理性、完全移除之外的备选方案、放弃的能力、重新引入的条件,以及证明已彻底移除的验证。过时的实现清单和只验证已删除行为的测试不属于当前验证约定。仅移除一种传输、默认值、实现或展示属于部分取代;仍有任何持久数据或兼容处理也同样如此。 +只有当一项功能已从生产代码、配置、schema、持久化格式或协议格式、迁移和兼容行为中完全消失,当前文档不再将其描述为可用,且没有测试把它作为受支持行为来执行时,新增该功能的 Agent Note 才可合并进后续的移除记录。移除决策的依据和验证该功能已不存在的测试可以保留。移除决策的持有记录必须保留最初动机、为什么该动机已不足以证明保留该功能的合理性、完全移除之外的备选方案、放弃的能力、重新引入的条件,以及证明已彻底移除的验证。过时的实现清单和只验证已删除行为的测试不属于当前验证证据。仅移除一种传输、默认值、实现或展示属于部分取代;仍有任何持久数据或兼容处理也同样如此。 @@ -126,4 +126,4 @@ Status: ### 中文对侧文件 -`.zh.md` 对侧文件按 [i18n 约定](../../docs/i18n/README.md)逐章节镜像其英文对侧文件的结构;机器检查的头部标记(`# Agent Note: ` 和 `Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件——配对门禁负责它们的一致性。 +`.zh.md` 对侧文件按 [i18n 约定](../../docs/i18n/README.md)逐章节与其英文对侧文件保持相同结构;机器检查的头部标记(`# Agent Note: ` 和 `Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件;配对门禁检查它们的一致性。 diff --git a/.agents/notes/implemented/AGENTS.md b/.agents/notes/implemented/AGENTS.md index b8da5dc8ef..5a22674a96 100644 --- a/.agents/notes/implemented/AGENTS.md +++ b/.agents/notes/implemented/AGENTS.md @@ -10,4 +10,4 @@ When a shipped note is unlikely to guide future work, archive its complete tripl ### This is not a license to rewrite the *decision* -Update factual realization in place. A reversal of the decision or its rationale requires a new Agent Note and cross-link; a fully superseded old note may be deleted only through the consolidation rule in the [Agent Note contract](../README.md). +Update factual realization in place. A reversal of the decision or its rationale requires a new Agent Note and cross-link; a fully superseded old note may be deleted only through the consolidation rule in the [Agent Note rules](../README.md). diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml index d47c50cdb7..c7be4bbdf1 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md -2026-07-19-gui-layering-and-rpc-protocol.md: 705b0df5feb5fedaae4d198aed71758b54586e93 -2026-07-19-gui-layering-and-rpc-protocol.zh.md: b28b08b4b9da058e01af62e610d4e226d794151f +2026-07-19-gui-layering-and-rpc-protocol.md: f9c95176321496e965a95b6358d6feaa8466fe89 +2026-07-19-gui-layering-and-rpc-protocol.zh.md: 7d20c5a2662c9036382b30a96bc9973c8f0349bd diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md index 705b0df5fe..f9c9517632 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md @@ -8,12 +8,12 @@ English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md) ## Problem -We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product UI shapes are coming — Web (server), Electron, and others. We call these shapes Clients, uniformly, and want the following capabilities: +We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product clients are coming — Web (server), Electron, and others. We call them Clients and want the following capabilities: - One `dsh` process supporting both `dsh web` (serve) and `dsh run` (headless) — one process, two modes (a design reservation) -- Launching inside Electron with the same Web technology shape as `dsh web` +- Launching inside Electron with the same Web technologies as `dsh web` -That demands a stable layered responsibility model in the engineering codebase, so future client shapes plug in cleanly. +That demands a stable layered responsibility model in the engineering codebase, so future clients plug in cleanly. At the same time the physical channels differ per consumer (browser HTTP/WebSocket, in-process fetch/SSE, IPC later), so we also need a channel-independent message model and a single contract source of truth — "adding a method" and "swapping a carrier" must not entangle each other, and every message on the wire must be type-validatable, observable, and reconcilable. @@ -29,19 +29,19 @@ Directories layer as follows: - **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table. - **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dshClient` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else. - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services. -- `apps/` holds the externally exported application shapes, assembled from Client / Host mixtures. +- `apps/` holds the externally exported applications, assembled from Client / Host mixtures. - `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`. - - `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer. - - A future Electron shape reuses the same web client packages over an IPC fetch carrier. + - `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer. + - A future Electron application reuses the same web client packages over an IPC fetch carrier. ``` -apps/* (application shapes: apps/web = vite app, apps/cli = bin dispatch) +apps/* (applications: apps/web = vite app, apps/cli = bin dispatch) │ consume ▼ packages/host/* packages/client/* apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives runtime assembly / host entity dshClient plugins ×8 (node half = empty apply, - webserver web-shape HTTP carriage client half = src/client/) + webserver Web HTTP carriage client half = src/client/) │ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths ▼ │ (type-only + the client base class) harness core packages ──────────────────┘ (types reach the browser via import type) @@ -51,12 +51,12 @@ Direction discipline (every rule auditable from package deps): - `runtime → apiproxy` is one-way; apiproxy depends only on type definitions. - Client-side packages **never import** host-side package runtime (they consume only the two browser-safe subpaths `/api` and `/client`). -- `webserver` does not depend on `runtime`: it provides a `{ fetch }`-shaped implementation — "webserver ← runtime" is a runtime injection relationship, not a package dependency. +- `webserver` does not depend on `runtime`: it provides an implementation of the `{ fetch }` interface — "webserver ← runtime" is a runtime injection relationship, not a package dependency. - Cross-package client imports use the `/client` subpath for plugin packages, and between plugin packages they are type-only — a cross-plugin value import is a build error at the tsdown purity gate (value cooperation goes through cordis services; the [client plugin loading note](2026-07-23-client-plugin-loading-model.md) owns the edge rules). TypeScript checks in **two aggregate programs** referenced by a solution root (`tsconfig.json` = solution; `tsconfig.host.json` = host side + tests, excluding `packages/client`; `tsconfig.client.json` = client packages and their tests): both sides merge the cordis `Context` interface under the same keys (`sessions`, `loader`) with different services, so one program would see both declaration merges and report a collision. Shared leaves (session/llm/tools/apiproxy…) build once and are referenced by both programs ([topology](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md)). -On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Node dependencies, browser-importable); wire messages unify under a **bidirectional model** — each logical message is shaped by "who initiates × request/response" (two axes, four cells, called the four quadrants below), decoupled from the physical channel; clients all inherit `AbstractApiClient` (protocol invariants live entirely in the base class, platform differences are just the `doFetch` transport aspect). +On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Node dependencies, browser-importable); wire messages unify under a **bidirectional model** — each logical message is classified by "who initiates × request/response" (two axes, four cells, called the four quadrants below), decoupled from the physical channel; clients all inherit `AbstractApiClient` (protocol invariants live entirely in the base class, platform differences are just the `doFetch` transport aspect). #### Layer roles @@ -64,22 +64,22 @@ On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Nod |---|---|---|---| | Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx | | Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dshClient packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly | -| Carrier layer | `dsh-host-webserver` | Web-shape HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it | +| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it | | Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell | | Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy | -| Application shape | `@deepseek-ai/dsh` (apps/cli) + `dsh-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per shape (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Shapes dynamic-import so they never load each other; workspace knowledge like dist location stays in the app | +| Application | `@deepseek-ai/dsh` (apps/cli) + `dsh-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per application (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Applications use dynamic imports so they never load each other; workspace knowledge like dist location stays in the app | #### Naming rule Packages under `packages/host/*` and `packages/client/*` **must carry the directory-group prefix in the package name**: host/runtime → `dsh-host-runtime`, client/runtime → `dsh-client-runtime`. The directory name does not repeat the group prefix (host/ already expresses it). The package-name tail therefore ≠ the directory name, so the `dsh-*` wildcard in tsconfig.base.json (which resolves by directory name) misses them — **each package in these two groups needs an explicit paths entry**, including separate entries for the client packages' `/client` subpaths so source-level resolution matches the exports map. -#### How to integrate a new shape (operational checklist) +#### How to integrate a new application (operational checklist) 1. **Pick a fetch impersonation**: browser same-origin HTTP / in-process `host.handler.fetch` injection / your own transport-aspect subclass (e.g. future Electron IPC, see the "Subclass table" below). -2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the shape's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app. +2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the application's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app. 3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports. -The two existing shapes preserve the boundary: the Web shape mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem, mount via `ctx.plugin(entry-point plugin)` directly, and wear no fetch. +The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch. ## Message protocol @@ -169,7 +169,7 @@ The remaining methods (`session.create`/`session.history`/`session.rename`/`sess ### Frames (server→client, named unions) -Two logical streams: the mux stream (`/api/events.mux`, all-session aggregate) and the host stream (`/api/events.host`, host-level events). The browser consumes one downlink WebSocket per stream, while the in-process fetch carrier retains SSE to preserve the same shape; see the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) for the physical boundary. One example frame row: +Two logical streams: the mux stream (`/api/events.mux`, all-session aggregate) and the host stream (`/api/events.host`, host-level events). The browser consumes one downlink WebSocket per stream, while the in-process fetch carrier retains SSE with the same event framing; see the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) for the physical boundary. One example frame row: | frame type | payload | when | |---|---|---| @@ -216,7 +216,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation | Subclass | Package | doFetch | Purpose | |---|---|---|---| | `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh run` drives core directly | -| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser shape; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) | +| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser client; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) | | `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) | | IPC bridge subclass (hypothetical example — no such shell exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged | @@ -234,15 +234,15 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation ## Consequences -Every client shape consumes one contract: adding a unary method is a five-step mechanical change radiating from a single signature, swapping a carrier touches only a `doFetch` subclass, and every wire message is zod-validated, observable through the envelope tap, and reconcilable by rpcId. Ordinary unary calls remain bounded, while `host.pickDirectory` and `command.execute` may stay pending until the operation finishes or caller/connection cancellation arrives; this accepts that a non-cooperative user-paced operation can hang its request rather than treating valid operation duration as transport failure. The other accepted costs: two groups of packages need explicit tsconfig paths entries, and the reserved methods (fork/inject/task.list/listModels/hostInstanceId) stay dormant until a real consumer arrives. +Every client consumes one contract: adding a unary method is a five-step mechanical change from a single signature, swapping a carrier touches only a `doFetch` subclass, and every wire message is zod-validated, observable through the envelope tap, and reconcilable by rpcId. Ordinary unary calls remain bounded, while `host.pickDirectory` and `command.execute` may stay pending until the operation finishes or caller/connection cancellation arrives; this accepts that a non-cooperative user-paced operation can hang its request rather than treating valid operation duration as transport failure. The other accepted costs: two groups of packages need explicit tsconfig paths entries, and the reserved methods (fork/inject/task.list/listModels/hostInstanceId) stay dormant until a real consumer arrives. ## Alternatives considered | Rejected | One-line reason | |---|---| -| Packaging by "product shape" (a web family, an electron family) | What shapes share is host/client capability, not the shape itself; capability-provider layering means a new shape needs zero new packages | +| Packaging by product (a web family, an electron family) | Products share host/client capabilities rather than an application implementation; capability-provider layering means a new application needs zero new packages | | A package per mixture (e.g. a standalone headless package) | A mixture has exactly one consumer (its own app); packaging it is ownerless abstraction, while assembly in the app is readable and disposable | -| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Client shapes require wire validation, observability, and multi-client consistency. Direct headless is a local entry point with no client boundary and uses the public Agent/Session seams rather than a client command plane | +| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Clients require wire validation, observability, and multi-client consistency. Direct headless is a local entry point with no client boundary and uses the public Agent/Session seams rather than a client command plane | | webserver depending on runtime (saving the handler injection) | Structural-typing injection keeps webserver reusable by sidecars/tests with zero workspace deps; a package dependency would drag assembly knowledge into the carrier layer | | Package names without the group prefix (continuing dsh-) | `dsh-runtime`/`dsh-web-ui` lose their belonging in the flat npm namespace; the cost is one explicit paths entry per package | | Reusing the in-repo JSON-RPC 2.0 (dsh-jsonrpc) | Numeric error codes degrade to a single fallback code, contracts get aligned by hand in two copies, and naming drifts without a convention | diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md index b28b08b4b9..7d20c5a266 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md @@ -8,11 +8,11 @@ Status: implemented ## Problem -需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品 UI 形态。我们把这些形态统一称为 Client。希望具备以下能力: +需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品客户端。我们把它们统一称为 Client。希望具备以下能力: - 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh run`(headless),一个进程两种模式(设计预留) -- 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动 +- 在 Electron 中使用与 `dsh web` 相同的 Web 技术启动 -那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client 形态。 +那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client。 同时各消费端的物理通道不同(浏览器 HTTP/WebSocket、进程内 fetch/SSE、将来 IPC),还需要一个通道无关的消息模型和单一约定事实源,让「加一个方法」「换一种载体」互不牵连,且 wire 上的每条消息可类型校验、可观测、可对账。 @@ -27,19 +27,19 @@ Status: implemented - **纯库**(`ui-slots`、`web-react`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。 - **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dshClient` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。 - **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。 -- `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。 +- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。 - `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`。 - - `apps/cli`(`@deepseek-ai/dsh`)做形态分发:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。 - - 将来的 Electron 形态经由 IPC fetch 载体复用同一套 web client 包。 + - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。 + - 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。 ``` -apps/* (application shapes: apps/web = vite app, apps/cli = bin dispatch) +apps/* (applications: apps/web = vite app, apps/cli = bin dispatch) │ consume ▼ packages/host/* packages/client/* apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives runtime assembly / host entity dshClient plugins ×8 (node half = empty apply, - webserver web-shape HTTP carriage client half = src/client/) + webserver Web HTTP carriage client half = src/client/) │ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths ▼ │ (type-only + the client base class) harness core packages ──────────────────┘ (types reach the browser via import type) @@ -54,7 +54,7 @@ harness core packages ──────────────────┘ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.json` = solution;`tsconfig.host.json` = host 侧 + 测试,排除 `packages/client`;`tsconfig.client.json` = client 各包及其测试):两侧在相同键(`sessions`、`loader`)下以不同服务合并 cordis `Context` 接口,单一 program 会同时看到两份声明合并而报冲突。共享叶子包(session/llm/tools/apiproxy 等)只构建一次,由两个 program 共同引用([拓扑](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md))。 -协议侧:TS interface(`packages/host/apiproxy/src/api/`,零 Node 依赖,浏览器可 import);wire 消息统一为**双向模型**——每条逻辑消息由「谁发起 × request/response」定形(两轴四格,后文称四象限),与物理通道解耦;客户端统一继承 `AbstractApiClient`(协议不变量全在基类,平台差异只是 `doFetch` 传输切面)。 +协议侧:TS interface(`packages/host/apiproxy/src/api/`,零 Node 依赖,浏览器可 import);wire 消息统一为**双向模型**——每条逻辑消息按「谁发起 × request/response」分类(两轴四格,后文称四象限),与物理通道解耦;客户端统一继承 `AbstractApiClient`(协议不变量全在基类,平台差异只是 `doFetch` 传输切面)。 #### 分层角色 @@ -62,22 +62,22 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig. |---|---|---|---| | 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 做简单、所有接入方都要;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api | | 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dshClient 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 | -| 承载层 | `dsh-host-webserver` | Web 形态 HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 | +| 承载层 | `dsh-host-webserver` | Web HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 | | client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 | | client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费者、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy | -| 应用态 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-frontend`(apps/web,vite 应用) | bin 粗分发 + 每形态一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 形态间动态 import 互不加载;dist 定位等 workspace 知识留在 app | +| 应用 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-frontend`(apps/web,vite 应用) | bin 粗分发 + 每个应用一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 各应用使用动态 import,因此不会互相加载;dist 定位等 workspace 知识留在 app | #### 命名规则 `packages/host/*` 与 `packages/client/*` 下的包名**必须含目录组前缀**:host/runtime → `dsh-host-runtime`、client/runtime → `dsh-client-runtime`。目录名不重复组前缀(host/ 已表达)。因此包名尾段 ≠ 目录名,tsconfig.base.json 的 `dsh-*` 通配(按目录名解析)命不中——**这两组的每包需显式 paths 条目**,且 client 各包的 `/client` 子路径要单列条目,使源码级解析与 exports map 一致。 -#### 怎么接入一个新形态(操作清单) +#### 怎么接入一个新应用(操作清单) 1. **选 fetch 伪造方式**:浏览器同源 HTTP / 进程内 `host.handler.fetch` 注入 / 自写传输切面子类(如将来 Electron IPC,见下文「子类表」)。 -2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该形态私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。 +2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该应用私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。 3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。 -现有两种形态保持这一边界:Web 形态挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不套 fetch。 +现有两个应用保持这一区分:Web 应用挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。 ## 消息协议 @@ -214,7 +214,7 @@ export type ResponseValue = | 子类 | 所在包 | doFetch | 用途 | |---|---|---|---| | `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh run` 直接驱动 core | -| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器形态;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) | +| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器客户端;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) | | `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) | | IPC 桥子类(假想示例——尚无此形态) | Electron 壳 | IPC 序列化往返 | 只需换 doFetch,约定/基类零改 | @@ -232,15 +232,15 @@ export type ResponseValue = ## Consequences -所有 client 形态消费同一约定:加一个 unary 方法是从单一签名辐射的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留方法(fork/inject/task.list/listModels/hostInstanceId)在真实消费者出现前保持休眠。 +所有 client 使用同一约定:加一个 unary 方法是从单一签名出发的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留方法(fork/inject/task.list/listModels/hostInstanceId)在真实消费者出现前保持休眠。 ## Alternatives considered | 放弃项 | 一句话理由 | |---|---| -| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力支持方分层让新形态零新包 | +| 按产品分包(web 一族、electron 一族) | 产品共享的是 host/client 两侧能力,而不是某个应用实现;能力支持方分层让新应用零新包 | | 混合体建包(如 headless 独立包) | 混合体只有一个消费者(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 | -| 消费型 client 直连 ctx(省 apiproxy 一层) | client 形态需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地入口,使用公开的 Agent/Session seam,而不是 client 命令面 | +| 消费型 client 直连 ctx(省 apiproxy 一层) | client 需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地入口,使用公开的 Agent/Session seam,而不是 client 命令面 | | webserver 依赖 runtime(省 handler 注入) | 结构 typing 注入让 webserver 可被 sidecar/测试复用且零 workspace 依赖;包依赖会把装配知识拖进承载层 | | 包名不带组前缀(沿用 dsh-<尾段>) | `dsh-runtime`/`dsh-web-ui` 在扁平 npm 命名空间里失去归属信息;代价只是每包一条显式 paths | | 复用仓内 JSON-RPC 2.0(dsh-jsonrpc) | 数字错误码退化成单码兜底、约定双份人肉对齐、命名无 convention 自然漂移 | diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml index 8979270e36..0b2df8cc51 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md -2026-08-04-configuration-source-ownership.md: d656d329cbc5b3dfbae2561775ed1afc878cebd0 -2026-08-04-configuration-source-ownership.zh.md: 29ef3b83d18d8836b28e4c151ad44c52542b0a11 +2026-08-04-configuration-source-ownership.md: 8ec750de2efaf148fe44a58b415d7da44b87bdc6 +2026-08-04-configuration-source-ownership.zh.md: 64daa54843d55c6f4bde5a8fdece66dbbe835479 diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md index d656d329cb..8ec750de2e 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md +++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md @@ -60,7 +60,7 @@ The line is that these take effect with no user action, before any turn, outside ## Alternatives considered -**Unify credentials into the non-secret ordering, by who authored each source.** Attempted and abandoned: it reads well, but the settings seam already fixes composition *below* the user section, so "authored by deployment" is not a tier the seam can express — and moving `.credentials.yaml` above the launching environment would take away the one override CI, containers, and a per-run `DEEPSEEK_API_KEY=…` depend on. Two orderings that each say why they are shaped that way beat one that describes neither accurately. +**Unify credentials into the non-secret ordering, by who authored each source.** Attempted and abandoned: it reads well, but the settings seam already fixes composition *below* the user section, so "authored by deployment" is not a tier the seam can express — and moving `.credentials.yaml` above the launching environment would take away the one override CI, containers, and a per-run `DEEPSEEK_API_KEY=…` depend on. Two orderings that each explain their precedence beat one that describes neither accurately. **Withhold routing and credentials from the invoking project until it is explicitly trusted.** Rejected as the product's stance: a checkout is trusted by default, with no prompt and no stored trust record. The residual is real and worth naming — cloning a repository that carries a `.env` naming another endpoint or key routes that session through it — and a later project-trust gate is where that gets addressed, not a rule that makes the common case require ceremony. diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md index 29ef3b83d1..64daa54843 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md @@ -62,7 +62,7 @@ inherited process environment (read-only, wins) ## Alternatives considered -**按「来源由谁书写」把凭据并入非机密顺序。** 尝试过并放弃:它读起来很顺,但 settings seam 已经把 composition 固定在用户 section *之下*,因此「由部署方写入」根本不是该 seam 能表达的一层;而把 `.credentials.yaml` 抬到启动环境之上,会夺走 CI、容器和一次性 `DEEPSEEK_API_KEY=…` 所依赖的那唯一一种覆盖。两条各自说清自身形状成因的顺序,好过一条两边都描述不准的顺序。 +**按「来源由谁书写」把凭据并入非机密顺序。** 尝试过并放弃:它读起来很顺,但 settings seam 已经把 composition 固定在用户 section *之下*,因此「由部署方写入」根本不是该 seam 能表达的一层;而把 `.credentials.yaml` 抬到启动环境之上,会夺走 CI、容器和一次性 `DEEPSEEK_API_KEY=…` 所依赖的那唯一一种覆盖。两条各自说明优先顺序的规则,好过一条两边都描述不准的规则。 **在项目被显式信任之前,不给它路由与凭据能力。** 作为产品立场被否决:checkout 默认可信,不询问,也不存储信任记录。残留风险是真实的、值得写明——克隆一个携带 `.env`、其中指定了另一个 endpoint 或密钥的仓库,会让该会话经由它——处理它的地方是日后的 project trust 门禁,而不是一条让常见情形都要走仪式的规则。 diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml index a334bdc7d5..f8cf184ea9 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-06-15-code-mode.md -2026-06-15-code-mode.md: 5ad7be9506c2356f90050b86d85aa77f602946ce -2026-06-15-code-mode.zh.md: 1902c6ba86b6b8d3fecd06c417347a9c082f67b4 +2026-06-15-code-mode.md: 51c53c56f5755d56f49d8e5166a1ceeef0ffc202 +2026-06-15-code-mode.zh.md: dbf8d409dea152b39d215f5dd636989c3f4fa0bb diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.md index 5ad7be9506..51c53c56f5 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.md @@ -18,7 +18,7 @@ Tool presentation belongs to the registry that owns tool visibility: implementin Three decisions, each elaborated in its own section below: -1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation. +1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation. 2. **Code execution is a capability seam** — `packages/code-runtime/` contains the Service Definition package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service provider package, not a redesign. 3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority. @@ -120,11 +120,11 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem **The worker is not a hard security boundary.** Deliberate and documented (§Trust posture): posture equals the existing bash tool, containment exceeds it, gating uses the same approval and sandbox policies. Deployments needing more need a future `isolation: 'container'` backend — tracked as the seam's designed extension, not a TODO on this design. -**`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite pins the behaviors relied on (position preservation, erasable-only rejection message shape loosely), the call sits behind one private function, and `amaro`/`sucrase` are drop-in replacements if the API shifts. The erasable-only subset is a model-facing contract line, and the error path is a working feedback loop, not a dead end. +**`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite checks position preservation and the required parts of the erasable-only rejection message, the call sits behind one private function, and `amaro`/`sucrase` are direct replacements if the API shifts. The erasable-only subset is a model-facing input restriction, and the error tells the model how to correct the program. **Prompt cost of the SDK, especially under `'both'`.** The `.d.ts` can rival the native schemas it complements; `'both'` carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning. -**Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Contained by module boundaries inside the package (`ts-types.ts`, `code-mode.ts` beside `schema.ts`/`json-schema.ts`/`presentation.ts`) and by the seam: everything substrate-shaped lives behind `ctx.codeRuntime`. +**Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Package modules separate these responsibilities (`ts-types.ts` and `code-mode.ts` beside `schema.ts`, `json-schema.ts`, and `presentation.ts`), while `ctx.codeRuntime` owns all code-runtime-specific implementation. **Large lossless JSON values can exhaust memory.** Tool bindings snapshot lossless JSON before dispatch and return canonical JSON resolutions whole. The runtime validates both sides of the worker port and applies no per-binding byte cap; structured-clone cost and process or worker memory are the practical bounds. The combined outer-output ledger for logs, the completion value, and a failure diagnostic is the only byte-capped boundary. diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md index 1902c6ba86..dbf8d409de 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md @@ -18,7 +18,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一 三项决策,各自在下方独立小节中展开: -1. **Code Mode 是 `ToolRegistry`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头塑造其权威贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。 +1. **Code Mode 是 `ToolRegistry`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头构建其权威贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。 2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.md);Consumer = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service provider 包,而非重新设计。 3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker`**:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格*更高*的环境权限执行模型编写的任意 shell 命令。 @@ -120,11 +120,11 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认 **Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,约束能力强于它,门禁使用相同的审批与沙箱策略。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计的扩展点跟踪,而非本设计的 TODO。 -**`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件固定了所依赖的行为(位置保持、可擦除限制的拒绝消息形状宽松匹配),调用位于一个私有函数之后,且 `amaro`/`sucrase` 是 API 变化时的直接替代品。仅可擦除子集是面向模型的约定线,错误路径是一个可工作的反馈循环,而非死胡同。 +**`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件会检查位置保持和可擦除限制拒绝消息中的必需部分;调用位于一个私有函数之后,且 `amaro`/`sucrase` 可在 API 变化时直接替换它。仅可擦除子集是面向模型的输入限制,错误消息会告诉模型如何修正程序。 **SDK 的提示词成本,尤其在 `'both'` 下。** `.d.ts` 可能与它补充的原生 schema 体量相当;`'both'` 携带两种表示。前缀稳定性 + 提供方缓存摊销了每会话成本;mode 是每部署的;本 Agent Note 不做无条件节省的声明。何时优先使用哪种模式的量化指导明确属于上线后学习。 -**注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。包内的模块边界(`ts-types.ts`、`code-mode.ts` 与 `schema.ts`/`json-schema.ts`/`presentation.ts` 并列)和 seam 共同约束了这一增长:所有基底相关的内容都在 `ctx.codeRuntime` 之后。 +**注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。包内模块把这些职责分开(`ts-types.ts`、`code-mode.ts` 与 `schema.ts`、`json-schema.ts`、`presentation.ts` 并列),所有 code-runtime 专用实现都由 `ctx.codeRuntime` 提供。 **大型无损 JSON 值可能耗尽内存。** 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 返回值。运行时会校验 worker 端口两侧,但不对单次绑定设置字节数上限;结构化克隆成本以及进程或 worker 内存构成实际边界。只有包含日志、完成值和失败诊断的组合外层输出账本受字节数上限约束。 diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml index c68db1d6fd..57e84b91ff 100644 --- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md -2026-06-21-subagent-capability-seam.md: 28ecf985db1289ce4f80570583eac6a051cd2c0b -2026-06-21-subagent-capability-seam.zh.md: 5f349ddcacebe66c346976b822c9dcc65bda8d18 +2026-06-21-subagent-capability-seam.md: 2b9ea93c40bd4b3547cfda83c3ad0bd52c047792 +2026-06-21-subagent-capability-seam.zh.md: 8b647fa51511e1c4cac55c3a9ea39b7e059bc381 diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md index 28ecf985db..2b9ea93c40 100644 --- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md @@ -10,7 +10,7 @@ English | [中文](2026-06-21-subagent-capability-seam.zh.md) The harness has a long-deferred seam for **subagents** — an agent delegating work to another agent. The intent was sketched in the `Agent`/`AgentLoop` interfaces ([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts), [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)): a creation option referencing a parent agent (fork = seed the child session with the parent's event log; spawn = fresh session), with the child returned as an `Agent` handle so steering and event subscription work uniformly. -The distinctive requirement — the one that shapes the whole design — is that **multiple subagent implementations must coexist at runtime**. A parent may want a cheap in-process child for a scoped subtask AND an isolated out-of-process child (over ACP) in the same session. The transports: +**Multiple subagent implementations must coexist at runtime.** A parent may want a cheap in-process child for a scoped subtask AND an isolated out-of-process child (over ACP) in the same session. The transports: - **in-process** — a child concrete `Agent` on the same `Context` (the cheapest, and nearly free given the existing agent factory); - **ACP** — act as an ACP *client* driving another agent process (which can be another instance of ourselves); @@ -66,7 +66,7 @@ Each in-process subagent runs in its **own `Session`** (own id, `parentSession` ## Testing -Registry and tool tests replace only the nondeterministic child boundary with a package-local scripted provider while exercising the real `SubagentService`, lifecycle, task integration, and model-facing tool. Provider and consumer export shapes retain their Loader regression coverage for the failure described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md). Registry tests cover reload safety, duplicate names, and start-time capability rejection; nested-agent scenarios replay keylessly through [per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md); in-process backends also have real-loop unit tests and a with-key e2e. +Registry and tool tests replace only the nondeterministic child with a package-local scripted provider while exercising the real `SubagentService`, lifecycle, task integration, and model-facing tool. Loader regression tests still cover the provider and consumer exports for the failure described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md). Registry tests cover reload safety, duplicate names, and start-time capability rejection; nested-agent scenarios replay keylessly through [per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md); in-process backends also have real-loop unit tests and a with-key e2e. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md index 5f349ddcac..8b647fa515 100644 --- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md @@ -10,7 +10,7 @@ Status: implemented harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent 将工作委派给另一个 agent。这一意图在 `Agent`/`AgentLoop` 接口中已有草案([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts)、[packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)):一个创建选项引用父 agent(fork = 用父会话的事件日志初始化子会话;spawn = 全新会话),子 agent 以 `Agent` 句柄返回,使 steering(中途引导)和事件订阅可以统一工作。 -决定整体设计走向的核心需求是:**多种 subagent 实现必须在运行时共存**。一个父 agent 可能在同一个会话中既需要一个廉价的进程内子 agent 处理有限范围的子任务,又需要一个隔离的进程外子 agent(通过 ACP)。传输方式: +**多种 subagent 实现必须在运行时共存。**一个父 agent 可能在同一个会话中既需要一个廉价的进程内子 agent 处理有限范围的子任务,又需要一个隔离的进程外子 agent(通过 ACP)。传输方式: - **进程内**:在同一个 `Context` 上创建一个具体的子 `Agent`(最廉价,且鉴于现有 agent 工厂几乎零成本); - **ACP**:作为 ACP *客户端*驱动另一个 agent 进程(可以是自身的另一个实例); @@ -66,7 +66,7 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在 ## 测试 -注册表与工具测试仅用包内脚本化提供方替换非确定性的子进程边界,同时运行真实的 `SubagentService`、生命周期、任务集成和面向模型的工具。提供方与消费方的 export 形状仍保留 Loader 回归覆盖,以防止[事故复盘(postmortem)0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) 中描述的失败。注册表测试覆盖重载安全性、重名和启动时能力拒绝;嵌套 agent 场景通过[逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md)进行无密钥回放;进程内后端还有真实循环的单元测试和带密钥的 e2e 测试。 +注册表与工具测试仅用包内脚本化提供方替换非确定性的子 agent,同时运行真实的 `SubagentService`、生命周期、任务集成和面向模型的工具。Loader 回归测试仍覆盖提供方与消费方的 export,以防止[事故复盘(postmortem)0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)中描述的失败。注册表测试覆盖重载安全性、重名和启动时能力拒绝;嵌套 agent 场景通过[逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md)进行无密钥回放;进程内后端还有真实循环的单元测试和带密钥的 e2e 测试。 ## 后果 diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml index 96e76345cc..c9ace8d3d4 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-06-30-hook-bridges.md -2026-06-30-hook-bridges.md: eecc1fc75c10618481231083f890e0e4b122c1ee -2026-06-30-hook-bridges.zh.md: dcc8f496cb3bc6ea7857ce804a9159cbfcbc3f08 +2026-06-30-hook-bridges.md: 0dce552820186755e9243bc5ce1dd3361e2e739d +2026-06-30-hook-bridges.zh.md: 2df3a47fd6219e4007e2cad8f2ffa9bef9600a87 diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md index eecc1fc75c..0dce552820 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md @@ -8,7 +8,7 @@ English | [中文](2026-06-30-hook-bridges.zh.md) The harness's extension surface is its typed interception points ([the interception extension-points Agent Note](2026-06-30-interception-extension-points.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/pre-step`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed extension points, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)). -The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome to a typed Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols. +The core rule is: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome to a typed Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols. ## Decision diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md index dcc8f496cb..2df3a47fd6 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md @@ -8,7 +8,7 @@ Status: implemented harness 的扩展面是其类型化拦截点(见[拦截扩展点 Agent Note](2026-06-30-interception-extension-points.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/pre-step`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化扩展点上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。 -贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为类型化 Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。 +核心规则是:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为类型化 Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。 ## 决策 diff --git a/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml index d01c01c55a..a67e52a0b4 100644 --- a/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md -2026-07-16-durable-per-step-time-context.md: e1a5c65894913ad93f46db8ae45e5ef5ead215f3 -2026-07-16-durable-per-step-time-context.zh.md: 129920a02dc21a91ddc92de3d920ddad24968656 +2026-07-16-durable-per-step-time-context.md: 3305d3644fa3baf7e1522311b98b4eb29d08f631 +2026-07-16-durable-per-step-time-context.zh.md: dd7e63710ae99d1a04bc0e87d49976e28af1dae5 diff --git a/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md index e1a5c65894..3305d3644f 100644 --- a/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md +++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md @@ -6,7 +6,7 @@ English | [中文](2026-07-16-durable-per-step-time-context.zh.md) ## Problem -A request-only clock can tell the model the current time, but replacing that value in the system prompt removes the evidence behind earlier time-sensitive reasoning. Multi-step turns need requests to retain the readings that shaped preceding steps. The request must remain reconstructable after restart, and automatic compaction must account for the same timing context the model receives. +A request-only clock can tell the model the current time, but replacing that value in the system prompt removes the evidence behind earlier time-sensitive reasoning. Multi-step turns need requests to retain the readings used by preceding steps. The request must remain reconstructable after restart, and automatic compaction must account for the same timing context the model receives. A process-local refresh cache makes displayed time depend on state that cannot survive resume or be reconstructed from the durable session. Durable interval scheduling can reduce append frequency without introducing that hidden state. diff --git a/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md index 129920a02d..dd7e63710a 100644 --- a/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md +++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -仅存在于请求中的时钟可以告诉模型当前时间,但在系统提示词中替换这个值会移除先前对时间敏感的推理所依据的证据。在包含多个步骤的轮次中,请求需要保留影响先前步骤的读数。系统必须能在重启后重建请求,自动压缩(compaction)也必须将模型实际收到的同一份时间上下文纳入考量。 +仅存在于请求中的时钟可以告诉模型当前时间,但在系统提示词中替换这个值会移除先前对时间敏感的推理所依据的证据。在包含多个步骤的轮次中,请求需要保留先前步骤使用的读数。系统必须能在重启后重建请求,自动压缩(compaction)也必须将模型实际收到的同一份时间上下文纳入考量。 进程本地刷新缓存会使显示时间依赖于一种既无法在恢复后保留、也无法从持久会话重建的状态。持久的间隔调度可以减少追加频率,而不引入这种隐藏状态。 diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml index 6bb0f7e156..f597828cec 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md -2026-07-26-code-dispatch-log-spill.md: 19d37c7ec2dbe5192e4c7e7ecd26e5b7f1467606 -2026-07-26-code-dispatch-log-spill.zh.md: 8dd270278932011db9658f9e4d5c1a1b6fe707ec +2026-07-26-code-dispatch-log-spill.md: a4b8deee86b1f6102e48e34079a93a74dfcfa288 +2026-07-26-code-dispatch-log-spill.zh.md: 0d89329dd91d74491f3f9af0812bb9b4db9c8793 diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md index 19d37c7ec2..a4b8deee86 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md @@ -4,28 +4,28 @@ Status: implemented English | [中文](2026-07-26-code-dispatch-log-spill.zh.md) -> Scope: bounding the `tool/code-dispatch` event's content with the existing spill machinery. The [host foundation note](2026-07-26-code-dispatch-ui-foundation.md) accepted the unbounded log deliberately with this spill integration as the payoff point; the [live-parallel note](2026-07-26-code-mode-live-parallel-dispatch.md) settled the event pair this shaping hooks into. +> Scope: limiting the `tool/code-dispatch` event's content with the existing spill implementation. The [host foundation note](2026-07-26-code-dispatch-ui-foundation.md) deliberately accepted the unlimited log and deferred spill support to this change; the [live-parallel note](2026-07-26-code-mode-live-parallel-dispatch.md) defines the event pair that this listener processes. ## Problem -Since the full-content dispatch logging landed, a `run_code` program that reads a large file wrote the complete rendered text into the session log — uncapped and outside spill policy, while native results were bounded to `maxInlineBytes` before logging. The asymmetry was backwards: sub-calls (built for bulk data work) were precisely the calls most likely to carry huge results, and the JSONL grew by megabytes per such turn. +After full-content dispatch logging was added, a `run_code` program that reads a large file wrote the complete rendered text into the session log without a limit or spill policy, while native results were limited to `maxInlineBytes` before logging. This treated the most likely large results differently: sub-calls are intended for bulk data work, and each affected turn added megabytes to the JSONL. ## Decision -**A log-shaping waterfall on the registry, and the spill policy as its first listener.** +**A `tools/code-dispatch-log` waterfall on the registry, with spill policy as its first listener.** -- **Extension point**: `tools/code-dispatch-log` — a scope-filtered waterfall the bridge runs (via the registry's PRIVATE `shapeDispatchLog` invoker, handed to the bridge as a capability closure in `RunCodeBridgeOptions` — the waterfall is the public contract, the invoker never widens the service surface; contained: a throwing listener falls back to the unshaped content, with total error formatting so a hostile thrown value cannot escape the containment) over each settled sub-dispatch before appending `tool/code-dispatch`. The payload (`CodeDispatchLog`) carries the outer execution, the hoisted `agent` routing key, the sub-call identity, and the default content — the RENDERED result projection a native `tool/result` would carry (the program itself received the structured `value`). Only the durable copy is shapeable; the model sees neither. Shaping runs OFF the program path as tracked side work, but bounded: past `maxParallelSubCalls` pending log tasks the ordered commit lane holds, so a slow spill backend backpressures the run instead of accumulating unbounded pending I/O; run settlement still drains every task inside the open turn. -- **Policy**: `dsh-spill-policy` registers a second arm on the new extension point sharing the exact replacement pipeline of its model-facing arm (same `maxInlineBytes` cap, same preview + locator + within-cap invariant, same best-effort fallbacks), with the artifact labeled `dispatch` under the sub-call id. UIs and replay read the full text through the spill artifact exactly as they do for spilled native results, so the native-parity rendering story survives bounding. -- **One deliberate asymmetry**: the model-facing arm skips `read` (the `read → spill → read again` loop); the dispatch-log arm bounds `read` sub-calls too — a log copy is not model context, so the loop cannot happen, and `read` is precisely the tool that produces huge logs. +- **Extension point**: `tools/code-dispatch-log` is a scope-filtered waterfall that the bridge runs over each settled sub-dispatch before appending `tool/code-dispatch`. The bridge receives the registry's private `shapeDispatchLog` invoker as a capability closure in `RunCodeBridgeOptions`; the waterfall is the public contract, and the invoker does not add a service method. If a listener throws, the invoker reports any thrown value safely and uses the original settled content. The `CodeDispatchLog` payload carries the outer execution, the `agent` routing key, the sub-call identity, and the default content: the rendered result projection that a native `tool/result` would carry, while the program receives the structured `value`. A listener can replace only the durable copy, which the model never sees. The listener runs as tracked work outside the program's result path. When more than `maxParallelSubCalls` log tasks are pending, the ordered commit loop waits, so a slow spill backend limits later sub-call starts instead of accumulating unlimited pending I/O. Run settlement still waits for every task inside the open turn. +- **Policy**: `dsh-spill-policy` registers a listener for this event and uses the same replacement code as its model-result listener: the same `maxInlineBytes` limit, preview and locator, within-limit invariant, and best-effort fallback. The spill artifact is labeled `dispatch` under the sub-call id. UIs and replay read its full text through the same path used for spilled native results, so both result kinds render with the same information. +- **One deliberate difference**: the model-result listener skips `read` to prevent a `read → spill → read again` loop. The dispatch-log listener also replaces oversized `read` sub-call content because a log copy is not model context, so that loop cannot occur, and `read` is the tool most likely to produce a large log entry. ## Alternatives considered -**Bound inside the bridge with a plain cap (no spill).** Rejected: truncation without a locator loses data replay/UIs may need, and re-introduces the "truncated summary" degraded render path the stack removed. +**Apply a plain byte limit inside the bridge without spill storage.** Rejected: truncation without a locator loses data that replay or UIs may need and restores the less informative "truncated summary" rendering that earlier changes removed. -**Spill inside the bridge directly (call `ctx.spillStore` from code-mode.ts).** Rejected: the registry would grow a hard dependency on the spill capability; the waterfall keeps the policy where every other spill decision lives, composable and disable-able (omitted `maxInlineBytes` still means a true no-op). +**Spill inside the bridge directly by calling `ctx.spillStore` from `code-mode.ts`.** Rejected: the registry would require the spill capability. The waterfall keeps this policy with the other spill decisions and allows compositions to omit it; omitting `maxInlineBytes` still makes the listener a no-op. -**Reuse `tools/post-execute` for nested calls instead of a new event.** Rejected: post-execute shapes the PROGRAM-facing result (nested calls deliberately skip it so programs get complete data); the durable copy needs its own decision point after the program has its value. +**Reuse `tools/post-execute` for nested calls instead of a new event.** Rejected: post-execute can change the program-facing result, so nested calls deliberately skip it and programs receive complete data. The durable copy needs a separate listener that runs after the program has its value. ## Consequences -The session log is bounded again for Code Mode turns — the README's Known Limitations entry about uncapped dispatch logging is resolved and now points here. Old logs with oversized dispatch content still replay (the event shape is unchanged; only future appends shrink). The web UI renders spilled sub-call output as the preview + locator text through the identical native path, no special casing. +Code Mode dispatch entries in the session log now have the configured byte limit, and the README's Known Limitations entry about unlimited dispatch logging now points here. Old logs with oversized dispatch content still replay because the event fields are unchanged; only future appends contain less text. The web UI renders spilled sub-call output as preview and locator text through the same path as native results, with no special case. diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md index 8dd2702789..0d89329dd9 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md @@ -4,28 +4,28 @@ Status: implemented [English](2026-07-26-code-dispatch-log-spill.md) | 中文 -> 范围:用既有的 spill 机制为 `tool/code-dispatch` 事件的内容施加边界。[宿主侧基础 Agent Note](2026-07-26-code-dispatch-ui-foundation.md)当初有意接受了不设上限的日志,并以这次 spill 集成为兑现点;[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md)敲定了本次整形所挂接的事件对。 +> 范围:用既有的 spill 实现限制 `tool/code-dispatch` 事件的内容。[宿主侧基础 Agent Note](2026-07-26-code-dispatch-ui-foundation.md)有意接受了不设上限的日志,并把 spill 支持留到本次更改;[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md)定义了该监听器处理的事件对。 ## 问题 -自携带完整内容的分发日志落地以来,读取大文件的 `run_code` 程序过去会把完整的渲染文本写进会话日志,不设上限、位于 spill 策略之外;而原生结果在记录之前就已被限制在 `maxInlineBytes` 以内。这种不对称的方向完全反了:子调用(本就为批量数据工作而设计)恰恰是最可能携带巨大结果的调用,而每个这样的轮次都会让 JSONL 增长数 MB。 +加入完整内容的分发日志后,读取大文件的 `run_code` 程序会把完整的渲染文本写进会话日志,既没有上限,也不经过 spill 策略;原生结果则会在记录之前限制在 `maxInlineBytes` 以内。两类结果受到不同处理,而为批量数据工作设计的子调用最可能产生巨大结果;每个受影响的轮次都会让 JSONL 增长数 MB。 ## 决策 -**在注册表上增设一个日志整形 waterfall(瀑布式事件),spill 策略作为其第一个监听器。** +**在注册表上增设 `tools/code-dispatch-log` waterfall(瀑布式事件),spill 策略作为其第一个监听器。** -- **扩展点**:`tools/code-dispatch-log`,一个按作用域过滤的 waterfall,由桥接层在追加 `tool/code-dispatch` 之前对每个已结算的子分发运行(经由注册表的私有 `shapeDispatchLog` 调用器——作为能力闭包经 `RunCodeBridgeOptions` 交给桥接层;waterfall 才是公开约定,调用器绝不扩大服务接口。故障被兜住:监听器抛出异常时回退到未整形的内容,并用可处理任意抛出值的错误格式化,确保恶意抛出值无法逃出兜底)。载荷(`CodeDispatchLog`)携带外层执行、提升出来的 `agent` 路由键、子调用标识与默认内容——即原生 `tool/result` 所载的渲染后结果投影(程序本身收到的是结构化 `value`)。可整形的只有持久副本;模型两者都看不到。整形作为被跟踪的旁路工作在程序路径之外运行,但有界:待处理日志任务超过 `maxParallelSubCalls` 时有序提交通道会暂停,因此慢速 spill 后端会对整个 run 施加背压,而不是无限累积待完成 I/O;run 结算仍会在开放轮次内排空全部任务。 -- **策略**:`dsh-spill-policy` 在新扩展点上注册第二个分支,与其面向模型的分支共用一模一样的替换流水线(同样的 `maxInlineBytes` 上限、同样的预览 + 定位符 + 不超上限不变式、同样的尽力而为回退),产物以 `dispatch` 为标签,记在子调用 id 名下。UI 与回放通过 spill 产物读取全文,方式与读取被 spill 的原生结果完全相同,因此与原生同等保真的渲染在施加边界之后依然成立。 -- **一处有意的不对称**:面向模型的分支跳过 `read`(避免 `read → spill → read again` 循环);分发日志分支则连 `read` 子调用也施加边界:日志副本不是模型上下文,该循环因此不可能发生,而 `read` 恰恰是会产生巨大日志的那个工具。 +- **扩展点**:`tools/code-dispatch-log` 是一个按作用域过滤的 waterfall,桥接层会在追加 `tool/code-dispatch` 之前,对每个已结算的子分发运行它。桥接层通过 `RunCodeBridgeOptions` 接收注册表私有的 `shapeDispatchLog` 调用器;waterfall 是公开约定,该调用器不会增加服务方法。监听器抛出异常时,调用器会安全地报告任意抛出值,并使用原始的已结算内容。`CodeDispatchLog` 载荷包含外层执行、`agent` 路由键、子调用标识和默认内容;默认内容是原生 `tool/result` 会携带的渲染后结果投影,而程序收到结构化 `value`。监听器只能替换持久化副本,模型不会看到这份副本。监听器作为受跟踪任务在程序的返回路径之外运行。待处理日志任务超过 `maxParallelSubCalls` 时,有序提交循环会等待,因此慢速 spill 后端会限制后续子调用启动,而不会无限累积待完成 I/O。run 结算仍会在开放轮次内等待全部任务完成。 +- **策略**:`dsh-spill-policy` 为该事件注册监听器,并复用面向模型结果的监听器所用的替换代码:相同的 `maxInlineBytes` 上限、预览和定位符、不超上限不变式,以及尽力而为回退。spill 产物以 `dispatch` 为标签,记录在子调用 id 名下。UI 与回放通过读取被 spill 原生结果的同一路径读取全文,因此两类结果会渲染出相同的信息。 +- **一处有意差异**:面向模型结果的监听器跳过 `read`,以防出现 `read → spill → read again` 循环。分发日志监听器也会替换过大的 `read` 子调用内容,因为日志副本不是模型上下文,该循环不会发生,而 `read` 最可能产生巨大的日志条目。 ## 曾考虑的替代方案 -**在桥接层内部用普通上限施加边界(不做 spill)。** 否决:没有定位符的截断会丢失回放与 UI 可能需要的数据,还会重新引入本堆叠 PR(Pull Request)链已经移除的「截断摘要」降级渲染路径。 +**在桥接层内部使用普通字节数上限,不存入 spill。** 否决:没有定位符的截断会丢失回放或 UI 可能需要的数据,还会恢复之前更改已经移除的、信息较少的「截断摘要」渲染。 -**直接在桥接层内做 spill(从 code-mode.ts 调用 `ctx.spillStore`)。** 否决:注册表会因此对 spill 能力产生硬依赖;waterfall 则把策略留在所有其他 spill 决策所在的地方,既可组合也可禁用(省略 `maxInlineBytes` 依然意味着真正的 no-op)。 +**直接在桥接层内做 spill,即从 `code-mode.ts` 调用 `ctx.spillStore`。** 否决:注册表会要求提供 spill 能力。waterfall 把该策略与其他 spill 决策放在一起,并允许组合不加载它;省略 `maxInlineBytes` 时,该监听器仍不执行任何操作。 -**让嵌套调用复用 `tools/post-execute`,而不是新增一个事件。** 否决:post-execute 整形的是面向程序的那份结果(嵌套调用有意跳过它,好让程序拿到完整数据);持久副本需要一个属于自己的决策点,位于程序取得其值之后。 +**让嵌套调用复用 `tools/post-execute`,而不是新增一个事件。** 否决:post-execute 可以修改面向程序的结果,因此嵌套调用有意跳过它,让程序取得完整数据。持久化副本需要一个单独的监听器,在程序取得其值之后运行。 ## 后果 -对 Code Mode 轮次而言,会话日志重新有了边界:README 中关于分发日志不设上限的 「已知限制」条目已经解决,现在指向本篇。携带超大分发内容的旧日志仍可回放(事件形状未变;只有今后的追加才会变小)。Web UI 经由与原生完全相同的路径,把被 spill 的子调用输出渲染为预览 + 定位符文本,没有任何特殊处理。 +会话日志中的 Code Mode 分发条目现在遵守已配置的字节数上限,README 中关于分发日志不设上限的「已知限制」条目现在指向本篇。携带超大分发内容的旧日志仍可回放,因为事件字段没有变化;只有今后的追加包含更少文本。Web UI 经由与原生结果相同的路径,把被 spill 的子调用输出渲染为预览和定位符文本,不需要特殊处理。 diff --git a/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.i18n.yaml index 0e4f334485..30111dab4b 100644 --- a/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.md -2026-07-30-plan-review-presentation-intent.md: f524f7ec67dff3e6d7a6bf59ac204f5c8aae13c6 -2026-07-30-plan-review-presentation-intent.zh.md: 69e0ae00fbc2a98ed45a6d2b711a17f7c947c7e6 +2026-07-30-plan-review-presentation-intent.md: 62e1c24aba7a2901a46da4e6ace9707814472974 +2026-07-30-plan-review-presentation-intent.zh.md: 8415b94cab697e0fa9c4094b938d8ceb99107c7f diff --git a/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.md b/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.md index f524f7ec67..62e1c24aba 100644 --- a/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.md +++ b/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.md @@ -12,15 +12,15 @@ Every one of those affordances is wrong for the surface. Reviewing a plan is one ## Decision -A question may declare a **presentation intent**, and the Web composer renders a declared intent as its own surface. `AskUserQuestionItem` gains `intent?: AskUserQuestionIntent`, a tagged shape whose one member is `{ kind: 'plan-review', approve: string }`; `plan-mode` sets it on the review question, naming `Approve` as the label that approves. +A question may declare a **presentation intent**, and the Web composer renders a declared intent as its own surface. `AskUserQuestionItem` gains `intent?: AskUserQuestionIntent`, a tagged union whose one member is `{ kind: 'plan-review', approve: string }`; `plan-mode` sets it on the review question, naming `Approve` as the label that approves. -An intent shapes presentation only. The answer protocol is untouched: a UI honouring the intent answers with the same option labels a generic UI would send, so `exit_plan_mode` reads one answer shape regardless of which surface collected it, and a UI that does not know a tag renders the generic flow with nothing lost but the layout. +An intent changes presentation only. The answer protocol is untouched: a UI honouring the intent answers with the same option labels a generic UI would send, so `exit_plan_mode` reads the same answer fields regardless of which surface collected them, and a UI that does not know a tag renders the generic flow with nothing lost but the layout. `approve` names the affirmative option instead of relying on option order, so no UI infers a verdict from a position. Two assertions an intent makes are beyond the types, and `UserInteractionService.ask()` rejects both as `BAD_INTENT` at the asker: an `approve` naming none of that question's own options — before any UI can answer a choice never offered — and an intent on a question with no `detail`, the thing it declares itself a review of, which would ask the user to approve something invisible. On the wire the intent is a discriminated union, so an unrecognised tag is a rejected frame rather than a silently generic render. `ui-question` renders the intent as `PlanReviewPanel`, in the waiting-approval card language: the amber strip carries `Plan review`, the plan is the scrolling markdown body, and the decision row holds three actions — `Chat about it`, `Refuse`, `Approve`. The question text becomes the card's accessible name rather than a headline, because the buttons already say what the decision is. Approve and Refuse answer with the asker's own option labels and keep the asker's descriptions as tooltips; `Chat about it` cancels the request, which returns the composer so the user can simply say what they want. All copy is bilingual under the existing `question` namespace. -Routing lives inside the single composer entry (`QuestionComposer` chooses the shape) rather than in a second chain registration, and `planReviewOf` claims a request only when the card can send every answer that request allows: one question declaring the intent, the plan as its `detail`, the named approve label offered, and a binary single choice — at most one option besides approve, and not multi-select. A third option or a multi-select batch has answers two buttons cannot express, so the generic flow keeps it, and keeps anything else the card cannot render. "Presentation only" is therefore literal: an intent never costs the user a reachable answer, and the client — downstream of a wire boundary — leaves every request answerable. +Routing lives inside the single composer entry (`QuestionComposer` chooses the presentation) rather than in a second chain registration, and `planReviewOf` claims a request only when the card can send every answer that request allows: one question declaring the intent, the plan as its `detail`, the named approve label offered, and a binary single choice — at most one option besides approve, and not multi-select. A third option or a multi-select batch has answers two buttons cannot express, so the generic flow keeps it, and keeps anything else the card cannot render. "Presentation only" is therefore literal: an intent never costs the user a reachable answer, and the client — downstream of a wire boundary — leaves every request answerable. Dismissal became its own model-facing outcome. `ASK_CANCELLED` previously reached the model as "the user cancelled ask_user_question", naming a tool it never called; `exit_plan_mode` now reports that the user dismissed the review to speak instead and to stay in plan mode and wait. Every other ask failure — an abort from turn cancel or provider teardown, where no user is coming — keeps its own message. diff --git a/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.zh.md b/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.zh.md index 69e0ae00fb..8415b94cab 100644 --- a/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-plan-review-presentation-intent.zh.md @@ -12,15 +12,15 @@ Status: implemented ## 决定 -一个问题可以声明**呈现意图(presentation intent)**,Web 输入区把已声明的意图渲染为它自己的界面。`AskUserQuestionItem` 新增 `intent?: AskUserQuestionIntent`,一个带标签的形状,目前唯一成员是 `{ kind: 'plan-review', approve: string }`;`plan-mode` 在审阅问题上设置它,并指明 `Approve` 是表示批准的标签。 +一个问题可以声明**呈现意图(presentation intent)**,Web 输入区把已声明的意图渲染为它自己的界面。`AskUserQuestionItem` 新增 `intent?: AskUserQuestionIntent`,这是一个带标签的联合,目前唯一成员是 `{ kind: 'plan-review', approve: string }`;`plan-mode` 在审阅问题上设置它,并指明 `Approve` 是表示批准的标签。 -意图只塑造呈现。回答协议不变:遵循意图的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此无论由哪个界面收集,`exit_plan_mode` 读到的都是同一种回答形状;而不认识某个标签的 UI 渲染通用流程,除布局之外一无所失。 +意图只改变呈现。回答协议不变:遵循意图的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此无论由哪个界面收集,`exit_plan_mode` 读到的都是同一组回答字段;而不认识某个标签的 UI 渲染通用流程,除布局之外一无所失。 `approve` 指名肯定选项,而不依赖选项顺序,因此没有任何 UI 会从位置推断裁决。意图作出的两项断言超出类型的表达能力,`UserInteractionService.ask()` 都以 `BAD_INTENT` 在提问方一侧拒绝:`approve` 未命中该问题自身的任一选项 —— 早于任何 UI 回答一个从未被提供过的选择;以及意图落在没有 `detail` 的问题上,而 `detail` 正是它自称在审阅的东西,那会让用户去批准一件看不见的事。在协议格式(wire format)上意图是可辨识联合,因此无法识别的标签是被拒绝的帧,而不是静默退回通用渲染。 `ui-question` 把该意图渲染为 `PlanReviewPanel`,沿用等待审批卡片的语言:琥珀色条带写着 `Plan review`,计划是可滚动的 markdown 主体,决定行放三个操作 —— `Chat about it`、`Refuse`、`Approve`。问题文本成为卡片的无障碍名称而非标题,因为按钮已经说明了这次决定是什么。Approve 与 Refuse 用提问方自己的选项标签回答,并把提问方的描述保留为 tooltip;`Chat about it` 取消该请求,从而让输入区归位,用户直接说他想说的话即可。所有文案在既有 `question` 命名空间下双语。 -路由住在单一输入区条目内部(由 `QuestionComposer` 选择形状),而不是第二个链式注册;`planReviewOf` 仅在卡片能够发出该请求允许的每一个答案时才接管:只有一个问题且声明了意图、以 `detail` 承载计划、提供了被指名的批准标签,且是二元单选 —— 除批准外最多一个选项,且非多选。出现第三个选项或多选批次时,其答案是两个按钮无法表达的,通用流程保留它,也保留其他任何卡片渲染不了的请求。因此"只塑造呈现"是字面意义上的:意图绝不让用户失去一个可达的答案,而位于协议边界下游的客户端让每个请求都保持可回答。 +路由住在单一输入区条目内部(由 `QuestionComposer` 选择呈现),而不是第二个链式注册;`planReviewOf` 仅在卡片能够发出该请求允许的每一个答案时才接管:只有一个问题且声明了意图、以 `detail` 承载计划、提供了被指名的批准标签,且是二元单选 —— 除批准外最多一个选项,且非多选。出现第三个选项或多选批次时,其答案是两个按钮无法表达的,通用流程保留它,也保留其他任何卡片渲染不了的请求。因此"只改变呈现"是字面意义上的:意图绝不让用户失去一个可达的答案,而位于协议边界下游的客户端让每个请求都保持可回答。 放弃审阅成为面向模型的独立结果。`ASK_CANCELLED` 以前传到模型的是"the user cancelled ask_user_question",指名了一个它从未调用的工具;现在 `exit_plan_mode` 报告用户放弃审阅是为了改用说话,并要求留在 plan mode 中等待。其余每一种 ask 失败 —— 轮次取消或提供方拆卸导致的中止,那里并没有用户会来 —— 保留它们自己的消息。 diff --git a/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.i18n.yaml b/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.i18n.yaml index b768fb0abc..1e3db15084 100644 --- a/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.md -2026-07-23-translation-prompt-v4-contract.md: adeb63e0f4bf03ec0b94a371f596bb84138d81ed -2026-07-23-translation-prompt-v4-contract.zh.md: bd9a18788cbc04f733f52270ec486923c552459e +2026-07-23-translation-prompt-v4-contract.md: 68d6837971a0bbb40fbc5ee093e5911515905ba3 +2026-07-23-translation-prompt-v4-contract.zh.md: d5370f5d6d62a6ca89187d9911fc4dd5c5758b07 diff --git a/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.md b/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.md index adeb63e0f4..68d6837971 100644 --- a/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.md +++ b/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.md @@ -10,7 +10,7 @@ Automated counterpart generation needs a stable prompt that reproduces the regis ## Decision -The committed [translation prompt](../../../../docs/i18n/translation-prompt.md) is the calibrated pipeline asset. Its renderer injects only the source language, target language, and current [terminology table](../../../../docs/i18n/terminology.md), and rejects unknown, missing, or malformed placeholder syntax before assembling a request. The request assembler retains the source basename outside the model-visible prompt and places each reviewed whole-document pair into one bare-text user/assistant example turn before the real source document. The template may carry model-specific calibration rules, but those rules remain subordinate to the repository's binding pairing, terminology, structure, and emphasis contracts. +The committed [translation prompt](../../../../docs/i18n/translation-prompt.md) is the calibrated pipeline asset. Its renderer injects only the source language, target language, and current [terminology table](../../../../docs/i18n/terminology.md), and rejects unknown, missing, or malformed placeholder syntax before assembling a request. The request assembler retains the source basename outside the model-visible prompt and places each reviewed whole-document pair into one bare-text user/assistant example turn before the real source document. The template may carry model-specific calibration rules, but those rules remain subordinate to the repository's pairing, terminology, structure, and emphasis contracts. The v7 calibration retains that v4 protocol and makes the instruction priority explicit: source meaning and protected structure, then the terminology table, then whole-document gold-pair voice, then general guidance and embedded examples. It directs the model to draft as a native technical author and then compare clause by clause, preserving actors, conditions, negation, modality, lifecycle conditions, direction, result channels, ownership, and quantities. Style guidance cannot invent an actor or vary a terminology-table form, defined concept, or contract verb merely for variety. Unresolved terminology stays unchanged in the translation and is reported only as pending review. @@ -26,7 +26,7 @@ The executable contract lives in [the renderer, request assembler, parser, and r **Inject `translation-rules.md` into every request.** That document governs humans and agents as well as the automated pipeline. Injecting it couples each editorial clarification to model behavior and displaces the manually calibrated prompt constraints; the pipeline instead injects the binding terminology table and verifies its own asset directly. -**Use a strict CDATA XML document.** CDATA provides general XML framing but adds a nested protocol, an additional `]]>` escape, and XML-parser behavior that the three-section contract does not otherwise need. Reserving and escaping six delimiter lines keeps the calibrated response shape while preserving arbitrary Markdown. +**Use a strict CDATA XML document.** CDATA provides general XML framing but adds a nested protocol, an additional `]]>` escape, and XML-parser behavior that the three-section contract does not otherwise need. Reserving and escaping six delimiter lines keeps the calibrated response sections while preserving arbitrary Markdown. **Return only the final translation.** A single body is simpler to parse but discards the explicit correction pass used to catch tone, structure, terminology, and punctuation defects before publication. @@ -36,4 +36,4 @@ The executable contract lives in [the renderer, request assembler, parser, and r ## Consequences -Prompt wording is executable behavior and receives code review, a translation-prompt verifier, and a runnable request/response snapshot. Focused tests pin the retained v4 examples and selected v7 safeguards. The `translation-prompt-v4` snapshot directory names the stable renderer/parser protocol lineage rather than the current calibration revision. The calibrated asset and the general translation rules can evolve for their different audiences, but review must reject contradictions with binding repository contracts. The line escape is visible only when source documentation contains a wrapper tag on its own line, and parser tests pin its lossless behavior. +Prompt wording is executable behavior and receives code review, a translation-prompt verifier, and a runnable request/response snapshot. Focused tests pin the embedded examples and selected v7 safeguards. The `translation-prompt-v4` snapshot directory names the stable renderer/parser protocol series rather than the current calibration revision. The calibrated asset and the general translation rules can evolve for their different audiences, but review must reject contradictions with repository contracts. The line escape is visible only when source documentation contains a wrapper tag on its own line, and parser tests pin its lossless behavior. diff --git a/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.zh.md b/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.zh.md index bd9a18788c..d5370f5d6d 100644 --- a/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.zh.md +++ b/.agents/notes/implemented/process/2026-07-23-translation-prompt-v4-contract.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -提交入库的[翻译提示词](../../../../docs/i18n/translation-prompt.md)是经过校准的流水线资源。其渲染器仅注入源语言、目标语言和当前[术语表](../../../../docs/i18n/terminology.md),并在组装请求前拒绝未知、缺失或语法格式错误的占位符。请求组装器在模型可见的提示词之外保留源文件基本名,并在真正的源文档之前,将每组经评审的整篇文档对编排为一个纯文本 user/assistant 示例轮次。模板可以包含针对特定模型的校准规则,但这些规则必须服从仓库中具约束力的配对、术语、结构与强调格式约定。 +提交入库的[翻译提示词](../../../../docs/i18n/translation-prompt.md)是经过校准的流水线资源。其渲染器仅注入源语言、目标语言和当前[术语表](../../../../docs/i18n/terminology.md),并在组装请求前拒绝未知、缺失或语法格式错误的占位符。请求组装器在模型可见的提示词之外保留源文件基本名,并在真正的源文档之前,将每组经评审的整篇文档对编排为一个纯文本 user/assistant 示例轮次。模板可以包含针对特定模型的校准规则,但这些规则必须服从仓库的配对、术语、结构与强调格式约定。 v7 校准保留这套 v4 协议,并明确指令优先级:先保持源文含义与受保护结构,再遵循术语表,然后以整篇金标校准语体,最后应用一般指导与内嵌示例。模型先以母语技术作者的方式起草,再逐项对照源文,保留执行主体、条件、否定、情态、生命周期条件、方向、结果通道、所有权和数量。文体指导不得虚构执行主体,也不得仅为丰富措辞而改换术语表词形、已定义概念或约定动词。无法裁定的术语在译文中保持不变,只在评审段标为待人工确认。 @@ -26,7 +26,7 @@ v7 校准保留这套 v4 协议,并明确指令优先级:先保持源文含 **在每个请求中注入 `translation-rules.md`。** 该文档既约束人类与 agent,也约束自动翻译流水线。注入它会让编辑规范的每次澄清都与模型行为耦合,并挤占经过人工校准的提示词约束;因此流水线仅注入具约束力的术语表,并直接校验自身资源。 -**使用严格的 CDATA XML 文档。** CDATA 提供通用的 XML 封装,但会引入一层嵌套协议、额外的 `]]>` 转义规则,以及三段式约定原本不需要的 XML 解析器行为。预留并转义六种定界行,既能维持经校准的响应形态,也能保留任意 Markdown 内容不变。 +**使用严格的 CDATA XML 文档。** CDATA 提供通用的 XML 封装,但会引入一层嵌套协议、额外的 `]]>` 转义规则,以及三段式约定原本不需要的 XML 解析器行为。预留并转义六种定界行,既能维持经校准的响应分段,也能保留任意 Markdown 内容不变。 **只返回最终译文。** 单一正文更易解析,却会丢弃显式修正步骤;这个步骤用于在发布前发现语气、结构、术语和标点缺陷。 @@ -36,4 +36,4 @@ v7 校准保留这套 v4 协议,并明确指令优先级:先保持源文含 ## 影响 -提示词措辞属于可执行行为,因此需要经过代码评审、翻译提示词校验器校验及可运行的请求/响应快照验证。聚焦测试固定保留的 v4 示例与选定的 v7 保护规则。`translation-prompt-v4` 快照目录命名的是稳定的渲染器/解析器协议谱系,而不是当前校准修订号。经校准的资源与通用翻译规则可以针对各自的受众分别演进,但评审必须拒绝任何与仓库约束性约定冲突的改动。只有当源文档中的封装标签独占一行时,行转义才会显现;解析器测试锁定这一无损行为。 +提示词措辞属于可执行行为,因此需要经过代码评审、翻译提示词校验器校验及可运行的请求/响应快照验证。聚焦测试固定内嵌示例与选定的 v7 保护规则。`translation-prompt-v4` 快照目录命名的是稳定的渲染器/解析器协议系列,而不是当前校准修订号。经校准的资源与通用翻译规则可以针对各自的受众分别演进,但评审必须拒绝任何与仓库约定冲突的改动。只有当源文档中的封装标签独占一行时,行转义才会显现;解析器测试锁定这一无损行为。 diff --git a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml b/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml index 72574c7cf5..1c58887b2b 100644 --- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md -2026-07-31-installer-adopts-existing-checkout.md: 531b780974e1490b27d8b96734ccfadae66c2fc2 -2026-07-31-installer-adopts-existing-checkout.zh.md: 405b58d8d53b3f0895cc7746c957582455667cda +2026-07-31-installer-adopts-existing-checkout.md: 1d45a14273095610e3ed8047da2ce9f4ca95cbb6 +2026-07-31-installer-adopts-existing-checkout.zh.md: 971bc3b389c341b314872b8e45ab20ebd2ed5b2c diff --git a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md b/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md index 531b780974..1d45a14273 100644 --- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md +++ b/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md @@ -6,9 +6,9 @@ English | [中文](2026-07-31-installer-adopts-existing-checkout.zh.md) ## Problem -`scripts/install.sh` produced two incompatible install shapes. A `curl … | sh` install built the managed layout — a master clone at `~/.dsh/source/master`, a staging worktree on `dsh-staging/`, and the stable `current` symlink the PATH launcher resolves through. Running the same script from a checkout instead linked `dsh` straight at that checkout's `bin/dsh`, per the earlier [in-repo skip-clone decision](../../archived/process/2026-07-22-installer-in-repo-skip-clone.md). +`scripts/install.sh` produced two incompatible installation layouts. A `curl … | sh` install built the managed layout — a master clone at `~/.dsh/source/master`, a staging worktree on `dsh-staging/`, and the stable `current` symlink the PATH launcher resolves through. Running the same script from a checkout instead linked `dsh` straight at that checkout's `bin/dsh`, per the earlier [in-repo skip-clone decision](../../archived/process/2026-07-22-installer-in-repo-skip-clone.md). -The direct link is a terminal state. `current` is what an upgrade repoints, so an install without it is not upgradable by [`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md); the PATH symlink dangles if the checkout moves; and the launcher resolves to whatever branch the contributor happened to have checked out, which the upgrade contract forbids as a launcher target. The upgrade skill already described this shape as a legacy install needing a one-time migration, so the layouts diverged at install time and were reconciled only later, if ever. +The direct link cannot be upgraded. `current` is what an upgrade repoints, so an install without it is not upgradable by [`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md); the PATH symlink dangles if the checkout moves; and the launcher resolves to whatever branch the contributor happened to have checked out, which the upgrade contract forbids as a launcher target. The upgrade skill already described this layout as a legacy install needing a one-time migration, so the layouts diverged at install time and were reconciled only later, if ever. ## Decision @@ -28,9 +28,9 @@ Before `current` is repointed, the installer rejects a staging path that resolve **Make `~/.dsh/source/master` a symlink to the arbitrary clone.** Rejected. Git resolves the symlink and records the *real* path: a worktree created through it stores `gitdir: …//.git/worktrees/`, and `git worktree list` reports the clone. The symlink is therefore decorative — nothing reads it — while implying the container owns the repository. It also fails silently: moving the clone leaves `master` present but dangling and every staging worktree dead with `fatal: not a git repository`. Worst, it aliases two names onto one tree, so the "current must never be the master clone" check passes by string comparison while being false. `~/.dsh/source/master` is a location, not a name, and only the location is authoritative. -**Promote the checkout itself to the `current` target.** Rejected: the upgrade contract requires `current` to be a clean staging worktree on a staging branch, never a feature, review, or detached checkout. It would also make every upgrade rewrite the tree the contributor is editing. +**Promote the checkout itself to the `current` target.** Rejected: the upgrade contract requires `current` to point to a clean staging worktree on a staging branch, never a feature, review, or detached checkout. It would also make every upgrade rewrite the tree the contributor is editing. -**Keep link-in-place behind a prompt or a `DSH_ADOPT` flag.** Rejected, and an earlier revision of this change shipped exactly that before it was removed. The divergent shape was the defect itself, so retaining it as an option preserves the problem and doubles the states every later change must reason about — the prompt, the flag, the dirty-tree warning, and a second linking path all existed only to keep a shape nothing should produce. The original motivation for link-in-place, keeping the script testable against local source, survives adoption: a staging worktree branched from the checkout's `HEAD` runs the same code. `DSH_SOURCE` remains the escape hatch for installing a separate tree. +**Keep link-in-place behind a prompt or a `DSH_ADOPT` flag.** Rejected, and an earlier revision of this change shipped exactly that before it was removed. The second layout was the defect itself, so retaining it as an option preserves the problem and doubles the states every later change must handle — the prompt, the flag, the dirty-tree warning, and a second linking path all existed only to keep a layout nothing should produce. The original motivation for link-in-place, keeping the script testable against local source, survives adoption: a staging worktree branched from the checkout's `HEAD` runs the same code. `DSH_SOURCE` remains available for installing a separate tree. **Warn or prompt when the tree is dirty.** Rejected: `worktree add` from `HEAD` cannot carry uncommitted work, so the behavior is determined and a prompt only adds a decision the user cannot act on differently. The contract is documented instead. @@ -38,7 +38,7 @@ Before `current` is repointed, the installer rejects a staging path that resolve ## Consequences -One layout now serves every install, so an adopted clone is upgradable by `dsh-upgrade` without the one-time migration that skill described, and the installer has no branch that produces an unupgradable shape. In-repo runs still never mutate the working tree. +One layout now serves every install, so an adopted clone is upgradable by `dsh-upgrade` without the one-time migration that skill described, and the installer has no branch that produces an unupgradable layout. In-repo runs still never mutate the working tree. The cost is that a contributor can no longer point PATH at a checkout and have `dsh` follow that working tree as they switch branches: the launcher now resolves to a staging worktree pinned to the `HEAD` adopted at install time. Re-running the installer adopts the current `HEAD` again. diff --git a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md b/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md index 405b58d8d5..971bc3b389 100644 --- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md +++ b/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md @@ -6,9 +6,9 @@ Status: implemented ## Problem -`scripts/install.sh`会产生两种互不兼容的安装形态。`curl … | sh`安装会构建受管布局——`~/.dsh/source/master`处的 master 克隆、位于`dsh-staging/<时间戳>`分支上的 staging worktree,以及 PATH 启动器据以解析的稳定`current`符号链接。而从检出中运行同一脚本时,则依据此前的[检出内跳过克隆决策](../../archived/process/2026-07-22-installer-in-repo-skip-clone.md),把`dsh`直接链接到该检出的`bin/dsh`。 +`scripts/install.sh`会产生两种互不兼容的安装布局。`curl … | sh`安装会构建受管布局——`~/.dsh/source/master`处的 master 克隆、位于`dsh-staging/<时间戳>`分支上的 staging worktree,以及 PATH 启动器据以解析的稳定`current`符号链接。而从检出中运行同一脚本时,则依据此前的[检出内跳过克隆决策](../../archived/process/2026-07-22-installer-in-repo-skip-clone.md),把`dsh`直接链接到该检出的`bin/dsh`。 -这种直接链接是一种终态。升级重指的正是`current`,因此缺少它的安装无法通过[`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md)升级;检出一旦移动,PATH 符号链接就会失效;而且启动器会解析到贡献者恰好检出的任意分支,这正是升级约定禁止作为启动器目标的情形。升级技能早已把这种形态描述为需要一次性迁移的旧式安装,于是两种布局在安装时就已分叉,并且要到很久以后才会被调和——甚至永远不会。 +这种直接链接无法升级。升级重指的正是`current`,因此缺少它的安装无法通过[`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md)升级;检出一旦移动,PATH 符号链接就会失效;而且启动器会解析到贡献者恰好检出的任意分支,这正是升级约定禁止作为启动器目标的情形。升级技能早已把这种布局描述为需要一次性迁移的旧式安装,因此两种布局从安装时起便不相同,只有以后执行迁移才会一致,而迁移也可能永远不执行。 ## Decision @@ -28,9 +28,9 @@ Status: implemented **把`~/.dsh/source/master`做成指向该任意克隆的符号链接。** 已否决。Git 会解析该符号链接并记录*真实*路径:经由它创建的 worktree 会存储`gitdir: …/<克隆>/.git/worktrees/<名称>`,而`git worktree list`报告的是该克隆。因此这个符号链接纯属装饰——没有任何代码读取它——却又暗示容器拥有该仓库。它还会静默失效:移动克隆后,`master`看似仍在却已悬空,而每个 staging worktree 都会以`fatal: not a git repository`失败。最糟的是,它把两个名称别名到同一棵树上,于是"current 绝不能是 master 克隆"这项检查会在字符串比较下通过,实则为假。`~/.dsh/source/master`是位置而非名称,且只有位置具有权威性。 -**把检出自身提升为`current`的目标。** 已否决:升级约定要求`current`必须是位于 staging 分支上的干净 staging worktree,绝不能是 feature、review 或 detached 检出。这还会使每次升级都改写贡献者正在编辑的那棵树。 +**把检出自身提升为`current`的目标。** 已否决:升级约定要求`current`指向 staging 分支上的干净 staging worktree,绝不能指向 feature、review 或 detached 检出。这还会使每次升级都改写贡献者正在编辑的那棵树。 -**把就地链接保留在提示或`DSH_ADOPT`开关之后。** 已否决;本次变更的早期修订版本正是如此实现,之后被移除。分叉的形态本身就是缺陷,因此把它保留为一个选项等于保留了问题,并使此后每次改动需要推敲的状态翻倍——提示、开关、工作树不干净的警告,以及第二条链接路径,全都只为维持一种本不该产生的形态而存在。就地链接最初的动机——让脚本能针对本地源码进行测试——在接管方案下依然成立:以检出的`HEAD`为起点创建的 staging worktree 运行的是同一份代码。`DSH_SOURCE`仍是安装另一棵树的退路。 +**把就地链接保留在提示或`DSH_ADOPT`开关之后。** 已否决;本次变更的早期修订版本正是如此实现,之后被移除。第二种布局本身就是缺陷,因此把它保留为一个选项等于保留了问题,并使此后每次改动必须处理的状态翻倍——提示、开关、工作树不干净的警告,以及第二条链接路径,全都只为维持一种本不该产生的布局而存在。就地链接最初的动机——让脚本能针对本地源码进行测试——在接管方案下依然成立:以检出的`HEAD`为起点创建的 staging worktree 运行的是同一份代码。`DSH_SOURCE`仍可用于安装另一棵树。 **在工作树不干净时发出警告或提示。** 已否决:以`HEAD`为起点的`worktree add`本就无法带上未提交的内容,因此该行为是确定的,提示只会增加一个用户无法做出不同选择的决策点。改为在文档中说明该约定。 @@ -38,7 +38,7 @@ Status: implemented ## Consequences -现在一套布局服务于所有安装,因此被接管的克隆无需该技能所述的一次性迁移,即可由`dsh-upgrade`升级,而且安装器不再有任何一条分支会产生无法升级的形态。检出内运行仍然绝不改动工作树。 +现在一套布局服务于所有安装,因此被接管的克隆无需该技能所述的一次性迁移,即可由`dsh-upgrade`升级,而且安装器不再有任何一条分支会产生无法升级的布局。检出内运行仍然绝不改动工作树。 代价是:贡献者不能再把 PATH 指向某个检出、并让`dsh`随其切换分支而跟随该工作树;启动器现在解析到的是一个固定在安装时所接管`HEAD`上的 staging worktree。重新运行安装器会再次接管当前的`HEAD`。 diff --git a/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.i18n.yaml index 295dcfd0bf..1c22b0d7f6 100644 --- a/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md -2026-08-08-unified-github-label-taxonomy.md: 44508b4f0e2dc4de97950b0738829c0b362cc506 -2026-08-08-unified-github-label-taxonomy.zh.md: b1e3629fd93dcbff6e253058b4c465dc9bb5fd63 +2026-08-08-unified-github-label-taxonomy.md: 8005629861f306d20293af6b348f19bf79cbe1c3 +2026-08-08-unified-github-label-taxonomy.zh.md: 855a2b98f44d517abe1f7718ae4e81262cb031b6 diff --git a/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md b/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md index 44508b4f0e..8005629861 100644 --- a/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md +++ b/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md @@ -27,18 +27,18 @@ The kind set is closed and mutually exclusive: | `kind/cleanup` | Preserves behavior while maintaining or simplifying implementation or repository process. | | `kind/dependency` | Updates dependencies without another dominant intent. | -The kind records the dominant intent. Accompanying tests, documentation, cleanup, or dependency movement do not override a feature or bug fix. A new kind changes this classification contract and requires an explicit taxonomy and policy change. +The kind records the dominant intent. Accompanying tests, documentation, cleanup, or dependency movement do not override a feature or bug fix. A new kind changes these classification rules and requires an explicit taxonomy and policy change. Repository policy rejects unsupported `kind/*` values and reserves every alias removed by the unification: `kind/bug`, `kind/documentation`, `feature`, `bug-fix`, `doc`, `cleanup`, `testing`, `dependencies`, `ci`, `cli`, `llm`, and `web-search`. Reserving the exact migrated set prevents an obsolete synonym from being recreated as an apparently unrelated operational label. ### Areas -Areas name durable semantic domains rather than temporary initiatives, ownership, or every path touched incidentally. A pull request carries multiple areas when it changes distinct contracts, but it does not combine an umbrella and a narrower label for the same contract. GitHub's live `area/*` names and descriptions own the current inventory; this record owns the selection rule and the non-obvious boundaries that cannot fit reliably in short label descriptions. +Areas name durable product or engineering subjects rather than temporary initiatives, ownership, or every path touched incidentally. A pull request carries multiple areas when it changes distinct behavior or APIs, but it does not combine an umbrella and a narrower label for the same change. GitHub's live `area/*` names and descriptions own the current inventory; this record defines selection cases that cannot fit reliably in short label descriptions. - `area/web` covers browser and Electron graphical interfaces, `area/vscode` covers the editor extension, and `area/api` covers cross-interface protocols and language SDKs. - `area/planning` covers goals, plans, todos, and scheduling, while `area/workflow` covers executable workflows and background task runtimes. - `area/artifact` deliberately combines artifacts, attachments, and multimodal delivery. Split labels become justified only when those concerns again need independent review or queries. -- `area/tools` applies to generic registry, schema, and execution contracts. A concrete capability uses its own area unless it also changes that generic contract. +- `area/tools` applies to generic registry, schema, and execution contracts. A concrete capability uses its own area unless it also changes one of those contracts. - `area/hooks` means the Claude Code and Codex bridges, `area/infra` covers build, release, CI, repository gates, generators, dependencies, and developer tooling, and `area/windows` covers native Windows product support rather than CI runner selection. The area set is intentionally extensible. When no existing description honestly covers a durable and reusable domain, an agent may create a concise `area/` label without separate approval. It must not create an area for one pull request, an incidental path, a temporary project, a status, or a person or team, and it reports the new label and rationale to the requester after applying it. Reusing an inaccurate area merely to avoid a justified addition is not acceptable. @@ -61,11 +61,11 @@ Label migrations preserve meaning before removing aliases: add the canonical rep **Separate labels for every delivery shell or media lifecycle.** Browser and Electron delivery share one graphical domain, and artifact, attachment, and multimodal delivery currently share one review/query domain. A split belongs in a later taxonomy change only when it restores useful independent classification. -**Broad implementation labels in place of semantic domains.** A concrete capability is not merely its tool, interface, filesystem, or process implementation. Generic implementation areas apply only when their own contracts change. +**Broad implementation labels in place of product or engineering subjects.** A concrete capability is not merely its tool, interface, filesystem, or process implementation. Generic implementation areas apply only when their own behavior or API changes. **Kinds on Issues.** Native Issue Type already owns that classification; duplicating it as a label creates drift. -**Exactly one area per pull request.** Coherent changes can materially affect several independent contracts, and dropping secondary areas hides affected scope. +**Exactly one area per pull request.** Coherent changes can materially affect several independent APIs or behaviors, and dropping secondary areas hides affected scope. ## Consequences diff --git a/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md b/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md index b1e3629fd9..855a2b98f4 100644 --- a/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md @@ -27,18 +27,18 @@ Issue 已有原生 Issue Type 和独立的来源分类体系。在这两类对 | `kind/cleanup` | 在保持行为不变的前提下,维护或简化实现或仓库流程。 | | `kind/dependency` | 在没有其他主导意图时更新依赖。 | -类型记录主导意图。配套测试、文档、清理或依赖调整不会盖过功能变更或缺陷修复这一主导意图。新增类型会改变这项分类约定,因此必须明确修改分类体系和政策。 +类型记录主导意图。配套测试、文档、清理或依赖调整不会盖过功能变更或缺陷修复这一主导意图。新增类型会改变这些分类规则,因此必须明确修改分类体系和政策。 仓库政策会拒绝不支持的 `kind/*` 值,并将统一过程中移除的所有别名列为保留名称:`kind/bug`、`kind/documentation`、`feature`、`bug-fix`、`doc`、`cleanup`、`testing`、`dependencies`、`ci`、`cli`、`llm` 和 `web-search`。精确保留这组已迁移的名称,可以防止过时的同义名称被重新创建成看似无关的管理用途标签。 ### 领域 -领域表示持久的语义领域,而不是临时专项、归属关系或偶然触及的每条路径。一项 PR 修改不同约定时带有多个领域标签,但不会用一个总括标签和一个较窄标签重复描述同一项约定。GitHub 上现行的 `area/*` 名称和说明定义当前清单;本记录定义选择规则,以及简短标签说明无法可靠容纳的非显然边界。 +领域表示持久的产品或工程主题,而不是临时专项、归属关系或偶然触及的每条路径。一项 PR 修改不同的行为或 API 时带有多个领域标签,但不会用一个总括标签和一个较窄标签重复描述同一项变更。GitHub 上现行的 `area/*` 名称和说明定义当前清单;本记录定义简短标签说明无法可靠容纳的选择情形。 - `area/web` 覆盖浏览器与 Electron 图形界面,`area/vscode` 覆盖编辑器扩展,`area/api` 覆盖跨界面协议与各语言 SDK。 - `area/planning` 覆盖目标、计划、待办和调度,`area/workflow` 则覆盖可执行工作流与后台任务运行时。 - `area/artifact` 有意合并产物、附件与多模态交付。只有当这些关注点再次需要独立评审或查询时,才有理由拆分标签。 -- `area/tools` 适用于通用注册表、schema 与执行约定。具体能力使用自身的领域标签,除非它还修改了这项通用约定。 +- `area/tools` 适用于通用注册表、schema 与执行约定。具体能力使用自身的领域标签,除非它还修改了其中一项约定。 - `area/hooks` 表示 Claude Code 与 Codex 桥接,`area/infra` 覆盖构建、发布、CI、仓库门禁、生成器、依赖与开发者工具,`area/windows` 覆盖原生 Windows 产品支持,而不是 CI runner 的选型。 领域集合有意保持可扩展。当现有说明都无法如实涵盖一个持久且可复用的领域时,agent(智能体)无需另行批准,即可创建一个简洁的 `area/` 标签。agent 不得为单个 PR、偶然涉及的路径、临时项目、状态、个人或团队创建领域,并且必须在应用新标签后向请求者报告该标签及理由。仅为避免新增一个确有必要的领域标签而复用不准确的领域,不可接受。 @@ -61,11 +61,11 @@ Issue 使用原生 Issue Type,而不是 `kind/*`;其 `area/*` 标签仍然 **为每种交付载体或媒体生命周期单设标签。** 浏览器与 Electron 交付共用一个图形界面领域,产物、附件与多模态交付目前也共用一个评审/查询领域。只有当拆分能恢复有用的独立分类时,才应在后续分类体系变更中进行。 -**用宽泛的实现标签取代语义领域。** 一项具体能力并不只是其工具、接口、文件系统或进程实现。通用实现领域只在其自身约定变化时适用。 +**用宽泛的实现标签取代产品或工程主题。** 一项具体能力并不只是其工具、接口、文件系统或进程实现。通用实现领域只在其自身行为或 API 变化时适用。 **在 Issue 上使用类型标签。** 原生 Issue Type 已负责这项分类;再用标签复制会造成漂移。 -**每个 PR 恰好一个领域。** 内聚的变更可能对多个独立约定产生实质影响,丢弃次要领域会隐藏受影响范围。 +**每个 PR 恰好一个领域。** 内聚的变更可能对多个独立 API 或行为产生实质影响,丢弃次要领域会隐藏受影响范围。 ## 后果 diff --git a/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.i18n.yaml b/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.i18n.yaml index 408f164e06..9997e7252d 100644 --- a/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.md -2026-08-09-chinese-contract-terminology.md: d2fea45c6864a19b89c78a52f614ccac341a0127 -2026-08-09-chinese-contract-terminology.zh.md: 772b78daba00c5d881fd0091951557dfc0951ee9 +2026-08-09-chinese-contract-terminology.md: fa9e3ab91133a995a63f817816c01b59d28f66e8 +2026-08-09-chinese-contract-terminology.zh.md: 299e9ac9cdb2b8166d2364723b92f878b15cfd66 diff --git a/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.md b/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.md index d2fea45c68..fa9e3ab911 100644 --- a/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.md +++ b/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.md @@ -14,7 +14,7 @@ English `convention` also commonly renders as `约定`. That overlap is intentio The terminology source of truth defines `contract` as `约定` and `adapter contract` as `适配器约定(adapter contract)` on first mention. Every active Chinese documentation pair follows that ruling; archived Agent Notes remain frozen. Unpaired bilingual calibration assets and the translation prompt's explanatory prose follow the same terms so they cannot teach the superseded rendering. -The migration is semantic prose maintenance, not a rename of identifiers. Inline code, file paths, links, API names, English filenames containing `contract`, and machine-readable values remain unchanged. `convention` does not receive a global terminology row or corpus-wide rewrite: translators preserve natural Chinese and explicitly disambiguate only where the source contrasts the two concepts. +The migration is semantic prose maintenance, not a rename of identifiers. Inline code, file paths, links, API names, English filenames containing `contract`, and machine-readable values remain unchanged. `convention` does not receive a global terminology row or corpus-wide rewrite: translators preserve natural Chinese and explicitly disambiguate only where the source contrasts the two concepts. The [concrete prose decision](2026-08-09-concrete-prose-names-actors-and-recorded-facts.md) separately decides when English prose should replace a vague `contract` use with the exact rule, API, or behavior before translation. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.zh.md b/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.zh.md index 772b78daba..299e9ac9cd 100644 --- a/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.zh.md +++ b/.agents/notes/implemented/process/2026-08-09-chinese-contract-terminology.zh.md @@ -14,7 +14,7 @@ Status: implemented 术语真源规定 `contract` 译为「约定」,`adapter contract` 首次出现时写作「适配器约定(adapter contract)」。所有活跃中文文档配对均遵循该裁决;归档 Agent Note 保持冻结。未参与配对的双语校准资产和翻译提示词说明文字也采用相同术语,避免继续教授已被取代的译法。 -这次迁移只维护语义正文,不重命名标识符。行内代码、文件路径、链接、API 名称、文件名中包含的英文 `contract` 以及机器可读值均保持不变。`convention` 不新增全局术语行,也不做全语料改写:翻译时保留自然中文,只在源文明确对比两个概念时消歧。 +这次迁移只维护语义正文,不重命名标识符。行内代码、文件路径、链接、API 名称、文件名中包含的英文 `contract` 以及机器可读值均保持不变。`convention` 不新增全局术语行,也不做全语料改写:翻译时保留自然中文,只在源文明确对比两个概念时消歧。[具体行文决策](2026-08-09-concrete-prose-names-actors-and-recorded-facts.md)另行规定:如果英文正文中的 `contract` 含糊不清,应在翻译前将其改为确切的规则、API 或行为。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.i18n.yaml b/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.i18n.yaml index 0813498ae6..4c9b955615 100644 --- a/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-08-09-committed-artifact-citations.md -2026-08-09-committed-artifact-citations.md: a578b1e32268af2669ee26c59d19c738bc9f707b -2026-08-09-committed-artifact-citations.zh.md: d525dc0f58884bdf808b597213a7e23c1d6a0301 +2026-08-09-committed-artifact-citations.md: 044f7683d51ebf2038f56d2b5a27755ecc9be6d5 +2026-08-09-committed-artifact-citations.zh.md: 7f194aef45710e2f24e462c99877ec2e112f9981 diff --git a/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.md b/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.md index a578b1e322..044f7683d5 100644 --- a/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.md +++ b/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.md @@ -18,12 +18,12 @@ Durable prose — comments, JSDoc, docs, notes, test comments and titles — cit - Implemented notes state shipped reality: a "deferred to a later PR" claim whose target shipped names the shipped note instead. - Recorded fixtures, snapshots, and archived notes are exempt: recorded model output and sealed history keep their original voice. Inside a note's change-story sections, a historical stage name ("the first cut shipped X") is current-state-safe; indexical stamps ("this cut") stay banned everywhere. -One repo-wide purge applied these rules across the prose surfaces, including the generator-owned templates (`scripts/gen-doc-graphs.ts`, `scripts/gen-tool-catalog.ts`, the typert generator's page notice) with regeneration, the type-equiv source JSDoc with page re-pastes, and the bilingual counterparts with pair re-records. The [dsh-trim-cot-leakage skill](../../../skills/dsh-trim-cot-leakage/SKILL.md) operationalizes these rules: the audit taxonomy, the committed recall batteries, and few-shot calibration for the keep/delete boundary. +One repo-wide purge applied these rules across the prose surfaces, including the generator-owned templates (`scripts/gen-doc-graphs.ts`, `scripts/gen-tool-catalog.ts`, the typert generator's page notice) with regeneration, the type-equiv source JSDoc with page re-pastes, and the bilingual counterparts with pair re-records. The [dsh-trim-cot-leakage skill](../../../skills/dsh-trim-cot-leakage/SKILL.md) operationalizes these rules: the audit taxonomy, the committed recall batteries, and few-shot examples for deciding what to keep or delete. ## Alternatives considered - **Commit the design ledgers and audit documents so the ordinals resolve.** Rejected: session transcripts are working artifacts, not maintained references; committing them would create a parallel, ungated decision corpus beside Agent Notes, and their internal numbering would still drift. -- **A mechanical gate for the banned vocabulary.** Deferred: the vocabulary is unbounded natural language, and the audit's recall batteries need judgment to separate leakage from legitimate prose ("wait" the noun, contrastive "actually", runtime old/new states). A narrow high-precision gate (for example `\(decision \d`, `\(audit [A-Z]\d`, `\bcut \d`, `this cut`, a bare `\bT\d\b`, `P-I`, `used to `, a bare `\bv1\b`, and `§\d` — the last excluding citations whose section numbering has a committed owner, such as web-styling.md's own §N) is the candidate if the pattern recurs; review of the purge itself caught residuals in exactly these post-battery shapes, so they lead the candidate list. +- **A mechanical gate for the banned vocabulary.** Deferred: the vocabulary is unbounded natural language, and the audit's recall batteries need judgment to separate leakage from legitimate prose ("wait" the noun, contrastive "actually", runtime old/new states). A narrow high-precision gate (for example `\(decision \d`, `\(audit [A-Z]\d`, `\bcut \d`, `this cut`, a bare `\bT\d\b`, `P-I`, `used to `, a bare `\bv1\b`, and `§\d` — the last excluding citations whose section numbering has a committed owner, such as web-styling.md's own §N) is the candidate if the pattern recurs; review of the purge itself caught residuals in exactly the cases those searches missed, so they lead the candidate list. - **Delete the rationale that cited dead artifacts.** Rejected: the factual clauses were preserved or restated; only citations, review choreography, and derivation transcripts were removed, per the prose standard's complete-proposition rule. ## Verification diff --git a/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.zh.md b/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.zh.md index d525dc0f58..7f194aef45 100644 --- a/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.zh.md +++ b/.agents/notes/implemented/process/2026-08-09-committed-artifact-citations.zh.md @@ -18,12 +18,12 @@ Status: implemented - 已实现的 Agent Note 陈述已交付的现实:「推迟到后续 PR」的说法若其目标已经交付,就改为点名那篇已交付的 note。 - 已录制的 fixture(测试前置数据)、快照与已归档的 Agent Note 不受此约束:已录制的模型输出与封存的历史保持原有行文。在 note 的变更故事段落内,历史阶段名称(「首版交付了 X」)属于安全的现状表述;指示性切次戳("this cut")在任何地方都仍被禁止。 -一次全仓库清理把这些规则应用到了各个行文表面,包括生成器持有的模板(`scripts/gen-doc-graphs.ts`、`scripts/gen-tool-catalog.ts`、typert 生成器的页面提示语,改后重新生成)、type-equiv 源码 JSDoc(改后把文档页重新粘贴)以及双语对侧文件(改后重新记录配对)。[dsh-trim-cot-leakage 技能](../../../skills/dsh-trim-cot-leakage/SKILL.md)把这些规则落地为可执行工作流:审计分类法、已提交的成批召回检索,以及校准保留/删除边界的少样本示例。 +一次全仓库清理把这些规则应用到了各个行文表面,包括生成器持有的模板(`scripts/gen-doc-graphs.ts`、`scripts/gen-tool-catalog.ts`、typert 生成器的页面提示语,改后重新生成)、type-equiv 源码 JSDoc(改后把文档页重新粘贴)以及双语对侧文件(改后重新记录配对)。[dsh-trim-cot-leakage 技能](../../../skills/dsh-trim-cot-leakage/SKILL.md)把这些规则落地为可执行工作流:审计分类法、已提交的成批召回检索,以及用于判断保留或删除内容的少样本示例。 ## 曾考虑的替代方案 - **把设计台账与审计文档提交入库,让序号得以解析。**不予采纳:会话 transcript 是工作产物,不是持续维护的参考资料;提交它们会在 Agent Note 之外形成一套平行且不受门禁约束的决策语料,其内部编号也仍会漂移。 -- **为被禁词汇建一道机械门禁。**暂缓:这类词汇是无界的自然语言,审计中以查全为目标的成批检索需要人工判断,才能把泄漏与正当行文区分开(作名词的「wait」、表转折的「actually」、运行时的新旧状态)。若该模式再次出现,候选方案是一道窄而高查准的门禁(例如 `\(decision \d`、`\(audit [A-Z]\d`、`\bcut \d`、`this cut`、裸 `\bT\d\b`、`P-I`、`used to `、裸 `\bv1\b` 与 `§\d`——最后一种需排除章节编号有已提交归属的引用,如 web-styling.md 自身的 §N);对本次清扫自身的评审恰好在这些电池之外的形态中发现残留,因此它们位居候选清单之首。 +- **为被禁词汇建一道机械门禁。**暂缓:这类词汇是无界的自然语言,审计中以查全为目标的成批检索需要人工判断,才能把泄漏与正当行文区分开(作名词的「wait」、表转折的「actually」、运行时的新旧状态)。若该模式再次出现,候选方案是一道窄而高查准的门禁(例如 `\(decision \d`、`\(audit [A-Z]\d`、`\bcut \d`、`this cut`、裸 `\bT\d\b`、`P-I`、`used to `、裸 `\bv1\b` 与 `§\d`——最后一种需排除章节编号有已提交归属的引用,如 web-styling.md 自身的 §N);对本次清扫自身的评审恰好在这些检索未覆盖的案例中发现残留,因此它们位居候选清单之首。 - **删除引用了失效产物的设计理由。**不予采纳:事实性语句都得到保留或改写;依行文标准的完整命题规则,删掉的只有引用、评审编排与推导过程记录。 ## 验证 diff --git a/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.i18n.yaml b/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.i18n.yaml index b18d088e03..8f9e929996 100644 --- a/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.i18n.yaml @@ -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 .agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md -2026-08-09-concrete-prose-names-actors-and-recorded-facts.md: efb6e9bcc6f1c817bb0be07ccd31bafc4b2ac3fb -2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md: 41b0ce82631536c53ea45d60c1405a8387885d0f +2026-08-09-concrete-prose-names-actors-and-recorded-facts.md: b7df5403ea11ff8ba32be0f9bede5a32d5bcd6ee +2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md: 51a6c42dc75ad04f7d35ef90dd0d54046585dd6d diff --git a/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md b/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md index efb6e9bcc6..b7df5403ea 100644 --- a/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md +++ b/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md @@ -18,11 +18,13 @@ The rule applies to Markdown, READMEs, active Agent Notes, JSDoc and comments, p Exact code identifiers, public APIs, durable fields, protocol members, type names, headings with external references, and filenames stay unchanged unless a coordinated contract rename is independently required. Surrounding prose explains their fields or behavior directly. Generated documents and catalogs update from their owning source. +Before using `contract`, `boundary`, or `shape`, writers check whether the sentence means a more specific rule, operation, data structure, field set, validation point, timing point, API, type, or failure condition. `Contract` remains correct for preconditions, postconditions, invariants, compatibility promises, and other obligations that callers, callees, implementers, providers, producers, or consumers rely on. `Boundary` remains correct for a literal security, trust, wire, process, serialization, transaction, or lifecycle division. `Shape` remains correct when the structural form itself is the subject and no narrower term such as fields, schema, type, union variant, file layout, or export form states the fact. Code and API names containing these words remain unchanged unless a separate coordinated rename is required. + This decision complements the [documentation tiers and budgets](2026-07-04-doc-tiers-and-budgets.md) decision, which continues to own placement, document form, and word budgets. ## Alternatives considered -**Ban a fixed list of words.** Rejected because a word may be an exact identifier or the clearest term in another contract. Sentence-level review catches ambiguity without rejecting valid names. +**Ban a fixed list of words.** Rejected because a word may be an exact identifier or the clearest term in another contract. For example, caller/callee invariants are real contracts, and process or wire boundaries identify real divisions. Sentence-level review catches ambiguity without rejecting valid names. **Replace every abstract label with “source,” “origin,” or “metadata.”** Rejected because another broad label still leaves readers to infer whether the sentence means a file, caller, event seq, provider/model pair, commit, or build job. diff --git a/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md b/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md index 41b0ce8263..51a6c42dc7 100644 --- a/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md +++ b/.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md @@ -18,11 +18,13 @@ Status: implemented 除非另一项独立需求明确要求协调重命名约定,否则确切的代码标识符、公开 API、持久字段、协议成员、类型名、带有外部引用的标题和文件名均保持不变。它们周围的行文直接说明其字段或行为。生成的文档和目录在维护它们的源文件修改后更新。 +使用 `contract`、`boundary` 或 `shape` 之前,写作者要确认句子是否实际指更具体的规则、操作、数据结构、字段集合、校验点、时间点、API、类型或失败条件。调用方、被调用方、实现方、提供方、生产方或消费方依赖的前置条件、后置条件、不变量、兼容性承诺及其他义务仍可准确称为 `contract`。真实的安全、信任、wire、进程、序列化、事务或生命周期分界仍可准确称为 `boundary`。当结构形式本身就是主题,且字段、schema、类型、联合变体、文件布局或导出形式等更窄的词无法说明事实时,仍可使用 `shape`。除非另一项独立需求要求协调重命名,否则包含这些词的代码和 API 名称保持不变。 + 该决策补充了[文档层级与字数预算](2026-07-04-doc-tiers-and-budgets.md)决策;后者继续规定内容位置、文档形式和字数预算。 ## 曾考虑的替代方案 -**禁止一份固定词表中的所有词。** 不予采纳:某个词可能是确切的标识符,也可能是另一项约定中最清楚的用词。逐句审查可以找出歧义,且不会拒绝有效名称。 +**禁止一份固定词表中的所有词。** 不予采纳:某个词可能是确切的标识符,也可能是另一项约定中最清楚的用词。例如,调用方与被调用方依赖的不变量属于真实 contract,进程或 wire boundary 也表示真实分界。逐句审查可以找出歧义,且不会拒绝有效名称。 **将每个抽象名称都替换为“来源”、“起源”或“元数据”。** 不予采纳:另一个宽泛名称仍会让读者自行推测句子指的是文件、调用方、事件 seq、提供方/模型组合、commit 还是构建任务。 diff --git a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml index 7c4e1fac3d..6ab79c8447 100644 --- a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml @@ -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 .agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md -2026-07-29-shared-base-config-overlays.md: b2ca5cc8d3a160dd09550827d5f8148c14048ea4 -2026-07-29-shared-base-config-overlays.zh.md: adec7c793839e84edb14173d71735d264cf2ec28 +2026-07-29-shared-base-config-overlays.md: bbffcda725e3e2c5c0d0ce56a4ee0f1f61559806 +2026-07-29-shared-base-config-overlays.zh.md: e883ee2c5fd5fc887690e85653a9fc07408b6c6f diff --git a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md index b2ca5cc8d3..bbffcda725 100644 --- a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md +++ b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md @@ -22,7 +22,7 @@ Precedence is list order, last write winning per row: base, then the surface ove `--config ` now applies an overlay **instead of** the personal overlay, so a demo or test tree never inherits the user's provider and model. `--config-replace ` boots a file as the entire tree, bypassing base, surface overlay, and personal overlay alike; that is what the old `--config` did, so trees like `examples/web-cordis` moved to the new flag. Both flags survive the `/resume` execve handoff, or resuming would silently change the agent. -A patch replaces its target row's whole `config` rather than merging, which shapes the split: a row whose value differs per surface lives in the overlays, never in the base, so no row is patched by three layers at once. Session identity therefore cannot ride a config key at all — it moved to `dsh-agent-loop`'s `CONFIGURED_AGENT_IDENTITIES_KEY`, as the launcher-owned identity record documented. +A patch replaces its target row's whole `config` rather than merging. Therefore, a row whose value differs per surface lives in the overlays, never in the base, so no row is patched by three layers at once. Session identity cannot ride a config key at all — it moved to `dsh-agent-loop`'s `CONFIGURED_AGENT_IDENTITIES_KEY`, as the launcher-owned identity record documented. `examples/tui-agent`, `examples/cordis-agent`, `examples/code-mode`, and `packages/examples/tui-demo` are deleted. The TUI tests move to `apps/cli/tests/`, the cordis-toolset e2e to `packages/self-modification/tool-cordis/tests/`, and the supported Code Mode demo remains the ACP overlay at `examples/acp-agent/code-mode.cordis.yml`. diff --git a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md index adec7c7938..e883ee2c5f 100644 --- a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md @@ -22,7 +22,7 @@ Status: implemented `--config ` 现在应用一个 overlay 来**取代**个人 overlay,因此 demo 或测试用的树绝不会继承用户的提供方与 model。`--config-replace ` 则把某个文件作为整棵树启动,同时绕过 base、surface overlay 与个人 overlay;这正是旧 `--config` 的行为,所以像 `examples/web-cordis` 这样的树改用了新 flag。两个 flag 都会在 `/resume` 的 execve 交接中保留,否则恢复时会静默更换 agent(智能体)。 -patch 会整体替换目标配置项的 `config` 而不合并,这决定了拆分方式:取值因 surface 而异的配置项住在 overlay 中,绝不住在 base 里,从而没有任何配置项会被三层同时 patch。因此会话身份根本不能经由配置键传递——它迁移到了 `dsh-agent-loop` 的 `CONFIGURED_AGENT_IDENTITIES_KEY`,正如启动器持有身份的记录所述。 +patch 会整体替换目标配置项的 `config` 而不合并。因此,取值因 surface 而异的配置项住在 overlay 中,绝不住在 base 里,从而没有任何配置项会被三层同时 patch。会话身份根本不能经由配置键传递——它迁移到了 `dsh-agent-loop` 的 `CONFIGURED_AGENT_IDENTITIES_KEY`,正如启动器持有身份的记录所述。 `examples/tui-agent`、`examples/cordis-agent`、`examples/code-mode` 与 `packages/examples/tui-demo` 均被删除。TUI 测试迁往 `apps/cli/tests/`,cordis 工具集的 e2e 迁入 `packages/self-modification/tool-cordis/tests/`,受支持的 Code Mode demo 则保留为 `examples/acp-agent/code-mode.cordis.yml` 中的 ACP(Agent Client Protocol)overlay。 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml index 8fd7d1dd91..b062440270 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml @@ -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 .agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md -2026-07-28-storage-root-and-derived-medium-recovery.md: 45e8b3dfcad0590a26581e20c7bc6bb5692e5c36 -2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 330473b2b17669e373a6dc433a74d8bb3ba37628 +2026-07-28-storage-root-and-derived-medium-recovery.md: 9463af00e2a2f5e2d3cee4cc7d386173007abf45 +2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 215e43fac6bb52e1ecaf15d6d9d1ce1b5f913bc3 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md index 45e8b3dfca..9463af00e2 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md @@ -35,7 +35,7 @@ Two independent changes, one per gap. **Launcher patch + a `storageRoot` profile key** — not taken: one `!!js` yml expression reaches the global root with the same layering the session root already has; a launcher patch adds a second rewrite point, and the profile key is an empty seat until a real consumer exists (per-row overrides already have the personal config.yaml patch layer). -**Patch only the projection cache's route to a global root, leave `workspace.json` per-cwd** — rejected: the workspace registry has the identical global-vs-cwd mismatch, and the user decision that shaped the cache placed it deliberately beside `workspace.json` — one hub root keeps the media co-located and the mental model single. +**Patch only the projection cache's route to a global root, leave `workspace.json` per-cwd** — rejected: the workspace registry has the identical global-vs-cwd mismatch, and the user chose to place the cache beside `workspace.json` — one hub root keeps the media co-located and the mental model single. **Cache-plugin-local recovery (catch damage errors in `SessionProjectionCache[Service.init]`, delete the file, reopen)** — rejected: the plugin cannot name the medium path without reaching around the backend abstraction, and every future derived domain would re-implement the same catch; the facility is the one place that already classifies open failures. diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md index 330473b2b1..215e43fac6 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md @@ -35,7 +35,7 @@ Status: proposed **launcher patch + `storageRoot` profile 键**——未采:一行 yml `!!js` 表达式即达全局根,与会话根的既有分层完全一致;launcher patch 多引入一个改写点,profile 键在有真实消费者前是空席(按行覆盖已有个人 config.yaml patch 层可用)。 -**只把投影缓存的 route 指到全局根,`workspace.json` 留在 per-cwd**——拒绝:工作区注册表有一模一样的全局 vs per-cwd 错位,而且塑造缓存的用户决策就是刻意把它放在 `workspace.json` 旁边——一个 hub 根让介质同址、心智模型单一。 +**只把投影缓存的 route 指到全局根,`workspace.json` 留在 per-cwd**——拒绝:工作区注册表有一模一样的全局 vs per-cwd 错位,而且用户选择把缓存放在 `workspace.json` 旁边——一个 hub 根让介质同址、心智模型单一。 **缓存插件本地恢复(在 `SessionProjectionCache[Service.init]` 捕获损坏错误、删文件、重开)**——拒绝:插件不越过后端抽象就叫不出介质路径,且未来每个派生域都要重抄同一段 catch;facility 是唯一已经在分类 open 失败的地方。 diff --git a/.agents/skills/dsh-archive-agent-notes/SKILL.md b/.agents/skills/dsh-archive-agent-notes/SKILL.md index e4df7e1fe7..ef20de8097 100644 --- a/.agents/skills/dsh-archive-agent-notes/SKILL.md +++ b/.agents/skills/dsh-archive-agent-notes/SKILL.md @@ -1,6 +1,6 @@ --- name: dsh-archive-agent-notes -description: Use when adding, auditing, pruning, archiving, restoring, or reviewing Agent Notes in deepseek-harness; checks every new note for superseded active records, classifies implemented notes by future decision value, deletes rejected notes that no longer prevent a tempting fallacy, and applies the frozen archived/{kind} triplet and manifest contract. +description: Use when adding, auditing, pruning, archiving, restoring, or reviewing Agent Notes in deepseek-harness; checks every new note for superseded active records, classifies implemented notes by future decision value, deletes rejected notes that no longer prevent a tempting fallacy, and applies the frozen archived/{kind} triplet and manifest rules. --- # Archive DeepSeek Harness Agent Notes @@ -9,18 +9,18 @@ Reduce the active decision corpus without erasing history that can still guide w ## Read the contracts -Read [the Agent Note contract](../../notes/README.md), [the archive instructions](../../notes/archived/AGENTS.md), and the applicable active lifecycle instructions before classifying. Use current code, configuration, package docs, generated catalogs, newer Agent Notes, and inbound links to establish whether a rationale still owns or constrains anything. +Read [the Agent Note rules](../../notes/README.md), [the archive instructions](../../notes/archived/AGENTS.md), and the applicable active lifecycle instructions before classifying. Use current code, configuration, package docs, generated catalogs, newer Agent Notes, and inbound links to establish whether a rationale still owns or constrains anything. ## Check supersession when adding a note -Every new Agent Note triggers a scoped audit of active notes covering the same decision, mechanism, or rejected alternative. Classify each full or partial supersession while writing the new note: archive qualifying implemented triplets in the same PR, retain and cross-link partial supersessions or independently useful rationale, reject obsolete proposals, and delete rejected notes that no longer prevent a plausible mistake. Apply the Agent Note contract's consolidation rule when the new owner absorbs every unique proposition; do not defer a known match to a later corpus audit. +Every new Agent Note triggers a scoped audit of active notes covering the same decision, mechanism, or rejected alternative. Classify each full or partial supersession while writing the new note: archive qualifying implemented triplets in the same PR, retain and cross-link partial supersessions or independently useful rationale, reject obsolete proposals, and delete rejected notes that no longer prevent a plausible mistake. Apply the Agent Note consolidation rule when the new owner absorbs every unique proposition; do not defer a known match to a later corpus audit. ## Classify by future value Apply these lifecycle-specific outcomes: - **Implemented — keep active:** retain a note when its rationale, alternatives, negative guarantees, durable/wire semantics, ownership boundary, security rule, or reintroduction condition is likely to guide a future change. Length does not matter. -- **Implemented — archive:** archive a note when the shipped decision is complete and its body is unlikely to guide future work, such as one-off UI chrome, a narrow adapter, a minor closed bug, superseded implementation detail, or process history whose current contract is obvious elsewhere. +- **Implemented — archive:** archive a note when the shipped decision is complete and its body is unlikely to guide future work, such as one-off UI chrome, a narrow adapter, a minor closed bug, superseded implementation detail, or process history whose current behavior is obvious elsewhere. - **Proposed — never archive:** keep a live proposal active; if it is no longer worth pursuing, reject it with an honest reason and satisfy the rejected lifecycle format. - **Rejected — keep only as a guardrail:** retain a rejection only when the losing proposal remains a tempting, meaningful mistake and the note explains why it loses. - **Rejected — delete:** delete the whole triplet when the rejected idea is obsolete, superseded, no longer plausible, or unlikely to prevent re-litigation. Repair or delete inbound links. @@ -47,7 +47,7 @@ Keep implemented notes such as: For rejected notes: -- keep folding the compaction package split — 426 words: the package-boundary temptation remains meaningful; +- keep folding the compaction package split — 426 words: the temptation to merge the packages remains meaningful; - delete streaming workflow progress through tool calls — 972 words: its ACP/UI premise is obsolete; - delete dropping ACP terminal metadata — 362 words: the later automation-only ACP decision resolved the question. @@ -65,4 +65,4 @@ After the triplet is sealed, never edit, move, translate, reformat, or delete it Run the archive verifier's focused test, `pnpm run verify-archived-agent-notes`, `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`; select any additional evidence through [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md). -Report active implemented notes kept, implemented notes archived, rejected notes kept/deleted, proposed notes rejected if any, and every genuinely borderline case with its word count and chosen outcome. Do not claim archived outbound links are valid: the contract intentionally never checks them. +Report active implemented notes kept, implemented notes archived, rejected notes kept/deleted, proposed notes rejected if any, and every genuinely borderline case with its word count and chosen outcome. Do not claim archived outbound links are valid: the archive verifier intentionally never checks them. diff --git a/.agents/skills/dsh-code-review/SKILL.md b/.agents/skills/dsh-code-review/SKILL.md index c8a52f5d50..572c36a8a1 100644 --- a/.agents/skills/dsh-code-review/SKILL.md +++ b/.agents/skills/dsh-code-review/SKILL.md @@ -5,11 +5,11 @@ description: Use when reviewing a pull request in the deepseek-harness repo — # Reviewing a DeepSeek-Harness PR -**This skill is guidance, not a complete checklist.** Verify and fetch the PR's live base and exact head, then run `pnpm --silent run change-scope --base --head ` before reading the diff and enough surrounding code to understand the design. The report identifies paths and dirty layers but does not replace semantic review. Re-establish the base and rerun it after a retarget or merge. Prioritize correctness, lifecycle, security, and contract failures over style; a short review with one substantiated blocker is better than a list of nits. +**This skill is guidance, not a complete checklist.** Verify and fetch the PR's live base and exact head, then run `pnpm --silent run change-scope --base --head ` before reading the diff and enough surrounding code to understand the design. The report identifies paths and dirty layers but does not replace semantic review. Re-establish the base and rerun it after a retarget or merge. Prioritize correctness, lifecycle, security, and broken required behavior over style; a short review with one substantiated blocker is better than a list of nits. ## Sources of truth -- [AGENTS.md](../../../AGENTS.md) and [packages/AGENTS.md](../../../packages/AGENTS.md): standing repository and package authoring contracts. +- [AGENTS.md](../../../AGENTS.md) and [packages/AGENTS.md](../../../packages/AGENTS.md): standing repository and package authoring rules. - [docs/defensive-patterns.md](../../../docs/defensive-patterns.md): subprocess, callback, async-state, and disposal bug classes. - [docs/AGENTS.md](../../../docs/AGENTS.md): documentation placement and prose discipline. - [dsh-prose-standard](../dsh-prose-standard/SKILL.md): required coverage and editorial judgment for comments, docs, prompts, and visible strings. @@ -22,22 +22,22 @@ description: Use when reviewing a pull request in the deepseek-harness repo — 1. **New prose receives semantic review.** Use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) to critically review every added or changed Markdown passage, JSDoc, comment, prompt, description, diagnostic, and visible string. Verify required coverage, accuracy, placement, and editorial quality against the owning code or behavior; automated checks do not establish those properties. 2. **Docs match the code.** Config, defaults, errors, wire fields, events, and public behavior update the package README and JSDoc in the same diff. Comments state non-obvious contracts; flag implementation narration, test walkthroughs, review history, and duplicated rationale for deletion or a link to their one home. 3. **Core type docs match.** Changes to spine or seam vocabulary update the appropriate [subsystems](../../../docs/subsystems/README.md) page and any `type-equiv` entry. Internal types need no catalog entry. -4. **Registrations clean up.** Verify each new registry contribution satisfies the disposal-test contract in [packages/AGENTS.md](../../../packages/AGENTS.md). -5. **Invariant companions are semantic.** For every touched `./invariant`, require an owner event-stream or mutable-data relationship at its authoritative boundary; service or method presence, plugin metadata or effects, and fixed pure examples belong in type, load, or unit tests. Accept an empty installer when its package-specific reason establishes that no plausible runtime relationship exists; do not demand an invented check merely to eliminate emptiness ([repository rule](../../../AGENTS.md#conventions); [package contract](../../../packages/AGENTS.md)). +4. **Registrations clean up.** Verify each new registry contribution passes the disposal tests required by [packages/AGENTS.md](../../../packages/AGENTS.md). +5. **Invariant companions are semantic.** For every touched `./invariant`, require an owner event-stream or mutable-data relationship at the point where that package can observe it; service or method presence, plugin metadata or effects, and fixed pure examples belong in type, load, or unit tests. Accept an empty installer when its package-specific reason establishes that no plausible runtime relationship exists; do not demand an invented check merely to eliminate emptiness ([repository rule](../../../AGENTS.md#conventions); [package invariant rules](../../../packages/AGENTS.md)). 6. **Required evidence exists.** Verify the author ran the [relevant local checks](../../../AGENTS.md#run-relevant-checks-locally) for the diff and that CI covers the exhaustive matrix; review the semantic gaps neither can detect. ## Manual checks -- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any Agent Note, including errors, cancellation, ownership, and disposal. +- **Intent and interface contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any Agent Note, including errors, cancellation, ownership, and disposal. - **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns.md](../../../docs/defensive-patterns.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, ownership before reentry, complete detach cleanup, and quiescent disposal. -- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package contract](../../../packages/AGENTS.md). Flag the inverse too: a new public method on a generic service (registry, session, agent) whose only caller is one internal consumer is an ad-hoc surface widening — require a private capability closure handed to that consumer at construction instead. -- **Scope, ownership, and necessity:** map each abstraction, state machine, option, defensive copy, and compatibility path to its current contract, production consumer, and owning plugin or service. Challenge unrelated features and speculative generality, then test the PR's coherence against [the root contract](../../../AGENTS.md#conventions). +- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package rules](../../../packages/AGENTS.md). Flag the inverse too: a new public method on a generic service (registry, session, agent) whose only caller is one internal consumer is an unnecessary API expansion — require a private capability closure handed to that consumer at construction instead. +- **Scope, ownership, and necessity:** map each abstraction, state machine, option, defensive copy, and compatibility path to its current contract, production consumer, and owning plugin or service. Challenge unrelated features and speculative generality, then test the PR against [the root rules](../../../AGENTS.md#conventions). - **Configuration and public choices:** ask what current-consumer evidence or prior art supports each default, public operation set, format, or imported external concept. Require an explicit choice or deferral when that evidence is absent. - **Model perspective:** inspect the exact prompts, tool schemas, results, and diagnostics the model receives across affected modes. Flag concepts outside the model's task, then verify stable text verbatim and dynamic behavior through snapshots or end-to-end coverage. -- **Enforcement boundaries:** follow every denial path to the operation that executes it; exercise direct and alternate callers that can bypass schemas, prompts, facades, wrappers, or listener ordering. -- **Borrowed and derived state:** classify each retained value under the package boundary contract, then trace notifications and every cache, prompt, UI echo, replay, and query view to the documented success point and authoritative source. +- **Enforcement:** follow every denial path to the operation that executes it; exercise direct and alternate callers that can bypass schemas, prompts, facades, wrappers, or listener ordering. +- **Borrowed and derived state:** determine whether each retained value is borrowed or owned under the package contract, then trace notifications and every cache, prompt, UI echo, replay, and query view to the documented success point and authoritative source. - **Bounds cover the final operation:** locate the owner of the complete emitted or retained result, including wrappers and metadata. Probe tiny and exact limits, oversized single chunks, and multibyte text for byte limits. -- **Real entry path:** tests exercise the shipped Loader, bin, worker, ACP bridge, or subprocess where relevant. A hand-mounted plugin does not catch Loader export-shape failures; a function plugin must named-export its namespace and have no default export. +- **Real entry path:** tests exercise the shipped Loader, bin, worker, ACP bridge, or subprocess where relevant. A hand-mounted plugin does not catch invalid Loader exports; a function plugin must named-export its namespace and have no default export. - **Test strength:** assertions fail on the intended regression and verify external state, logs, events, or disposal rather than restating the implementation or trusting an agent's report. Coverage is necessary but not evidence that the scenario is correct. - **Invariant lifecycle and negative controls:** verify candidate observations are rejected before publication where possible, session-backed checks reconstruct durable history after late loading or HMR, and a deliberately invalid case fails through the real runner for the intended rule. - **Implemented Agent Notes match shipped reality:** when a PR implements a proposed Agent Note, move and rewrite it as present-tense shipped state in the same diff, then verify paths, names, and mechanisms against the implementation. diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md index 3f93a6560a..5d118f2257 100644 --- a/.agents/skills/dsh-doc-site-sync/SKILL.md +++ b/.agents/skills/dsh-doc-site-sync/SKILL.md @@ -38,7 +38,7 @@ Set every `DocsPage` field deliberately: - `order`: stable order within the section. - `sourceAliases`: optional additional repository paths that should resolve to this page when links are projected. It does not create another public route. -Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary. +Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly expands what the site publishes. ## Preserve link behavior diff --git a/.agents/skills/dsh-doc-standards/SKILL.md b/.agents/skills/dsh-doc-standards/SKILL.md index dcbf2f2b21..a42c27cfda 100644 --- a/.agents/skills/dsh-doc-standards/SKILL.md +++ b/.agents/skills/dsh-doc-standards/SKILL.md @@ -5,25 +5,25 @@ description: 'Use when writing, moving, reviewing, or auditing documentation in # Applying the DeepSeek Harness Documentation Standard -The contract lives in [docs/AGENTS.md](../../../docs/AGENTS.md). This workflow covers placement, corpus audits, budgets, and validation across Markdown, JSDoc, and code comments. It is guidance, not a script; use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) for required coverage and editorial judgment, and never treat length alone as a defect. +The documentation rules live in [docs/AGENTS.md](../../../docs/AGENTS.md). This workflow covers placement, corpus audits, budgets, and validation across Markdown, JSDoc, and code comments. It is guidance, not a script; use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) for required coverage and editorial judgment, and never treat length alone as a defect. ## Sources of truth (read, don't re-summarize) - [docs/AGENTS.md](../../../docs/AGENTS.md) — hierarchy, tutorial/reference forms, taxonomy, budgets, and slop checklist. - [.agents/notes/README.md](../../notes/README.md) — when a decision earns an Agent Note, how to file it, and what goes inside one (the header block, per-lifecycle skeleton, and Alternatives-considered mandate, gated by `verify-agent-note-format`); [docs/postmortem/README.md](../../../docs/postmortem/README.md) — when an incident earns a postmortem. -- [docs/i18n/README.md](../../../docs/i18n/README.md) — the bilingual pairing contract; editing either side of a pair obligates the counterpart in the same change. +- [docs/i18n/README.md](../../../docs/i18n/README.md) — the bilingual pairing rules; editing either side of a pair obligates the counterpart in the same change. - Root [AGENTS.md](../../../AGENTS.md) — the standing orders whose budget discipline this skill protects. - [Archived Agent Notes](../../notes/archived/AGENTS.md) — frozen historical snapshots excluded from editorial maintenance and evolving documentation gates. ## Review structure before prose -Apply the standard's authoring order to every human-facing document in scope. Do not apply this structural pass to Agent Notes. Classify a postmortem as a reference scoped to one incident; preserve the chronological evidence required by its contract without treating chronology as a teaching sequence. +Apply the standard's authoring order to every human-facing document in scope. Do not apply this structural pass to Agent Notes. Classify a postmortem as a reference scoped to one incident; preserve its required chronological evidence without treating chronology as a teaching sequence. 1. Locate the document in the repository and navigation trees. State its own subject and identify its direct children. -2. Set the detail boundary. Keep full detail about the document's subject, summarize direct children by purpose, responsibility, and high-level behavior, and move deeper explanations to their owning descendants with links. Treat test infrastructure as descendant-owned unless it is the document's subject. +2. Set the permitted level of detail. Keep full detail about the document's subject, summarize direct children by purpose, responsibility, and high-level behavior, and move deeper explanations to their owning descendants with links. Treat test infrastructure as descendant-owned unless it is the document's subject. 3. Classify the document from its intended use, not its path or title. A tutorial must lead through ordered work to an observable outcome; a reference must support lookup within an explicit scope without requiring sequential reading. 4. For a tutorial, privately classify the starting reader and concepts as beginner, intermediate, or advanced. Trace each concept to its prerequisites, reorder premature material, and move optional advanced detail to a later tutorial or reference. -5. Split substantial mixed forms. Keep a small secondary form only behind a clear structural boundary. +5. Split substantial mixed forms. Put a small secondary form in a clearly labeled section. Then check constraints that make placement expensive or wrong: @@ -37,7 +37,7 @@ Then check constraints that make placement expensive or wrong: After the structural pass, hunt the standard's slop checklist with the cheapest probes first. Verify and fetch the PR's live base, then run `pnpm --silent run change-scope --base ` to identify committed and dirty paths before applying semantic judgment. After a retarget or base merge, rerun the report and audit prose introduced by the new base. 1. Measure: `pnpm run verify-doc-budgets --list`, then `git ls-files '*.md' ':(exclude)vendor/**' | xargs wc -w | sort -rn | head -30` to spot unbudgeted outliers. -2. Hunt reasoning-transcript leakage — narrated history, dead design-session citations, review choreography, control-flow narration, test walkthroughs — with [dsh-trim-cot-leakage](../dsh-trim-cot-leakage/SKILL.md), which owns the taxonomy, recall batteries, and the keep/delete boundary. Preserve only a non-obvious contract or durable rationale; the same rationale repeated beside sibling methods keeps one home. +2. Hunt reasoning-transcript leakage — narrated history, dead design-session citations, review choreography, control-flow narration, test walkthroughs — with [dsh-trim-cot-leakage](../dsh-trim-cot-leakage/SKILL.md), which defines the taxonomy, recall batteries, and rules for what to keep or delete. Preserve only a non-obvious contract or durable rationale; the same rationale repeated beside sibling methods keeps one home. 3. Hunt duplication by grepping distinctive phrases. Keep one home and replace other copies with links. 4. Replace hand-written catalogs, test/status inventories, and JSDoc restatements with the authoritative tree, script, or generated reference. 5. In `implemented/` Agent Notes, remove migration plans, acceptance-task checklists, and future-tense spec language. Keep concise verification contracts that identify the behaviors and tiers pinning the shipped decision, plus named coverage gaps. diff --git a/.agents/skills/dsh-find-simplifications/SKILL.md b/.agents/skills/dsh-find-simplifications/SKILL.md index 33e5b4fd88..88b025e298 100644 --- a/.agents/skills/dsh-find-simplifications/SKILL.md +++ b/.agents/skills/dsh-find-simplifications/SKILL.md @@ -11,19 +11,19 @@ This skill helps turn a broad "find things to simplify" request into evidence-ba - Read `AGENTS.md`, especially the pre-release stance and the conventions (including the tests-are-not-golden-truth and Agent Notes-are-not-golden-truth doctrines), plus [docs/defensive-patterns.md](../../../docs/defensive-patterns.md) and [docs/testing.md](../../../docs/testing.md). - Skim [docs/architecture.md](../../../docs/architecture.md) before judging anything under `packages/`; simplifications that fight the service map or event taxonomy need extra evidence. -- Use the Agent Note tree and its [contract](../../notes/README.md) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md), [shared persistence write coordinator](../../notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), [capability seams](../../notes/implemented/architecture/2026-06-13-capability-seams.md), and the twin adapter / dual persistence backend Agent Notes. +- Use the Agent Note tree and its [rules](../../notes/README.md) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md), [shared persistence write coordinator](../../notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), [capability seams](../../notes/implemented/architecture/2026-06-13-capability-seams.md), and the twin adapter / dual persistence backend Agent Notes. - Treat dual LLM adapters and dual persistence backends as intentional by default. Do not propose deleting either twin/backend as "low effort" unless the user explicitly overrides that constraint. Removing an unused method or hook inside a protected seam can still be valid if it does not collapse the protected design. ## What Counts As A Strong Candidate -A strong simplification removes, folds, or demotes something real and has clear evidence that the current shape costs more than it buys: +A strong simplification removes, folds, or demotes something real and has clear evidence that the current design costs more than it buys: - A public method, event, config knob, registry notification, helper, package, durable event, or test artifact has no production consumer. - Tests or docs are the only consumers, and the behavior they pin is not load-bearing. - Two representations mirror the same fact, especially across durable session events and transient `agent/*` events. - A seam has methods every implementation must support but no consumer uses. -- A package boundary exists only for test/demo/support code and adds publish or dependency overhead. -- A feature implements speculative product generality: multi-session/session-load, background task rosters, live registry invalidation, mid-turn steering, tool-owned UI rendering, and similar shapes with no product owner. +- A separate package exists only for test/demo/support code and adds publish or dependency overhead. +- A feature implements speculative product generality: multi-session/session-load, background task rosters, live registry invalidation, mid-turn steering, tool-owned UI rendering, and similar designs with no product owner. - An invariant, rollback path, set of expected outputs, or special-case test exists only to protect an unused surface. - Hand-rolled code reimplements what a well-maintained external package or a Node builtin at the engine floor already provides, and the swap would delete the implementation plus its dedicated tests ([dependency policy](../../notes/implemented/process/2026-07-26-dependencies-over-hand-rolling.md)). - The simplified behavior may differ slightly, but the new behavior is still reasonable and easier to explain. @@ -38,7 +38,7 @@ Use parallel subagents when the user asks for breadth or many candidates. Give e - ACP automation and human UI surfaces: prompt settlement and teardown on the protocol side; transcript rendering and interaction state on the UI side. - LLM/tools/system prompt: stream/generate surfaces, assemblers, registries, tool schema defaults, presentation hooks. - Bash and tool execution: foreground/background split, task ownership, output spill files, executor methods. -- Packages/examples/scripts/tests: package boundaries, static inventories, redundant snapshot expected outputs, support packages. +- Packages/examples/scripts/tests: package splits, static inventories, redundant snapshot expected outputs, support packages. If subagents are unavailable, simulate the same breadth yourself. Do not let the first good candidate stop the survey. @@ -46,9 +46,9 @@ Start with the largest production-code deltas. A broad simplification audit that ## Audit Trust And Lifecycle Boundaries -Classify every defensive copy, freeze, validator, and callback capture by the boundary it crosses. Same-process typed service/plugin calls ordinarily borrow readonly values; parser/config, queue, model/tool JSON, durable/file, worker, process, and wire boundaries own or validate data. Tests built around hostile getters, fake typed objects, callback replacement, or mutation after a same-process handoff are evidence of a potentially speculative contract, not automatic justification for keeping it. +For every defensive copy, freeze, validator, and callback capture, name where the value came from and who owns it next. Same-process typed service/plugin calls ordinarily borrow readonly values; parsers, config loaders, queues, model/tool JSON, durable files, workers, processes, and wire decoders own or validate their data. Tests built around hostile getters, fake typed objects, callback replacement, or mutation after a same-process handoff are evidence of a potentially speculative contract, not automatic justification for keeping it. -For complex asynchronous code, draw the ownership graph and map each sentinel, readiness promise, cancellation path, disposer, and state flag to a distinct owner or transition. When several mechanisms mirror the same liveness or settlement fact, propose one transaction or lifecycle controller instead. Preserve separate machinery where it protects a real boundary: synchronous publication and rollback, callback containment, first-terminal-outcome arbitration, worker/process ownership, or dispose-to-quiescence. +For complex asynchronous code, draw the ownership graph and map each sentinel, readiness promise, cancellation path, disposer, and state flag to a distinct owner or transition. When several mechanisms mirror the same liveness or settlement fact, propose one transaction or lifecycle controller instead. Preserve separate machinery where it protects synchronous publication and rollback, callback containment, first-terminal-outcome arbitration, worker/process ownership, or dispose-to-quiescence. ## Hand-Rolled Code Versus A Dependency @@ -75,7 +75,7 @@ Reject or downgrade a candidate when: - A production caller exists and the simplification would be a feature decision rather than a cleanup. - The surface is explicitly justified by an implemented Agent Note or a hard-won defensive pattern, and the new evidence does not beat that reason. -- The removal would force unrelated churn without actually making the contract smaller. +- The removal would force unrelated churn without actually reducing the public API or required behavior. - The idea is correct but tiny. Add a targeted TODO/FIXME/XXX instead, using the urgency semantics in [docs/development.md](../../../docs/development.md). ## Coalesce Superseded Agent Notes @@ -84,23 +84,23 @@ Audit the Agent Note tree when the user asks to reduce or coalesce it, or when t Use [`dsh-archive-agent-notes`](../dsh-archive-agent-notes/SKILL.md) for retention judgment and archive mechanics. Low-future-value implemented notes move as frozen triplets to `archived/{kind}`; proposed notes are never archived; rejected notes that no longer prevent a tempting mistake are deleted. Do not edit an archived note while simplifying current prose or code. -Follow the deletion rule in the [Agent Note contract](../../notes/README.md#when-to-write-one); do not duplicate or weaken it here. For each candidate chain: +Follow the deletion rule in the [Agent Note rules](../../notes/README.md#when-to-write-one); do not duplicate or weaken it here. For each candidate chain: 1. Identify the current owner from shipped code, configuration, generated catalogs, package docs, newer Agent Notes, and inbound links; dates and titles are discovery hints, not proof. 2. Classify the old note as fully or partially superseded. Any surviving behavior, current contract, durable format, compatibility obligation, or independently current rejected alternative makes it partial. Rationale that can be transferred to the current owner does not by itself make supersession partial. -3. For full supersession, move every unique rationale, alternative, consequence, shipped verification contract, and named coverage gap into the current owner. An inventory that only describes deleted implementation mechanics is not one of those decision facts. +3. For full supersession, move every unique rationale, alternative, consequence, shipped verification evidence, and named coverage gap into the current owner. An inventory that only describes deleted implementation mechanics is not one of those decision facts. 4. Repair every inbound link, then delete the English note, Chinese counterpart, and consistency record together. 5. Search exact filenames, symbols, config keys, event names, and wire strings after the edit. Keep partial supersessions cross-linked and current. -An added-then-removed feature is a common full-supersession case. Let the removal note own the history only when the feature is absent from production code, configuration, schemas, durable or wire formats, migration, and compatibility behavior; no current documentation presents it as available; and no test exercises it as supported behavior. Removal rationale and tests that enforce absence may remain. Preserve why the feature originally existed, why that motivation no longer justified it, alternatives to full removal, the capability given up, conditions for reintroduction, and evidence that removal is complete. Old tests and implementation mechanics that verified only the deleted behavior are not current verification contracts. +An added-then-removed feature is a common full-supersession case. Let the removal note own the history only when the feature is absent from production code, configuration, schemas, durable or wire formats, migration, and compatibility behavior; no current documentation presents it as available; and no test exercises it as supported behavior. Removal rationale and tests that enforce absence may remain. Preserve why the feature originally existed, why that motivation no longer justified it, alternatives to full removal, the capability given up, conditions for reintroduction, and evidence that removal is complete. Old tests and implementation mechanics that verified only the deleted behavior are not current verification evidence. Reject consolidation when the removal is only one transport, default, implementation, or presentation of a feature; when persisted data or compatibility handling survives; or when the removal note does not yet carry enough rationale to prevent accidental reintroduction. A current negative design decision may legitimately need its own note even though the removed implementation is gone. ## Write The Agent Note -Create one file per durable proposal under `.agents/notes///yyyy-mm-dd-topic.md`, following the lifecycle/classification contract in `.agents/notes/README.md`. Keep prose paragraphs on one physical line and use relative Markdown links. +Create one file per durable proposal under `.agents/notes///yyyy-mm-dd-topic.md`, following the lifecycle and classification rules in `.agents/notes/README.md`. Keep prose paragraphs on one physical line and use relative Markdown links. -Prefer this shape, adjusting when the idea needs it: +Prefer this structure, adjusting when the idea needs it: - `# Agent Note: ` - `Status: proposed` diff --git a/.agents/skills/dsh-prose-standard/SKILL.md b/.agents/skills/dsh-prose-standard/SKILL.md index 2381d459c2..6ad226baf4 100644 --- a/.agents/skills/dsh-prose-standard/SKILL.md +++ b/.agents/skills/dsh-prose-standard/SKILL.md @@ -5,7 +5,9 @@ description: Use when writing, reviewing, restoring, trimming, or auditing prose # DeepSeek Harness Prose Standard -Write enough to preserve the contract, then remove reasoning transcripts, repetition, and decoration. This skill owns editorial judgment and required prose coverage; use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) for placement, budgets, bilingual pairs, and documentation gates, and [dsh-trim-cot-leakage](../dsh-trim-cot-leakage/SKILL.md) for hunting and fixing reasoning-transcript leakage. It is guidance, not a script. +Write enough to preserve the contract, then remove reasoning transcripts, repetition, and decoration. A contract is an obligation, invariant, precondition, postcondition, or compatibility promise that a caller, callee, implementer, producer, or consumer relies on. This skill owns editorial judgment and required prose coverage; use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) for placement, budgets, bilingual pairs, and documentation gates, and [dsh-trim-cot-leakage](../dsh-trim-cot-leakage/SKILL.md) for hunting and fixing reasoning-transcript leakage. It is guidance, not a script. + +Treat `contract`, `boundary`, `shape`, `surface`, `seam`, `gate`, and `vocabulary` as terms to check before use, not banned words. First ask whether the exact rule, API, field set, type, validation, timing point, component split, or failure states the fact better. Keep a term when it names the exact technical subject, including caller/callee contracts and security/process boundaries. Comments describe non-obvious contracts or rationale that code cannot express; they do not restate what code already implies. @@ -45,14 +47,14 @@ This is not a one-way shortening pass. Add or restore prose when code, types, an - **Public JSDoc:** document caller-visible return distinctions, throws or rejections, side effects, ownership, timing, cancellation, and durability. - **Internal comments:** orient non-local structure and obviously complicated local structure, including invariants, race ordering, ownership, security boundaries, and surprising failure behavior. Delete control-flow narration and code restatement. -- **Module comments:** state the module's role, boundaries, and non-obvious architecture choices; link architecture choices to their owning explanation. +- **Module comments:** state the module's role, dependencies, responsibilities, and non-obvious architecture choices; link architecture choices to their owning explanation. - **Tests:** explain only non-obvious test design—why a fixture, assertion, platform accommodation, real entry path, or indirect observation is necessary. Delete walkthroughs and inventories. - **Cookbooks:** include prerequisites, required actions, the real entry path, observable verification, and concise warnings. -- **READMEs:** include the consumer contract: configuration, semantics, failures, limitations, extension points, and model-visible effects. Quote stable model-visible text owned by the package; link generated catalogs and cross-package owners. Keep durable gaps and maintainer traps, not ordinary cleanup inventories. Follow the [package README contract](../../../docs/cookbook/adding-a-package.md#4-write-the-package-readme). -- **Agent Notes:** retain unique rationale, mechanisms, alternatives, consequences, shipped verification contracts, and named coverage gaps. Implemented Agent Notes state shipped reality in the present tense; remove planning checklists, not evidence of what pins the decision. +- **READMEs:** include the consumer contract: configuration, semantics, failures, limitations, extension points, and model-visible effects. Quote stable model-visible text owned by the package; link generated catalogs and cross-package owners. Keep durable gaps and maintainer traps, not ordinary cleanup inventories. Follow the [package README requirements](../../../docs/cookbook/adding-a-package.md#4-write-the-package-readme). +- **Agent Notes:** retain unique rationale, mechanisms, alternatives, consequences, shipped verification evidence, and named coverage gaps. Implemented Agent Notes state shipped reality in the present tense; remove planning checklists, not evidence of what pins the decision. - **Postmortems:** retain the incident sequence, evidence, causal chain, impact, and prevention. Remove repeated persuasion or implementation detail that does not establish causality. - **Skills and agent instructions:** state behavioral guardrails and explicit scope limitations such as “guidance, not a script/checklist.” Keep the workflow concise and link its source of truth. -- **Examples and configuration comments:** explain boundaries, non-obvious wiring or load order, security stance, replay behavior, exceptions, and likely misuse. Do not narrate entries that the configuration already shows. +- **Examples and configuration comments:** explain access limits, non-obvious wiring or load order, security stance, replay behavior, exceptions, and likely misuse. Do not narrate entries that the configuration already shows. - **Prompts and visible strings:** treat wording as behavior. Inspect generated output and run behavior validation or state why no snapshot applies. - **Diagnostics:** name the failing subject or path, violated rule, and correction when it is non-obvious. Remove internal execution narration. @@ -70,7 +72,7 @@ Preserve searchable mechanism names and meaningful modal, temporal, or negative ## Borderline decisions -A case is borderline only when at least two versions satisfy the complete-proposition rule but trade accepted principles, and this skill does not already resolve the tradeoff. A new prose shape with one contract-preserving answer is not borderline. +A case is borderline only when at least two versions satisfy the complete-proposition rule but trade accepted principles, and this skill does not already resolve the tradeoff. A rewrite with one proposition-preserving answer is not borderline. In automatic mode, apply clear edits when authorized and report genuine borderline cases without asking questions. Do not weaken a proposition to make progress. diff --git a/.agents/skills/dsh-prose-standard/references/examples.md b/.agents/skills/dsh-prose-standard/references/examples.md index c4270ecc98..cb3f616b71 100644 --- a/.agents/skills/dsh-prose-standard/references/examples.md +++ b/.agents/skills/dsh-prose-standard/references/examples.md @@ -40,13 +40,13 @@ Keep the test tiers, required action, real entry path, and observable verificati **Over-detailed:** A chronological account of every promise and callback used to implement teardown. -The actor, ordering, ownership boundary, and completion guarantee are separate factual clauses. +The actor, ordering, point where ownership changes, and completion guarantee are separate factual clauses. ## Event JSDoc preserves boundary timing **Over-trimmed:** “Composes and caches the session prefix.” -**Balanced:** “Composes the session prefix once before the first pre-step and request boundary. Listener appends join the current request, and pre-step pressure accounting receives the composed prefix.” +**Balanced:** “Composes the session prefix once before the first pre-step and model request. Listener appends join the current request, and pre-step pressure accounting receives the composed prefix.” **Over-detailed:** A walkthrough of the loop helpers, cache fields, and promise callbacks that implement the ordering. @@ -60,7 +60,7 @@ Event order and its current-request consequence are caller-visible behavior, not **Over-detailed:** A paragraph-by-paragraph preview of the classes and helper functions below. -Keep role, boundaries, and non-obvious lifecycle behavior. Link architecture rationale and let the code show local control flow. +Keep the module's role, dependencies, responsibilities, and non-obvious lifecycle behavior. Link architecture rationale and let the code show local control flow. ## Public JSDoc includes failures @@ -76,11 +76,11 @@ Throws and state preconditions are caller-visible contract facts. **Over-trimmed:** “Search provider backed by an external API.” -**Balanced:** “Maps each provider result to the shared search-result shape, preserving the title, URL, and text while omitting provider-only ranking metadata.” +**Balanced:** “Maps each provider result to the shared search-result fields, preserving the title, URL, and text while omitting provider-only ranking metadata.” **Over-detailed:** A field-by-field restatement of the mapping code, including fields with identical names and obvious assignments. -Keep mapping details that explain an abstraction boundary or intentional information loss. +Keep mapping details that explain where an adapter drops or changes information. ## Link rationale while keeping the local contract @@ -110,7 +110,7 @@ Remove migration tasks and test narration. Keep the tiers, behaviors they pin, r **Over-detailed:** A list of every service a plugin could misuse and every hypothetical exploit. -Keep one example when it makes an otherwise abstract boundary operationally clear. +Keep one example when it makes an otherwise abstract security limit operationally clear. ## Delete reasoning transcripts entirely @@ -134,7 +134,7 @@ Keep the consequence of order, a surprising scope rule, or a security boundary. **Shorter but worse:** “The adapter normalizes provider errors.” -**Balanced decision:** Keep the current sentence unless a link or surrounding contract already carries the failure categories. The shorter version loses the consequence and distinctions without improving structure. +**Balanced decision:** Keep the current sentence unless a link or surrounding contract already lists the failure categories. The shorter version loses the consequence and distinctions without improving structure. ## Model-visible text follows ownership @@ -154,7 +154,7 @@ Wording that reaches a model is behavior, but duplication still drifts. Exactnes **Balanced:** “Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session.” Keep non-catalog detail in later sentences. -Know what the generator extracts. That fragment must preserve the contract needed on its generated surface. +Know what the generator extracts. That fragment must preserve the contract needed on its generated output. ## Limitations are contracts, not debt inventories diff --git a/.agents/skills/dsh-translate-docs/SKILL.md b/.agents/skills/dsh-translate-docs/SKILL.md index e35096a5a2..586eb554c8 100644 --- a/.agents/skills/dsh-translate-docs/SKILL.md +++ b/.agents/skills/dsh-translate-docs/SKILL.md @@ -64,4 +64,4 @@ When translations need to be written from scratch, the orchestrating agent does ## How to respond to translation review -Follow the [code-review reporting guidance](../dsh-code-review/SKILL.md#reporting-findings): evaluate each comment on its merits, and for terminology comments, remember the table is the contract — a reviewer's rendering decision gets applied to [terminology.md](../../../docs/i18n/terminology.md) so it binds every future translation, not just patched into one file. +Follow the [code-review reporting guidance](../dsh-code-review/SKILL.md#reporting-findings): evaluate each comment on its merits, and for terminology comments, remember the terminology table is the contract — apply a reviewer's rendering decision to [terminology.md](../../../docs/i18n/terminology.md), not only to one file. diff --git a/.agents/skills/dsh-trim-cot-leakage/SKILL.md b/.agents/skills/dsh-trim-cot-leakage/SKILL.md index c80dfbd465..7fad4d50c2 100644 --- a/.agents/skills/dsh-trim-cot-leakage/SKILL.md +++ b/.agents/skills/dsh-trim-cot-leakage/SKILL.md @@ -24,7 +24,7 @@ For every suspect passage ask: **could a reader at HEAD, with no access to any s ## What is not leakage -The citation boundary is where unaided passes fail in both directions — deleting durable references and keeping dead ones. Apply these keeps as written; [examples](references/examples.md) calibrates each: +Unaided citation passes fail in both directions by deleting durable references and keeping dead ones. Apply these keep rules as written; [examples](references/examples.md) calibrates each: - **Issue references** — `#1470`, `TODO(name):`, "issue #N owns the follow-up" resolve at HEAD; keep them on any surface, including READMEs. Do not relocate them to Agent Notes. - **Merged-PR and issue citations inside Agent Notes and postmortems** — sanctioned evidence per the [documentation standard](../../../docs/AGENTS.md)'s change-story routing. @@ -39,7 +39,7 @@ The citation boundary is where unaided passes fail in both directions — deleti ## Workflow 1. Scope and exclusions per [dsh-prose-standard](../dsh-prose-standard/SKILL.md): require an explicit scope; never touch `vendor/`, `.agents/notes/archived/`, or recorded fixtures and snapshots — recorded model output and sealed history keep their original voice. -2. Audit read-only first: run the [recall batteries](references/recall-batteries.md) (with `--hidden` so `.agents/` is searched), then judge every hit semantically. The batteries are probes, not the definition — each review round of the original purge surfaced shapes the batteries missed, so also read the densest prose in scope (module JSDoc, READMEs, Agent Notes) without a pattern in hand. +2. Audit read-only first: run the [recall batteries](references/recall-batteries.md) (with `--hidden` so `.agents/` is searched), then judge every hit semantically. The batteries are probes, not the definition — each review round of the original purge found cases the batteries missed, so also read the densest prose in scope (module JSDoc, READMEs, Agent Notes) without a pattern in hand. 3. Fix owner-first per surface: generated catalogs → fix the source JSDoc or generator template, then regenerate; type-equivalence fences → fix the source JSDoc, then re-paste both bilingual pages (`verify-type-equiv` pins them); bilingual pairs → update the counterpart and re-record per [dsh-translate-docs](../dsh-translate-docs/SKILL.md); model-visible strings → wording is behavior, so flag for a snapshot-backed change instead of silently rewording. 4. Before deleting anything, enumerate the passage's propositions (prose-standard) and check the [overcorrection traps](references/examples.md#overcorrection-traps): trims that flip an obligation into an endorsement, promote a hypothetical to a shipped feature, delete a true fact, or drop provenance. 5. Verify: re-run the batteries expecting only sanctioned keeps, this skill's own directory, and the owning note's quoted evidence; confirm every remaining citation resolves at HEAD; run the gates for touched surfaces (`doc-sync` for docs, `verify-type-equiv`, `verify-translation-pairing`). diff --git a/.agents/skills/dsh-trim-cot-leakage/references/examples.md b/.agents/skills/dsh-trim-cot-leakage/references/examples.md index 22e73e10f4..9afc22c209 100644 --- a/.agents/skills/dsh-trim-cot-leakage/references/examples.md +++ b/.agents/skills/dsh-trim-cot-leakage/references/examples.md @@ -1,6 +1,6 @@ # Few-shot leakage examples -Distilled from the 2026-08 repo-wide purge and its review rounds. Use them to identify the governing principle, not as text templates. This file deliberately quotes leaked shapes as calibration material — the [recall batteries](recall-batteries.md) exclude the skill's directory, and its wording is not a license elsewhere. +Distilled from the 2026-08 repo-wide purge and its review rounds. Use them to identify the governing principle, not as text templates. This file deliberately quotes leaked wording as calibration material — the [recall batteries](recall-batteries.md) exclude the skill's directory, and its wording is not a license elsewhere. ## Dead citations diff --git a/.agents/skills/dsh-trim-cot-leakage/references/recall-batteries.md b/.agents/skills/dsh-trim-cot-leakage/references/recall-batteries.md index 8f9cce4aad..9b1c79054c 100644 --- a/.agents/skills/dsh-trim-cot-leakage/references/recall-batteries.md +++ b/.agents/skills/dsh-trim-cot-leakage/references/recall-batteries.md @@ -1,12 +1,12 @@ # Recall batteries -Probes for [the taxonomy](../SKILL.md#taxonomy), tuned during the 2026-08 purge. Every hit needs semantic judgment — the batteries over-match by design, and they under-match by nature: each review round of the purge found shapes no battery caught, so pair them with an unpatterned read of the densest prose in scope. +Probes for [the taxonomy](../SKILL.md#taxonomy), tuned during the 2026-08 purge. Every hit needs semantic judgment — the batteries over-match by design, and they under-match by nature: each review round of the purge found cases no battery caught, so pair them with an unpatterned read of the densest prose in scope. ## Invocation rules - Add `--hidden --glob '!.git/**'` so `.agents/` is searched; ripgrep skips dot-directories by default and the purge's biggest miss risk was Agent Notes. -- Exclusions go last so a later include cannot re-admit them: `--glob '!vendor/**' --glob '!node_modules/**' --glob '!.agents/notes/archived/**' --glob '!.agents/skills/dsh-trim-cot-leakage/**'` (the skill's own files quote leaked shapes as calibration), plus recorded fixture and snapshot directories in scope. The [owning note](../../../notes/implemented/process/2026-08-09-committed-artifact-citations.md) also self-hits through its quoted evidence; judge it as evidence, not usage. -- Natural-language lines carry `-i` so sentence-initial capitals hit ("This PR adds…", "Probably fine…"); the code-shaped first line stays case-sensitive — `-i` would turn `\bT\d\b` and `\bP-I\b` into noise. +- Exclusions go last so a later include cannot re-admit them: `--glob '!vendor/**' --glob '!node_modules/**' --glob '!.agents/notes/archived/**' --glob '!.agents/skills/dsh-trim-cot-leakage/**'` (the skill's own files quote leaked wording as calibration), plus recorded fixture and snapshot directories in scope. The [owning note](../../../notes/implemented/process/2026-08-09-committed-artifact-citations.md) also self-hits through its quoted evidence; judge it as evidence, not usage. +- Natural-language lines carry `-i` so sentence-initial capitals hit ("This PR adds…", "Probably fine…"); the first line, which matches code patterns, stays case-sensitive — `-i` would turn `\bT\d\b` and `\bP-I\b` into noise. - A zero-hit pattern proves nothing until you have seen it match: test it against a known-positive string before trusting the negative. ## English battery diff --git a/.agents/skills/record-browser-gif/SKILL.md b/.agents/skills/record-browser-gif/SKILL.md index d7df478074..3566dfb763 100644 --- a/.agents/skills/record-browser-gif/SKILL.md +++ b/.agents/skills/record-browser-gif/SKILL.md @@ -15,7 +15,7 @@ A pull request that changes product-user-visible GUI behavior MUST include a dem The recording itself is part of the evidence: use a real server booted from that pull request's branch tree, a real API key, and real model rounds. Never substitute fixture queries, mock transports, synthetic event injection, or test-only hooks unless the user explicitly asked for a fixture recording. Next to the embed, state the exact demonstrated commit SHA, the tree and origin that served it, any mode flags or browser-state exceptions, and whether a real model round ran, so reviewers know exactly what the recording proves. -## Keep the boundary explicit +## Keep recording separate from publication - Recording produces frame images and one local `.gif` artifact only; it never mutates remote state. - Publication — pushing the GIF to an assets branch and embedding it in a pull request body — is the separate final step, performed only when the task includes attaching the GIF to a pull request. It never touches the pull request's own branch. @@ -33,9 +33,9 @@ A GIF for a specific pull request demonstrates that pull request's tree, so stag ## Record the flow -1. Invoke the available browser-control skill and follow its setup, interaction, and cleanup instructions. Use the user's existing Chrome state only when requested or required; state that exception next to the GIF and do not claim fresh client state. If browser control is unavailable, use the repository-declared Playwright dependency in an isolated headless browser; do not install another driver or launch the user's browser. State that fallback next to the GIF. -2. Resolve the evidence boundary before recording: identify the exact origin, whether the app is built or in development, the transport, and any fixture or mock mode. Record only claims that the observed setup supports. -3. When a production default opens a native operating-system surface that headless automation cannot drive, select an official browser-operable production backend through the application's normal configuration. State that override next to the GIF; a fixture, mock transport, or test-only hook is not an acceptable substitute. +1. Invoke the available browser-control skill and follow its setup, interaction, and cleanup instructions. Use the user's existing Chrome state only when requested or required; state that exception in the provenance and do not claim fresh client state. If browser control is unavailable, use the repository-declared Playwright dependency in an isolated headless browser; do not install another driver or launch the user's browser. State that fallback in the provenance. +2. Before recording, identify the exact origin, whether the app is built or in development, the transport, and any fixture or mock mode. Record only claims that the observed setup supports. +3. When a production default opens a native operating-system surface that headless automation cannot drive, select an official browser-operable production backend through the application's normal configuration. State the override in the provenance; a fixture, mock transport, or test-only hook is not an acceptable substitute. 4. Choose three to six states that tell one story, such as typed, running, settled, and detail. Prefer semantic state changes over continuous capture; omit loading churn that does not help the viewer. 5. Keep one viewport and crop for every frame, and name frames lexically: `00-initial.png`, `01-typed.png`, and so on. 6. Store frames under the repository's gitignored `.playwright-mcp/` directory — browser-tool screenshots can only be written under the tool's allowed roots, and relative filenames resolve against the repository root. Create the frame subdirectory first (`mkdir -p .playwright-mcp/gif-frames-