Forráskód Böngészése

python: derive release version from repository

Yichen Jiang 1 hónapja
szülő
commit
fe3777cf27

+ 28 - 12
.github/workflows/build-exe-for-python-sdk.yml

@@ -61,7 +61,21 @@ jobs:
     timeout-minutes: 5
     outputs:
       matrix: ${{ steps.plan.outputs.matrix }}
+      version: ${{ steps.version.outputs.version }}
     steps:
+      - uses: actions/checkout@v6
+
+      - name: Resolve repository version
+        id: version
+        run: |
+          set -euo pipefail
+          version="$(jq -r '.version // empty' package.json)"
+          [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
+            echo "::error::package.json version must be stable X.Y.Z, got '$version'"
+            exit 1
+          }
+          echo "version=$version" >> "$GITHUB_OUTPUT"
+
       - name: Compute matrix from targets input
         id: plan
         env:
@@ -99,7 +113,7 @@ jobs:
 
   sdk-wheel:
     needs: plan
-    name: deepseek_harness-0.0.0-py3-none-any.whl
+    name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
     runs-on: ubuntu-latest
     timeout-minutes: 5
     steps:
@@ -116,13 +130,12 @@ jobs:
         run: >-
           python scripts/build-python-release.py
           --package sdk
-          --tag python-v0.0.0
           --output-dir dist-python
 
       - uses: actions/upload-artifact@v6
         with:
-          name: deepseek_harness-0.0.0-py3-none-any.whl
-          path: dist-python/deepseek_harness-0.0.0-py3-none-any.whl
+          name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
+          path: dist-python/deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
           if-no-files-found: error
 
   build:
@@ -189,15 +202,16 @@ jobs:
         id: runtime
         env:
           TARGET: ${{ matrix.target }}
+          VERSION: ${{ needs.plan.outputs.version }}
         run: |
           set -euo pipefail
           platform="${TARGET#node24-}"
           exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform"
           [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
           case "$platform" in
-            linux-x64) wheel=deepseek_harness_runtime_bin-0.0.0-py3-none-manylinux_2_28_x86_64.whl ;;
-            linux-arm64) wheel=deepseek_harness_runtime_bin-0.0.0-py3-none-manylinux_2_28_aarch64.whl ;;
-            macos-arm64) wheel=deepseek_harness_runtime_bin-0.0.0-py3-none-macosx_11_0_arm64.whl ;;
+            linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
+            linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
+            macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_11_0_arm64.whl ;;
             *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
           esac
           echo "platform=$platform" >> "$GITHUB_OUTPUT"
@@ -215,23 +229,24 @@ jobs:
         run: >-
           python scripts/build-python-release.py
           --package runtime
-          --tag python-v0.0.0
           --platform "${{ steps.runtime.outputs.platform }}"
           --runtime-exe "${{ steps.runtime.outputs.exe }}"
           --output-dir dist-python
 
       - uses: actions/download-artifact@v8
         with:
-          name: deepseek_harness-0.0.0-py3-none-any.whl
+          name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
           path: dist-python
 
       - name: Install only the SDK into a clean venv and run zero-config
+        env:
+          VERSION: ${{ needs.plan.outputs.version }}
         run: |
           set -euo pipefail
           python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
           "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
             --find-links dist-python \
-            deepseek-harness==0.0.0
+            deepseek-harness=="$VERSION"
           "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
             --scenario sdk-default
 
@@ -251,6 +266,7 @@ jobs:
         if: runner.os == 'Linux'
         env:
           RUNNER_ARCH: ${{ runner.arch }}
+          VERSION: ${{ needs.plan.outputs.version }}
         run: |
           set -euo pipefail
           case "$RUNNER_ARCH" in
@@ -258,9 +274,9 @@ jobs:
             ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
             *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
           esac
-          docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
+          docker run --rm -e VERSION -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
             /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
-            /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness==0.0.0
+            /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness=="$VERSION"
             /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
           '
 

+ 10 - 7
.gitlab-ci.yml

@@ -15,6 +15,8 @@ variables:
   before_script:
     - python3 -m venv .ci-python
     - . .ci-python/bin/activate
+    - export DSH_VERSION="$(python -c 'import json; print(json.load(open("package.json"))["version"])')"
+    - test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; }
     - python -m pip install uv==0.11.23
 
 sdk-wheel:
@@ -40,7 +42,7 @@ sdk-wheel:
     - uv run --python 3.10 --group test --project python/sdk python scripts/smoke-python-runtime.py --scenario all --exe "$EXE"
     - python scripts/build-python-release.py --package runtime --tag "$CI_COMMIT_TAG" --platform "$PLATFORM" --runtime-exe "$EXE" --output-dir "release/$PLATFORM"
     - python -m venv .wheel-smoke
-    - .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness=="${CI_COMMIT_TAG#python-v}"
+    - .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness=="$DSH_VERSION"
     - .wheel-smoke/bin/python scripts/smoke-python-runtime.py --scenario sdk-default
     - |
       if [ "${PLATFORM#linux-}" != "$PLATFORM" ]; then
@@ -53,7 +55,7 @@ sdk-wheel:
           linux-arm64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
           *) echo "Unsupported Linux platform $PLATFORM"; exit 1 ;;
         esac
-        docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness==${CI_COMMIT_TAG#python-v} && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
+        docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness==$DSH_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
       fi
   artifacts:
     paths: [release/$PLATFORM/*.whl]
@@ -105,14 +107,15 @@ publish-python:
   before_script:
     - python3 -m venv .ci-python
     - . .ci-python/bin/activate
+    - export DSH_VERSION="$(python -c 'import json; print(json.load(open("package.json"))["version"])')"
+    - test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; }
     - python -m pip install twine==6.2.0
   script:
-    - version="${CI_COMMIT_TAG#python-v}"
     - test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 4
-    - test -f "release/sdk/deepseek_harness-${version}-py3-none-any.whl"
-    - test -f "release/linux-x64/deepseek_harness_runtime_bin-${version}-py3-none-manylinux_2_28_x86_64.whl"
-    - test -f "release/linux-arm64/deepseek_harness_runtime_bin-${version}-py3-none-manylinux_2_28_aarch64.whl"
-    - test -f "release/macos-arm64/deepseek_harness_runtime_bin-${version}-py3-none-macosx_11_0_arm64.whl"
+    - test -f "release/sdk/deepseek_harness-${DSH_VERSION}-py3-none-any.whl"
+    - test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
+    - test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_aarch64.whl"
+    - test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-macosx_11_0_arm64.whl"
     - python -m twine check release/*/*.whl
     - export TWINE_USERNAME=gitlab-ci-token
     - export TWINE_PASSWORD="$CI_JOB_TOKEN"

+ 1 - 1
docs/cookbook/adding-a-package.md

@@ -18,7 +18,7 @@ packages/<group>/<pkg>/
 
 Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
 
-package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
+package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
 
 In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
 

+ 2 - 2
docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 474a7af7f2cfe9177dec779e3eb50fd6034e806d
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 1f436d63fc91420c6da6d92e32bd16d5936ced5d
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: 08fd6f530d1f564ca169a168c0f7d77bff6bf29f
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 050e4f9a6e8bb03e59835f2c06dd3c701aa34eed

+ 2 - 2
docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md

@@ -42,13 +42,13 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
 
 [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. CI treats them as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
 
-CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. The run retains only four artifacts, each containing one release file: the platform-independent SDK wheel and the three native runtime wheels; bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
+CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. The run retains only four artifacts, each containing one release file: the platform-independent SDK wheel and the three native runtime wheels; bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
 
 ### Python SDK distribution: two carriers, exe for production, node for development
 
 The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds three kinds of content: the checked-in default `runtime/cordis.yml`, the build-injected platform exe, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
 
-[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) accepts only stable `python-vX.Y.Z` tags and stages both packages at `X.Y.Z`, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms.
+[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms.
 
 The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`.
 

+ 2 - 2
docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md

@@ -42,13 +42,13 @@ deploy root 是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runti
 
 [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):runtime 闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直落** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`→ 注入 pkg 配置(`bin` 指闭包内 `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`,`assets` 全量 glob——动态 import 对 pkg 静态分析不可见,必须显式全量打入)→ 每 target 一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 落 `dist-exe/` 并拷回 runtime 目录。CI 把它们作为测试中间输入,只保留对应的平台 wheel。deploy 四 flag 均有实测依据:`--legacy` 是未开 inject-workspace-packages 时的必选路径;hoisted 产出零符号链接文件树(pkg VFS 最稳、物理保证 cordis 单实例);关 peer 自动安装避免未发布包名触发 registry 解析;link-workspace-packages 让闭包指向 workspace/vendor 源。
 
-CI:[`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),仅显式触发——`workflow_dispatch` 手动派发,或给 PR 打 `build-exe` 标签;linux-x64 / linux-arm64(`ubuntu-24.04-arm`)/ macos-arm64 三平台原生构建,并缓存 `~/.pkg-cache`;macOS ad-hoc 签名由 pkg 处理。每个平台都以 mock SSE 模型分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再以 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应,最后把 release 形态的 wheel 安装到干净 venv 中并在不传 `runtime_bin` 的情况下运行;Linux 还检查 GLIBC 依赖并在 manylinux 2.28 容器中运行。整次运行只保留 4 个产物,每个只含一个发布文件:平台无关的 SDK wheel 与 3 个原生 runtime wheel;裸 exe 和源码 bundle 只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受 `python-vX.Y.Z` tag 流水线,构建一个 SDK wheel 与 3 个原生 runtime wheel,再由单个串行 job 校验并发布这 4 个文件到项目 PyPI 注册表。Windows 是非目标。
+CI:[`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),仅显式触发——`workflow_dispatch` 手动派发,或给 PR 打 `build-exe` 标签;linux-x64 / linux-arm64(`ubuntu-24.04-arm`)/ macos-arm64 三平台原生构建,并缓存 `~/.pkg-cache`;macOS ad-hoc 签名由 pkg 处理。每个平台都以 mock SSE 模型分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再以 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应,最后把 release 形态的 wheel 安装到干净 venv 中并在不传 `runtime_bin` 的情况下运行;Linux 还检查 GLIBC 依赖并在 manylinux 2.28 容器中运行。整次运行只保留 4 个产物,每个只含一个发布文件:平台无关的 SDK wheel 与 3 个原生 runtime wheel;裸 exe 和源码 bundle 只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` tag 流水线,构建一个 SDK wheel 与 3 个原生 runtime wheel,再由单个串行 job 校验并发布这 4 个文件到项目 PyPI 注册表。Windows 是非目标。
 
 ### Python SDK 分发:双载体,exe 为生产、node 为开发
 
 Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk`(客户端)+ `python/sdk-runtime`(运行时载体包)。runtime 包数据目录三类内容:检入的默认 `runtime/cordis.yml`、构建注入的平台 exe、构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 自动解析**只找 exe**;node 载体仅显式 `DSH_RUNTIME_MODE=node` 启用(跑 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`,需系统 node ≥22.19),定位是本仓库成员的开发验证通道,不进 wheel 分发物。
 
-[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 只接受稳定的 `python-vX.Y.Z` tag,并以 `X.Y.Z` 暂存两个包,同时让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。SDK 是 `py3-none-any` wheel;只提供 wheel 的 runtime 包恰好包含一个 exe,tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用 tag、混合可执行载荷以及不支持的平台。
+[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录 `package.json` 读取权威的稳定 `X.Y.Z`,并以该版本暂存两个包,同时让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布 tag 只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel;只提供 wheel 的 runtime 包恰好包含一个 exe,tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用 tag、混合可执行载荷以及不支持的平台。
 
 exe「必须显式配置」的硬语义不变;零配置体验由 wrapper 恢复:调用方没给 `cordis`、没显式指定 runtime、环境无 `DSH_CORDIS_CONFIG` 时,客户端把检入的默认 `cordis.yml`(agent-core + 预载 llm-deepseek + JSONL 持久化 + bash-local + `dsh-jsonrpc` serving 条目,`!!js` 环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。
 

+ 1 - 1
docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md

@@ -15,7 +15,7 @@ Adopt **pnpm 11.7.0**, pinned via the `packageManager` field and installed throu
 - **Workspaces** move from the `package.json` `workspaces` array + `.yarnrc.yml` to `pnpm-workspace.yaml` (`vendor/*`, `packages/*` — the same globs; `examples/*` stay non-workspace, matching the prior setup and tsdown's explicit globs).
 - **Strict symlinked linker** (pnpm's default) replaces Yarn's hoisted `node-modules` linker. We deliberately add **no** `node-linker=hoisted` / `shamefully-hoist` escape hatch: pnpm's non-flat `node_modules` makes phantom dependencies (importing an undeclared transitive dep) fail loudly, which is a *feature* for a repo whose whole quality story is mechanical gates ([mechanical quality gates](2026-06-11-quality-gates.md)). The gate suite — typecheck, lint, test, build, knip — is the safety net that proves no such phantom imports exist.
 - **Build-script allowlist.** pnpm 10+ does not run dependency lifecycle scripts unless allowlisted. `pnpm-workspace.yaml` carries an explicit `allowBuilds` map (`esbuild`, `lefthook`, `@google/genai`, `protobufjs`) — the same supply-chain-hardening posture the repo already takes toward model/tool output, now applied to install-time code execution. `peerDependencyRules.allowedVersions.typescript: '>=5 <7'` silences benign peer-range warnings for the in-repo TypeScript.
-- **Constraints become package-manager-independent.** `yarn.config.cjs` (which imported `@yarnpkg/types` and used `Yarn.workspaces()` / `workspace.set()`) is replaced by `scripts/check-workspace-constraints.ts`, a plain tsx script run as `pnpm run constraints`. It enforces the identical invariants — every package `private: true`; `@deepseek-ai/dsh-*` packages declare `cordis` as both a peer- and dev-dependency with matching ranges, `version: 0.0.1`, `type: module`; vendored packages checked for privacy only — over the same `vendor` + `packages` scope.
+- **Constraints become package-manager-independent.** `yarn.config.cjs` (which imported `@yarnpkg/types` and used `Yarn.workspaces()` / `workspace.set()`) is replaced by `scripts/check-workspace-constraints.ts`, a plain tsx script run as `pnpm run constraints`. It enforces the identical invariants — every package `private: true`; `@deepseek-ai/dsh-*` packages declare `cordis` as both a peer- and dev-dependency with matching ranges, use the root `package.json` version, and set `type: module`; vendored packages checked for privacy only — over the same `vendor` + `packages` scope.
 - All `yarn …` verbs across CI, lefthook hooks, `package.json` scripts, and docs become `pnpm …` / `pnpm run …`. `yarn.lock` → `pnpm-lock.yaml` (lockfile v9). `.gitignore` swaps `.yarn/` for `.pnpm-store/`. Vendored READMEs (e.g. `vendor/cordis/README.md`) keep their upstream `yarn` examples untouched per the Vendoring Policy.
 
 ## Alternatives considered

+ 2 - 2
python/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-README.md: 2e2e3bff7e6903a1183cc445f590ecb327b92e65
-README.zh.md: f14dad4688ed3c54f46ae28475779b0a8d163ae0
+README.md: d9403733b7daff9b3e8dbf21e1bc0c68d61b88a7
+README.zh.md: 28859e43b7332a8ef4c3cf199b97860447a011a8

+ 6 - 5
python/README.md

@@ -49,15 +49,16 @@ Two flavors, both for repo members:
 
 ## Distributing the Python packages
 
-Python releases use stable tags of the form `python-vX.Y.Z`. The common staging script derives both distribution versions from the tag, pins `deepseek-harness-runtime-bin==X.Y.Z` in the SDK metadata, and rejects any other tag form. Build the pure SDK wheel once and one runtime wheel on each native platform:
+The root [`package.json`](../package.json) version is authoritative for both Python distributions. The common staging script reads that version, injects it into both wheels, and pins the SDK metadata to the same `deepseek-harness-runtime-bin==X.Y.Z`; an optional `python-vX.Y.Z` release tag is accepted only when it matches the repository version. Build the pure SDK wheel once and one runtime wheel on each native platform:
 
 ```sh
-python scripts/build-python-release.py --package sdk --tag python-v0.1.0 --output-dir dist-python
-python scripts/build-python-release.py --package runtime --tag python-v0.1.0 --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
-pip install --find-links dist-python deepseek-harness==0.1.0
+version="$(node -p "require('./package.json').version")"
+python scripts/build-python-release.py --package sdk --output-dir dist-python
+python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
+pip install --find-links dist-python deepseek-harness=="$version"
 ```
 
-The runtime distribution is wheel-only and rejects sdist builds, missing executables, and mixed-platform payloads. Its three wheel tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the SDK remains `py3-none-any`. A tag pipeline builds these four non-conflicting files and publishes them together, so a normal `pip install deepseek-harness==X.Y.Z` selects the matching runtime wheel and `import deepseek_harness` needs no `runtime_bin`.
+The runtime distribution is wheel-only and rejects sdist builds, missing executables, and mixed-platform payloads. Its three wheel tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the SDK remains `py3-none-any`. A matching `python-vX.Y.Z` tag pipeline builds these four non-conflicting files and publishes them together, so a normal `pip install deepseek-harness==X.Y.Z` selects the matching runtime wheel and `import deepseek_harness` needs no `runtime_bin`.
 
 ## Zero-config semantics
 

+ 6 - 5
python/README.zh.md

@@ -49,15 +49,16 @@ print(DeepSeekHarness().run("say hi").final_response)   # auto-resolution picks
 
 ## 分发 Python 包
 
-Python 发布只使用 `python-vX.Y.Z` 形式的稳定 tag。统一暂存脚本从 tag 推导两个分发物的版本,在 SDK 元数据中钉死 `deepseek-harness-runtime-bin==X.Y.Z`,并拒绝其他 tag 形式。纯 SDK wheel 只构建一次,runtime wheel 则在每个原生平台各构建一个:
+根目录 [`package.json`](../package.json) 的版本是两个 Python 分发物的权威版本。统一暂存脚本读取这个版本并注入两个 wheel,同时在 SDK 元数据中钉死相同版本的 `deepseek-harness-runtime-bin==X.Y.Z`;可选的 `python-vX.Y.Z` 发布 tag 只有与仓库版本匹配时才会被接受。纯 SDK wheel 只构建一次,runtime wheel 则在每个原生平台各构建一个:
 
 ```sh
-python scripts/build-python-release.py --package sdk --tag python-v0.1.0 --output-dir dist-python
-python scripts/build-python-release.py --package runtime --tag python-v0.1.0 --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
-pip install --find-links dist-python deepseek-harness==0.1.0
+version="$(node -p "require('./package.json').version")"
+python scripts/build-python-release.py --package sdk --output-dir dist-python
+python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
+pip install --find-links dist-python deepseek-harness=="$version"
 ```
 
-runtime 分发物只提供 wheel,并拒绝 sdist 构建、缺失可执行文件以及混合平台载荷。三个 wheel tag 分别是 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;SDK 保持 `py3-none-any`。tag 流水线统一构建并发布这 4 个互不冲突的文件,因此常规的 `pip install deepseek-harness==X.Y.Z` 会选中匹配平台的 runtime wheel,`import deepseek_harness` 不需要 `runtime_bin`。
+runtime 分发物只提供 wheel,并拒绝 sdist 构建、缺失可执行文件以及混合平台载荷。三个 wheel tag 分别是 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;SDK 保持 `py3-none-any`。匹配的 `python-vX.Y.Z` tag 流水线统一构建并发布这 4 个互不冲突的文件,因此常规的 `pip install deepseek-harness==X.Y.Z` 会选中匹配平台的 runtime wheel,`import deepseek_harness` 不需要 `runtime_bin`。
 
 ## 零配置语义
 

+ 2 - 2
python/sdk-runtime/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-README.md: 1a63f76f62b95cb36f53e3f8ff42898b26122d05
-README.zh.md: b214ed5e6d25a013def5429b92a14b7393f1e028
+README.md: 4beac57526761bb150e90b60f0030ed311c4034d
+README.zh.md: 3c715bce17ed1630ef0439b042d2b67047968c55

+ 1 - 1
python/sdk-runtime/README.md

@@ -15,7 +15,7 @@ Both carriers hold the same content, defined once: the [package.json](package.js
 
 Missing carriers raise `FileNotFoundError` naming the acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or install the matching platform runtime wheel produced by the `build-exe-for-python-sdk` CI workflow. The workflow retains wheels rather than standalone executable archives. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers.
 
-Each wheel contains exactly one executable. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple executables, and unsupported platform tags. Stable `python-vX.Y.Z` releases use the same `X.Y.Z` for this package and the SDK.
+Each wheel contains exactly one executable. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple executables, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-vX.Y.Z` release tag must match it.
 
 ## Resolution API
 

+ 1 - 1
python/sdk-runtime/README.zh.md

@@ -15,7 +15,7 @@ Python SDK 的运行时载体包(dist 名 `deepseek-harness-runtime-bin`,模
 
 载体缺失时抛出 `FileNotFoundError` 并写明获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台 runtime wheel。该工作流只保留 wheel,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不动任何调用方。
 
-每个 wheel 只包含一个可执行文件。固定 tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、可执行文件缺失或重复以及不支持的平台 tag。稳定的 `python-vX.Y.Z` 发布为本包和 SDK 使用同一个 `X.Y.Z`
+每个 wheel 只包含一个可执行文件。固定 tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、可执行文件缺失或重复以及不支持的平台 tag。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-vX.Y.Z` 发布 tag 必须与其匹配
 
 ## 解析 API
 

+ 39 - 0
python/sdk/tests/test_release_version.py

@@ -0,0 +1,39 @@
+"""Tests for repository-owned Python release versions."""
+
+from __future__ import annotations
+
+import json
+import runpy
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+
+
+ROOT = Path(__file__).resolve().parents[3]
+SCRIPT = ROOT / "scripts" / "build-python-release.py"
+build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
+
+
+def test_repository_version_matches_root_package_json() -> None:
+    expected = json.loads((ROOT / "package.json").read_text())["version"]
+
+    assert build_python_release.repository_version() == expected
+
+
+def test_release_tag_is_optional_for_non_release_builds() -> None:
+    build_python_release.validate_release_tag(None, "1.2.3")
+
+
+def test_release_tag_must_match_repository_version() -> None:
+    build_python_release.validate_release_tag("python-v1.2.3", "1.2.3")
+
+    with pytest.raises(ValueError, match="expected 'python-v1.2.3'"):
+        build_python_release.validate_release_tag("python-v1.2.4", "1.2.3")
+
+
+def test_repository_version_rejects_non_stable_versions(tmp_path: Path) -> None:
+    (tmp_path / "package.json").write_text('{"version":"1.2.3-dev"}\n')
+
+    with pytest.raises(ValueError, match="must be stable X.Y.Z"):
+        build_python_release.repository_version(tmp_path)

+ 30 - 8
scripts/build-python-release.py

@@ -1,10 +1,11 @@
 #!/usr/bin/env python3
-"""Stage and build one Python release wheel from a stable ``python-vX.Y.Z`` tag."""
+"""Stage and build one Python wheel at the repository version."""
 
 from __future__ import annotations
 
 import argparse
 import email
+import json
 import os
 import re
 import shutil
@@ -26,12 +27,16 @@ PLATFORMS = {
 def main() -> None:
     parser = argparse.ArgumentParser(description=__doc__)
     parser.add_argument("--package", choices=("sdk", "runtime"), required=True)
-    parser.add_argument("--tag", required=True)
+    parser.add_argument(
+        "--tag",
+        help="optional python-vX.Y.Z release tag; it must match package.json",
+    )
     parser.add_argument("--output-dir", type=Path, required=True)
     parser.add_argument("--platform", choices=tuple(PLATFORMS))
     parser.add_argument("--runtime-exe", type=Path)
     args = parser.parse_args()
-    version = version_from_tag(args.tag)
+    version = repository_version()
+    validate_release_tag(args.tag, version)
     if args.package == "runtime" and (args.platform is None or args.runtime_exe is None):
         parser.error("runtime builds require --platform and --runtime-exe")
     if args.package == "sdk" and (args.platform is not None or args.runtime_exe is not None):
@@ -58,11 +63,28 @@ def main() -> None:
     print(expected)
 
 
-def version_from_tag(tag: str) -> str:
-    match = re.fullmatch(r"python-v(\d+\.\d+\.\d+)", tag)
-    if match is None:
-        raise ValueError(f"release tag must match python-vX.Y.Z, got {tag!r}")
-    return match.group(1)
+def repository_version(root: Path = ROOT) -> str:
+    package_json = root / "package.json"
+    try:
+        payload = json.loads(package_json.read_text())
+    except (OSError, json.JSONDecodeError) as error:
+        raise ValueError(f"could not read repository version from {package_json}") from error
+    version = payload.get("version") if isinstance(payload, dict) else None
+    if not isinstance(version, str) or re.fullmatch(r"\d+\.\d+\.\d+", version) is None:
+        raise ValueError(
+            f"{package_json} version must be stable X.Y.Z, got {version!r}"
+        )
+    return version
+
+
+def validate_release_tag(tag: str | None, version: str) -> None:
+    if tag is None:
+        return
+    expected = f"python-v{version}"
+    if tag != expected:
+        raise ValueError(
+            f"release tag must match repository version: expected {expected!r}, got {tag!r}"
+        )
 
 
 def copy_package(source: Path, destination: Path) -> None:

+ 16 - 4
scripts/check-workspace-constraints.ts

@@ -61,6 +61,9 @@ function readJson(path: string): PackageManifest {
   return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
 }
 
+const rootManifest = readJson(join(root, 'package.json'))
+const repositoryVersion = rootManifest.version
+
 /** Repo-relative dirs holding a package.json, walked to the configured depth. */
 function packageDirs(base: string, depth: number): string[] {
   if (depth === 1) {
@@ -78,7 +81,7 @@ function packageDirs(base: string, depth: number): string[] {
 
 function workspaceManifests(): WorkspaceManifest[] {
   const manifests: WorkspaceManifest[] = [
-    { dir: '.', manifest: readJson(join(root, 'package.json')) },
+    { dir: '.', manifest: rootManifest },
   ]
 
   for (const { dir: base, depth } of workspaceGlobs) {
@@ -147,8 +150,8 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
     if (peer && dev && peer !== dev) {
       errors.push(`${label}: cordis peer (${peer}) and dev (${dev}) ranges must match`)
     }
-    if (manifest.version !== '0.0.1') {
-      errors.push(`${label}: package.json must set "version": "0.0.1"`)
+    if (manifest.version !== repositoryVersion) {
+      errors.push(`${label}: package.json version must match root version ${repositoryVersion ?? '(missing)'}`)
     }
     if (manifest.type !== 'module') {
       errors.push(`${label}: package.json must set "type": "module"`)
@@ -205,7 +208,16 @@ function checkHierarchyShape(): string[] {
   return errors
 }
 
-const errors = [...workspaceManifests().flatMap(checkWorkspace), ...checkHierarchyShape()]
+function checkRepositoryVersion(): string[] {
+  if (repositoryVersion && /^\d+\.\d+\.\d+$/.test(repositoryVersion)) return []
+  return ['package.json: version must be stable X.Y.Z']
+}
+
+const errors = [
+  ...checkRepositoryVersion(),
+  ...workspaceManifests().flatMap(checkWorkspace),
+  ...checkHierarchyShape(),
+]
 if (errors.length > 0) {
   console.error(errors.join('\n'))
   process.exitCode = 1