From 34abf697306592177c493fea8b7043a864718c82 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 10 Aug 2026 15:20:52 +0800 Subject: [PATCH] cleanup: remove managed source installer --- ...-07-29-dsh-source-launch-tsx-esm.i18n.yaml | 4 +- .../2026-07-29-dsh-source-launch-tsx-esm.md | 2 +- ...2026-07-29-dsh-source-launch-tsx-esm.zh.md | 2 +- ...26-07-20-dsh-cli-personal-config.i18n.yaml | 4 +- .../2026-07-20-dsh-cli-personal-config.md | 4 +- .../2026-07-20-dsh-cli-personal-config.zh.md | 4 +- ...-23-personal-staging-maintenance-skills.md | 35 -- ...-personal-staging-maintenance-skills.zh.md | 35 -- ...30-generated-third-party-notices.i18n.yaml | 4 +- ...026-07-30-generated-third-party-notices.md | 4 +- ...-07-30-generated-third-party-notices.zh.md | 4 +- ...staller-adopts-existing-checkout.i18n.yaml | 6 - ...7-31-installer-adopts-existing-checkout.md | 51 --- ...1-installer-adopts-existing-checkout.zh.md | 51 --- ...e-run-without-managed-installer.i18n.yaml} | 6 +- ...10-source-run-without-managed-installer.md | 31 ++ ...source-run-without-managed-installer.zh.md | 31 ++ README.i18n.yaml | 4 +- README.md | 29 +- README.zh.md | 29 +- THIRD_PARTY_NOTICES.md | 2 +- apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 4 +- apps/cli/README.zh.md | 4 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 10 +- apps/cli/reference/README.zh.md | 10 +- apps/cli/tests/install-script.spec.ts | 140 ------ apps/cli/tests/source-launch.compat.spec.ts | 4 +- bin/dsh | 26 -- docs/api-gateway.i18n.yaml | 4 +- docs/api-gateway.md | 2 +- docs/api-gateway.zh.md | 2 +- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- scripts/gen-third-party-notices.ts | 8 +- scripts/install.sh | 425 ------------------ .../request-response.expected.json | 4 +- skills/dsh-customize/SKILL.md | 37 -- skills/dsh-upgrade/SKILL.md | 48 -- skills/dsh-upstream-customization/SKILL.md | 27 -- 42 files changed, 138 insertions(+), 975 deletions(-) delete mode 100644 .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md delete mode 100644 .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md delete mode 100644 .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml delete mode 100644 .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md delete mode 100644 .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md rename .agents/notes/implemented/{process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml => simplification/2026-08-10-source-run-without-managed-installer.i18n.yaml} (53%) create mode 100644 .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md create mode 100644 .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md delete mode 100644 apps/cli/tests/install-script.spec.ts delete mode 100755 bin/dsh delete mode 100755 scripts/install.sh delete mode 100644 skills/dsh-customize/SKILL.md delete mode 100644 skills/dsh-upgrade/SKILL.md delete mode 100644 skills/dsh-upstream-customization/SKILL.md diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml index f4fb753e79..a0d0c2adf9 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.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-29-dsh-source-launch-tsx-esm.md -2026-07-29-dsh-source-launch-tsx-esm.md: 5cf4a227f388a1ac8315594af4e0256864ef17f5 -2026-07-29-dsh-source-launch-tsx-esm.zh.md: b5a52b3d01840337c0091310e50d8fac34245519 +2026-07-29-dsh-source-launch-tsx-esm.md: e1016e726c32c27104b7cfe258c412502c277948 +2026-07-29-dsh-source-launch-tsx-esm.zh.md: 2dcb7f1c24f4f73d2c259e8d44ca0f63226bdbaf diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md index 5cf4a227f3..e1016e726c 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md @@ -14,7 +14,7 @@ Startup latency also mattered: the off-thread `module.register()` hooks worker s ## Decision -The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. `bin/dsh`, the root `dsh`/`demo:tui`/`demo:web` scripts, and the Code Mode TUI overlay use the same vector; `bin/dsh` references the hook and tsconfig by absolute checkout paths (bare `tsx/esm` does not resolve from an arbitrary cwd) and pins `TSX_TSCONFIG_PATH` to the root tsconfig. The CJS hook stays off because the CLI source graph is ESM-only; measured TUI time-to-banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain. +The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh`/`demo:headless`/`demo:web` scripts use the same vector from the repository root. The CJS hook stays off because the CLI source graph is ESM-only; measured TUI time-to-banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain. `scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-tasks` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; since fixed.) diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md index b5a52b3d01..2dcb7f1c24 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md @@ -14,7 +14,7 @@ Status: implemented ## 决策 -`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。`bin/dsh`、根目录的 `dsh`/`demo:tui`/`demo:web` 脚本以及 Code Mode TUI overlay 使用同一向量;`bin/dsh` 以 checkout 的绝对路径引用钩子与 tsconfig(裸的 `tsx/esm` 无法从任意 cwd 解析),并将 `TSX_TSCONFIG_PATH` 固定到根 tsconfig。CJS 钩子保持关闭,因为 CLI 源码图是纯 ESM;实测 TUI 到 banner 约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。 +`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh`/`demo:headless`/`demo:web` 脚本从仓库根目录使用同一启动方式。CJS 钩子保持关闭,因为 CLI 源码图是纯 ESM;实测 TUI 到 banner 约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。 `scripts/tspath-loader.ts` 与 `apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader「仅为已声明运行时依赖映射 workspace import」的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest(元数据清单)走 workspace constraints。(该运行时规则确实发现过真实缺陷:`dsh-plan-mode` 与 `dsh-tool-tasks` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies;后已修复。) diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml index a08966f790..57af8f4ed0 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.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-20-dsh-cli-personal-config.md -2026-07-20-dsh-cli-personal-config.md: 02883c89f27e51d6091d4d65167ebdd6a96f6f51 -2026-07-20-dsh-cli-personal-config.zh.md: 6e56e892cf682ea514b036750e44dbb06e944a80 +2026-07-20-dsh-cli-personal-config.md: 4d2ed7de3357a3eb54f066d3ba4eff2fb38a1a4a +2026-07-20-dsh-cli-personal-config.zh.md: 14964beecfcd7fcd699236cb7fce4c8cc7684514 diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md index 02883c89f2..4d2ed7de33 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md @@ -14,7 +14,7 @@ The entry modes and the personal file's name and location below are superseded b Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh web` PR (#443): -**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. The committed `bin/dsh` launcher resolves the checkout through its own real path and runs the app with tsx's ESM hook; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns that contract. `pnpm run demo:tui` runs the same entry. +**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. From a source checkout, the root `pnpm run dsh` script runs the same entry with tsx's ESM hook; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns that contract. **Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI, Web, and headless surfaces consume its two optional files; the demo bins boot their committed trees verbatim: @@ -40,7 +40,7 @@ The TUI and Web register the exact personal path through Cordis HMR after boot. ## Consequences -- `dsh` from any directory (and `pnpm run demo:tui`) can apply personal providers, models, installed bundle entries, and other Loader entries with no checkout edit; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip. +- An installed `dsh` command can run from any directory, while source users invoke `pnpm run dsh` from the checkout; both can apply personal providers, models, installed bundle entries, and other Loader entries with no checkout edit. The behavior was verified end to end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip. - Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings and [`dsh --dump-config`](../../../../apps/cli/README.md#profiles) (which prints the composed tree those patches produce) are the diagnostics. - Personal patches resolve ids against the booted file's own tree, so nested-include overlays (Code Mode) are not personalized; live-run parity for those leaves is deferred. - `dsh-app-boot` depends on `js-yaml` and imports the include's `!!js` YAML dialect (`entryListSchema`) directly, and, like `apps/cli`, depends on `@deepseek-ai/dsh-paths` for `resolveDshHome`. diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md index 6e56e892cf..14964beecf 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md @@ -14,7 +14,7 @@ Status: implemented 两个耦合的部分,与 `dsh web` PR(#443)提出的 `apps/` 装配层对齐: -**`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout,并使用 tsx 的 ESM hook 运行应用;该约定由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护。`pnpm run demo:tui` 运行同一入口。 +**`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。在源码检出中,根目录的 `pnpm run dsh` 脚本使用 tsx 的 ESM hook 运行同一入口;该约定由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护。 **个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)(`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI、Web 和无头界面使用其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动: @@ -40,7 +40,7 @@ TUI 和 Web 启动后通过 Cordis HMR(热模块替换)注册确切的个人 ## Consequences -- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`),无需修改 checkout,即可应用个人提供方、模型、已安装组合包的配置项和其他 Loader 配置项;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。 +- 已安装的 `dsh` 命令可从任意目录运行,源码用户则从 checkout 调用 `pnpm run dsh`;两者都无需修改 checkout 即可应用个人提供方、模型、已安装组合包的配置项和其他 Loader 配置项。该行为已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。 - 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;诊断手段是 loader 的「配置项未找到/名称不匹配」警告和 [`dsh --dump-config`](../../../../apps/cli/README.md#profiles)(打印这些补丁合成出的配置树)。 - 个人补丁只在被启动文件自身的树里解析 id,因此嵌套 include 的 overlay(Code Mode)不会被个性化;这些叶子的实际运行等价性暂缓。 - `dsh-app-boot` 依赖 `js-yaml`,并直接导入 include 的 `!!js` YAML 方言(`entryListSchema`);与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`。 diff --git a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md b/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md deleted file mode 100644 index a7ccc5b1e0..0000000000 --- a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Personal staging maintenance skills - -Status: implemented - -English | [中文](2026-07-23-personal-staging-maintenance-skills.zh.md) - -## Problem - -Personal dsh customizations need a repeatable way to locate the installed source, isolate task work, serialize integration, and incorporate upstream changes without rewriting the checkout used by running sessions. User-local instructions solve this for one installation but cannot guide other users or remain synchronized with repository installer behavior. - -## Decision - -The repository distributes [`dsh-customize`](../../../../skills/dsh-customize/SKILL.md), [`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md), and [`dsh-upstream-customization`](../../../../skills/dsh-upstream-customization/SKILL.md) from its root `skills/` directory. Their descriptions name both the operation and user requests that select it. The shipped TUI supplies that directory to the local skill provider at startup, below project and user roots in discovery priority. The workflows derive the active checkout and staging branch from the installed launcher rather than a user-specific path or branch name, defer to repository-local instructions, require task worktrees, and serialize staging mutations with the staging worktree's established `.agents/merge.lock`. - -Before rebasing, an upgrade inspects the Git log and commit ranges to identify incoming upstream changes, personal commits, duplicates, and likely conflicts. It drops customizations already supplied upstream; when only a documentary local diff remains for such a customization, it also drops that account unless it adds an independently useful current contract absent upstream. Each attempt uses one UTC basic timestamp for its independent `dsh-staging-` sibling clone, local `dsh-upgrade/prepare-` branch, new `dsh-staging/` branch, private upstream and recovery refs, and launcher backup. The sibling name does not derive from the current directory name, and collisions fail rather than acquiring ad hoc suffixes. The workflow derives the current DSH process source from the process command and runtime environment rather than the shell working directory, then treats the repository and checkout behind the installed launcher as immutable except for holding its existing merge lock. - -After validation in the independent clone, the workflow creates and verifies the timestamped staging branch, then atomically moves the launcher once from the unchanged old staging checkout to the new staging checkout. The launcher never targets a preparation, feature, review, publication, or detached checkout. Failure before cutover leaves the installed checkout and launcher unchanged; failure after cutover restores and verifies the launcher backup. The old staging checkout, its branch, the recovery ref, and the launcher backup remain available until a restarted process proves that DSH runs from the new staging branch and the user explicitly approves rollback cleanup. - -`dsh-upstream-customization` owns upstream publication independently from local maintenance and upgrades. It recommends bug fixes, additive non-conflicting plugin features, and visual improvements; intrusive changes require maintainer approval first. At the end of an upgrade, the agent classifies remaining customizations, explains their upstream value, recommends whether to propose each one, and asks which named candidate the user wants to upstream. Only that selection loads the publication workflow; each feature still requires explicit approval before a push or draft PR. Approved changes start from current upstream `master` without unrelated personal commits. Draft PRs for TUI features preferably include a screenshot from the assembled application after credentials and personal data are removed. `dsh-customize` requires interactive TUI behavior to be exercised in a dedicated tmux session before integration. - -## Alternatives considered - -**Keep the workflows user-scoped.** This preserves personal flexibility but prevents other users from discovering the same safety rules and lets the workflow drift from the installer shipped by the repository. - -**Rebase the active staging checkout in place.** This is simpler but changes many files during preparation, can disrupt new dsh launches, and cannot provide atomic publication or an unchanged rollback checkout. - -**Update the existing staging checkout after moving the launcher elsewhere.** This retains one staging path but requires a mid-upgrade launcher target that is not a staging branch and still rewrites a checkout that may host a running process. - -**Lock only the final branch switch.** This shortens lock duration but permits a customization merge against the old base while the rebase is being prepared, invalidating the prepared history. - -**Open one upstream PR for all personal changes.** This reduces branch management but publishes unrelated customizations and removes the user's per-feature approval boundary. - -## Consequences - -Upgrade preparation holds the installed staging merge lock while dependencies and checks run, so local customization integration waits for a consistent result. One upgrade creates an independent timestamped clone and staging branch, performs one atomic launcher cutover, and requires one restart afterward; it never writes into the repository or checkout behind the launcher except to hold its existing lock. Each workflow records preconditions, repeats them before mutation, inspects state after interrupted mutations, restores the launcher backup on cutover failure, reruns failed checks after correction, and reports final state. The old staging checkout remains rollback storage until explicit user-approved cleanup. Checked-in evaluations cover selection, process-source protection, unsafe repository states, rollback, and publication authorization; repository documentation checks validate skill links and formatting, while technical review remains responsible for Git and filesystem correctness. diff --git a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md b/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md deleted file mode 100644 index 8593291f51..0000000000 --- a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: 个人集成分支维护 skill - -Status: implemented - -[English](2026-07-23-personal-staging-maintenance-skills.md) | 中文 - -## 问题 - -个人 dsh 定制需要一套可重复执行的方法,用于定位已安装的源码、隔离各项任务的修改、串行集成变更,并在不改写运行中会话所用检出的前提下合入上游变更。用户本地指令能解决某一套安装中的问题,却无法指导其他用户,也无法持续与仓库安装脚本的行为保持同步。 - -## 决策 - -仓库从其根 `skills/` 目录分发 [`dsh-customize`](../../../../skills/dsh-customize/SKILL.md)、[`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md) 和 [`dsh-upstream-customization`](../../../../skills/dsh-upstream-customization/SKILL.md)。它们的描述同时说明操作内容和选择该 skill(技能)的用户请求。分发的 TUI 在启动时将该目录提供给本地 skill 提供方,在发现优先级上位于项目根目录和用户根目录之后。这些 skill 根据已安装的启动器而非个人路径或分支名称定位当前生效的检出和集成分支,遵从仓库内指令,要求使用任务 worktree,并利用集成分支所在 worktree 的既有 `.agents/merge.lock`,串行执行每一次个人集成分支修改。 - -升级流程在变基前检查 Git 日志和提交范围,以识别将进入升级的上游变更、个人提交、重复内容和可能发生冲突的区域。它会丢弃上游已经提供的定制;如果这类定制在本地只剩文档差异,也会一并丢弃,除非该说明包含上游缺失且可独立使用的当前约定。每次升级尝试使用同一个 UTC 基本格式时间戳,用于其独立的 `dsh-staging-` 同级克隆、本地 `dsh-upgrade/prepare-` 分支、新的 `dsh-staging/` 分支、私有的上游引用与恢复引用,以及启动器备份。同级克隆的名称不派生自当前目录名,名称冲突会直接失败,而不是追加临时后缀。流程根据进程命令和运行时环境而非 shell 工作目录推导当前 DSH 进程的源码位置,随后将已安装启动器所指向的仓库和检出视为不可变,唯一例外是持有其既有合并锁。 - -在独立克隆中验证通过后,工作流会创建并验证带时间戳的集成分支,然后以原子方式将启动器从保持不变的旧集成分支检出一次性切换到新集成分支检出。启动器绝不会指向准备、功能、评审、发布或处于分离状态的检出。切换前的失败会让已安装的检出和启动器保持不变;切换后的失败则恢复并验证启动器备份。旧的集成分支检出、其分支、恢复引用和启动器备份会一直保留,直到重启后的进程证明 DSH 运行于新的集成分支,且用户明确批准回滚清理为止。 - -`dsh-upstream-customization` 独立于本地维护和升级,负责向上游发布。它推荐 bug 修复、附加式且不冲突的插件功能,以及视觉改进;侵入式变更需先取得维护者批准。在升级结束时,agent 会对剩余定制进行分类、说明其上游价值、建议是否提交,并询问用户希望向上游贡献哪个具名候选项。只有用户做出选择后才会加载发布工作流;每项功能在推送或创建草稿 PR(Pull Request)前仍必须得到明确批准。获批的变更均以当前上游 `master` 为起点,不带入无关的个人提交。TUI 功能的草稿 PR 建议在移除凭证与个人数据后,附上组装后应用的截图。`dsh-customize` 要求在集成前于专用 tmux 会话中检验交互式 TUI 行为。 - -## 备选方案 - -**将这些工作流限定在用户本地。** 这样可以保留个人使用的灵活性,但其他用户无法发现同一套安全规则,工作流也可能逐渐偏离仓库分发的安装脚本行为。 - -**在当前集成分支检出中原地变基。** 此方案更简单,但准备期间会修改大量文件,可能干扰新的 dsh 启动,也无法实现原子发布或提供一份保持不变的回滚检出。 - -**在将启动器迁往别处后更新现有的集成分支检出。** 此方案可以保留单一的集成分支路径,却要求在升级中途让启动器指向一个并非集成分支的目标,且仍会改写可能承载运行中进程的检出。 - -**只在最终切换分支时加锁。** 这样可以缩短持锁时间,却允许写入方在变基准备期间继续基于旧基线合并定制变更,导致准备好的历史失效。 - -**用一个上游 PR 发布所有个人变更。** 这会减少分支管理工作,却会发布无关的定制,并取消用户按功能逐项批准的边界。 - -## 影响 - -升级准备流程在安装依赖和运行检查期间持有已安装集成分支的合并锁,因此本地定制的集成必须等待一致的结果。一次升级会创建独立的带时间戳的克隆和集成分支,执行一次原子的启动器切换,并在切换后要求重启一次;除持有其既有锁之外,升级绝不会写入启动器所指向的仓库或检出。各工作流会记录前置条件、在修改前重复检查、在修改被中断后检查状态、在切换失败时恢复启动器备份、修复后重新运行失败的检查,并报告最终状态。旧的集成分支检出会作为回滚存储一直保留,直到用户明确批准清理为止。仓库内评估覆盖 skill 选择、进程源码保护、不安全的仓库状态、回滚和发布授权;仓库文档检查会验证 skill 的链接和格式,Git 与文件系统操作的正确性仍由技术评审负责。 diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml index b60f3ae3ed..6f6823e3c3 100644 --- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.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-30-generated-third-party-notices.md -2026-07-30-generated-third-party-notices.md: 58e3fd5007a16d6391956f423d1ec91e851df295 -2026-07-30-generated-third-party-notices.zh.md: 834ec4ebc349bdee37b0873109de566223bb7d48 +2026-07-30-generated-third-party-notices.md: d13eef8412ecd5c8387a7239b812c835671b6bce +2026-07-30-generated-third-party-notices.zh.md: 0af6a6bcd665197acd6a0b5e9e6f51940a0267e7 diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md index 58e3fd5007..d13eef8412 100644 --- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md @@ -20,9 +20,9 @@ One trigger gap is accepted rather than worked around: lefthook inspects only fi The file discloses **direct** dependencies by default. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy. The one explicit transitive disclosure is the official Claude platform payload set declared by `@anthropic-ai/claude-agent-sdk` through `optionalDependencies`, because those packages carry the distributed Claude Code executable rather than ordinary library implementation detail. -**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the `bin/dsh` launcher execs through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly). +**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the root source-run scripts execute through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly). -The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. `scripts/install.sh` installs the repository itself, so a user's `cordis.yml` can mount any plugin package; `@modelcontextprotocol/sdk` and the OpenTelemetry packages reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice. +The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. Source execution can mount any plugin package from a user's `cordis.yml`; `@modelcontextprotocol/sdk` and the OpenTelemetry packages therefore reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice. The manifest set is derived from the `packages:` members the root `pnpm-workspace.yaml` declares, including the Landlock workspace and its public packages, so a new member area is read the day it is declared rather than the day someone remembers to extend a list. License and repository metadata come from the root workspace's installed pnpm store and package-local link farms, so the generator requires an installed tree and fails loud when a package resolves to neither, rather than emitting an empty cell. `OVERRIDES` carries the packages whose published manifest cannot answer — Rust-built npm bins that omit `license`, and the `modelcontextprotocol/servers` packages whose repository is mid MIT→Apache-2.0 relicensing, so their effective terms are per-contribution. A runtime dependency whose license is not on the permissive list is a hard error: shipping copyleft is a distribution decision, not something a regenerated table may absorb silently. Vendored packages are cross-checked against `vendor/README.md` and rejected if any is not MIT, and `pnpm-workspace.yaml`'s `patchedDependencies` are listed under the runtime table because pnpm applies those patches at install time — shipped artifacts carry modified copies of `@earendil-works/pi-tui` and `node-pty`, and the patch files are the record of what changed. diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md index 834ec4ebc3..0af6a6bcd6 100644 --- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md @@ -20,9 +20,9 @@ Status: implemented 文件默认只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。唯一明确披露的传递依赖,是 `@anthropic-ai/claude-agent-sdk` 通过 `optionalDependencies` 声明的官方 Claude 平台载荷集合,因为这些包承载随产品分发的 Claude Code 可执行文件,而非普通的库实现细节。 -**分层依据是声明方所在区域,而非 manifest 字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一 manifest——即根 manifest、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而 `bin/dsh` 启动器通过 `tsx` 执行,根本没有任何 manifest 把它声明为运行时依赖,只能由生成器显式标记。 +**分层依据是声明方所在区域,而非 manifest 字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一 manifest——即根 manifest、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而根目录的源码运行脚本通过 `tsx` 执行,根本没有任何 manifest 把它声明为运行时依赖,只能由生成器显式标记。 -运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。`scripts/install.sh` 安装的就是仓库本身,用户的 `cordis.yml` 可以挂载任何插件包;`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。 +运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。从源码运行时,用户可以通过 `cordis.yml` 挂载任何插件包;因此,`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。 manifest 集合由根 `pnpm-workspace.yaml` 声明的 `packages:` 成员派生,其中包括 Landlock 工作区及其公开包,因此新增成员区域在声明当天就会被读取,而不必等谁想起来去补一份列表。许可证与仓库地址取自根工作区已安装的 pnpm store 和包本地链接场;某个包两处都解析不到时直接失败,而不是留下空单元格。`OVERRIDES` 收录已发布 manifest 答不上来的包:用 Rust 构建、发布时省略 `license` 字段的 npm 可执行包,以及 `modelcontextprotocol/servers` 系列——该仓库正处在 MIT 向 Apache-2.0 的重新许可过程中,实际条款按贡献逐条而定。运行时依赖的许可证若不在宽松清单内即为硬失败:交付 copyleft 是一项分发决策,不该被一次重新生成悄悄吸收。被源码收编的包会与 `vendor/README.md` 交叉核对,出现非 MIT 即报错;`pnpm-workspace.yaml` 的 `patchedDependencies` 列入运行时表格,因为 pnpm 在安装期就会打上这些补丁——交付产物携带的是改动过的 `@earendil-works/pi-tui` 与 `node-pty`,补丁文件本身就是改动的完整记录。 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 deleted file mode 100644 index 1c58887b2b..0000000000 --- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/process/2026-07-31-installer-adopts-existing-checkout.md -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 deleted file mode 100644 index 1d45a14273..0000000000 --- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md +++ /dev/null @@ -1,51 +0,0 @@ -# Agent Note: the installer adopts an existing checkout into the managed layout - -Status: implemented - -English | [中文](2026-07-31-installer-adopts-existing-checkout.zh.md) - -## Problem - -`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 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 - -In-repo mode still never clones and never modifies the working tree, but it now **adopts** the checkout into the managed layout unconditionally. There is no opt-out: one layout serves every install. - -The container owns staging worktrees and `current`; the repository is *discovered*, not owned. `git rev-parse --git-common-dir` resolves the shared git directory behind the checkout — for a linked worktree that is the real clone rather than the worktree itself — and its parent is the repository that serves as the upgrade base. A staging worktree branched from the checkout's `HEAD` is then created under `$DSH_SOURCE`, and `current` points at it. A clone anywhere on disk therefore converges on the same layout as a `curl` install, and the two paths share one worktree/exclude/lock/link sequence: they differ only in whether the repository was discovered by `git clone` or by `git rev-parse`. - -The installer records nothing about where that repository lives. A container whose repository sits outside it is not self-contained — each staging worktree holds an absolute gitdir pointer into that clone, so deleting the clone breaks them — but git already owns that fact: the worktree's `.git` file names the path, and `git worktree list` in the clone enumerates every worktree depending on it. - -Adoption branches from `HEAD`, so committed work is what runs and uncommitted changes stay in the checkout. This is not prompted or warned about: the installer builds the layout and gets out of the way. Setting `DSH_SOURCE` to a different directory remains the one documented way to opt back into cloning a separate tree. - -Every path comparison runs on physical paths through a `resolve_dir` helper, and every compared value is resolved at assignment rather than at the comparison. Git always reports resolved paths, so comparing one against an unresolved path disagrees whenever a symlink sits anywhere above the checkout — a symlinked home directory is enough, and macOS reaches every `mktemp` path that way through `/var` -> `private/var`. The mismatch misclassified an existing managed install as a foreign clone and would have built a second container beside the real one. The same defect class recurs whenever one side of a comparison is left unresolved — as it did with a curl install's `REPO_ROOT` and with the container path it was compared against. `resolve_dir` therefore echoes a missing path back rather than failing, so a not-yet-created container needs no per-call fallback and no site can compare against an empty path by forgetting one; callers that need "does not exist" test the directory explicitly. `git rev-parse --path-format=absolute` would do the same job but requires git 2.31+. - -Before `current` is repointed, the installer rejects a staging path that resolves to the repository itself, enforcing the upgrade contract that the launcher never resolves to the master clone. - -## Alternatives considered - -**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 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 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. - -**Put an adopted clone's staging worktrees beside the clone** (`~/src/staging-*`) rather than in `~/.dsh/source`. Rejected: `current` and the PATH launcher are per-user singletons, so scattering worktrees across clone parents reintroduces the sibling-clone sprawl the source container exists to prevent. - -## 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 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. - -A container adopting an outside clone is also no longer self-contained: deleting that clone breaks its staging worktrees. This is inherent to reusing an existing clone rather than a property of this design — the rejected symlink hides it rather than fixing it — and git's own worktree records are what diagnose it. - -## Testing - -`scripts/install.sh` now has a real-shell PTY regression suite in `apps/cli/tests/install-script.spec.ts`, covering adoption and curl-style paths with stubbed dependencies. Curl-style installs default to the public `deepseek-ai/deepseek-harness-sdk` source, while replacing the installer with pnpm/npx remains separate work. - -Verification was manual, through a throwaway harness driving the real script with a stubbed `pnpm`: adopting a standalone clone; adopting from a linked worktree into its existing container; an explicit `DSH_SOURCE` still opting back into cloning; a dirty tree adopting silently with no prompt or warning while its uncommitted file stays behind; a non-git checkout failing with guidance; and a `curl`-style clone install asserting the built layout, the check that catches an unresolved `REPO_ROOT`. The interactive path was exercised under tmux from a dirty checkout, confirming the run reaches the launcher with no adoption prompt and ends with `dsh` running from the new staging worktree while the original checkout keeps its branch and its uncommitted file. 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 deleted file mode 100644 index 971bc3b389..0000000000 --- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md +++ /dev/null @@ -1,51 +0,0 @@ -# Agent Note: 安装器把已有检出接管进受管布局 - -Status: implemented - -[English](2026-07-31-installer-adopts-existing-checkout.md) | 中文 - -## 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`。 - -这种直接链接无法升级。升级重指的正是`current`,因此缺少它的安装无法通过[`dsh-upgrade`](../../../../skills/dsh-upgrade/SKILL.md)升级;检出一旦移动,PATH 符号链接就会失效;而且启动器会解析到贡献者恰好检出的任意分支,这正是升级约定禁止作为启动器目标的情形。升级技能早已把这种布局描述为需要一次性迁移的旧式安装,因此两种布局从安装时起便不相同,只有以后执行迁移才会一致,而迁移也可能永远不执行。 - -## Decision - -检出内模式仍然绝不克隆、绝不修改工作树,但现在它会无条件地把该检出**接管**进受管布局。不存在退出选项:一套布局服务于所有安装。 - -容器拥有 staging worktree 和`current`;仓库是被*发现*的,而非被拥有的。`git rev-parse --git-common-dir`会解析出该检出背后的共享 git 目录——对于 linked worktree,那是真正的克隆而非 worktree 自身——其父目录即是充当升级基础的仓库。随后以该检出的`HEAD`为起点,在`$DSH_SOURCE`下创建 staging worktree,并让`current`指向它。因此,磁盘上任意位置的克隆都会收敛到与`curl`安装相同的布局,且两条路径共用同一套 worktree/exclude/lock/link 流程:二者的唯一差别,只在于仓库是由`git clone`发现的,还是由`git rev-parse`发现的。 - -安装器不会记录该仓库位于何处。仓库位于容器之外时,容器就不是自包含的——每个 staging worktree 都持有指向该克隆的绝对 gitdir 指针,删除该克隆就会破坏它们——但这一事实本就由 git 自己掌握:worktree 的`.git`文件写明了该路径,而在该克隆中执行`git worktree list`会列出依赖于它的每一个 worktree。 - -接管以`HEAD`为分支起点,因此运行的是已提交的内容,未提交的更改仍留在检出中。这一点既不提示也不警告:安装器构建好布局后便不再打扰。把`DSH_SOURCE`设为其他目录,仍是唯一有文档记载的、回到克隆另一棵树的方式。 - -所有路径比较都通过`resolve_dir`辅助函数在物理路径上进行,且每个参与比较的值都在赋值时解析,而非在比较时解析。git 报告的始终是已解析的路径,因此只要检出之上任意一层存在符号链接,拿它与未解析的路径相比较就会不相等——家目录本身是符号链接即已足够,而 macOS 通过`/var` -> `private/var`使每个`mktemp`路径都如此。这种不匹配会把已有的受管安装误判为外来克隆,并在真正的容器旁再建一个容器。只要比较的一侧未经解析,同类缺陷就会重现——curl 安装的`REPO_ROOT`以及与之比较的容器路径都曾如此。因此`resolve_dir`在路径不存在时原样回显该路径而非失败,这样尚未创建的容器无需在每个调用点单独兜底,也就没有调用点会因遗漏兜底而与空路径比较;需要判断"不存在"的调用方则显式检测该目录。`git rev-parse --path-format=absolute`能完成同样的工作,但要求 git 2.31 及以上版本。 - -在重指`current`之前,安装器会拒绝解析结果等于仓库自身的 staging 路径,以此落实"启动器绝不解析到 master 克隆"这一升级约定。 - -## Alternatives considered - -**把`~/.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 检出。这还会使每次升级都改写贡献者正在编辑的那棵树。 - -**把就地链接保留在提示或`DSH_ADOPT`开关之后。** 已否决;本次变更的早期修订版本正是如此实现,之后被移除。第二种布局本身就是缺陷,因此把它保留为一个选项等于保留了问题,并使此后每次改动必须处理的状态翻倍——提示、开关、工作树不干净的警告,以及第二条链接路径,全都只为维持一种本不该产生的布局而存在。就地链接最初的动机——让脚本能针对本地源码进行测试——在接管方案下依然成立:以检出的`HEAD`为起点创建的 staging worktree 运行的是同一份代码。`DSH_SOURCE`仍可用于安装另一棵树。 - -**在工作树不干净时发出警告或提示。** 已否决:以`HEAD`为起点的`worktree add`本就无法带上未提交的内容,因此该行为是确定的,提示只会增加一个用户无法做出不同选择的决策点。改为在文档中说明该约定。 - -**把被接管克隆的 staging worktree 放在该克隆旁边**(`~/src/staging-*`),而非放进`~/.dsh/source`。已否决:`current`和 PATH 启动器都是每用户唯一的,因此把 worktree 散落到各个克隆的父目录中,会重新引入 source 容器本就为之而设、意在杜绝的同级克隆蔓延问题。 - -## Consequences - -现在一套布局服务于所有安装,因此被接管的克隆无需该技能所述的一次性迁移,即可由`dsh-upgrade`升级,而且安装器不再有任何一条分支会产生无法升级的布局。检出内运行仍然绝不改动工作树。 - -代价是:贡献者不能再把 PATH 指向某个检出、并让`dsh`随其切换分支而跟随该工作树;启动器现在解析到的是一个固定在安装时所接管`HEAD`上的 staging worktree。重新运行安装器会再次接管当前的`HEAD`。 - -此外,接管外部克隆的容器不再自包含:删除该克隆会破坏其 staging worktree。这是复用已有克隆的固有属性,而非本设计带来的性质——被否决的符号链接方案只是掩盖它,而非修复它——诊断依据则是 git 自身的 worktree 记录。 - -## Testing - -`scripts/install.sh` 现有一套位于 `apps/cli/tests/install-script.spec.ts` 的真实 shell PTY 回归测试,使用 stub 依赖覆盖接管路径和 curl 风格路径。curl 风格安装默认使用公开的 `deepseek-ai/deepseek-harness-sdk` 源,而以 pnpm/npx 替换安装器仍是另一项工作。 - -验证是手工完成的,通过一个一次性测试装置以打桩的`pnpm`驱动真实脚本:接管独立克隆;从 linked worktree 接管进其已有容器;显式`DSH_SOURCE`仍回到克隆路径;工作树不干净时静默接管、既不提示也不警告,且其未提交文件留在原处;非 git 检出失败并给出指引;以及`curl`式克隆安装断言所构建的布局——该检查能捕获未解析的`REPO_ROOT`。交互路径在 tmux 下从一个不干净的检出走通,确认整个过程不出现接管提示即可到达启动器,最终`dsh`从新的 staging worktree 运行,而原检出保持其分支不变、未提交文件仍在。 diff --git a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.i18n.yaml similarity index 53% rename from .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml rename to .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.i18n.yaml index eb11cc1b92..13bf0cac84 100644 --- a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.i18n.yaml @@ -1,6 +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/process/2026-07-23-personal-staging-maintenance-skills.md -2026-07-23-personal-staging-maintenance-skills.md: a7ccc5b1e0f13e880c58a93d2e4c2cd4f06e2a93 -2026-07-23-personal-staging-maintenance-skills.zh.md: 8593291f510352429cbb29679e7d033589312e41 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md +2026-08-10-source-run-without-managed-installer.md: 959b6fc6cad5113bb3ba0d17ee27985db5ece82e +2026-08-10-source-run-without-managed-installer.zh.md: 9df969f3754b967fd1b3498191b7c9c2b0a41656 diff --git a/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md new file mode 100644 index 0000000000..959b6fc6ca --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md @@ -0,0 +1,31 @@ +# Agent Note: Source run without a managed installer + +Status: implemented + +English | [中文](2026-08-10-source-run-without-managed-installer.zh.md) + +## Problem + +A repository-owned source installer can provide a stable launcher, isolated staging worktrees, atomic upgrades, rollback storage, and shared maintenance workflows for personal customizations. It also makes the repository responsible for a second lifecycle beside the package manager: host dependency installation, credential prompting, checkout adoption, symlink ownership, staging branch coordination, upgrade recovery, and continued compatibility between the installer and bundled maintenance skills. + +That lifecycle is not required to run or develop DeepSeek Harness from a source checkout. Maintaining it expands the supported filesystem and Git state space without improving the repository-native execution path. + +## Decision + +The repository supports source execution through its `pnpm run` scripts. The root README directs users to clone the repository, run `pnpm install`, configure credentials through the environment or root `.env`, and launch the required interface with `pnpm run demo:web`, `pnpm run demo:headless`, `pnpm run demo:acp`, or `pnpm run dsh`. + +The repository does not distribute a source installer, an installer test suite, or skills that assume a managed `current` symlink and timestamped staging worktrees. Users own source checkout placement, Git updates, and any launcher they create outside the repository. + +## Alternatives considered + +**Keep the installer but document `pnpm run` as another path.** This retains the managed launcher and rollback capability but keeps both lifecycle contracts active, including the installer tests and staging-aware skills. + +**Keep generic customization and upstream-publication skills.** Their safety rules can apply beyond the staging layout, but the shipped workflows form one coupled maintenance system: customization discovers the installed staging checkout, upgrade performs the cutover, and upstream publication is selected from those personal changes. General Git contribution guidance already belongs to repository instructions and does not require product-bundled skills. + +**Replace the installer with a smaller launcher-link script.** This reduces setup behavior but still makes the repository responsible for host PATH mutation and launcher ownership. Source scripts provide the entry points without that state. + +## Consequences + +Source users invoke repository scripts rather than an installed `dsh` command. The repository provides no atomic upgrade cutover or preserved staging rollback checkout, and it does not automate the integration or upstream publication of personal source modifications. A future distribution mechanism must justify its ownership of installation and upgrade state, define recovery behavior, and add tests and user documentation without making the source-run path depend on it. Any future publication workflow must isolate one approved feature and obtain explicit approval before its first push and draft PR. + +Verification covers repository-wide references to the removed entry points, documentation links, generated third-party-notice freshness, and a source CLI smoke through `pnpm run`. diff --git a/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md new file mode 100644 index 0000000000..9df969f375 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 无需托管安装器的源码运行 + +Status: implemented + +[English](2026-08-10-source-run-without-managed-installer.md) | 中文 + +## 问题 + +仓库自带的源码安装器可以提供稳定的启动器、相互隔离的 staging worktree、原子升级、回滚存储,以及用于个人定制的共享维护工作流。与此同时,仓库还必须在包管理器之外负责第二套生命周期:安装宿主依赖、提示输入凭证、接管检出、管理符号链接归属、协调 staging 分支、处理升级恢复,以及持续保持安装器与随附维护 skill(技能)的兼容性。 + +从源码检出运行或开发 DeepSeek Harness 并不需要这套生命周期。维护它会扩大需要支持的文件系统和 Git 状态空间,却无法改进仓库原生的执行路径。 + +## 决策 + +仓库通过 `pnpm run` 脚本支持从源码运行。根 README 指示用户克隆仓库、运行 `pnpm install`、通过环境变量或根目录 `.env` 配置凭证,并使用 `pnpm run demo:web`、`pnpm run demo:headless`、`pnpm run demo:acp` 或 `pnpm run dsh` 启动所需接口。 + +仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。 + +## 考虑过的备选方案 + +**保留安装器,但将 `pnpm run` 记作另一条路径。**这样可以保留受管理的启动器和回滚能力,但两套生命周期约定仍会同时生效,其中包括安装器测试和依赖 staging 布局的 skill。 + +**保留通用的定制与上游发布 skill。**其中的安全规则也能用于 staging 布局之外,但现有工作流共同构成了一套耦合的维护系统:定制工作流查找已安装的 staging 检出,升级工作流执行切换,上游发布工作流则从这些个人修改中选择发布内容。通用 Git 贡献指南已经属于仓库指令,无需以产品随附 skill 的形式提供。 + +**用更小的启动器链接脚本替换安装器。**这样可以简化设置过程,但仓库仍需负责修改宿主 PATH 和管理启动器归属。源码脚本无需引入这类状态即可提供入口点。 + +## 影响 + +源码用户通过仓库脚本运行程序,而非使用已安装的 `dsh` 命令。仓库不提供原子升级切换,也不保留 staging 回滚检出;仓库同样不会自动集成个人源码修改或将其发布到上游。未来的分发机制必须说明为何应由其管理安装和升级状态,定义恢复行为,并补充测试与用户文档,同时不得让源码运行路径依赖该机制。未来任何发布工作流都必须隔离出一项获批功能,并在首次推送和创建草稿 PR(Pull Request)前取得明确批准。 + +验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度,以及通过 `pnpm run` 启动源码 CLI(命令行界面)的冒烟测试。 diff --git a/README.i18n.yaml b/README.i18n.yaml index 43185dc239..dd2d183633 100644 --- a/README.i18n.yaml +++ b/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 README.md -README.md: 9b8944027572072e08a39bd9e482996f8128224c -README.zh.md: a1b2b6a36c4baac8a49b88a42c9f17c986287ff1 +README.md: f929f9746864857fdaf2313f0d5b63d3647adab5 +README.zh.md: 8886016f8afdcd710715a9f4178125e3af9fc788 diff --git a/README.md b/README.md index 9b89440275..f929f97468 100644 --- a/README.md +++ b/README.md @@ -12,41 +12,38 @@ DeepSeek Harness is under internal testing. Features and interfaces may change. The internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group. -## Install +## Run from source -Clone the repository, then run the installer: +Install `git`, Node `^22.19 || >=24`, and Corepack-enabled `pnpm`, then prepare a source checkout: ```sh -git clone +git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git cd deepseek-harness -scripts/install.sh +pnpm install ``` -The installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI. - -The default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options. +Set `DEEPSEEK_API_KEY` in the environment or the repository's root `.env`. Run DeepSeek Harness from this checkout through the `pnpm run` commands below. ## Use DeepSeek Harness ### Web UI -For the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run: +Start the recommended local interface from the repository root: ```sh -(cd ~/.dsh/source/current && pnpm run build) -dsh web +pnpm run demo:web ``` -The path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default. +The command builds the repository before starting the Web UI, which is served at `http://127.0.0.1:3080` by default. ### Profiles -`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`: +The source CLI boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`: ```sh -dsh --profile web # the browser UI (same as: dsh web) -dsh plugin --profile tui add # install a plugin into a custom profile -dsh --profile tui # boot it +pnpm run dsh --profile web # the browser UI +pnpm run dsh plugin --profile tui add # install a plugin into a custom profile +pnpm run dsh --profile tui # boot it ``` The [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands. @@ -56,7 +53,7 @@ The [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer Run one task, print the final answer, and exit: ```sh -dsh --profile headless "summarize this workspace" +pnpm run demo:headless "summarize this workspace" ``` ### Automation and SDKs diff --git a/README.zh.md b/README.zh.md index a1b2b6a36c..8886016f8a 100644 --- a/README.zh.md +++ b/README.zh.md @@ -12,41 +12,38 @@ DeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化 为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。 -## 安装 +## 从源码运行 -克隆仓库,然后运行安装器: +请安装 `git` 和 Node `^22.19 || >=24`,并通过 Corepack 启用 `pnpm`,然后准备源码检出: ```sh -git clone +git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git cd deepseek-harness -scripts/install.sh +pnpm install ``` -安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。 - -默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。 +请通过环境变量或仓库根目录的 `.env` 设置 `DEEPSEEK_API_KEY`。使用下方 `pnpm run` 命令从该检出运行 DeepSeek Harness。 ## 使用 DeepSeek Harness ### Web UI -推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行: +请从仓库根目录启动推荐的本地界面: ```sh -(cd ~/.dsh/source/current && pnpm run build) -dsh web +pnpm run demo:web ``` -上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。 +该命令会先构建仓库,再启动 Web UI。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。 ### Profile -`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层: +源码 CLI(命令行界面)会启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层: ```sh -dsh --profile web # the browser UI (same as: dsh web) -dsh plugin --profile tui add # install a plugin into a custom profile -dsh --profile tui # boot it +pnpm run dsh --profile web # the browser UI +pnpm run dsh plugin --profile tui add # install a plugin into a custom profile +pnpm run dsh --profile tui # boot it ``` profile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。 @@ -56,7 +53,7 @@ profile 布局、层语义与配置输出命令详见 [CLI(命令行界面) 运行一项任务,打印最终答案后退出: ```sh -dsh --profile headless "summarize this workspace" +pnpm run demo:headless "summarize this workspace" ``` ### 自动化与 SDK diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 586cd240a0..0ecae4b668 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -27,7 +27,7 @@ The Cordis framework and its foundation libraries are source-vendored into this ## Runtime npm dependencies -External packages that a workspace package resolves at runtime. `scripts/install.sh` installs this repository itself, so the tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default. +External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default. | Package | License | | --- | --- | diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index 04026262c8..db6bb56a41 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/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 apps/cli/README.md -README.md: 4fae5338a89ce12c2620e123530acf883ae9efff -README.zh.md: a2d086b8ff12fb07f2446fc4162de09739bcdeab +README.md: 082c0b98c08a26fd0468b770c3a9b7332bc5bcfd +README.zh.md: a3a983939ed6fef02c4929dde957762d1fae4c64 diff --git a/apps/cli/README.md b/apps/cli/README.md index 4fae5338a8..082c0b98c0 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -31,8 +31,8 @@ dsh --help # the launcher's own help A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it. -The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and the source launcher. +The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and source execution. ## Development -Production runs require built package and frontend artifacts. From a checkout, `pnpm run dsh` runs the TypeScript entry and forwards arguments; the [source-launcher reference](reference/README.md#source-launcher) describes the PATH symlink and module-resolution contract. +Production runs require built package and frontend artifacts. From the repository root, use `pnpm run dsh ` to run the TypeScript entry and forward arguments; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract. diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index a2d086b8ff..a3a983939e 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -31,8 +31,8 @@ dsh --help # the launcher's own help profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 -[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码启动器。 +[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码执行。 ## 开发 -生产运行需要已构建的包与前端产物。在 checkout 中,`pnpm run dsh` 会运行 TypeScript 入口并转发参数;[源码启动器参考](reference/README.md#source-launcher)说明 PATH 符号链接和模块解析契约。 +生产运行需要已构建的包与前端产物。请从仓库根目录使用 `pnpm run dsh ` 运行 TypeScript 入口并转发参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 8f6db85427..b98a541986 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/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 apps/cli/reference/README.md -README.md: fd0647347312051a1814a5e3464b34032ae70dfc -README.zh.md: afe4b9ba5651e962288ffebbe7c095ad20bcc617 +README.md: e9db27515cf6fa6c910bc684f0250da0a189d335 +README.zh.md: a30fab8da26056956d1f8cad7c73e7cb39287b85 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index fd06473473..e9db27515c 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -79,12 +79,6 @@ Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` sel Install external plugin bundles through `dsh plugin --profile add `. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox. -## Source launcher +## Source execution -Link the source-running launcher onto PATH: - -```sh -ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh -``` - -It resolves the checkout through its real path and launches `apps/cli/src/bin.ts` with `node --import tsx/esm`. `TSX_TSCONFIG_PATH` is pinned to the checkout root, so workspace package resolution is independent of the invoking directory. `pnpm run dsh` uses the same entry and forwards arguments. The built form is `apps/cli/lib/bin.js` after `pnpm run build`. +From the repository root, use `pnpm run dsh `. The script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` and forwards every argument. The built form is `apps/cli/lib/bin.js` after `pnpm run build`. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index afe4b9ba56..a30fab8da2 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -79,12 +79,6 @@ dsh web --help 通过 `dsh plugin --profile add ` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。 -## 源码启动器 +## 源码执行 -把源码运行启动器链接到 PATH: - -```sh -ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh -``` - -它通过 real path 解析 checkout,并使用 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`。`TSX_TSCONFIG_PATH` 固定到 checkout 根目录,因此 workspace 包解析不依赖调用目录。`pnpm run dsh` 使用同一入口并转发参数。运行 `pnpm run build` 后,构建形式为 `apps/cli/lib/bin.js`。 +请从仓库根目录使用 `pnpm run dsh `。该脚本通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。运行 `pnpm run build` 后,构建形式为 `apps/cli/lib/bin.js`。 diff --git a/apps/cli/tests/install-script.spec.ts b/apps/cli/tests/install-script.spec.ts deleted file mode 100644 index 871f72d056..0000000000 --- a/apps/cli/tests/install-script.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { execa } from 'execa' -import { afterEach, describe, expect, it } from 'vitest' - -const installer = fileURLToPath(new URL('../../../scripts/install.sh', import.meta.url)) -const fixtures: string[] = [] - -const PTY_DRIVER = String.raw` -import errno, json, os, pty, select, signal, sys, time -script, cwd, env_json, actions_json = sys.argv[1:] -env = os.environ.copy() -env.update(json.loads(env_json)) -actions = json.loads(actions_json) -pid, fd = pty.fork() -if pid == 0: - os.chdir(cwd) - os.execvpe("sh", ["sh", script], env) - -output = bytearray() -action_index = 0 -deadline = time.monotonic() + 15 -status = None -while time.monotonic() < deadline: - ready, _, _ = select.select([fd], [], [], 0.05) - if ready: - try: - chunk = os.read(fd, 65536) - except OSError as error: - if error.errno != errno.EIO: - raise - chunk = b"" - output.extend(chunk) - while action_index < len(actions) and actions[action_index]["waitFor"].encode() in output: - os.write(fd, actions[action_index]["send"].encode()) - action_index += 1 - waited, candidate = os.waitpid(pid, os.WNOHANG) - if waited == pid: - status = candidate - break - -if status is None: - os.kill(pid, signal.SIGKILL) - _, status = os.waitpid(pid, 0) -sys.stdout.buffer.write(output) -if action_index != len(actions): - sys.stderr.write(f"completed {action_index}/{len(actions)} PTY actions\n") - sys.exit(124) -sys.exit(os.waitstatus_to_exitcode(status)) -` - -interface Action { - readonly waitFor: string - readonly send: string -} - -interface Fixture { - readonly binDirectory: string - readonly launchLog: string - readonly pnpmLog: string - readonly root: string - readonly script: string -} - -afterEach(async () => { - await Promise.all(fixtures.splice(0).map(async (fixture) => { await rm(fixture, { force: true, recursive: true }) })) -}) - -function executable(path: string, content: string): void { - writeFileSync(path, content) - chmodSync(path, 0o755) -} - -async function createFixture(): Promise { - const root = await mkdtemp(join(tmpdir(), 'dsh-install-')) - fixtures.push(root) - const checkoutDirectory = join(root, 'checkout') - const scriptsDirectory = join(checkoutDirectory, 'scripts') - const sourceBinDirectory = join(checkoutDirectory, 'bin') - const fakeBinDirectory = join(root, 'fake-bin') - const binDirectory = join(root, 'path-bin') - for (const directory of [scriptsDirectory, sourceBinDirectory, fakeBinDirectory, binDirectory, join(root, 'home/.dsh')]) { - mkdirSync(directory, { recursive: true }) - } - const script = join(scriptsDirectory, 'install.sh') - copyFileSync(installer, script) - const launchLog = join(root, 'launch.log') - const pnpmLog = join(root, 'pnpm.log') - executable(join(sourceBinDirectory, 'dsh'), '#!/bin/sh\nprintf \'%s\\n\' "$*" >"$DSH_TEST_LAUNCH_LOG"\n') - executable(join(fakeBinDirectory, 'pnpm'), `#!/bin/sh -if [ "\${1:-}" = --version ]; then printf '11.7.0\\n'; exit 0; fi -printf '%s\\n' "$*" >>"$DSH_TEST_PNPM_LOG" -`) - await execa('git', ['init', '-q'], { cwd: checkoutDirectory }) - await execa('git', ['add', 'bin/dsh', 'scripts/install.sh'], { cwd: checkoutDirectory }) - await execa('git', [ - '-c', 'user.name=dsh-test', - '-c', 'user.email=dsh-test@example.invalid', - 'commit', '-qm', 'fixture', - ], { cwd: checkoutDirectory }) - writeFileSync(join(root, 'home/.dsh/.env'), 'DEEPSEEK_API_KEY=test\n') - return { binDirectory, launchLog, pnpmLog, root, script } -} - -async function runInstaller(fixture: Fixture, actions: readonly Action[]): Promise { - const result = await execa('python3', [ - '-c', - PTY_DRIVER, - fixture.script, - fixture.root, - JSON.stringify({ - DSH_BIN_DIR: fixture.binDirectory, - DSH_HOME: join(fixture.root, 'home/.dsh'), - DSH_TEST_LAUNCH_LOG: fixture.launchLog, - DSH_TEST_PNPM_LOG: fixture.pnpmLog, - HOME: join(fixture.root, 'home'), - PATH: `${join(fixture.root, 'fake-bin')}:${fixture.binDirectory}:${process.env.PATH ?? ''}`, - }), - JSON.stringify(actions), - ], { reject: false, stripFinalNewline: false, timeout: 20_000 }) - expect(result.exitCode, result.stderr).toBe(0) - return result.stdout -} - -describe.runIf(process.platform !== 'win32')('one-line installer launch', { timeout: 25_000 }, () => { - it('builds and launches the Web UI', async () => { - const fixture = await createFixture() - - const output = await runInstaller(fixture, [ - { waitFor: 'Replace it?', send: '\n' }, - ]) - - expect(output).toContain('launching Web UI') - expect(readFileSync(fixture.pnpmLog, 'utf8')).toBe('install\nrun build\n') - expect(readFileSync(fixture.launchLog, 'utf8')).toBe('web\n') - }) -}) diff --git a/apps/cli/tests/source-launch.compat.spec.ts b/apps/cli/tests/source-launch.compat.spec.ts index bd2268231b..29f17ac7b9 100644 --- a/apps/cli/tests/source-launch.compat.spec.ts +++ b/apps/cli/tests/source-launch.compat.spec.ts @@ -3,9 +3,9 @@ import { execa } from 'execa' import { describe, expect, it } from 'vitest' /** - * Keyless smoke for the SOURCE `dsh` launcher: run `apps/cli/src/bin.ts` + * Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts` * with the exact production launch vector (`node --import tsx/esm`, the same - * executable and arguments as `bin/dsh` and the root `dsh`/`demo:web` scripts) and assert the + * shape as the root `dsh` script) and assert the * required-config diagnostic. The Node compatibility matrix runs this * WHOLE file, so a Node release changing module hooks or TypeScript handling * breaks this gate instead of every developer's `pnpm dsh`; the built-bin diff --git a/bin/dsh b/bin/dsh deleted file mode 100755 index c578d78e74..0000000000 --- a/bin/dsh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -# dsh launcher: runs the apps/cli `dsh` bin FROM SOURCE through the tsx ESM -# hook, so a symlink from anywhere (e.g. ~/.local/bin/dsh) always executes the -# current working tree without a build step. -set -eu - -# Resolve symlink chains without readlink -f (not on every macOS). -script=$0 -while [ -L "$script" ]; do - target=$(readlink "$script") - case $target in - /*) script=$target ;; - *) script=$(dirname "$script")/$target ;; - esac -done -root=$(CDPATH='' cd -- "$(dirname -- "$script")/.." && pwd) - -# The ESM-only tsx hook transforms TypeScript and projects this checkout's -# tsconfig paths into Node resolution (the CJS hook stays off: the graph is -# ESM-only and the CJS resolver costs ~0.4s of startup). Absolute paths keep -# both the hook and the tsconfig anchored to this checkout when the launcher -# runs from any cwd, where bare `tsx/esm` would not resolve. -NODE_USE_ENV_PROXY=1 \ - TSX_TSCONFIG_PATH="$root/tsconfig.json" \ - exec node --import "$root/node_modules/tsx/dist/esm/index.mjs" \ - "$root/apps/cli/src/bin.ts" "$@" diff --git a/docs/api-gateway.i18n.yaml b/docs/api-gateway.i18n.yaml index ba4cbd5f55..9f5e41be77 100644 --- a/docs/api-gateway.i18n.yaml +++ b/docs/api-gateway.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/api-gateway.md -api-gateway.md: 06f98546c8858af08efcb07c007d8e1b95b90c19 -api-gateway.zh.md: 82bf8923c45151d0c00e71848be89806427d1ce3 +api-gateway.md: 7c666dbc431b076262a9a93f1baefd88a29237e8 +api-gateway.zh.md: eee783def7e981dc888e1192c6e1a1f9ccd424c4 diff --git a/docs/api-gateway.md b/docs/api-gateway.md index 06f98546c8..7c666dbc43 100644 --- a/docs/api-gateway.md +++ b/docs/api-gateway.md @@ -147,7 +147,7 @@ pnpm run build Web development normally starts the source Host after one complete build and runs the Client plugin watcher in another terminal: ```sh -pnpm run dsh -- web --dev +pnpm run dsh web --dev pnpm run dev:web ``` diff --git a/docs/api-gateway.zh.md b/docs/api-gateway.zh.md index 82bf8923c4..eee783def7 100644 --- a/docs/api-gateway.zh.md +++ b/docs/api-gateway.zh.md @@ -147,7 +147,7 @@ pnpm run build Web 开发通常在完成一次构建后启动源码 Host,并在另一个终端运行 Client plugin watcher: ```sh -pnpm run dsh -- web --dev +pnpm run dsh web --dev pnpm run dev:web ``` diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index d1f9a72df5..cd80f4b8eb 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/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/boot/app-boot/README.md -README.md: 1d56b2b6d22c08574f8e361955bee1dbe2aca601 -README.zh.md: 5429a1322d0311f03c7c43946753a290e28cd936 +README.md: 9880a4789b85f516ed16ead293ebb5c10da0d57f +README.zh.md: b070696e5195feb663ad44605f55553eac672656 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 1d56b2b6d2..9880a4789b 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -29,7 +29,7 @@ The Loader mounts entries concurrently, so a surface can already own the termina `cordis:group` is registered beside `cordis:include` so a composition can give one `isolate` realm to a provider and its consumers together. Both load through the ambient module pipeline rather than the included tree's own specifier resolution, which is what lets a composition outside this workspace — an agent preset under the Harness home — use a group row at all. -Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`, npm packages) resolve through the Cordis Loader's internal module loader. They resolve from the config directory by default; a closed runtime passes `bareModuleBaseUrl` to `boot` or `mountRootInclude` so its installed package tree remains authoritative even when the config lives inside another Node project. Relative specifiers always resolve against the config directory. Repository bins install Loader's optional `node-addon-require-builtin` peer; external callers must supply it or install plugins where plain Node import resolution can find them. The built `dsh-app-boot` artifact embeds the statically mounted Include implementation while leaving Loader external, so the include tree and host bind to one Loader peer. The `dsh` source launcher additionally maps manifest-declared workspace packages to their TypeScript source; its configuration gate requires every shipped raw/Web bare plugin to appear in the resolver manifest's `dependencies`. +Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`, npm packages) resolve through the Cordis Loader's internal module loader. They resolve from the config directory by default; a closed runtime passes `bareModuleBaseUrl` to `boot` or `mountRootInclude` so its installed package tree remains authoritative even when the config lives inside another Node project. Relative specifiers always resolve against the config directory. Repository bins install Loader's optional `node-addon-require-builtin` peer; external callers must supply it or install plugins where plain Node import resolution can find them. The built `dsh-app-boot` artifact embeds the statically mounted Include implementation while leaving Loader external, so the include tree and host bind to one Loader peer. The `pnpm run dsh` source path additionally maps manifest-declared workspace packages to their TypeScript source; its configuration gate requires every shipped raw/Web bare plugin to appear in the resolver manifest's `dependencies`. This package carries no loader hooks and no dev-mode surface. The [`dsh` app](../../../apps/cli/README.md) owns its Node source-launch hook and consumes these helpers for the boot sequence; built consumers continue to use plain Node package resolution. diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 5429a1322d..b070696e51 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -29,7 +29,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 `cordis:group` 与 `cordis:include` 一并注册,使一份组装能把一个提供方与它的消费方放进同一个 `isolate` realm。两者都通过宿主的模块管线加载,而非被包含树自身的说明符解析,这正是让本工作区之外的组装——放在 Harness home 下的 agent preset——能够使用 group 行的原因。 -配置中的裸插件 specifier(`@deepseek-ai/dsh-*`、npm 包)通过 Cordis Loader 的内部模块 loader 解析。默认情况下,它们从配置目录解析;封闭运行时会向 `boot` 或 `mountRootInclude` 传入 `bareModuleBaseUrl`,使已安装包树保持权威,即使配置位于另一个 Node 项目中也不受遮蔽。相对 specifier 始终以配置目录为基准解析。仓库 bin 会安装 Loader 的可选 peer `node-addon-require-builtin`;外部调用方必须提供该组件,或者把插件安装到普通 Node import 解析可以找到的位置。构建后的 `dsh-app-boot` 产物内嵌静态挂载的 Include 实现,但仍将 Loader 保持为外部依赖,因此 include 树与宿主会绑定到同一个 Loader peer。`dsh` 源码启动器还会将 manifest(元数据清单)声明的 workspace 包映射到其 TypeScript 源码;其配置门禁要求每个随附的原始/Web 裸插件都出现在解析所用 manifest 的 `dependencies` 中。 +配置中的裸插件 specifier(`@deepseek-ai/dsh-*`、npm 包)通过 Cordis Loader 的内部模块 loader 解析。默认情况下,它们从配置目录解析;封闭运行时会向 `boot` 或 `mountRootInclude` 传入 `bareModuleBaseUrl`,使已安装包树保持权威,即使配置位于另一个 Node 项目中也不受遮蔽。相对 specifier 始终以配置目录为基准解析。仓库 bin 会安装 Loader 的可选 peer `node-addon-require-builtin`;外部调用方必须提供该组件,或者把插件安装到普通 Node import 解析可以找到的位置。构建后的 `dsh-app-boot` 产物内嵌静态挂载的 Include 实现,但仍将 Loader 保持为外部依赖,因此 include 树与宿主会绑定到同一个 Loader peer。`pnpm run dsh` 源码路径还会将 manifest(元数据清单)声明的 workspace 包映射到其 TypeScript 源码;其配置门禁要求每个随附的原始/Web 裸插件都出现在解析所用 manifest 的 `dependencies` 中。 此包不包含 loader 钩子,也不提供开发模式接口。[`dsh` 应用](../../../apps/cli/README.md) 持有自己的 Node 源码启动钩子,并在启动序列中使用这些 helper;构建后的消费方仍使用普通 Node 包解析。 diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index faf8c756dd..fb6838de54 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -27,8 +27,8 @@ const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'p * root manifest), test infrastructure, the documentation site, the runnable * demo leaves, and the native launcher's build workspace. A runtime * declaration by anything outside these areas is a disclosure-relevant - * runtime dependency, because `scripts/install.sh` installs the repository - * itself and any plugin package can be mounted from a user's `cordis.yml`. + * runtime dependency because any plugin package can be mounted from a user's + * `cordis.yml`. */ const DEV_ONLY_AREAS = [ 'package.json', @@ -369,7 +369,7 @@ function collectNpmDeps(): ExternalDep[] { */ export function tierExternalDeps(manifests: Map, names: Set): Map { const tiers = new Map() - // `tsx` is runtime by fiat: `bin/dsh` execs the CLI through its ESM hook. + // `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook. tiers.set('tsx', true) for (const [path, manifest] of manifests) { const devOnly = DEV_ONLY_AREAS.some(area => (area.endsWith('/') ? path.startsWith(area) : path === area)) @@ -707,7 +707,7 @@ ${vendored.map(row => `| \`${row.npmName}\` | \`${row.upstreamName}\` | [${row.u ## Runtime npm dependencies -External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default. +External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default. ${renderNpmTable(runtimeDeps)} diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index f290782892..0000000000 --- a/scripts/install.sh +++ /dev/null @@ -1,425 +0,0 @@ -#!/bin/sh -# dsh one-line installer. -# -# curl -fsSL https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/scripts/install.sh | sh -# -# It clones the harness under ~/.dsh/source (the master clone at -# ~/.dsh/source/master), adds a per-install staging worktree at -# ~/.dsh/source/staging- on branch dsh-staging/, checks -# host dependencies (git, Node, pnpm) and offers to install a missing pnpm, runs -# `pnpm install`, points the stable `~/.dsh/source/current` symlink -# at that staging worktree and symlinks `dsh` onto PATH at `current/bin/dsh`, -# records your API credentials in the Harness home (`~/.dsh`) dsh reads at boot, -# builds the repository artifacts, and launches the Web UI. Keeping every -# checkout under ~/.dsh/source keeps successive -# upgrades in one place instead of scattered sibling clones, and lets staging -# worktrees share the master clone's object store. The PATH symlink resolves through -# `current`, so an upgrade repoints one stable symlink instead of relinking PATH: -# the `dsh` on PATH never moves and can never dangle. -# -# When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather -# than `curl ... | sh`) it never clones and never touches that working tree; -# DSH_REF is ignored. Instead it *adopts* the checkout: `git rev-parse -# --git-common-dir` resolves the repository behind it (for a linked worktree that -# is the real clone, not the worktree), and a fresh staging worktree branched -# from the checkout's HEAD lands in the source container beside `current`. The -# container owns staging worktrees and `current`; the clone is discovered, not -# owned, so an arbitrary clone (~/src/dsh) and a managed one converge on one -# layout and stay upgradable. Adoption carries committed work only: the staging -# worktree branches from HEAD, so uncommitted changes stay in the checkout. -# Setting DSH_SOURCE to a different directory opts back into the normal -# clone/worktree path. -# -# Adopting an arbitrary clone leaves the container not self-contained: its -# staging worktrees hold an absolute gitdir pointer into that clone, so deleting -# it breaks them. `git worktree list` in that clone is the record of which -# worktrees depend on it. -# -# When run through `curl | sh` the script text arrives on stdin, so every -# prompt and the final launch read the controlling terminal (/dev/tty) directly; -# with no terminal the script prints the manual next steps instead. -# -# Overridable via environment: -# DSH_REF branch or tag to clone/checkout (default: master) -# DSH_REPO clone URL (default: the GitHub repo) -# DSH_SOURCE source container directory (default: ~/.dsh/source) -# DSH_MASTER master clone directory (default: $DSH_SOURCE/master) -# DSH_CURRENT stable symlink to the active worktree (default: $DSH_SOURCE/current) -# DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin) -# DSH_HOME Harness home holding profiles and user patches (default: ~/.dsh) -set -eu - -DSH_REF=${DSH_REF:-master} -DSH_REPO=${DSH_REPO:-https://github.com/deepseek-ai/deepseek-harness-sdk.git} -# DSH_SOURCE is the staging-worktree container and the default home of `current`. -# DSH_MASTER names the main clone: clone mode defaults it inside DSH_SOURCE, -# while adoption discovers an existing clone anywhere on disk. Remember whether -# DSH_SOURCE was explicit so a different path selects clone mode. -if [ -n "${DSH_SOURCE:-}" ]; then DSH_SOURCE_EXPLICIT=1; else DSH_SOURCE_EXPLICIT=0; fi -DSH_SOURCE=${DSH_SOURCE:-$HOME/.dsh/source} -DSH_MASTER=${DSH_MASTER:-$DSH_SOURCE/master} -# The stable symlink the PATH launcher resolves through: PATH/dsh -> -# current/bin/dsh -> /bin/dsh. Installs and upgrades repoint `current`; -# the PATH target remains current/bin/dsh. -DSH_CURRENT=${DSH_CURRENT:-$DSH_SOURCE/current} -DSH_BIN_DIR=${DSH_BIN_DIR:-$HOME/.local/bin} -# One UTC basic timestamp names this install's staging branch and worktree. -DSH_STAMP=$(date -u +%Y%m%dT%H%M%SZ) -DSH_STAGING_BRANCH=dsh-staging/$DSH_STAMP -DSH_STAGING=$DSH_SOURCE/staging-$DSH_STAMP - -# --- path helpers --------------------------------------------------------------- -# Every path comparison below runs on physical paths. Git always reports resolved -# paths, so comparing one against an unresolved path disagrees whenever a symlink -# sits anywhere above the checkout — a symlinked home directory is enough, and -# macOS reaches every mktemp path that way through /var -> private/var. The -# mismatch silently misclassifies an existing managed install as a foreign clone -# and builds a second container beside the real one. -# `git rev-parse --path-format=absolute` would do this, but it needs git 2.31+. -# -# A not-yet-created directory (the container on a fresh install) has no physical -# path. Falling back here rather than at each call site keeps every caller a -# plain assignment, so no site can compare against an empty path by forgetting -# its own fallback. -resolve_dir() { CDPATH= cd -- "$1" 2>/dev/null && pwd -P || printf '%s\n' "$1"; } - -# --- in-repo detection --------------------------------------------------------- -# Under `curl ... | sh` the script text arrives on stdin, so $0 is the shell -# name and no file path resolves; running a checked-out copy (`sh -# scripts/install.sh`) makes $0 the script file. When $0 is a readable file whose -# parent is a scripts/ dir inside a real dsh checkout (bin/dsh launcher present), -# this is in-repo mode: never clone, never touch that working tree. An explicit -# DSH_SOURCE pointing elsewhere opts back into the clone/worktree path. -IN_REPO=0 -DSH_CHECKOUT='' -if [ -f "$0" ]; then - _self_dir=$(resolve_dir "$(dirname -- "$0")") - if [ -n "$_self_dir" ]; then - # Physical without its own resolve_dir: dirname is textual, so trimming a - # resolved path leaves one. The comparison below depends on that. - _repo_root=$(dirname -- "$_self_dir") - if [ "$(basename -- "$_self_dir")" = scripts ] \ - && [ -x "$_repo_root/bin/dsh" ] && [ -f "$_repo_root/scripts/install.sh" ]; then - # Compare the explicit DSH_SOURCE physically: an unresolved but equivalent - # path must still count as "the caller meant this checkout". - _src_resolved=$(resolve_dir "$DSH_SOURCE") - if [ "$DSH_SOURCE_EXPLICIT" = 0 ] || [ "$_src_resolved" = "$_repo_root" ]; then - IN_REPO=1 - DSH_CHECKOUT=$_repo_root - fi - fi - fi -fi - -# --- terminal-aware prompting -------------------------------------------------- -# stdin is the piped script, so read the controlling terminal for input. -if { true /dev/null; then - HAS_TTY=1 - # Restore terminal echo on exit or interrupt: ask_secret disables echo between - # its stty toggles, and dash (a common `sh`) does not run an EXIT trap when the - # shell is killed by a signal, so the fatal signals need their own handler. A - # successful run ends in exec, which replaces this process and drops the traps. - trap 'stty echo /dev/null || true' EXIT - trap 'stty echo /dev/null || true; exit 130' INT TERM HUP -else - HAS_TTY=0 -fi - -# Colour only when writing to a terminal. -if [ -t 1 ]; then - B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m') - GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RST=$(printf '\033[0m') -else - B=''; DIM=''; RED=''; GRN=''; YEL=''; RST='' -fi - -info() { printf '%s==>%s %s\n' "$GRN" "$RST" "$1"; } -step() { printf '\n%s==>%s %s%s%s\n' "$GRN" "$RST" "$B" "$1" "$RST"; } -warn() { printf '%s warn%s %s\n' "$YEL" "$RST" "$1" >&2; } -die() { printf '%serror%s %s\n' "$RED" "$RST" "$1" >&2; exit 1; } - -# ask PROMPT [DEFAULT] -> answer on stdout (plain-text line). -ask() { - [ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell" - printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty - IFS= read -r _ans answer on stdout, with terminal echo suppressed. -ask_secret() { - [ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell" - printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty - stty -echo /dev/null || true - IFS= read -r _sec /dev/null || true - printf '\n' >/dev/tty - printf '%s' "$_sec" -} - -# confirm PROMPT [Y] -> exit 0 on yes. Default is no unless second arg is "Y". -confirm() { - _def=${2:-N} - if [ "$HAS_TTY" != 1 ]; then - [ "$_def" = Y ] # non-interactive: take the default - return - fi - if [ "$_def" = Y ]; then _hint='[Y/n]'; else _hint='[y/N]'; fi - printf '%s%s%s %s ' "$B" "$1" "$RST" "$_hint" >/dev/tty - IFS= read -r _r /dev/null 2>&1 || die "git is required but not found. Install git, then re-run." -info "git ... ok" - -# Node ^22.19.0 || >=24.0.0 (see the root package.json "engines" field). -node_ok() { - command -v node >/dev/null 2>&1 || return 1 - _v=$(node -v 2>/dev/null) || return 1 - _v=${_v#v} - _major=${_v%%.*} - _rest=${_v#*.} - _minor=${_rest%%.*} - case "$_major" in ''|*[!0-9]*) return 1 ;; esac - case "$_minor" in ''|*[!0-9]*) _minor=0 ;; esac - [ "$_major" -ge 24 ] && return 0 - [ "$_major" -eq 22 ] && [ "$_minor" -ge 19 ] && return 0 - return 1 -} -if node_ok; then - info "node $(node -v) ... ok" -else - if command -v node >/dev/null 2>&1; then - die "Node $(node -v) is unsupported. dsh needs ^22.19.0 || >=24.0.0 — upgrade Node, then re-run." - fi - die "Node is required but not found. Install Node ^22.19.0 || >=24, then re-run." -fi - -# pnpm is the only dependency we offer to install for you. -if command -v pnpm >/dev/null 2>&1; then - info "pnpm $(pnpm --version) ... ok" -else - warn "pnpm is not installed." - if confirm "Install pnpm now?" Y; then - if command -v corepack >/dev/null 2>&1 && corepack enable pnpm >/dev/null 2>&1; then - info "enabled pnpm via corepack" - elif command -v npm >/dev/null 2>&1 && npm install -g pnpm >/dev/null 2>&1; then - info "installed pnpm via npm" - else - die "could not install pnpm automatically. Install it (https://pnpm.io/installation), then re-run." - fi - command -v pnpm >/dev/null 2>&1 || die "pnpm still not on PATH after install. Open a new shell, then re-run." - else - die "pnpm is required. Install it (https://pnpm.io/installation), then re-run." - fi -fi - -# --- 2. resolve the repository and lay out the staging worktree --------------- -# The source container owns staging worktrees and `current`; the repository is -# *discovered*, not owned. A curl install discovers it by cloning to $DSH_MASTER; -# in-repo adoption discovers it from the checkout. Both then run one shared -# worktree/exclude/lock path, so an arbitrary clone and a managed install -# converge on the same layout. -# -# REPO_COMMON is the shared git directory every worktree of the repository -# points at; REPO_ROOT is the working tree that owns it (the master clone). -REPO_COMMON='' -REPO_ROOT='' - -if [ "$IN_REPO" = 1 ]; then - step "Using existing checkout at $DSH_CHECKOUT" - info "running from inside the repo — never cloning, and DSH_REF is ignored" - - # Resolve the repository behind the checkout. --git-common-dir returns the - # SHARED git dir, so a linked worktree resolves to the real clone rather than - # itself; it is relative for a plain clone, so anchor it before resolving. - # Require the resolved git dir to exist: resolve_dir echoes its argument back - # for a missing path, so test the directory rather than the returned string. - if _common=$(git -C "$DSH_CHECKOUT" rev-parse --git-common-dir 2>/dev/null) && [ -n "$_common" ]; then - case "$_common" in /*) ;; *) _common=$DSH_CHECKOUT/$_common ;; esac - [ -d "$_common" ] && REPO_COMMON=$(resolve_dir "$_common") - fi - [ -n "$REPO_COMMON" ] || die "$DSH_CHECKOUT is not a git repository — cannot adopt it." - REPO_ROOT=$(dirname -- "$REPO_COMMON") - - # Reuse the container when the repository already lives inside it (the normal - # managed install re-running its own script); otherwise treat that clone as - # its own master and keep worktrees in the default container. - _src_resolved=$(resolve_dir "$DSH_SOURCE") - case "$REPO_ROOT/" in - "$_src_resolved"/*) info "repository $REPO_ROOT is already inside $DSH_SOURCE" ;; - *) info "adopting clone $REPO_ROOT as its own master" ;; - esac - DSH_MASTER=$REPO_ROOT -else - step "Fetching source into $DSH_MASTER" - if [ -d "$DSH_MASTER/.git" ]; then - info "existing master clone found — updating" - git -C "$DSH_MASTER" fetch origin "$DSH_REF" - # Reset the master checkout to the freshly fetched tip. FETCH_HEAD (not - # origin/) so this resolves for a tag as well as a branch, and -B makes - # the re-run idempotent whether or not DSH_REF changed since the last install. - git -C "$DSH_MASTER" checkout -q -B "$DSH_REF" FETCH_HEAD - else - mkdir -p "$DSH_SOURCE" - git clone --branch "$DSH_REF" "$DSH_REPO" "$DSH_MASTER" - fi - # Physical on both branches: REPO_ROOT is compared against resolved paths - # below, and REPO_COMMON stays symmetric with it so neither can be read as - # carrying a different kind of path. - REPO_COMMON=$(resolve_dir "$DSH_MASTER/.git") - REPO_ROOT=$(resolve_dir "$DSH_MASTER") -fi - -step "Adding staging worktree at $DSH_STAGING" -[ -e "$DSH_STAGING" ] && die "staging path $DSH_STAGING already exists — remove it or set DSH_SOURCE elsewhere, then re-run." -mkdir -p "$DSH_SOURCE" -# The staging worktree owns the branch dsh runs from; the repository stays as -# the fetch/upgrade base and is never a launcher target. A clone install -# branches from the ref it just fetched; adoption branches from the checkout's -# HEAD so the contributor's committed work is what runs. -if [ "$IN_REPO" = 1 ]; then - git -C "$DSH_CHECKOUT" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD -else - git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" FETCH_HEAD 2>/dev/null \ - || git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD -fi -# Exclude the per-worktree merge lock in the shared git dir's info/exclude, -# which every linked worktree inherits. -_exclude="$REPO_COMMON/info/exclude" -if [ -f "$_exclude" ] && ! grep -qxF '.agents/merge.lock' "$_exclude" 2>/dev/null; then - printf '.agents/merge.lock\n' >>"$_exclude" -fi -mkdir -p "$DSH_STAGING/.agents" -: >"$DSH_STAGING/.agents/merge.lock" - -# --- 3. install dependencies (no build; the launcher runs from source) -------- -step "Installing dependencies with pnpm (this can take a while)" -( cd "$DSH_STAGING" && pnpm install ) - -[ -x "$DSH_STAGING/bin/dsh" ] || die "launcher $DSH_STAGING/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?" - -# --- 4. put `dsh` on PATH ------------------------------------------------------ -# Every install goes through a stable `current` symlink so an upgrade repoints -# one symlink (current -> new worktree) and the PATH launcher never moves: -# PATH/dsh -> current/bin/dsh -> /bin/dsh. -step "Linking dsh into $DSH_BIN_DIR" -mkdir -p "$DSH_BIN_DIR" -# The launcher must resolve to a staging worktree, never to the repository -# itself: an upgrade repoints `current`, so aliasing it onto the master clone -# would make every upgrade rewrite the fetch/upgrade base. Compare physical -# paths — a symlinked or unresolved path would slip past a string compare. -_staging_resolved=$(resolve_dir "$DSH_STAGING") -[ "$_staging_resolved" = "$REPO_ROOT" ] \ - && die "refusing to point $DSH_CURRENT at the repository $REPO_ROOT — the launcher must resolve to a staging worktree." -# Point `current` at this staging worktree with `ln -sfn`: -f replaces an -# existing `current` (re-run or upgrade) and -n stops `ln` from dereferencing -# an existing symlink-to-directory and dropping the new link *inside* the old -# worktree. `mv` is unusable here — BSD/macOS `mv` follows the existing dir -# symlink the same way. The swap is one unlink+symlink pair on a local fs; the -# installer holds no other process racing this path. -ln -sfn "$DSH_STAGING" "$DSH_CURRENT" -info "pointed $DSH_CURRENT -> $DSH_STAGING" -DSH_LAUNCH_TARGET=$DSH_CURRENT/bin/dsh -ln -sf "$DSH_LAUNCH_TARGET" "$DSH_BIN_DIR/dsh" -info "linked $DSH_BIN_DIR/dsh -> $DSH_LAUNCH_TARGET" - -case ":$PATH:" in - *":$DSH_BIN_DIR:"*) ON_PATH=1 ;; - *) ON_PATH=0 ;; -esac -if [ "$ON_PATH" = 0 ]; then - warn "$DSH_BIN_DIR is not on your PATH." - _line="export PATH=\"$DSH_BIN_DIR:\$PATH\"" - _rc='' - _sh=${SHELL:-} # SHELL may be unset; word-removal on an unset var trips set -u under dash. - case "${_sh##*/}" in - zsh) _rc="$HOME/.zshrc" ;; - bash) _rc="$HOME/.bashrc" ;; - esac - if [ -n "$_rc" ] && [ -f "$_rc" ] && grep -qF "$_line" "$_rc" 2>/dev/null; then - info "$_rc already exports $DSH_BIN_DIR — open a new shell to pick it up" - elif [ -n "$_rc" ] && confirm "Add it to $_rc?" Y; then - printf '\n# Added by the dsh installer\n%s\n' "$_line" >>"$_rc" - info "updated $_rc — run 'source $_rc' or open a new shell to pick it up" - else - warn "add this line to your shell profile yourself:" - printf ' %s\n' "$_line" - fi -fi - -# --- 5. credentials ------------------------------------------------------------ -# Mirror app-boot's resolveDshHome precedence ($DSH_HOME, else ~/.dsh) so creds land where dsh reads them. -if [ -n "${DSH_HOME:-}" ]; then - CONF="$DSH_HOME" -else - CONF="$HOME/.dsh" -fi -ENV_FILE="$CONF/.env" - -step "Configuring credentials" -if [ -f "$ENV_FILE" ] && grep -q '^DEEPSEEK_API_KEY=' "$ENV_FILE" 2>/dev/null; then - info "DEEPSEEK_API_KEY already set in $ENV_FILE" - if ! confirm "Replace it?" N; then - SKIP_CREDS=1 - fi -fi -if [ "${SKIP_CREDS:-0}" != 1 ]; then - if [ "$HAS_TTY" = 1 ]; then - API_KEY=$(ask_secret "DeepSeek API key (input hidden):") - if [ -z "$API_KEY" ]; then - warn "no key entered — skipping. Set DEEPSEEK_API_KEY in $ENV_FILE before using dsh." - else - BASE_URL=$(ask "DeepSeek base URL (optional, Enter to skip):") - mkdir -p "$CONF" - # The installer owns exactly the two DEEPSEEK_* lines; any other lines the - # user keeps in this .env are preserved. The rewrite happens in a subshell - # so umask 077 (which closes the create-time permission race) does not leak - # into the exec'd dsh, and lands atomically via a same-dir temp + mv. - _tmp="$ENV_FILE.dsh.$$" - ( - umask 077 - if [ -f "$ENV_FILE" ]; then - grep -v -e '^DEEPSEEK_API_KEY=' -e '^DEEPSEEK_BASE_URL=' "$ENV_FILE" >"$_tmp" || true - else - : >"$_tmp" - fi - printf 'DEEPSEEK_API_KEY=%s\n' "$API_KEY" >>"$_tmp" - if [ -n "$BASE_URL" ]; then printf 'DEEPSEEK_BASE_URL=%s\n' "$BASE_URL" >>"$_tmp"; fi - ) - mv "$_tmp" "$ENV_FILE" - chmod 600 "$ENV_FILE" 2>/dev/null || true - info "wrote $ENV_FILE" - fi - else - warn "no terminal for credential input — set DEEPSEEK_API_KEY in $ENV_FILE before using dsh." - fi -fi - -# --- 6. build and launch the Web interface ------------------------------------- -step "Done" -if [ "$HAS_TTY" = 1 ]; then - step "Building DeepSeek Harness for Web UI" - ( cd "$DSH_STAGING" && pnpm run build ) - info "launching Web UI — run 'dsh web' anytime to start again" - exec "$DSH_BIN_DIR/dsh" web \ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh --profile headless \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n" + "content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Run from source\n\nInstall `git`, Node `^22.19 || >=24`, and Corepack-enabled `pnpm`, then prepare a source checkout:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness-sdk.git\ncd deepseek-harness\npnpm install\n```\n\nSet `DEEPSEEK_API_KEY` in the environment or the repository's root `.env`. Run DeepSeek Harness from this checkout through the `pnpm run` commands below.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nStart the recommended local interface from the repository root:\n\n```sh\npnpm run demo:web\n```\n\nThe command builds the repository before starting the Web UI, which is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\nThe source CLI boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:\n\n```sh\npnpm run dsh --profile web # the browser UI\npnpm run dsh plugin --profile tui add # install a plugin into a custom profile\npnpm run dsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\npnpm run demo:headless \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n" }, { "role": "assistant", - "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh --profile headless \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACP(Agent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n" + "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 从源码运行\n\n请安装 `git` 和 Node `^22.19 || >=24`,并通过 Corepack 启用 `pnpm`,然后准备源码检出:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness-sdk.git\ncd deepseek-harness\npnpm install\n```\n\n请通过环境变量或仓库根目录的 `.env` 设置 `DEEPSEEK_API_KEY`。使用下方 `pnpm run` 命令从该检出运行 DeepSeek Harness。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n请从仓库根目录启动推荐的本地界面:\n\n```sh\npnpm run demo:web\n```\n\n该命令会先构建仓库,再启动 Web UI。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n源码 CLI(命令行界面)会启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:\n\n```sh\npnpm run dsh --profile web # the browser UI\npnpm run dsh plugin --profile tui add # install a plugin into a custom profile\npnpm run dsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\npnpm run demo:headless \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACP(Agent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n" }, { "role": "user", diff --git a/skills/dsh-customize/SKILL.md b/skills/dsh-customize/SKILL.md deleted file mode 100644 index 5d67af1e51..0000000000 --- a/skills/dsh-customize/SKILL.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: dsh-customize -description: Customize or maintain any dsh source checkout — the one powering the current DSH process, the installed `dsh` command, or a sibling dsh/deepseek-harness clone. Use before any requested action that alters such a checkout's files or git state. Read-only questions that only inspect the checkout do not trigger this. Do not edit the personal staging checkout directly. ---- - -# DSH Customize - -Make personal DSH changes in task worktrees and integrate them under the staging lock. Repository instructions still apply. - -## Find staging - -Do not assume a path or branch name. DSH is usually installed from source with a personal staging branch; create one for the user only when none exists. - -1. Inspect `command -v dsh` in the user's launch environment before resolving symlinks. -2. Follow the launcher through the full symlink chain to reach the source checkout, then ask Git for everything else. The `dsh` on PATH is a symlink, usually through a stable `current` symlink into the active staging worktree; resolve the chain physically and take the launcher's parent directory as the checkout. Derive the rest from that checkout rather than from any path convention: `git -C rev-parse --show-toplevel` confirms the checkout root, and `git -C rev-parse --git-common-dir` gives the shared git directory — a linked worktree reports the real clone's, not its own — whose parent is the main clone, the one real clone whose object store every worktree shares. `--git-common-dir` answers relatively for a plain clone, so anchor it against the checkout before use, and resolve it physically: Git reports resolved paths, so comparing one against an unresolved path silently misidentifies the clone whenever a symlink sits anywhere above the checkout, which a symlinked home directory alone is enough to cause. `git -C
worktree list` then enumerates every checkout sharing it. - - This resolves every checkout, so depend on nothing else: not an environment variable, not a container path, not the main clone's location or branch. A checkout whose launcher links straight at it, with no `current` in the chain, resolves the same way. -3. Verify the checkout with Git, then record its branch, tip, status, remotes, worktrees, in-progress operations, and applicable `AGENTS.md` files. -4. Treat the launcher checkout's branch as staging unless the user says otherwise. The installed launcher must resolve to a staging worktree on a staging branch, never the main clone or a task, preparation, review, publication, or detached checkout. Ask if the launcher, checkout, or branch ownership is ambiguous; warn explicitly for a detached HEAD, the main clone, or a non-staging branch. - -## Customize - -1. Create a fresh task branch and worktree from the recorded staging tip, using the repository-required worktree location — default to `.worktrees/` under the repository root unless the repository requires otherwise. Never implement or commit directly on staging. -2. Implement the change, then select and run the repository-required review and checks. If a check fails, fix the cause and rerun it before integration. -3. Test assembled interactive behavior in the Web UI; unit tests and snapshots alone are insufficient. -4. Record the task tip and confirm the task worktree is clean before integration. - -## Integrate under the lock - -1. Resolve the worktree that owns staging and use `/.agents/merge.lock`. Keep it Git-ignored; never remove or replace it. Require `flock`. -2. Acquire the lock, then re-check branch ownership, exact staging tip, clean status, and absence of an in-progress Git operation. If staging moved, unlock and restart discovery against its current owner's lock. -3. Hold the same lock through final precondition checks, `git merge --no-ff`, required post-merge checks, conflict handling, and rollback. -4. If the merge or a post-merge check fails, abort the merge or restore the recorded clean staging tip before unlocking. Never discard unknown user files. -5. Before unlocking, verify staging's branch, commit, clean status, and required checks. Report that evidence and the commands run. -6. Remove the task worktree and branch only when their commits are reachable from staging and no longer needed. - -Use [`dsh-upstream-customization`](../dsh-upstream-customization/SKILL.md) when the user wants to contribute a personal feature upstream. diff --git a/skills/dsh-upgrade/SKILL.md b/skills/dsh-upgrade/SKILL.md deleted file mode 100644 index 119725274c..0000000000 --- a/skills/dsh-upgrade/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: dsh-upgrade -description: Upgrades a source-installed, personally customized DSH checkout to upstream master while preserving local changes and an unchanged rollback worktree. Use when the user asks to update or upgrade DSH. ---- - -# DSH Upgrade - -Prepare and validate the upgrade in a fresh staging worktree of the main clone, leave the worktree the installed launcher currently uses unchanged, then atomically repoint the stable `current` symlink once. Read and follow [`dsh-customize`](../dsh-customize/SKILL.md) before starting; it owns checkout discovery and lock handling. - -## Layout - -Resolve the layout, never assume it. [`dsh-customize`](../dsh-customize/SKILL.md) owns the procedure: follow the PATH launcher to the staging worktree, then derive the main clone from that checkout with Git. One resolution covers every checkout, so this workflow needs no special case and depends on no environment variable. - -The resolved layout is one container directory `` holding each staging checkout as a git worktree `/staging-` on branch `dsh-staging/`, plus the stable symlink `/current` pointing at the active one; the PATH launcher links to `/current/bin/dsh`, so it resolves PATH -> `current` -> staging worktree. The main clone is the one real clone whose object store every worktree shares, and is never a launcher target. It may live inside `` or anywhere else on disk, on any branch, with remotes that may point at a fork — so treat it strictly as the object store and worktree host, and take authoritative upstream from step 1 instead. Cutover repoints `current` alone, so the PATH launcher itself never moves. The main clone's `.git/info/exclude` is inherited by every linked worktree, so one `.agents/merge.lock` entry there excludes the lock in all of them. When the launcher links straight at a worktree with no `current` in the chain, the same resolution finds it, and cutover creates `current` and repoints PATH to `current/bin/dsh` as a one-time migration. - -## Names - -One upgrade attempt uses one UTC basic timestamp `YYYYMMDDTHHMMSSZ` for all names: - -- new staging worktree: `/staging-`; -- preparation branch: `dsh-upgrade/prepare-`; -- installed staging branch: `dsh-staging/`; -- fetched upstream ref: `refs/dsh-upgrade/upstream-`; -- recovery ref: `refs/dsh-upgrade/recovery-`; -- recorded `current` target before cutover: the old staging worktree path, kept for symlink rollback. - -The worktree name is always `staging-` under ``, never derived from the current staging directory name, so successive upgrades stay in one place and do not accumulate timestamps. The preparation branch and private refs are local-only and must never be pushed. Before starting, reject a current staging branch named exactly `dsh-staging`, because Git cannot also create `dsh-staging/`; require the user to choose a non-conflicting staging namespace rather than silently renaming it. If the new staging worktree path exists, resume only when it is a clean worktree of this main clone whose recorded old tip, upstream ref, recovery ref, and named branches exactly match this attempt; otherwise stop. Never add an ad hoc suffix or delete an unknown directory. - -## Upgrade - -1. Resolve the installed launcher, its staging worktree and branch, the main clone, the current DSH process source, and authoritative upstream. Record exact tips, paths, clean status, remotes, dependencies, worktrees, and in-progress Git operations. Require the installed staging worktree to be clean and its `.agents/merge.lock` to exist and be Git-excluded. Never stash automatically. -2. Treat the staging worktree behind the installed launcher as immutable for the whole attempt: do not touch its branch, HEAD, index, tracked or untracked files, dependencies, worktree registration, or lock file. Fetching into the shared main clone and creating new branches, worktrees, and private refs there are allowed because they are append-only and never alter the old worktree's checkout; opening and holding the existing lock is the only operation on the old worktree. -3. Allocate the timestamp and new staging worktree path. Acquire the installed worktree's existing `.agents/merge.lock`, repeat every precondition, and keep it through preparation, validation, and the `current` cutover. If staging moves while waiting, unlock and restart with a new timestamp; remove only attempt artifacts that this run created and verified as disposable. -4. In the main clone, create `refs/dsh-upgrade/recovery-` at the recorded old staging tip and `dsh-upgrade/prepare-` from that tip. Fetch exact authoritative upstream `master` into `refs/dsh-upgrade/upstream-` and record its object ID. Add a fresh worktree `/staging-` checked out on the preparation branch. Confirm the main clone's `.git/info/exclude` excludes `.agents/merge.lock`, which the new worktree inherits. -5. Inspect the Git log and commit ranges between the staging base, old staging tip, and fetched upstream tip. Identify incoming upstream changes, personal commits to preserve, likely duplicates, and conflict-prone areas before rebasing. -6. In the new worktree, rebase the preparation branch onto the fetched upstream commit. Preserve intentional customizations and drop behavior already upstream. If upstream contains the customization and its remaining local diff only documents that customization, prefer upstream and drop the documentary diff rather than retaining a stale local account. Preserve documentation only when it adds current, independently useful behavior or rules absent upstream. Abort without changing the installed launcher when resolution is uncertain. -7. Install dependencies in the new worktree, review the resulting diff, and run the repository-required checks. Fix failures and rerun affected checks. Test the new worktree's `bin/dsh` directly. -8. Point `dsh-staging/` at the validated prepared tip and check it out in the new worktree. Ensure its `.agents/merge.lock` exists (Git-excluded through the shared main-clone exclude). Verify its branch, exact commit, clean status, remotes, dependencies, and absence of in-progress Git operations, then smoke its `bin/dsh` from a clean temporary workspace. The preparation branch remains temporary; the timestamped staging branch owns the installed commit. -9. Recheck the old worktree, existing lock, launcher, `current`, main clone, new worktree, refs, and exact tips. Record `current`'s pre-cutover target, then repoint `current` at the new staging worktree in one atomic swap with `ln -sfn` (the `-n` stops `ln` from dereferencing the existing directory symlink and writing the link inside the old worktree; `mv` behaves the same way and is unusable). Leave the PATH launcher alone once it already resolves through `current`; if a legacy install still links PATH straight at a worktree, create `current` and repoint PATH to `current/bin/dsh` as a one-time migration here. The `current` target must be a clean staging worktree on a staging branch and must never be the main clone or a preparation, feature, review, publication, or detached checkout. Smoke the installed `dsh` command from a clean temporary workspace. -10. On failure before the `current` cutover, leave `current`, the launcher, and the old worktree unchanged and remove only verified attempt artifacts created by this run (including the new worktree registration if empty). On failure during or after cutover, inspect `current`'s observed target before acting; if cutover did not verify, atomically repoint `current` back to its recorded pre-cutover target with `ln -sfn` and verify that `dsh` starts from the unchanged old staging worktree. This rollback is the sole exception allowing `current` to return to the old staging worktree. Never retry a side-effecting operation blindly. -11. Release the old worktree's lock and tell the user to restart DSH through the installed launcher. The current process may continue from the old worktree, but no operation may mutate or remove it until the restarted process proves that it runs from `dsh-staging/` and the user confirms stability. Avoid customization integration during this confirmation window; if rollback is required after new work lands, reconcile that work explicitly rather than silently stranding it. -12. After confirmation, remove the preparation branch if no process uses it. Keep the old staging worktree and branch, the recovery ref, and the recorded pre-cutover `current` target as rollback until the user explicitly approves their removal; leave the actual `git worktree remove` and directory deletion to the user. Report old, upstream, prepared, and new staging commits; both staging worktree paths and branches; the main clone path; process-source evidence; the `current` pre-cutover target and cutover; commands and checks; final status; recovery ref; and retained rollback artifacts. - -The installed launcher always resolves through `current` to a staging worktree, never the main clone. Upgrade preparation adds a new worktree that shares the main clone's object store while leaving the old worktree's checkout untouched; cutover is one atomic `current` repoint to the separately validated timestamped staging worktree, and the PATH launcher never moves. - -## Recommend upstream candidates - -After a successful upgrade, load [`dsh-upstream-customization`](../dsh-upstream-customization/SKILL.md) and classify each remaining personal customization by its rules. For each candidate, explain its classification and upstream value and recommend whether to propose it, then ask which named candidate, if any, the user wants to upstream. The answer selects a candidate to start that skill's publication workflow; it is not publishing approval, which that workflow still requires. diff --git a/skills/dsh-upstream-customization/SKILL.md b/skills/dsh-upstream-customization/SKILL.md deleted file mode 100644 index 7ec1618db3..0000000000 --- a/skills/dsh-upstream-customization/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: dsh-upstream-customization -description: Classifies personal DSH customizations for upstream contribution and, after explicit per-feature approval, rebuilds one on upstream master and opens a draft pull request. Use when the user asks to contribute, publish, or upstream a local DSH change, or asks whether one is worth proposing. ---- - -# DSH Upstream Customization - -Classify and propose personal customizations upstream one feature at a time. - -## Classify - -- **Definitely propose:** bug fixes. -- **Propose:** additive, non-conflicting features implemented as plugins; visual improvements. -- **Do not propose without maintainer approval:** intrusive changes that alter existing architecture, core behavior, or broad contracts. -- Explain the classification and upstream value. If unsure whether a change is intrusive, treat it as intrusive. - -Classification and a recommendation are not publishing approval. Obtain explicit user approval naming one feature before pushing or opening a PR; approval for another feature, an upgrade, or local integration does not apply. - -## Publish an approved feature - -1. Fetch current upstream `master`, then rebuild only the approved feature on a fresh branch and worktree at that exact commit. Never publish the personal staging branch or unrelated customizations. -2. Follow repository instructions for implementation, review, testing, disclosure, PR writing, and pre-push checks. Fix failures and rerun the affected checks before publishing. -3. Review the outgoing commits and diff against upstream. Confirm they contain only the approved feature, no credentials or personal data, and a clean worktree. -4. Reconfirm the approved feature name and publishing target before the first push. Do not infer authorization from earlier local work. -5. Push only that branch and open only a draft PR. Keep its description synchronized with later changes. -6. For a Web UI feature, attach a screenshot or GIF from the assembled application after removing credentials and personal data. -7. Report the upstream base and branch commits, commands and checks run, pushed branch, and draft PR URL.