@sqdr2/dsh-fusion 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,148 @@
1
+ # Changelog
2
+
3
+ Release notes for `@sqdr2/dsh-fusion`. Newest first.
4
+
5
+ ## Origin
6
+
7
+ This package is a fork of [`aa2246740/dsh-model-fusion`](https://github.com/aa2246740/dsh-model-fusion). 0.3.0 is the
8
+ first release published under this name, and it branches from upstream 0.2.9. Upstream 0.2.10 and 0.2.11 landed after
9
+ that point and are **not** included here.
10
+
11
+ Earlier versions — `dsh-model-fusion` 0.2.0 through 0.2.11, and the pre-0.3.0 tags in this repository's history — were
12
+ not published as `@sqdr2/dsh-fusion`, so they are not listed here. Their notes live in the upstream repository, under its
13
+ own tags and releases.
14
+
15
+ Entry format: what changed, then what was actually validated. "Not claimed" lines mark what a release did *not* test —
16
+ read them before assuming a platform is covered.
17
+
18
+ ## 0.3.3 — 2026-11-07
19
+
20
+ ### Changes
21
+
22
+ - The combo list selects from the whole row, and no longer paints a black pill inside it. Only the name and route text
23
+ was a button, so the row's padding and the empty space beside the text ignored clicks; and the generic
24
+ `.fusion-settings button` fill (`--dsw-alias-button-primary-fill`, which is `#0f1115` in the light theme) outranked the
25
+ row's `background:none`, so the one clickable strip rendered as a black capsule. The row is now `padding:0` with the
26
+ button spanning its full width up to the Delete button (89% of the row in a headless-Chrome measurement), the button
27
+ keeps `background:none`, and Delete keeps its own hit area and stops its click from selecting. A radio-style marker,
28
+ a hover surface and a brand-tinted selected state carry the selection instead of the fill.
29
+ - The composer names the permission of the model working now. Fusion pins the conversation (Lead) session to
30
+ `read-only`, and that pair matches no configured preset, so the native permission control read `Custom` for the whole
31
+ task. A Fusion chip in the composer tool row shows `Lead · Read Only` / `Sidekick · Full access` and follows the role
32
+ that is running; `FusionStatus.task.permissions` carries the per-role modes (`lead`, `worker`) and `active` role, read
33
+ from the recorded role intent, the in-flight request and the phase. The native control keeps its slot, its selection
34
+ state and its meaning: it still sets the Sidekick's mode.
35
+ - Fusion is no longer limited to one saved pair. The sidebar panel now manages a list of combos (add, rename, delete),
36
+ and **each combo is its own entry in the native model menu** (`Fusion · <name>`), so several conversations can run
37
+ different Lead/Sidekick pairs at the same time.
38
+ - Each combo advertises its own Lead's input modalities and freezes its own profile, so a conversation keeps the pair it
39
+ selected even after the list is edited, reordered or a combo is deleted. Picking another combo inside a conversation
40
+ drains the current task and starts a new one, exactly like leaving Fusion.
41
+ - Up to 6 combos. The first combo keeps the historical model id `auto`, and a pre-combo `settings:profile` is migrated
42
+ into it (named "自动") on read, so durable selections, frozen bindings and deploy-time profile files keep working. The
43
+ first combo is still mirrored into `settings:profile` for an older tab or a downgrade.
44
+ - An existing tab running the previous bundle can still save: it sends one pair and edits the first combo, leaving the
45
+ others alone.
46
+ - The switch-notice filter now recognizes every combo id, including the bare `auto → pair-xxxxxxxx` form the native menu
47
+ emits when two combos of this provider replace each other.
48
+
49
+ ### Build
50
+
51
+ - `pnpm build` no longer requires a harness checkout with `tools/dshx`. It still uses that harness-owned builder when
52
+ `DSHX_HARNESS` provides it, and otherwise emits the same two artifacts with a local `esbuild` pass (now a
53
+ devDependency): `lib/dsh-fusion.js` as plain ESM with every package external, `lib/client.js` as the CommonJS body the
54
+ Harness module loader expects.
55
+
56
+ ### Validation
57
+
58
+ - `pnpm test` (53 files, 397 tests: 395 passed, 1 skipped, 1 failed), `tsc -p tsconfig.json --noEmit`. The native Host
59
+ suite was last run at 263 passed / 1 skipped and was not re-run for this change: nothing outside the client bundle
60
+ moved. The one failing case is `tests/check-programs.spec.ts`, whose probe returns `undefined` on this Windows host
61
+ both before and after the change, so it is not counted as a regression.
62
+ - Rebuilt `lib/` and loaded both artifacts in Node: the Host module exports `apply`/`Config`/`name`, and the client
63
+ bundle registers under `@sqdr2/dsh-fusion` and still exports `apply`/`inject`/`name`/`FUSION_PANEL_ID`/`FusionPanelIcon`.
64
+ - New `tests/pairs.spec.ts` (migration, id assignment, name/limit validation, compare-and-swap, per-combo profile
65
+ identity) and `tests/fusion-combos.spec.ts` (list/rename/add/delete and whole-list save payload).
66
+ - The combo row hit area: a new `tests/fusion-combos.spec.ts` case asserts the row carries no padding, the selection
67
+ button owns it (`flex:1 1 auto`, `padding:11px 12px`, `background:none`), the row switches selection from the row body,
68
+ and Delete removes its own row without changing which combo is selected. The same markup and CSS were rendered in
69
+ headless Chrome and hit-tested with `elementFromPoint`: the left edge, the marker, the text and the empty space right
70
+ of it all resolve to the selection button, the right edge resolves to Delete, and the selection button covers 89% of
71
+ the row width.
72
+ - The permission chip: new `tests/status.spec.ts` cases (role modes, the takeover exception and the released
73
+ conversation) and `tests/fusion-permission.spec.ts` (both roles, the Custom explanation, silence outside a
74
+ role-separated task, the fast poll, and the additive registration that never takes the native permission slot).
75
+
76
+ ## 0.3.2 — 2026-11-05
77
+
78
+ Verification release. No runtime behaviour changed from 0.3.0, and the package is identical to it apart from the version
79
+ field.
80
+
81
+ 0.3.1 was meant to be this release, but its CI run failed before publishing: `tests/status-index.spec.ts` timed out at
82
+ 5000ms while finishing in a fraction of a second locally. The fixture built 600 rows with one autocommit each, which a
83
+ CI runner's disk cannot absorb inside a limit meant for unit tests. Those fixtures now use a single transaction, and the
84
+ two tests that migrate and backfill a real database declare a 60s budget.
85
+
86
+ ## 0.3.1 — 2026-11-05
87
+
88
+ Verification release. No runtime behaviour changed from 0.3.0, and the package is byte-identical to it apart from the
89
+ version field.
90
+
91
+ 0.3.0 was published by hand with a granular token, before this repository could publish from CI: Actions are disabled by
92
+ default on forks, and a trusted publisher can only be added to a package that already exists. Both are now in place, so
93
+ 0.3.1 exists to prove the OIDC path end to end — the tag-push workflow builds, tests and publishes without any stored
94
+ token, and the result carries a provenance attestation naming the commit it was built from.
95
+
96
+ ## 0.3.0 — 2026-11-05
97
+
98
+ First release under the name `@sqdr2/dsh-fusion`, branching from upstream `dsh-model-fusion` 0.2.9. Everything 0.2.9
99
+ does is still here; the changes below are the fork's.
100
+
101
+ The package was renamed, so install `@sqdr2/dsh-fusion` once — an installed `dsh-model-fusion` is not upgraded in place.
102
+ The plugin id stays `dsh-fusion`, so the rename moves code only, not data.
103
+
104
+ ### Changes
105
+
106
+ - The Fusion entry moved from **Settings → Fusion** to its own left-sidebar panel, and the panel body scrolls inside the
107
+ main slot instead of clipping the form and history table on a short window.
108
+ - A role on a bridge route (`antigravity`, i.e. `dsh-agy-worker` / `dsh-agy-link`) can now reach the Host tools. That
109
+ route may only report a step as `agy_tool`, which the role ceiling never carried, so a delegation degraded to prose and
110
+ the Host reported `WORKER_TOOLS_UNREACHABLE`. The transport is unioned into the ceiling when this Host registers it,
111
+ for the Lead as well as the Sidekick. Full diagnosis: [docs/WORKER-TOOLS-UNREACHABLE.md](docs/WORKER-TOOLS-UNREACHABLE.md).
112
+ - A bridged Sidekick can now bind its brief and report back. Such a route runs the call in its own loop under a
113
+ bridge-local id, so `fusion_submit_result` was refused as "Worker tool requires a complete native model request" and
114
+ the task stalled. A bridged call is accepted for the message that emitted the transport; the native path is unchanged
115
+ and still preferred.
116
+ - A task whose Sidekick route cannot reach the tools at all has an exit: the verdict is recorded on the runtime record,
117
+ `fusion_wait` re-reports it, and `fusion_delegate` may replace the dead order instead of leaving the task in
118
+ `WORKER_RUNNING` with no way out.
119
+ - A frozen scope no longer deadlocks a task. A glob such as `allowedPaths: ['lib/types/**']` matched nothing, widening
120
+ was refused because the files it wanted to record already differed, and the scope could not be cleared. Globs match,
121
+ widening is compared against the delegation base, and the scope can be cleared explicitly.
122
+ - History records can be deleted again. Deletion now asks the coordinator, which is the only authority that sees live
123
+ Agents, tool calls, jobs and write leases, so a replaced process no longer leaves an undeletable `running` task and a
124
+ live task is no longer deleted out from under itself. A 409 names the resource that held it.
125
+ - Usage, the task list, cache keepalive and history are each a collapsible box, collapsed by default, so the model pair
126
+ stays above the fold.
127
+
128
+ ### Compatibility after the rename
129
+
130
+ - Message source kinds are read under both spellings (`plugin:dsh-fusion`, `plugin:dsh-model-fusion`) and written only
131
+ under the new one. Recorded history is never rewritten.
132
+ - A session that selected Fusion before the rename stored `dsh-model-fusion/auto`; it is rewritten to `dsh-fusion/auto`
133
+ when the plugin installs its selection, so the next turn dispatches under a registered provider.
134
+ - `$DSH_HOME/plugins/dsh-model-fusion/` is migrated to `$DSH_HOME/plugins/dsh-fusion/` on first start, with a copy
135
+ fallback across filesystems that removes only the files it copied.
136
+ - Durable write leases are adopted the same way, and a lock written under the old name still blocks a new writer rather
137
+ than being taken over.
138
+ - The panel's HTTP routes are now `/api/fusion`; `/api/model-fusion` stays registered for a browser tab still holding the
139
+ previous client bundle.
140
+
141
+ ### Validation
142
+
143
+ Type checking passes, all 379 unit tests pass, and `lib/` was rebuilt from the tagged source. The native Host suite passes
144
+ 263 tests with one Windows-only test skipped, on the pinned Host checkout. No model calls were made for this release, and
145
+ the plugin is not installed or activated in a running Host by this preparation.
146
+
147
+ Install with `dsh plugin --profile web add @sqdr2/dsh-fusion`, or `github:SQDR2/dsh-fusion` from the repository, which
148
+ ships the built `lib/`. See [INSTALL.md](INSTALL.md) / [INSTALL.zh.md](INSTALL.zh.md).
package/README.en.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Fusion · a DSH plugin
2
2
 
3
- [中文](README.md) · [Install](INSTALL.md) · [安装](INSTALL.zh.md)
3
+ [中文](README.md) · [Install](INSTALL.md) · [安装](INSTALL.zh.md) · [Changelog](CHANGELOG.md)
4
4
 
5
5
  > **This repository is a fork of [`aa2246740/dsh-model-fusion`](https://github.com/aa2246740/dsh-model-fusion), published as `@sqdr2/dsh-fusion`.**
6
6
  > Every upstream capability, algorithm and measurement is kept. This fork does three things: it moves the entry from
@@ -8,7 +8,7 @@
8
8
  > ([`dsh-agy-worker`](https://github.com/SQDR2/dsh-agy-worker)) work as a Sidekick, and it removes a few ways a frozen
9
9
  > scope or a stale task record could dead-end a run. See [What this fork changes](#what-this-fork-changes).
10
10
 
11
- In DeepSeek Harness (DSH), Fusion is one entry in the model menu: **Fusion · auto**. Behind it are two models you choose:
11
+ In DeepSeek Harness (DSH), Fusion is an ordinary model in the menu: **save N combos and the menu shows N entries** (**Fusion · combo name**). Behind each one are two models you choose:
12
12
 
13
13
  - **Lead**: a strong frontier model. It reads the request and the code, briefs the work and reviews the result.
14
14
  - **Sidekick**: a much cheaper model. It edits code, runs commands and runs tests.
@@ -51,8 +51,10 @@ After contaminated attempts were excluded, four tasks were clean in every condit
51
51
  ## Use
52
52
 
53
53
  1. Install the plugin ([INSTALL.md](INSTALL.md)).
54
- 2. Open the **Fusion** entry in the left sidebar (default below Memory System) and choose a Lead and a Sidekick. Any model you are signed in to in DSH works.
55
- 3. Select **Fusion · auto** in the model menu and work as usual.
54
+ 2. Open the **Fusion** entry in the left sidebar (default below Memory System), name the combo, and choose a Lead and a Sidekick. Any model you are signed in to in DSH works.
55
+ 3. Select that combo (**Fusion · name**) in the model menu and work as usual.
56
+
57
+ **Several combos can coexist** (up to 6): for example a cheap "fast" one and a strong "thorough" one, each its own model-menu entry. With several conversations open, each one can pick a different combo — a conversation freezes the combo it selected, so later edits or other conversations never change it. A pre-combo installation is migrated into one combo named "自动" whose model id stays `auto`, so existing conversations keep working.
56
58
 
57
59
  **Pairing:** Fusion saves money because the Sidekick costs far less than the Lead. Pick a strong frontier Lead and a Sidekick that is much cheaper (ideally 5× or more). The closer their prices, the smaller the saving.
58
60
 
@@ -70,6 +72,7 @@ After contaminated attempts were excluded, four tasks were clean in every condit
70
72
  - **The Lead does not write code.** The program enforces this, not the prompt:
71
73
  - the Lead has no file-writing tools;
72
74
  - its shell commands run in DSH's read-only sandbox.
75
+ - **The permission shown in the composer.** Fusion separates permissions by role and pins the Lead's session to read-only, and "read-only plus your approval policy" usually matches no configured preset, so the native permission control reads `Custom` once a task is dispatched. Fusion's own chip in the composer tool row names the role working right now and its permission: `Lead · Read Only` / `Sidekick · Full access`. That control still sets the Sidekick's permission: change it and the Sidekick's next step uses that mode while the Lead stays read-only. If you would rather the native control read a name instead of `Custom`, add a preset to your profile's `presets` with `sandbox: read-only` and `approval: never` — pure configuration, this plugin does not depend on it.
73
76
  - **The only exception is a takeover the Host unlocks when the Sidekick demonstrably cannot finish.** For example: checks still failing after 2 rework rounds, or the Sidekick hit its step limit or kept stalling. During a takeover:
74
77
  - the Lead may only change the files the task allows;
75
78
  - its work must pass the same acceptance;
@@ -186,6 +189,8 @@ For Windows development, put PowerShell 7 (`pwsh`), Node and Python on the test
186
189
 
187
190
  The plugin uses only DSH's public plugin APIs and never modifies DSH itself. How to adapt it to a new DSH release: [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
188
191
 
192
+ How to cut a release (bump the version, rebuild `lib/`, push a tag, let CI publish over OIDC): [RELEASING.md](RELEASING.md).
193
+
189
194
  ## License
190
195
 
191
196
  Apache-2.0. Forked from [`aa2246740/dsh-model-fusion`](https://github.com/aa2246740/dsh-model-fusion), also under Apache-2.0.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Fusion · DSH 插件
2
2
 
3
- [English](README.en.md) · [安装](INSTALL.zh.md) · [Install (English)](INSTALL.md)
3
+ [English](README.en.md) · [安装](INSTALL.zh.md) · [Install (English)](INSTALL.md) · [更新日志](CHANGELOG.md)
4
4
 
5
5
  > **本仓库是 [`aa2246740/dsh-model-fusion`](https://github.com/aa2246740/dsh-model-fusion) 的 fork,包名 `@sqdr2/dsh-fusion`。**
6
6
  > 上游的能力、算法和实测数据全部保留;本 fork 专注于三件事:把入口从「设置」搬到左侧边栏、
7
7
  > 让 agy 线路([`dsh-agy-worker`](https://github.com/SQDR2/dsh-agy-worker))真的能当 Sidekick、
8
8
  > 以及修掉冻结范围与任务记录上几个会卡住流程的问题。改动清单见 [本 fork 做了什么](#本-fork-做了什么)。
9
9
 
10
- Fusion 在 DeepSeek Harness(DSH)的模型菜单里是一个普通模型:**Fusion · 自动**。背后是两个你自己选的模型:
10
+ Fusion 在 DeepSeek Harness(DSH)的模型菜单里就是一个普通模型:**你保存几个组合,菜单里就有几项**(**Fusion · 组合名称**)。每个组合背后是两个你自己选的模型:
11
11
 
12
12
  - **Lead**:一个强的前沿模型。负责理解需求、看代码、写任务说明、审查结果。
13
13
  - **Sidekick**:一个便宜得多的模型。负责实际改代码、跑命令、跑测试。
@@ -48,8 +48,10 @@ Fusion 在 DeepSeek Harness(DSH)的模型菜单里是一个普通模型:**
48
48
  ## 怎么用
49
49
 
50
50
  1. 按 [安装说明](INSTALL.zh.md) 装好插件。
51
- 2. 打开左侧边栏的 **Fusion** 入口(默认位于记忆系统下方),选择 Lead 和 Sidekick。可以选 DSH 里任何已登录的模型。
52
- 3. 在模型菜单里选择 **Fusion · 自动**,像平时一样对话。
51
+ 2. 打开左侧边栏的 **Fusion** 入口(默认位于记忆系统下方),给组合起个名字,选择 Lead 和 Sidekick。可以选 DSH 里任何已登录的模型。
52
+ 3. 在模型菜单里选择这个组合(显示为 **Fusion · 组合名称**),像平时一样对话。
53
+
54
+ **多个组合可以同时存在**(最多 6 个):例如「快」用便宜模型、「稳」用强模型,分别作为模型菜单里的两项。同时开多个会话时,每个会话可以选不同的组合——选中的会话会冻结当时的组合,之后改设置或换别的会话都不影响它。旧版只有一个组合时保存的配置会自动迁移成名为「自动」的组合,模型 ID 仍是 `auto`,已有会话照常继续。
53
55
 
54
56
  **搭配建议:** 省钱靠的是 Sidekick 比 Lead 便宜得多。选一个强的前沿模型当 Lead,再选一个价格低很多(最好 5 倍以上)的模型当 Sidekick。两者价格越接近,越省不了钱。
55
57
 
@@ -67,6 +69,7 @@ Fusion 在 DeepSeek Harness(DSH)的模型菜单里是一个普通模型:**
67
69
  - **Lead 不写代码**:这不是靠提示词约束,而是由程序保证的:
68
70
  - Lead 没有写文件的工具;
69
71
  - 它执行的 shell 命令跑在 DSH 的只读沙盒里。
72
+ - **输入框里的权限**:Fusion 按角色分离权限,Lead 的会话被固定在只读,而"只读 + 你的确认策略"这个组合通常匹配不上任何已配置预设,所以下发任务后「权限」控件会显示 `Custom`。工具行里 Fusion 自己的标签随时显示**当前在干活的角色**及其权限:`Lead · 仅可查看` / `Sidekick · 完全权限`。那个控件本身仍然决定 Sidekick 的权限:在那里换一项,Sidekick 下一轮就用那一项,Lead 仍然只读。如果你更希望原生控件也不显示 `Custom`,在你 profile 的 `presets` 里加一条 `sandbox: read-only` + `approval: never` 的预设即可(纯配置,本插件不依赖它)。
70
73
  - **唯一的例外:程序判定 Sidekick 确实做不完时,才解锁 Lead 接手。** 例如 2 轮返工后检查仍然失败,或者 Sidekick 用完了步数、反复卡住。接手时:
71
74
  - Lead 只能改任务允许的文件;
72
75
  - 必须通过同样的验收;
@@ -176,6 +179,8 @@ Windows 开发环境需将 PowerShell 7(`pwsh`)、Node 和 Python 加入运
176
179
 
177
180
  插件只使用 DSH 公开的插件接口,不修改 DSH 源码。适配新版本 DSH 的做法见 [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md)。
178
181
 
182
+ 发版流程(改版本号、重建 `lib/`、打 tag、CI 经 OIDC 发到 npm)见 [RELEASING.zh.md](RELEASING.zh.md)。
183
+
179
184
  ## 许可
180
185
 
181
186
  Apache-2.0。上游 fork 自 [`aa2246740/dsh-model-fusion`](https://github.com/aa2246740/dsh-model-fusion),同样以 Apache-2.0 发布。
package/RELEASING.md ADDED
@@ -0,0 +1,129 @@
1
+ # Releasing
2
+
3
+ [中文](RELEASING.zh.md) · [Install](INSTALL.md) · [README](README.en.md)
4
+
5
+ Maintainer notes. Users do not need this page.
6
+
7
+ `@sqdr2/dsh-fusion` publishes to npm from GitHub Actions over OIDC, so no long-lived token exists anywhere. This page
8
+ covers the one-time setup that makes that work, and the loop for each release afterwards.
9
+
10
+ ## What triggers a publish
11
+
12
+ Only two events:
13
+
14
+ | Event | Result |
15
+ |---|---|
16
+ | Push a tag matching `v*` | build, test, publish |
17
+ | `workflow_dispatch` | same; `dry_run=true` stops before publishing |
18
+
19
+ **Pushing to `main` never publishes.** A merge, a docs fix or a version bump sitting on `main` has no effect on npm
20
+ until a tag exists.
21
+
22
+ The workflow skips a version that is already on npm, so re-running a tag is safe: it rebuilds and retests, then exits
23
+ without touching the registry.
24
+
25
+ ## One-time setup
26
+
27
+ Both steps are already done for this repository. They are recorded here because a fork that renames the package has to
28
+ repeat them, and because a misconfigured trusted publisher fails in a way that looks like a broken workflow.
29
+
30
+ ### 1. Enable Actions
31
+
32
+ This repository is a fork, and GitHub disables Actions on forks by default. Without this step a tag push is silently
33
+ ignored: no run is created, no failure appears, and the Actions tab reports `0 workflow runs`.
34
+
35
+ Open the **Actions** tab and accept the "I understand my workflows, go ahead and enable them" prompt.
36
+
37
+ ### 2. Add a trusted publisher on npm
38
+
39
+ OIDC publishing is authorised per package, on the package's own settings page, which a name that has never been
40
+ published does not have. So the very first release of a name can only be done by hand:
41
+
42
+ 1. Publish once with a granular token that bypasses 2FA (`bypass_2fa: true`, `package: write` on the right scope).
43
+ A token without 2FA bypass is rejected with `E403 Two-factor authentication ... is required to publish packages`,
44
+ and a token scoped to `@sqdr2` cannot publish an unscoped name at all.
45
+ 2. On **npmjs.com → the package → Settings → Trusted publishing**, add:
46
+
47
+ | Field | Value |
48
+ | --- | --- |
49
+ | Provider | GitHub Actions |
50
+ | Repository | `SQDR2/dsh-fusion` |
51
+ | Workflow | `publish.yml` |
52
+ | Permission | allow npm publish |
53
+
54
+ The account needs 2FA of its own first. The package page only says "2FA has been enabled for this package. It is
55
+ recommended that you configure 2FA for your npm account", and submitting the form bounces to
56
+ `/settings/<user>/tfa` asking for a security key or authenticator. Configure account-level 2FA before starting, or
57
+ the form silently turns into a 2FA enrolment page and nothing is created.
58
+
59
+ 3. Publish a version within **2 days**. A new trust configuration is only validated by a first successful publish;
60
+ unvalidated ones expire and cannot be edited afterwards, only deleted and recreated.
61
+
62
+ Once the trusted publisher is validated, recommend **Settings → Publishing access → Require two-factor authentication
63
+ and disallow tokens** on the package. Tokens stop being usable for publishing; only the workflow can publish.
64
+
65
+ The npm CLI cannot create a trusted publisher: `npm trust github` needs an interactive 2FA credential, and the API
66
+ rejects a 2FA-bypass token with `Granular access tokens that bypass two-factor authentication may not perform this
67
+ action`. This step is browser-only by design.
68
+
69
+ ## Cutting a release
70
+
71
+ `package.json` is the single source of truth for the version. The tag must match it exactly or the workflow stops at its
72
+ first step.
73
+
74
+ ```sh
75
+ # 1. Bump the version in package.json (and nowhere else), and add the entry at
76
+ # the top of CHANGELOG.md. That section becomes the GitHub release body.
77
+ # 2. Rebuild lib/ — it is tracked in git and shipped in the tarball, so a stale
78
+ # lib/ publishes code that does not match the tagged source.
79
+ DSHX_HARNESS=/path/to/deepseek-harness-0.2.0-rc.2 pnpm build
80
+ pnpm typecheck && pnpm test
81
+ # 3. Commit the source, the rebuilt lib/ and CHANGELOG.md together.
82
+ git add -A && git commit -m "chore(release): 0.3.1"
83
+ # 4. Tag and push both.
84
+ git tag v0.3.1 && git push origin main v0.3.1
85
+ ```
86
+
87
+ The workflow then runs `pnpm install`, `pnpm build`, `pnpm test` and, on a tag, `npm publish --access public`.
88
+ A published version also gets a provenance attestation, so `npm view` can point at the commit it was built from.
89
+
90
+ [`CHANGELOG.md`](CHANGELOG.md) is the only place release notes live. Do not add a `RELEASE-*.md` per version: a
91
+ directory of near-identical files that nothing links to stops being readable within a few releases, and the notes belong
92
+ under one version heading each. It lists only versions published as `@sqdr2/dsh-fusion`; upstream `dsh-model-fusion`
93
+ history stays in the upstream repository.
94
+
95
+ ### Two things the build is pinned to
96
+
97
+ `scripts/build.mjs` accepts only the Host versions in `engines.dsh` (`0.2.0-rc.2` and `0.2.1-rc.2`) and fails
98
+ otherwise. CI checks out `dsh-v0.2.0-rc.2` for that reason.
99
+
100
+ The bundler version in the workflow is pinned to exactly `tsdown@0.22.2`. `lib/` is committed, so a different bundler
101
+ would commit a bundle that does not match what was tested; `0.22.14`, for instance, reformats several hundred lines.
102
+
103
+ ## Checking a release
104
+
105
+ ```sh
106
+ npm view @sqdr2/dsh-fusion version dist-tags
107
+ npm view @sqdr2/dsh-fusion@0.3.0 dist.attestations # present means it came from CI
108
+ ```
109
+
110
+ To check the artefact rather than the metadata:
111
+
112
+ ```sh
113
+ npm install @sqdr2/dsh-fusion@0.3.0
114
+ node -e "import('@sqdr2/dsh-fusion').then(m => console.log(m.name, m.inject.length))"
115
+ grep -o 'id: \"[^\"]*\"' node_modules/@sqdr2/dsh-fusion/lib/client.js | head -1
116
+ ```
117
+
118
+ The host bundle must export `dsh-fusion`, and the client bundle's registration id must be the package name
119
+ `@sqdr2/dsh-fusion`. They differ on purpose: see [INSTALL.md](INSTALL.md) for why, and `src/identity.ts` for how.
120
+
121
+ ## If a publish fails
122
+
123
+ | Symptom | Cause |
124
+ | --- | --- |
125
+ | Tag pushed, `0 workflow runs` | Actions not enabled on the fork |
126
+ | `404` or `ENEEDAUTH` from `npm publish` | No trusted publisher on npm, or it expired unvalidated |
127
+ | `tag vX != package.json Y` | Version bumped in one place only |
128
+ | `Unsupported Host package version` | `HARNESS_REF` drifted from `engines.dsh` |
129
+ | Bundle rebuild differs from committed `lib/` | Bundler version not pinned to `0.22.2` |
@@ -0,0 +1,120 @@
1
+ # 发版
2
+
3
+ [English](RELEASING.md) · [安装](INSTALL.zh.md) · [README](README.md)
4
+
5
+ 维护者用。普通用户不需要看这一页。
6
+
7
+ `@sqdr2/dsh-fusion` 由 GitHub Actions 通过 OIDC 发到 npm,任何地方都不存在长期 token。本页说明让它跑起来的一次性配置,以及之后每一版的流程。
8
+
9
+ ## 什么会触发发布
10
+
11
+ 只有两种事件:
12
+
13
+ | 事件 | 结果 |
14
+ | --- | --- |
15
+ | 推送匹配 `v*` 的 tag | 构建、测试、发布 |
16
+ | `workflow_dispatch` | 同上;`dry_run=true` 在发布前停下 |
17
+
18
+ **往 `main` 推永远不会发布。** 合一个 PR、改一处文档、或者把版本号改好放在 `main` 上,在有 tag 之前都不会影响 npm。
19
+
20
+ workflow 会跳过 npm 上已存在的版本,所以同一个 tag 重跑是安全的:重新构建、重新测试,然后不碰 registry 直接退出。
21
+
22
+ ## 一次性配置
23
+
24
+ 两步本仓库都已完成。之所以记在这里,是因为 fork 后改名要重做一遍,而且信任配置配错时的失败现象很像「workflow 坏了」。
25
+
26
+ ### 1. 启用 Actions
27
+
28
+ 本仓库是 fork,GitHub 默认禁用 fork 的 Actions。这一步不做,推 tag 会被静默忽略:不建运行、不报错,Actions 页面显示 `0 workflow runs`。
29
+
30
+ 打开 **Actions** 标签页,接受 “I understand my workflows, go ahead and enable them” 提示。
31
+
32
+ ### 2. 在 npm 上加信任配置
33
+
34
+ OIDC 发布是按包授权的,配置在包自己的设置页上,而一个从未发布过的包名没有这个页面。所以某个包名的第一版只能手发:
35
+
36
+ 1. 用一个勾了 2FA bypass 的 granular token 手动发一次(`bypass_2fa: true`,`package: write`,作用域要对)。
37
+ 不勾 bypass 会被拒:`E403 Two-factor authentication ... is required to publish packages`;
38
+ 而作用域是 `@sqdr2` 的 token 根本无法发布无 scope 的包名。
39
+ 2. 在 **npmjs.com → 该包 → Settings → Trusted publishing** 添加:
40
+
41
+ | 字段 | 值 |
42
+ | --- | --- |
43
+ | Provider | GitHub Actions |
44
+ | Repository | `SQDR2/dsh-fusion` |
45
+ | Workflow | `publish.yml` |
46
+ | Permission | allow npm publish |
47
+
48
+ **账号自己得先有 2FA。** 包页面上只会写一句 “2FA has been enabled for this package. It is recommended that you
49
+ configure 2FA for your npm account”,而提交表单会跳到 `/settings/<user>/tfa` 要求录入安全密钥或认证器。
50
+ 动手之前先把账号级 2FA 配好,否则表单会静默变成 2FA 注册页,什么都不会创建。
51
+
52
+ 3. 在 **2 天内**发一个版本。新的信任配置只有首次成功发布才算验证通过;未验证的会过期,
53
+ 之后不能编辑,只能删掉重建。
54
+
55
+ 信任配置验证通过后,建议在包上开 **Settings → Publishing access → Require two-factor authentication and disallow tokens**。
56
+ 此后 token 不能再发布,只有 workflow 能发。
57
+
58
+ npm CLI 建不了信任配置:`npm trust github` 需要交互式 2FA 凭据,而 API 会拒绝 bypass 2FA 的 token,
59
+ 返回 `Granular access tokens that bypass two-factor authentication may not perform this action`。
60
+ 这一步在设计上只能走浏览器。
61
+
62
+ ## 发一版
63
+
64
+ 版本号的唯一真源是 `package.json`。tag 必须与它完全一致,否则 workflow 会在第一步退出。
65
+
66
+ ```sh
67
+ # 1. 改 package.json 里的版本号(别处不要改),并在 CHANGELOG.md 顶部加一条。
68
+ # 这一节就是 GitHub Release 的正文。
69
+ # 2. 重建 lib/ —— 它纳入版本库并随包发布,lib/ 陈旧等于发布了
70
+ # 与 tag 源码不一致的代码。
71
+ DSHX_HARNESS=/path/to/deepseek-harness-0.2.0-rc.2 pnpm build
72
+ pnpm typecheck && pnpm test
73
+ # 3. 源码、重建后的 lib/ 和 CHANGELOG.md 一起提交。
74
+ git add -A && git commit -m "chore(release): 0.3.1"
75
+ # 4. 打 tag 并一起推。
76
+ git tag v0.3.1 && git push origin main v0.3.1
77
+ ```
78
+
79
+ workflow 随后依次执行 `pnpm install`、`pnpm build`、`pnpm test`,在 tag 上再执行 `npm publish --access public`。
80
+ 发布出来的版本还带 provenance 证明,可以用 `npm view` 追溯它是从哪个 commit 构建的。
81
+
82
+ 发布说明只有 [`CHANGELOG.md`](CHANGELOG.md) 一处。不要再每个版本加一个 `RELEASE-*.md`:
83
+ 一堆无人引用、名字几乎相同的文件,几个版本之后就没法读了;同一个版本的内容应该按版本标题归在这一个文件里。
84
+ 它只列以 `@sqdr2/dsh-fusion` 名义发过的版本;上游 `dsh-model-fusion` 的历史留在上游仓库。
85
+
86
+ ### 构建被钉死的两处
87
+
88
+ `scripts/build.mjs` 只接受 `engines.dsh` 里列出的 Host 版本(`0.2.0-rc.2` 和 `0.2.1-rc.2`),其余直接报错。
89
+ CI 因此 checkout `dsh-v0.2.0-rc.2`。
90
+
91
+ workflow 里的打包器版本精确钉在 `tsdown@0.22.2`。`lib/` 是提交进仓库的,换个打包器就会提交一份与测试结果不一致的产物;
92
+ 比如 `0.22.14` 会重写几百行格式。
93
+
94
+ ## 检查一次发布
95
+
96
+ ```sh
97
+ npm view @sqdr2/dsh-fusion version dist-tags
98
+ npm view @sqdr2/dsh-fusion@0.3.0 dist.attestations # 有值说明出自 CI
99
+ ```
100
+
101
+ 想验证产物本身而不是元数据:
102
+
103
+ ```sh
104
+ npm install @sqdr2/dsh-fusion@0.3.0
105
+ node -e "import('@sqdr2/dsh-fusion').then(m => console.log(m.name, m.inject.length))"
106
+ grep -o 'id: "[^"]*"' node_modules/@sqdr2/dsh-fusion/lib/client.js | head -1
107
+ ```
108
+
109
+ 宿主 bundle 必须导出 `dsh-fusion`,客户端 bundle 的注册 id 必须是包名 `@sqdr2/dsh-fusion`。
110
+ 两者不同是刻意的:原因见 [INSTALL.zh.md](INSTALL.zh.md),实现见 `src/identity.ts`。
111
+
112
+ ## 发布失败时
113
+
114
+ | 现象 | 原因 |
115
+ | --- | --- |
116
+ | 推了 tag,`0 workflow runs` | fork 上没有启用 Actions |
117
+ | `npm publish` 报 `404` 或 `ENEEDAUTH` | npm 上没有信任配置,或它过期未验证 |
118
+ | `tag vX != package.json Y` | 版本号只改了一处 |
119
+ | `Unsupported Host package version` | `HARNESS_REF` 与 `engines.dsh` 不一致 |
120
+ | 重建后的 bundle 与已提交的 `lib/` 不同 | 打包器没钉在 `0.22.2` |
package/dshx.yml CHANGED
@@ -8,6 +8,9 @@ hotReload:
8
8
  - src/dsh-fusion.ts
9
9
  - src/host/coordinator.ts
10
10
  - src/host/settings.ts
11
+ - src/host/pairs.ts
12
+ - src/host/native-selection.ts
13
+ - src/identity.ts
11
14
  - src/host/status.ts
12
15
  - src/host/native-scopes.ts
13
16
  - src/host/native-worker.ts