Merge pull request #2152 from deepseek-harness/codex/remove-source-installer
cleanup: simplify source execution around pnpm dsh
This commit is contained in:
@@ -298,12 +298,18 @@
|
||||
"process/2026-07-23-browser-demo-gif-recording.i18n.yaml": "sha256:808ccdda39e540645b440e40a2124baed737b98636265d8f6d8cf036a70f0d50",
|
||||
"process/2026-07-23-browser-demo-gif-recording.md": "sha256:4d3a3dc829c75b66f4f57a6a763b4b9562ce10efb90a19308142f598c5ea8524",
|
||||
"process/2026-07-23-browser-demo-gif-recording.zh.md": "sha256:409e5d31ea87f35c5227fcd1bd105167f580a8a24d29a223ce390b29eb31639a",
|
||||
"process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml": "sha256:cbd5c32f2997713339699f4c12876b1950895fcb21594927c7509e3cb4c5b8d5",
|
||||
"process/2026-07-23-personal-staging-maintenance-skills.md": "sha256:a1ef960ddc47c8bc14578d8432ed0ac4272e31178937780989e4e3b3e3f4d49f",
|
||||
"process/2026-07-23-personal-staging-maintenance-skills.zh.md": "sha256:c0f3fcf0914af88cafcc7e094a2131bd1f764003defc6eaa446b743319e73ec1",
|
||||
"process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.i18n.yaml": "sha256:89b6dc255cb0dd9d97ae6f34f37d185412d4260d7bf2f87d6b25e340f01dc26e",
|
||||
"process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.md": "sha256:1742e09435ade4a09349c8843eb381e870a2be2d51b74449f08260422c8096b3",
|
||||
"process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md": "sha256:220bd53a88617b09ee8970627c46540fa8951724b102acfe296a7ed7d0f7b5fe",
|
||||
"process/2026-07-27-wine-windows-gates-experiment.i18n.yaml": "sha256:6f4cbc12ee9cddbb297bf7e138ccabcd204f66898a0f7411b1633f03d5a9eab5",
|
||||
"process/2026-07-27-wine-windows-gates-experiment.md": "sha256:8d37dcdab058098c7de3da1de00ce61bef92bbc8d6ee71add959474c6fb3e936",
|
||||
"process/2026-07-27-wine-windows-gates-experiment.zh.md": "sha256:77fbf04df36af09e55007a93bd6b22d08ff99869efe8de3e97dac5b4701e0a9e",
|
||||
"process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml": "sha256:e7d072856cd3df3b717d94647be4ea05db087b114df4b11d68a171677b94c970",
|
||||
"process/2026-07-31-installer-adopts-existing-checkout.md": "sha256:d97e39d07bde30d534417a406fc6c56ee6765dbc7b671888d17ef2cac9a7c6dc",
|
||||
"process/2026-07-31-installer-adopts-existing-checkout.zh.md": "sha256:ab445493c1aacf3f8dc091bdbd765ad56c551ae613e0d8156610f0c4614a7431",
|
||||
"process/2026-08-04-forward-only-pr-issue-status.i18n.yaml": "sha256:af23e203a66a95674154899410e2f420d1d0685dbf856c24cfccdaa547a17925",
|
||||
"process/2026-08-04-forward-only-pr-issue-status.md": "sha256:2d31077da47d95ab3ddf64d5efc6b1b8fb7c7709d39aca4a825ef9e9d382d501",
|
||||
"process/2026-08-04-forward-only-pr-issue-status.zh.md": "sha256:b61f865b7a8a0ac901250a3edbb92ea73177067c4c25448c7088925c2caeccd7",
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md
|
||||
2026-07-23-personal-staging-maintenance-skills.md: a7ccc5b1e0f13e880c58a93d2e4c2cd4f06e2a93
|
||||
2026-07-23-personal-staging-maintenance-skills.zh.md: 8593291f510352429cbb29679e7d033589312e41
|
||||
2026-07-23-personal-staging-maintenance-skills.md: 86654be1afe986107e7dae4440b497a266636012
|
||||
2026-07-23-personal-staging-maintenance-skills.zh.md: 7593a2e8cafc93df02c9418c94f84bbd3f154a0c
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Personal staging maintenance skills
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-10
|
||||
|
||||
English | [中文](2026-07-23-personal-staging-maintenance-skills.zh.md)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: 个人集成分支维护 skill
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-10
|
||||
|
||||
[English](2026-07-23-personal-staging-maintenance-skills.md) | 中文
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md
|
||||
2026-07-31-installer-adopts-existing-checkout.md: 1d45a14273095610e3ed8047da2ce9f4ca95cbb6
|
||||
2026-07-31-installer-adopts-existing-checkout.zh.md: 971bc3b389c341b314872b8e45ab20ebd2ed5b2c
|
||||
2026-07-31-installer-adopts-existing-checkout.md: 08eec47a1cf1254409df93ab83ddf503b4d7d171
|
||||
2026-07-31-installer-adopts-existing-checkout.zh.md: dc3cc5ee8dbbe287860ad02f4bdba66afb4fbeaf
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: the installer adopts an existing checkout into the managed layout
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-10
|
||||
|
||||
English | [中文](2026-07-31-installer-adopts-existing-checkout.zh.md)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: 安装器把已有检出接管进受管布局
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-10
|
||||
|
||||
[English](2026-07-31-installer-adopts-existing-checkout.md) | 中文
|
||||
|
||||
+2
-2
@@ -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: b2428602a780f2880f0f803ba59b16a76b39790e
|
||||
2026-07-29-dsh-source-launch-tsx-esm.zh.md: 866bc8886725788e6e619f9507f5c8ccab63f82d
|
||||
@@ -14,11 +14,11 @@ 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` script completes the repository build, then uses that vector from the repository root. The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI 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.)
|
||||
|
||||
The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
|
||||
The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production runtime vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -35,4 +35,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
|
||||
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
|
||||
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
|
||||
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
|
||||
- Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh --profile headless` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).
|
||||
- Runtime launch improves ~0.4s over the full tsx default; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path.
|
||||
@@ -14,11 +14,11 @@ 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` 脚本先完成仓库构建,然后从仓库根目录使用同一启动方式。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;后已修复。)
|
||||
|
||||
node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。
|
||||
node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产运行时启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。
|
||||
|
||||
## 备选方案
|
||||
|
||||
@@ -35,4 +35,4 @@ node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`
|
||||
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
|
||||
- TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
|
||||
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
|
||||
- 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` 现为同一条 `dsh --profile headless` 源码启动命令的别名;ACP 保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。
|
||||
- 运行时启动相比完整 tsx 默认形态快约 0.4s;ACP(Agent Client Protocol)保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上。
|
||||
@@ -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: ed04725e92848bbab550a27ef2f4c021536f765e
|
||||
2026-07-20-dsh-cli-personal-config.zh.md: cc97987f803f7fb513e94ce0ce079558f5e3dc75
|
||||
@@ -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 dsh` script builds the repository and 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 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`.
|
||||
|
||||
@@ -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 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 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`。
|
||||
|
||||
+2
-2
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`,补丁文件本身就是改动的完整记录。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
|
||||
2026-08-08-remove-cli-demo.md: 31879e8284daf5f34af6731b09fce743f9eb5391
|
||||
2026-08-08-remove-cli-demo.zh.md: d4dfef1ba8c4d27cd667e319519cfa1ace74baef
|
||||
2026-08-08-remove-cli-demo.md: f8bc09825797f2165c14a75e5841d74b2e1bc1af
|
||||
2026-08-08-remove-cli-demo.zh.md: 1044dd136888e33fc9935cf4e24cd3100b41b41f
|
||||
@@ -12,7 +12,7 @@ The replay suites still need canonical session events to pin assembled backend b
|
||||
|
||||
## Decision
|
||||
|
||||
Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. The root `demo:headless` script is retained only as a direct alias of `dsh --profile headless`; the product command owns final-text stdout, failure diagnostics on stderr, persistence, exit status, and shutdown.
|
||||
Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. Source users invoke the product command through `pnpm dsh --profile headless`; it owns final-text stdout, failure diagnostics on stderr, persistence, exit status, and shutdown.
|
||||
|
||||
`examples/headless-agent` becomes an explicit test composition. Its Loader configs mount `@deepseek-ai/dsh-agent-spine-demo`, one root agent, JSONL persistence, and checkpoint policy as separate rows instead of hiding them behind an app bundle. The support-tier `@deepseek-ai/dsh-loader-smoke` package owns the shared direct-agent turn helper; unexported example-local drivers select their Loader configuration and render canonical events as JSONL. They are launched only by tests, have no bin, and do not define a supported product output format.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。根目录的 `demo:headless` 脚本仅作为 `dsh --profile headless` 的直接别名保留;stdout 上的最终文本、stderr 上的失败诊断、持久化、退出状态和关闭行为均由产品命令负责。
|
||||
彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。源码用户通过 `pnpm dsh --profile headless` 调用产品命令;stdout 上的最终文本、stderr 上的失败诊断、持久化、退出状态和关闭行为均由该命令负责。
|
||||
|
||||
`examples/headless-agent` 成为显式测试组装。其 Loader 配置把 `@deepseek-ai/dsh-agent-spine-demo`、一个根 agent(智能体)、JSONL 持久化和检查点策略挂载为独立配置行,不再将其隐藏在应用组合包之后。支持层的 `@deepseek-ai/dsh-loader-smoke` 包负责共享的直接 agent 轮次 helper;未导出的示例本地 driver 选择各自的 Loader 配置,并将规范事件渲染为 JSONL。这些 driver 只由测试启动,不提供 bin,也不定义受支持的产品输出格式。
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md
|
||||
2026-08-10-source-run-without-managed-installer.md: ac506b72acab0dd6c92ce6111487d091b2bf4a73
|
||||
2026-08-10-source-run-without-managed-installer.zh.md: 86e44a90a9e7b34ad37b6a4bfd3ad14de40868f5
|
||||
+31
@@ -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 root `pnpm` scripts. The `dsh` entry in `package.json` runs `pnpm run build`, then launches `apps/cli/src/bin.ts` through `node --import tsx/esm`; build output remains visible before the CLI output. The package script forwards arguments and inherits the caller's environment, including `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
|
||||
|
||||
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, the build-first `package.json` command, and a source CLI smoke through the exact `node --import tsx/esm` runtime vector.
|
||||
+31
@@ -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` 脚本支持从源码运行。`package.json` 中的 `dsh` 项先执行 `pnpm run build`,再通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`;构建输出会显示在 CLI(命令行界面)输出之前。该包脚本会转发参数并继承调用方环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,调用方可设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACP(Agent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
|
||||
|
||||
仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。
|
||||
|
||||
## 考虑过的备选方案
|
||||
|
||||
**保留安装器,但将 `pnpm run` 记作另一条路径。**这样可以保留受管理的启动器和回滚能力,但两套生命周期约定仍会同时生效,其中包括安装器测试和依赖 staging 布局的 skill。
|
||||
|
||||
**保留通用的定制与上游发布 skill。**其中的安全规则也能用于 staging 布局之外,但现有工作流共同构成了一套耦合的维护系统:定制工作流查找已安装的 staging 检出,升级工作流执行切换,上游发布工作流则从这些个人修改中选择发布内容。通用 Git 贡献指南已经属于仓库指令,无需以产品随附 skill 的形式提供。
|
||||
|
||||
**用更小的启动器链接脚本替换安装器。**这样可以简化设置过程,但仓库仍需负责修改宿主 PATH 和管理启动器归属。源码脚本无需引入这类状态即可提供入口点。
|
||||
|
||||
## 影响
|
||||
|
||||
源码用户通过仓库脚本运行程序,而非使用已安装的 `dsh` 命令。仓库不提供原子升级切换,也不保留 staging 回滚检出;仓库同样不会自动集成个人源码修改或将其发布到上游。未来的分发机制必须说明为何应由其管理安装和升级状态,定义恢复行为,并补充测试与用户文档,同时不得让源码运行路径依赖该机制。未来任何发布工作流都必须隔离出一项获批功能,并在首次推送和创建草稿 PR(Pull Request)前取得明确批准。
|
||||
|
||||
验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度、`package.json` 中的先构建后启动命令,以及通过准确的 `node --import tsx/esm` 运行方式对源码 CLI 进行的冒烟测试。
|
||||
@@ -73,7 +73,7 @@ pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (nee
|
||||
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
|
||||
pnpm run doc-sync # all documentation gates; leaf list in scripts/run-gates.ts
|
||||
pnpm run website:build # VitePress build (doubles as dead-link check)
|
||||
pnpm run demo:headless "task" # one-shot agent (needs DEEPSEEK_API_KEY)
|
||||
pnpm dsh --profile headless "task" # build, then run one task (needs DEEPSEEK_API_KEY)
|
||||
pnpm run demo:cordis # the agent modifies its own runtime (needs key)
|
||||
pnpm run demo:acp # ACP automation server (needs DEEPSEEK_API_KEY)
|
||||
```
|
||||
|
||||
+2
-2
@@ -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: b2d84672275a4ca996b6ea596c104abfaa52432a
|
||||
README.zh.md: 747a3c88bc129ad5dd24f5fa150a1661c4db0b11
|
||||
@@ -12,41 +12,34 @@ 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:
|
||||
Clone this repo, complete the [dependency and API-key setup](docs/user/guide/quickstart.md#step-1-install-and-configure-the-api-key), then run:
|
||||
|
||||
```sh
|
||||
git clone <repo-url>
|
||||
cd deepseek-harness
|
||||
scripts/install.sh
|
||||
pnpm dsh web
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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 dsh 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/<name>`:
|
||||
The source CLI boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/<name>`:
|
||||
|
||||
```sh
|
||||
dsh --profile web # the browser UI (same as: dsh web)
|
||||
dsh plugin --profile tui add <package> # install a plugin into a custom profile
|
||||
dsh --profile tui # boot it
|
||||
pnpm dsh --profile web # the browser UI
|
||||
pnpm dsh plugin --profile tui add <package> # install a plugin into a custom profile
|
||||
pnpm dsh --profile tui # boot it
|
||||
```
|
||||
|
||||
The [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.
|
||||
@@ -56,7 +49,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 dsh --profile headless "summarize this workspace"
|
||||
```
|
||||
|
||||
### Automation and SDKs
|
||||
|
||||
+11
-18
@@ -12,41 +12,34 @@ DeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化
|
||||
|
||||
为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。
|
||||
|
||||
## 安装
|
||||
## 从源码运行
|
||||
|
||||
克隆仓库,然后运行安装器:
|
||||
克隆本仓库,完成[依赖安装和 API 密钥配置](docs/user/guide/quickstart.md#step-1-install-and-configure-the-api-key),然后运行:
|
||||
|
||||
```sh
|
||||
git clone <repo-url>
|
||||
cd deepseek-harness
|
||||
scripts/install.sh
|
||||
pnpm dsh web
|
||||
```
|
||||
|
||||
安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。
|
||||
|
||||
默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。
|
||||
|
||||
## 使用 DeepSeek Harness
|
||||
|
||||
### Web UI
|
||||
|
||||
推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:
|
||||
请从仓库根目录启动推荐的本地界面:
|
||||
|
||||
```sh
|
||||
(cd ~/.dsh/source/current && pnpm run build)
|
||||
dsh web
|
||||
pnpm dsh 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/<name>` 中的自有覆盖层:
|
||||
源码 CLI(命令行界面)会启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/<name>` 中的自有覆盖层:
|
||||
|
||||
```sh
|
||||
dsh --profile web # the browser UI (same as: dsh web)
|
||||
dsh plugin --profile tui add <package> # install a plugin into a custom profile
|
||||
dsh --profile tui # boot it
|
||||
pnpm dsh --profile web # the browser UI
|
||||
pnpm dsh plugin --profile tui add <package> # install a plugin into a custom profile
|
||||
pnpm dsh --profile tui # boot it
|
||||
```
|
||||
|
||||
profile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。
|
||||
@@ -56,7 +49,7 @@ profile 布局、层语义与配置输出命令详见 [CLI(命令行界面)
|
||||
运行一项任务,打印最终答案后退出:
|
||||
|
||||
```sh
|
||||
dsh --profile headless "summarize this workspace"
|
||||
pnpm dsh --profile headless "summarize this workspace"
|
||||
```
|
||||
|
||||
### 自动化与 SDK
|
||||
|
||||
@@ -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 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -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: 98a856261bc632c97f350db8fc7bb0b10c22235d
|
||||
README.zh.md: 283e54138e24202ed6b88d1d309538c58cd66b3e
|
||||
+2
-2
@@ -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, `pnpm dsh <args...>` builds those artifacts, runs the TypeScript entry, and forwards every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
|
||||
@@ -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 dsh <args...>` 会先构建这些产物,再运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
|
||||
@@ -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: cef687dc392f97886ea18b162e8f668a44ce2284
|
||||
README.zh.md: f6721ec256d404e2b602fb2ed921b41c03633a82
|
||||
@@ -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 <name> add <package-or-git-spec>`. 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 dsh <args...>`. The `package.json` script runs the complete repository build, launches `apps/cli/src/bin.ts` with `node --import tsx/esm`, and forwards every argument. Build output appears before CLI output. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
|
||||
@@ -79,12 +79,6 @@ dsh web --help
|
||||
|
||||
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `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 dsh <args...>`。`package.json` 中的脚本会完成整个仓库的构建,通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。构建输出会显示在 CLI 输出之前。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
@@ -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<Fixture> {
|
||||
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<string> {
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,12 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Keyless smoke for the SOURCE `dsh` launcher: 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
|
||||
* Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts`
|
||||
* with the exact production runtime vector (`node --import tsx/esm`, the
|
||||
* vector the root `dsh` script invokes after building) 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
|
||||
@@ -16,6 +17,13 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const dshSourceBin = 'apps/cli/src/bin.ts'
|
||||
|
||||
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
|
||||
it('builds before launching the source CLI', async () => {
|
||||
const rootPackage = JSON.parse(await readFile(new URL('../../../package.json', import.meta.url), 'utf8')) as {
|
||||
readonly scripts?: Record<string, string>
|
||||
}
|
||||
expect(rootPackage.scripts?.dsh).toBe('pnpm run build && node --import tsx/esm apps/cli/src/bin.ts')
|
||||
})
|
||||
|
||||
it('boots the source entry and requires a profile', async () => {
|
||||
const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], {
|
||||
cwd: repoRoot,
|
||||
|
||||
@@ -119,6 +119,10 @@ describe('web e2e: settled Markdown math rendering', () => {
|
||||
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6)
|
||||
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
|
||||
expect(await page.locator('.katex-error').count()).toBe(0)
|
||||
await expect.poll(
|
||||
() => page.getByText('Input 0 tok · Output 0 tok', { exact: false }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
|
||||
@@ -478,7 +478,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
requireDist()
|
||||
sessionsDir = mkdtempSync(join(tmpdir(), 'dsh-web-w5-'))
|
||||
const port = await probeFreePort()
|
||||
// tsx boot mirrors demo:web — lib/ may be unbuilt in this worktree. Isolate
|
||||
// tsx boot mirrors the runtime half of the root dsh script. Isolate
|
||||
// the host-level Harness and shared-agent homes inside the temp world; tsx
|
||||
// also needs the repo's loader and tsconfig paths pointed at explicitly.
|
||||
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
|
||||
|
||||
@@ -99,6 +99,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal and click a row action, re-hovering if a projection update replaces
|
||||
* the row before its hover-only button becomes visible.
|
||||
*/
|
||||
async function clickHoverAction(row: Locator, name: string): Promise<void> {
|
||||
const button = row.getByRole('button', { name })
|
||||
await expect.poll(async () => {
|
||||
await row.hover()
|
||||
return await button.isVisible()
|
||||
}, { timeout: 10_000 }).toBe(true)
|
||||
await button.click()
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
// Seed one cold session (Ungrouped bucket) for the flat view + hover card.
|
||||
@@ -137,10 +150,8 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
|
||||
|
||||
it('renames a workspace over the wire with a duplicate-name pre-check', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-rename'))
|
||||
// The actions button is display:none until its row hovers — hover the
|
||||
// group row first, then the revealed button becomes actionable.
|
||||
await page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first().hover()
|
||||
await page.getByRole('button', { name: 'Workspace actions for alpha-ws' }).click()
|
||||
const alphaRow = page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first()
|
||||
await clickHoverAction(alphaRow, 'Workspace actions for alpha-ws')
|
||||
await page.getByRole('menuitem', { name: 'Rename' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Rename workspace' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
@@ -214,17 +225,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
|
||||
// The header row is wrapped by its HoverCard anchor span, so the section
|
||||
// is the nearest groupSection ancestor, not the immediate parent.
|
||||
const groupSection = groupRow.locator('xpath=ancestor::*[contains(@class, "groupSection")][1]')
|
||||
if (await groupSection.locator('[role="treeitem"]').count() < 2) await groupRow.click()
|
||||
await expect.poll(
|
||||
() => groupSection.locator('[role="treeitem"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBeGreaterThanOrEqual(2)
|
||||
await expect.poll(async () => {
|
||||
const count = await groupSection.locator('[role="treeitem"]').count()
|
||||
if (count < 2 && await groupRow.getAttribute('aria-expanded') !== 'true') {
|
||||
await groupRow.click()
|
||||
await page.waitForTimeout(50)
|
||||
}
|
||||
return await groupSection.locator('[role="treeitem"]').count()
|
||||
}, { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
|
||||
const seededRow = groupSection.locator('[role="treeitem"]').nth(1)
|
||||
await seededRow.click()
|
||||
await expect.poll(() => seededRow.getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
|
||||
|
||||
await groupRow.hover()
|
||||
await page.getByRole('button', { name: `Workspace actions for ${workspace.title}` }).click()
|
||||
await clickHoverAction(groupRow, `Workspace actions for ${workspace.title}`)
|
||||
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Delete workspace' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
@@ -338,8 +351,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
|
||||
if (oldWorkspace === undefined) throw new Error('old same-name Workspace was not registered')
|
||||
|
||||
const oldRow = page.locator('[role="treeitem"]').filter({ hasText: title }).first()
|
||||
await oldRow.hover()
|
||||
await page.getByRole('button', { name: `Workspace actions for ${title}` }).click()
|
||||
await clickHoverAction(oldRow, `Workspace actions for ${title}`)
|
||||
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
|
||||
await page.getByRole('dialog', { name: 'Delete workspace' })
|
||||
.getByRole('button', { name: 'Delete workspace' }).click()
|
||||
@@ -509,9 +521,10 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-row-menu'))
|
||||
const sessionRow = await seededSessionRow()
|
||||
// The trigger is display:none until its row hovers.
|
||||
await sessionRow.hover()
|
||||
const trigger = sessionRow.locator('button[aria-label^="Session actions for "]')
|
||||
await trigger.click()
|
||||
const triggerName = await trigger.getAttribute('aria-label')
|
||||
if (triggerName === null) throw new Error('seeded Session row has no actions label')
|
||||
await clickHoverAction(sessionRow, triggerName)
|
||||
const item = page.getByRole('menuitem', { name: 'Rename' })
|
||||
await item.waitFor({ timeout: 5_000 })
|
||||
// Into the list, then back up to the trigger across the 4px gap below it:
|
||||
@@ -559,8 +572,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
|
||||
const rowTitle = await sessionRow.locator('[class*="title"]').innerText()
|
||||
// Row menu: hover reveals the actions button; Archive session commits
|
||||
// without a confirmation dialog (non-destructive: log + accounting stay).
|
||||
await sessionRow.hover()
|
||||
await sessionRow.getByRole('button', { name: `Session actions for ${rowTitle}` }).click()
|
||||
await clickHoverAction(sessionRow, `Session actions for ${rowTitle}`)
|
||||
await page.getByRole('menuitem', { name: 'Archive session' }).click()
|
||||
// The row disappears on the archive-set echo; with no other visible
|
||||
// stray, the whole Ungrouped bucket withdraws.
|
||||
|
||||
@@ -5,8 +5,8 @@ import react from '@vitejs/plugin-react'
|
||||
|
||||
const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url))
|
||||
const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
|
||||
+ 'Build with `pnpm run build && pnpm run build:web`, then run `dsh web` (repository checkout: `pnpm run dsh -- web`). '
|
||||
+ 'For client-plugin HMR, run `pnpm run dsh -- web --dev` together with `pnpm run dev:web`.'
|
||||
+ 'From a repository checkout, run `pnpm dsh web`; an installed package uses `dsh web`. '
|
||||
+ 'For client-plugin HMR, run `pnpm dsh web --dev` together with `pnpm run dev:web`.'
|
||||
|
||||
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
|
||||
function rejectStandaloneServe(): Plugin {
|
||||
|
||||
@@ -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" "$@"
|
||||
@@ -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: 3065f3f10861965b327a4412049878dc1ba7faec
|
||||
api-gateway.zh.md: aa9b726c33fd9f51fc0b2d2ed95c4c9658662796
|
||||
+2
-8
@@ -138,16 +138,10 @@ SRC solves only dispatch for a Host process running from source. The Client does
|
||||
|
||||
## Development mode
|
||||
|
||||
A complete build generates Host contracts before compiling the Host, Client, and Web, so it is the deterministic entry for creating or refreshing all artifacts:
|
||||
The repository `dsh` script completes the Host, Client, and Web build before starting the source Host. Web development runs that command and the Client plugin watcher in separate terminals:
|
||||
|
||||
```sh
|
||||
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 dsh web --dev
|
||||
pnpm run dev:web
|
||||
```
|
||||
|
||||
|
||||
@@ -138,16 +138,10 @@ SRC 只解决 Host 源码进程的分发问题。Client 不会从运行中的 Ho
|
||||
|
||||
## 开发模式
|
||||
|
||||
完整构建会先生成 Host 约定,再编译 Host、Client 与 Web,因此是建立或刷新所有产物的确定性入口:
|
||||
仓库的 `dsh` 脚本会先完成 Host、Client 与 Web 构建,再启动源码 Host。Web 开发需要在两个终端中分别运行该命令和 Client plugin watcher:
|
||||
|
||||
```sh
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Web 开发通常在完成一次构建后启动源码 Host,并在另一个终端运行 Client plugin watcher:
|
||||
|
||||
```sh
|
||||
pnpm run dsh -- web --dev
|
||||
pnpm dsh web --dev
|
||||
pnpm run dev:web
|
||||
```
|
||||
|
||||
|
||||
@@ -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/development.md
|
||||
development.md: 8e565f21c6e2ede7dab7dbda3c4b18b77ce0920f
|
||||
development.zh.md: d9c0fbfbb663334b8f7e2ca11d4e8d9a0652c22e
|
||||
development.md: 7d6ca74df1311560b0d61444c92e74927639ca31
|
||||
development.zh.md: 1461717cf496b1863f2f0c47ad4b42990dc789e9
|
||||
+1
-1
@@ -129,7 +129,7 @@ The root [contributor instructions](../AGENTS.md#commands) summarize common comm
|
||||
The one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:
|
||||
|
||||
```sh
|
||||
pnpm run demo:headless "summarize this workspace"
|
||||
pnpm dsh --profile headless "summarize this workspace"
|
||||
```
|
||||
|
||||
The self-referential cordis demo can inspect and modify its live plugin runtime and needs the same credentials (`web` by default, or `acp`):
|
||||
|
||||
@@ -129,7 +129,7 @@ keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若
|
||||
单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:
|
||||
|
||||
```sh
|
||||
pnpm run demo:headless "summarize this workspace"
|
||||
pnpm dsh --profile headless "summarize this workspace"
|
||||
```
|
||||
|
||||
自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):
|
||||
|
||||
@@ -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/user/develop/basic/index.md
|
||||
index.md: fe525018011809abe4dd18ac5739c54cd330c8f6
|
||||
index.zh.md: 0dbb7275be3c7147395041fb18d49a7f6ea69cdd
|
||||
index.md: e57a42b42690bd92450cc26876c13a1622bb80cc
|
||||
index.zh.md: 240623341618acd6501f2897ae2844fde0a5b73b
|
||||
@@ -56,7 +56,7 @@ Create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugi
|
||||
Start the Web UI with that overlay:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --patch ./scratch-plugin/cordis.yml
|
||||
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:3080`. The terminal prints `[hello-plugin] plugin loaded!` during startup.
|
||||
|
||||
@@ -56,7 +56,7 @@ export function apply(ctx: Context) {
|
||||
使用该覆盖层启动 Web UI:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --patch ./scratch-plugin/cordis.yml
|
||||
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
打开 `http://127.0.0.1:3080`。启动期间,终端会打印 `[hello-plugin] plugin loaded!`。
|
||||
|
||||
@@ -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/user/develop/basic/tool.md
|
||||
tool.md: f110bd2c10caf21ea8c87bc32fd43b01b209a9d1
|
||||
tool.zh.md: a237b6015de0ea6141a94c56aaa6f68c73fa3feb
|
||||
tool.md: a149f6876c573c9ece4b066ef6c211f69b0b03a8
|
||||
tool.zh.md: 1f9a1172bcde33a1d7319492f15686c47de01174
|
||||
@@ -40,7 +40,7 @@ export function apply(ctx: Context) {
|
||||
Restart the development command if it is not running:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --patch ./scratch-plugin/cordis.yml
|
||||
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:3080` and ask: `Use the greet tool to greet Ada.` The model can call `greet` and receives `Hello, Ada!` as the tool result.
|
||||
|
||||
@@ -40,7 +40,7 @@ export function apply(ctx: Context) {
|
||||
如果开发命令未在运行,请重新启动:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --patch ./scratch-plugin/cordis.yml
|
||||
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
打开 `http://127.0.0.1:3080`,然后输入:`Use the greet tool to greet Ada.` 模型可以调用 `greet`,并收到 `Hello, Ada!` 这一工具结果。
|
||||
|
||||
@@ -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/user/guide/providers.md
|
||||
providers.md: 4667e54161e77a62d454f3f78a8164d5c79b78c8
|
||||
providers.zh.md: 15e0ccc826a5c285c71d4776793f8048c11f6254
|
||||
providers.md: 31b4be666bd6b7a402a0bae7bc8f810fd2d928d8
|
||||
providers.zh.md: bd95c63cacd9718b3ffc1f78e17270e7022b21e0
|
||||
@@ -15,7 +15,7 @@ Adding a provider therefore rarely means editing `cordis.yml` — writing settin
|
||||
|
||||
## Configure from the web UI
|
||||
|
||||
Start `pnpm run dsh web` and open **Settings → Models**.
|
||||
Start `pnpm dsh web` and open **Settings → Models**.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Harness 出厂自带 DeepSeek,同时预装了一个通用的多提供方适配
|
||||
|
||||
## 在 Web 界面里配置
|
||||
|
||||
启动 `pnpm run dsh web`,打开**设置 → 模型**。
|
||||
启动 `pnpm dsh web`,打开**设置 → 模型**。
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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/user/guide/quickstart.md
|
||||
quickstart.md: 8e883efd470fef329a7308f1017cab0b7afcdd67
|
||||
quickstart.zh.md: e0e3f4e4754bca588831b6d204f77d1c97a140b3
|
||||
quickstart.md: 635c0dffd6119f95c1951a441691f87f04396b4a
|
||||
quickstart.zh.md: d0921382bcb0844fa0260c51546622c582867e68
|
||||
@@ -22,7 +22,6 @@ pnpm -v
|
||||
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
|
||||
cd deepseek-harness
|
||||
pnpm install
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Create the gitignored repository-root `.env`:
|
||||
@@ -36,7 +35,7 @@ DEEPSEEK_API_KEY=sk-your-key-here
|
||||
Run a non-interactive task and print its final answer:
|
||||
|
||||
```sh
|
||||
pnpm run dsh --profile headless "summarize the architecture of this workspace"
|
||||
pnpm dsh --profile headless "summarize the architecture of this workspace"
|
||||
```
|
||||
|
||||
`dsh --profile headless` creates and persists a fresh session, prints the final assistant answer, and exits. It starts no Web server or listening port, and a successful run leaves stderr empty.
|
||||
@@ -46,7 +45,7 @@ pnpm run dsh --profile headless "summarize the architecture of this workspace"
|
||||
Start the browser interface:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web
|
||||
pnpm dsh web
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:3080`. The agent can read and write files, run commands, delegate subtasks, and track a plan. Try: `Create hello.js in the current directory, print "Hello from Harness!", and run it`.
|
||||
|
||||
@@ -22,7 +22,6 @@ pnpm -v
|
||||
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
|
||||
cd deepseek-harness
|
||||
pnpm install
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
在仓库根目录创建已被 Git 忽略的 `.env`:
|
||||
@@ -36,7 +35,7 @@ DEEPSEEK_API_KEY=sk-your-key-here
|
||||
运行一个非交互式任务并打印最终回答:
|
||||
|
||||
```sh
|
||||
pnpm run dsh --profile headless "summarize the architecture of this workspace"
|
||||
pnpm dsh --profile headless "summarize the architecture of this workspace"
|
||||
```
|
||||
|
||||
`dsh --profile headless` 创建并持久化一个新会话,打印最终助手回答,然后退出。它不会启动 Web 服务器或监听端口;成功运行时 stderr 为空。
|
||||
@@ -46,7 +45,7 @@ pnpm run dsh --profile headless "summarize the architecture of this workspace"
|
||||
启动浏览器界面:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web
|
||||
pnpm dsh web
|
||||
```
|
||||
|
||||
打开 `http://127.0.0.1:3080`。agent 可以读写文件、运行命令、分配子任务和跟踪计划。可以尝试:`Create hello.js in the current directory, print "Hello from Harness!", and run it`。
|
||||
|
||||
@@ -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 examples/headless-agent/README.md
|
||||
README.md: 08b36e9c8e558db3b30175b20bd6e0af160ccef5
|
||||
README.zh.md: 40d11ebeeb1631af1421b3c013a5e548fd3fd488
|
||||
README.md: 2f854924d4bfd2bf66b3d6f47433136098d7b762
|
||||
README.zh.md: 4128645da8a0f8a84b17d819fc614051b68f2a11
|
||||
@@ -10,10 +10,10 @@ This directory owns the replay and real-model test composition for a headless co
|
||||
# repo root .env (gitignored) or exported env:
|
||||
# DEEPSEEK_API_KEY=sk-…
|
||||
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
|
||||
pnpm run dsh --profile headless "fix the failing test in this workspace"
|
||||
pnpm dsh --profile headless "fix the failing test in this workspace"
|
||||
```
|
||||
|
||||
The product command is [`dsh --profile headless`](../../apps/cli/README.md): it accepts one nonblank task, creates and persists a fresh session, prints the final assistant text, and exits. The root `demo:headless` script is only an alias of that command.
|
||||
The product command is [`dsh --profile headless`](../../apps/cli/README.md): it accepts one nonblank task, creates and persists a fresh session, prints the final assistant text, and exits.
|
||||
|
||||
Snapshot suites run this directory's configuration through [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts), an unexported test-only process that emits canonical session events as JSONL before its result record. That stream is test infrastructure, not a supported CLI output format. Child sessions surface only through parent tool events and results.
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
# repo root .env (gitignored) or exported env:
|
||||
# DEEPSEEK_API_KEY=sk-…
|
||||
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
|
||||
pnpm run dsh --profile headless "fix the failing test in this workspace"
|
||||
pnpm dsh --profile headless "fix the failing test in this workspace"
|
||||
```
|
||||
|
||||
产品命令是 [`dsh --profile headless`](../../apps/cli/README.md):它接受一项非空任务,创建并持久化新会话,打印最终 assistant 文本,然后退出。根目录的 `demo:headless` 脚本只是该命令的别名。
|
||||
产品命令是 [`dsh --profile headless`](../../apps/cli/README.md):它接受一项非空任务,创建并持久化新会话,打印最终 assistant 文本,然后退出。
|
||||
|
||||
快照套件通过 [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts) 运行本目录的配置。这个未导出且仅供测试使用的进程会在结果记录之前,以 JSONL 发出规范会话事件。该事件流属于测试基础设施,不是受支持的 CLI(命令行界面)输出格式。子会话只通过父会话的工具事件和结果对外显示。
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ pre-commit:
|
||||
- '.agents/notes/archived/**'
|
||||
run: node_modules/.bin/tsx scripts/verify-translation-pairing.ts --cached {staged_files}
|
||||
|
||||
- name: archived agent notes
|
||||
glob: '.agents/notes/archived/**'
|
||||
run: node_modules/.bin/tsx scripts/verify-archived-agent-notes.ts
|
||||
|
||||
- name: lint (staged)
|
||||
glob: '*.{ts,tsx,mts,cts,mjs}'
|
||||
exclude:
|
||||
@@ -41,6 +45,10 @@ pre-merge-commit:
|
||||
- '.agents/notes/archived/**'
|
||||
run: node_modules/.bin/tsx scripts/verify-translation-pairing.ts --cached {staged_files}
|
||||
|
||||
- name: archived agent notes
|
||||
glob: '.agents/notes/archived/**'
|
||||
run: node_modules/.bin/tsx scripts/verify-archived-agent-notes.ts
|
||||
|
||||
pre-push:
|
||||
jobs:
|
||||
- name: typecheck
|
||||
|
||||
+1
-3
@@ -127,12 +127,10 @@
|
||||
"release:pack": "tsx scripts/release/pack.ts",
|
||||
"release:verify-packed-install": "tsx scripts/release/verify-packed-install.ts",
|
||||
"release:publish": "tsx scripts/release/publish.ts",
|
||||
"dsh": "node --import tsx/esm apps/cli/src/bin.ts",
|
||||
"demo:headless": "node --import tsx/esm apps/cli/src/bin.ts --profile headless",
|
||||
"dsh": "pnpm run build && node --import tsx/esm apps/cli/src/bin.ts",
|
||||
"demo:code-mode": "node scripts/demo-code-mode.mjs",
|
||||
"demo:cordis": "node scripts/demo-cordis.mjs",
|
||||
"demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml",
|
||||
"demo:web": "npm run build && node --import tsx/esm apps/cli/src/bin.ts web",
|
||||
"mock:llm": "node --import tsx packages/support/llm-mock-server/src/bin.ts",
|
||||
"dev:web": "tsx scripts/dev-web.ts --poll",
|
||||
"postinstall": "node scripts/install-lefthook.mjs"
|
||||
|
||||
@@ -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: 9639f1c0a2ffe91fd509a2ffdf04be5f0895b700
|
||||
README.zh.md: e8bf0374aad2be2311b6e72e91e41f02f403b48a
|
||||
@@ -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 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.
|
||||
|
||||
|
||||
@@ -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 dsh` 源码路径还会将 manifest(元数据清单)声明的 workspace 包映射到其 TypeScript 源码;其配置门禁要求每个随附的原始/Web 裸插件都出现在解析所用 manifest 的 `dependencies` 中。
|
||||
|
||||
此包不包含 loader 钩子,也不提供开发模式接口。[`dsh` 应用](../../../apps/cli/README.md) 持有自己的 Node 源码启动钩子,并在启动序列中使用这些 helper;构建后的消费方仍使用普通 Node 包解析。
|
||||
|
||||
|
||||
@@ -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/support/llm-mock-server/README.md
|
||||
README.md: a535c086bf688ad48b1a3bb19c7b81da21cbad92
|
||||
README.zh.md: d814d8815b38bb34bd0d871d552e6f3ec75e042a
|
||||
README.md: 919bbc923110459e9ee4fc26f6db7e8b35ab1abb
|
||||
README.zh.md: d60c6b2395c14c0cf690c2a94e902eb1cfeffa94
|
||||
@@ -23,7 +23,7 @@ Point the shipping DeepSeek adapter at the server; it appends `/chat/completions
|
||||
```sh
|
||||
DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 \
|
||||
DEEPSEEK_API_KEY=mock-key \
|
||||
pnpm run demo:headless "test provider recovery"
|
||||
pnpm dsh --profile headless "test provider recovery"
|
||||
```
|
||||
|
||||
The repository script writes JSONL to stdout: a `ready` record carries the `/v1` base URL and random seed, followed by request/result records that name both the scripted behavior and the concrete selected behavior. The private support package exposes no installable binary.
|
||||
|
||||
@@ -23,7 +23,7 @@ pnpm run mock:llm -- \
|
||||
```sh
|
||||
DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 \
|
||||
DEEPSEEK_API_KEY=mock-key \
|
||||
pnpm run demo:headless "test provider recovery"
|
||||
pnpm dsh --profile headless "test provider recovery"
|
||||
```
|
||||
|
||||
仓库脚本将 JSONL 写入 stdout:`ready` 记录携带以 `/v1` 结尾的基础 URL 和随机种子,后续请求/结果记录同时命名脚本行为和实际选中的具体行为。这个私有支持包不公开可安装的二进制命令。
|
||||
|
||||
@@ -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<string, Manifest>, names: Set<string>): Map<string, boolean> {
|
||||
const tiers = new Map<string, boolean>()
|
||||
// `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)}
|
||||
|
||||
|
||||
@@ -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-<timestamp> on branch dsh-staging/<timestamp>, 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 -> <staging>/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/tty; } 2>/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/tty 2>/dev/null || true' EXIT
|
||||
trap 'stty echo </dev/tty 2>/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 </dev/tty || _ans=''
|
||||
[ -n "$_ans" ] || _ans=${2:-}
|
||||
printf '%s' "$_ans"
|
||||
}
|
||||
|
||||
# ask_secret PROMPT -> 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/tty 2>/dev/null || true
|
||||
IFS= read -r _sec </dev/tty || _sec=''
|
||||
stty echo </dev/tty 2>/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/tty || _r=''
|
||||
[ -n "$_r" ] || _r=$_def
|
||||
case "$_r" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
|
||||
printf '%s\n' "${B}DeepSeek Harness — dsh installer${RST}"
|
||||
if [ "$IN_REPO" = 1 ]; then
|
||||
printf '%scheckout %s%s\n' "$DIM" "$DSH_CHECKOUT" "$RST"
|
||||
else
|
||||
printf '%smaster %s @ %s%s\n' "$DIM" "$DSH_MASTER" "$DSH_REF" "$RST"
|
||||
printf '%sstaging %s%s\n' "$DIM" "$DSH_STAGING" "$RST"
|
||||
printf '%scurrent %s%s\n' "$DIM" "$DSH_CURRENT" "$RST"
|
||||
fi
|
||||
|
||||
# --- 1. dependency check -------------------------------------------------------
|
||||
step "Checking dependencies"
|
||||
|
||||
command -v git >/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/<ref>) 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 -> <staging>/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 </dev/tty
|
||||
else
|
||||
info "install complete. Build and start the Web UI with:"
|
||||
printf ' (cd %s && pnpm run build)\n' "$DSH_STAGING"
|
||||
printf ' %s web\n' "$DSH_BIN_DIR/dsh"
|
||||
fi
|
||||
File diff suppressed because one or more lines are too long
@@ -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 <checkout> rev-parse --show-toplevel` confirms the checkout root, and `git -C <checkout> 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 <main clone> 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 `<staging-worktree>/.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.
|
||||
@@ -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 `<source>` holding each staging checkout as a git worktree `<source>/staging-<timestamp>` on branch `dsh-staging/<timestamp>`, plus the stable symlink `<source>/current` pointing at the active one; the PATH launcher links to `<source>/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 `<source>` 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: `<source>/staging-<timestamp>`;
|
||||
- preparation branch: `dsh-upgrade/prepare-<timestamp>`;
|
||||
- installed staging branch: `dsh-staging/<timestamp>`;
|
||||
- fetched upstream ref: `refs/dsh-upgrade/upstream-<timestamp>`;
|
||||
- recovery ref: `refs/dsh-upgrade/recovery-<timestamp>`;
|
||||
- recorded `current` target before cutover: the old staging worktree path, kept for symlink rollback.
|
||||
|
||||
The worktree name is always `staging-<timestamp>` under `<source>`, 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/<timestamp>`; 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-<timestamp>` at the recorded old staging tip and `dsh-upgrade/prepare-<timestamp>` from that tip. Fetch exact authoritative upstream `master` into `refs/dsh-upgrade/upstream-<timestamp>` and record its object ID. Add a fresh worktree `<source>/staging-<timestamp>` 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/<timestamp>` 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/<timestamp>` 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user