@ckanner/dsh-plugin-flywheel 0.28.2 → 0.28.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/PROTOCOL.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Go 在工作目录启动 `flywheel-dsh-worker`,写入**一条** UTF-8 JSON 行并关闭 stdin。关闭 stdin 表示请求已完整发送,不表示取消任务。单条输入及 SDK 帧上限均为 8 MiB。stdout 只输出 JSONL,诊断走 stderr。
8
8
 
9
9
  ```json
10
- {"type":"start","protocol":1,"requestId":"call-123","cwd":"/absolute/worktree","provider":"deepseek-official","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","prompt":"执行冻结工作单元","timeoutMs":1800000,"maxTokens":32768}
10
+ {"type":"start","protocol":1,"requestId":"call-123","cwd":"/absolute/worktree","provider":"deepseek-account","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","prompt":"执行冻结工作单元","timeoutMs":1800000,"maxTokens":32768}
11
11
  ```
12
12
 
13
13
  必填字段:`type`、`protocol`、`requestId`、`cwd`、`provider`、`model`、`permissionMode`、`prompt`。`cwd` 必须是存在的绝对目录,worker 规范化为真实路径;Go 在比较前同样规范化。可选字段:
@@ -20,6 +20,8 @@ Go 在工作目录启动 `flywheel-dsh-worker`,写入**一条** UTF-8 JSON 行
20
20
 
21
21
  `permissionMode` 允许 `read-only`、`workspace-write` 和显式 `danger-full-access`。未知字段一律拒绝。v1 不接受 `sessionId` / resume、附件、任意额外 CLI 参数和额外写目录;不进行隐式冷启动替代恢复。
22
22
 
23
+ `provider` 明确选择认证路径:`deepseek-account` 使用 DSH 账号服务,`deepseek-official` 使用 API Key;不省略 provider 来猜桌面默认,worker 不在账号失败时自行换用 API。默认预设没有后备渠道,Go 仍可按用户显式配置的后备链执行原有策略。标准 SDK 与桌面在相同 DSH home 下默认共用账号授权存储,无需复制凭证。
24
+
23
25
  ## 运行参数
24
26
 
25
27
  | 环境变量 | 默认值 | 用途 |
@@ -37,10 +39,10 @@ worker 用 `dsh --profile <profile> --patch <临时覆盖文件>` 启动 DSH。
37
39
 
38
40
  每帧均含 `protocol:1`、原 `requestId`。顺序是 `ready` → 零或多个 `event` → 恰好一个 `result`;启动失败允许直接 `result`。创建 Session 时的权限事件会缓冲至 ready 之后。
39
41
 
40
- ### ready:真实能力握手已通过
42
+ ### ready:SDK 配置与权限握手已通过
41
43
 
42
44
  ```json
43
- {"protocol":1,"requestId":"call-123","type":"ready","sessionId":"flywheel-uuid","cwd":"/absolute/worktree","provider":"deepseek-official","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","approvalPolicy":"never"}
45
+ {"protocol":1,"requestId":"call-123","type":"ready","sessionId":"flywheel-uuid","cwd":"/absolute/worktree","provider":"deepseek-account","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","approvalPolicy":"never"}
44
46
  ```
45
47
 
46
48
  发出 ready 前必须依次满足:
@@ -53,6 +55,8 @@ worker 用 `dsh --profile <profile> --patch <临时覆盖文件>` 启动 DSH。
53
55
 
54
56
  任一项缺失,不发送 `session/prompt`。`never` 控制交互审批,不单独扩大访问范围;必须同时显式选择 `danger-full-access` 才请求完全权限。既有静态拒绝或外部权限不足仍按真实失败处理。
55
57
 
58
+ `initialize` 返回 serverInfo,并通过 SDK 配置校验。ready 中的 provider/model/reasoningEffort 是配置被接受后的 worker 请求回显;尤其 reasoningEffort 不是模型服务独立返回的实际强度证明,也不证明账号额度或回答质量。
59
+
56
60
  ### event:可审计的原始持久事件
57
61
 
58
62
  ```json
@@ -71,6 +75,10 @@ worker 用 `dsh --profile <profile> --patch <临时覆盖文件>` 启动 DSH。
71
75
 
72
76
  `status` 是 `ok`、`error` 或 `cancelled`。失败提供 `error:{code,message}`;code 仅使用 Go 已有分类。`settled` 只描述 SDK 关闭证据;Go watchdog 对整个拥有的进程组做最终判定。强制终止 SDK 或未获得正常 shutdown 确认时,不能返回成功,也不能声称其原生托管进程已全部清理。
73
77
 
78
+ DeepSeek 账号的 `ACCOUNT_SIGN_IN_REQUIRED`/`ACCOUNT_TOKEN_INVALID` 精确映射到 AUTH,`ACCOUNT_QUOTA` 映射到 QUOTA。`turn/end` 的 aborted 只在其 reason 为 `{kind:"hook",reason:"deepseek-account/signed-out"}` 时同归 AUTH。保留原始诊断与恢复同一账号的提示;未知 ACCOUNT 前缀、其他 hook 和普通 JSON-RPC 传输失败沿用原分类。worker 自身取消继续优先输出 cancelled,不把取消冒充认证失败。
79
+
80
+ Go 在验证协议身份、终态、已知 code 及 error 对应物理退出码 1 后独立保留结构化错误码,编排与 doctor 不从原始消息重新猜测该码。矛盾的退出码、进程超时、spawn/signal、截断、取消和未确认清理优先处理;这些事实不因收到 AUTH/QUOTA 而消失。合法 error 帧的 settled:false 记录 SDK 清理未确认并阻止自动重试;已启动 worker 缺少可信终态回执(包括身份损坏、缺失终态或坏尾帧)同样记录 CleanupError 并停止本次运行,外层进程已退出不代替 SDK shutdown 证明;worker 启动前拒绝且 settled:true 不等于清理未知。未知码或坏协议仍属于传输/协议失败,不能借错误正文冒充可信 code。Node 也要把 SDK 关闭期间的坏帧及 EOF 未换行残帧记为 protocolFailure,并覆盖先前账户错误;外部取消继续保留 cancelled 状态。
81
+
74
82
  使用量来自**每条唯一 `assistant/message` 的已提交 per-call usage**,跨该次 worker 调用累加:
75
83
 
76
84
  - DSH 的 `inputTokens` 是未命中缓存的输入;bridge 的 `input_tokens` 是它加已报告 cache read/write 的总输入。Go 不再重复加缓存。
@@ -175,7 +183,7 @@ config/doctor 的 Go CLI 将 SIGTERM/SIGINT 转为 context 取消,等待 watch
175
183
  | `dispatch` | `not-dispatched` 表示宿主证明本次请求没有启动 Go 控制进程;否则为 `unknown` |
176
184
  | `message` | 给用户诊断的错误文本,本身不证明进程是否启动,也不能作为另一个 envelope 递归解析 |
177
185
 
178
- 只有参数/目录等派发前校验失败、派发前取消,以及进程创建失败且没有成功启动的证据,才能返回 `not-dispatched`。进入控制进程后出现的非零退出、超时、取消、坏 JSON 或任务身份不符都不能取得这项证明,即使错误文本包含相同 JSON。host 对普通错误同样增加外层 envelope,只把原始错误放入 `message`,避免进程返回的文字冒充宿主确认。
186
+ 只有参数/目录等派发前校验失败、派发前取消,以及进程创建失败且没有成功启动的证据,才能返回 `not-dispatched`。省略 cwd 时,host 读取 `workingDirectory.get(agent.session)` 的当前目录;getter 失败、缺会话或缺目录属于派发前失败,不回退 header.cwd。显式 cwd 不读取会话目录。进入控制进程后出现的非零退出、超时、取消、坏 JSON 或任务身份不符都不能取得这项证明,即使错误文本包含相同 JSON。host 对普通错误同样增加外层 envelope,只把原始错误放入 `message`,避免进程返回的文字冒充宿主确认。
179
187
 
180
188
  界面只有在版本、类型、action、dispatch 和 message 都符合契约时才使用宿主证明。未知协议、旧 host 的纯文本错误、DSH 传输失败或丢失响应均保持 `unknown`。启动/停止的 `unknown` 结果必须先成功查询原任务状态,再决定后续变更;不得自动重试。已确认未派发的拒绝会显示原因,用户可以修正输入或环境后重新提交。一次未派发的状态查询也不能解除更早启动/停止留下的未确认结果。
181
189
 
package/README.en.md CHANGED
@@ -7,7 +7,7 @@ provides its DSH SDK and Codex Pro workers. The engine owns task state, budgets,
7
7
  verification, Git delivery, and process lifecycles. The desktop is a control and
8
8
  observation interface; it does not need a parent model session to keep a task alive.
9
9
 
10
- This guide describes the **Flywheel 0.28.2** interfaces. Source availability does
10
+ This guide describes the **Flywheel 0.28.3** interfaces. Source availability does
11
11
  not mean that npm `latest` already contains them. Check the registry version and
12
12
  the [release guide](https://github.com/ckanner/flywheel/blob/main/docs/en/releasing.md) before installing. The desktop
13
13
  compatibility baseline is **DSH Mac 0.2.1-rc.5**, not a claim that every DSH build
@@ -19,6 +19,8 @@ version; merging source does not update an installed plugin.
19
19
 
20
20
  Subscription preset migration and `permissions.mode` are available from **0.28.2**. Upgrades preserve existing configuration; only an explicit migration backs it up and updates model bindings and permissions.
21
21
 
22
+ **Changes in 0.28.3:** account defaults, account-error handling, and the current session directory. Version 0.28.2 still uses API-key defaults and the original directory. Reinstalling that version does not include these fixes. Upgrades preserve configuration; adopting the account preset requires explicit migration.
23
+
22
24
  ## Components
23
25
 
24
26
  | Component | Responsibility |
@@ -89,16 +91,22 @@ workers, not the complete Flywheel application.
89
91
 
90
92
  ## Prepare the SDK profile and selected models
91
93
 
92
- The native DSH worker uses `sdk` by default. A provider installed or authorized
93
- in the desktop profile is not automatically present in the SDK profile.
94
- `FLYWHEEL_DSH_PROFILE` can select another SDK profile, but cannot select
95
- `desktop`. Keep `DSH_HOME` consistent with the configuration you intend to use.
96
-
97
- The native DeepSeek provider is `deepseek-official`, with `deepseek-flash` in
98
- the default configuration. At the checked rc.5 baseline its effort values are
99
- `off`, `low`, `high`, and `max`; there is no `medium`. Other providers have their
100
- own accepted values. For native GPT routes, install and authorize the selected
101
- ChatGPT adapter in the SDK profile. One checked compatibility example is:
94
+ The native worker uses `sdk` by default. Its standard base already includes the
95
+ DeepSeek account service and both account/API-key adapters. With the same
96
+ `DSH_HOME`, standard desktop and SDK profiles share the account credential store;
97
+ the account route needs no additional API key or token copy. Check custom homes,
98
+ credential stores, and plugin sets separately. `FLYWHEEL_DSH_PROFILE` can select
99
+ another SDK profile, but cannot select the reserved `desktop` profile.
100
+
101
+ Current source explicitly selects `deepseek-account` and `deepseek-flash`.
102
+ `deepseek-official` is the separate API-key provider and remains an explicit
103
+ alternative. Desktop default-model selection does not control an independent
104
+ SDK session. The default preset has no cross-provider fallback for authentication or quota failures; explicit user-defined fallback chains keep their existing policy.
105
+ At the checked rc.5 baseline Flash accepts `off`, `low`, `high`, and `max`,
106
+ defaulting to high; it accepts neither medium nor xhigh. Other providers have
107
+ their own values; see [Reasoning effort](https://github.com/ckanner/flywheel/blob/main/docs/en/model-policy.md).
108
+ For native GPT routes, install and authorize the selected ChatGPT adapter in
109
+ the SDK profile. One checked compatibility example is:
102
110
 
103
111
  ```sh
104
112
  dsh plugin --profile sdk add dsh-plugin-chatgpt@0.1.17
@@ -162,16 +170,28 @@ actual inference result. Keep `warn`, `unknown`, and `skipped` distinct from
162
170
 
163
171
  ## Use the desktop controls
164
172
 
165
- Open a DSH session and choose **Flywheel** in the native view bar alongside
166
- Chat and Trajectory. If the entry is not directly visible, check that bar's
167
- More menu. Ask the chat assistant to prepare and review the task using
168
- `flywheel-new`; it reports the actual paths. **Task files directory** contains
169
- the task description and acceptance files, for example
170
- `/Users/me/project/docs/fix-login`. **Project directory (optional)** can stay
171
- blank to use the current DSH session directory; specify the matching project
172
- worktree when the task lives elsewhere. Configuration and diagnostic actions
173
- can also leave the task directory blank. These are project paths, not plugin
174
- installation paths.
173
+ The normal entry is `/flywheel-new` in DSH's composer. The assistant prepares
174
+ the description and acceptance files and reports their paths. Review them,
175
+ then use `/flywheel-run`; no panel fields need filling first. Preparation uses
176
+ the current conversation model. After startup, the engine uses Flywheel's
177
+ per-stage model configuration, independently of the chat selection.
178
+
179
+ To inspect or control an existing task, choose **Flywheel** beside Chat and
180
+ Trajectory, or in that view bar's More menu when necessary.
181
+
182
+ | Field | Purpose | When to fill it |
183
+ |---|---|---|
184
+ | Task files directory | Prepared Spec, packets, acceptance description, and task records in `docs/<slug>/`, e.g. `/Users/me/project/docs/fix-login` | Required for panel start/refresh/stop; optional for configuration and diagnostics. Use the path reported by the preparation Skill. |
185
+ | Project directory (optional) | Source project or task integration worktree, e.g. `/Users/me/project` | Current source defaults to the session's effective working directory; override for another project/worktree. |
186
+
187
+ From 0.28.3, Flywheel reads the host's `workingDirectory.get(session)`, following
188
+ directory changes. An explicit project path wins. Getter failures stop before
189
+ dispatch without falling back to the original `header.cwd`. Version 0.28.2
190
+ still uses the original directory, so explicitly fill the project path after
191
+ switching worktrees. The panel does not automatically bind tasks created by a
192
+ Skill: paste its reported task directory and refresh. These are project paths,
193
+ not plugin installation paths. The direct `/flywheel {…}` command is distinct
194
+ from the `/flywheel-*` Skill entries.
175
195
 
176
196
  The panel, `/flywheel`, and `flywheel_control` share these Go operations:
177
197
 
@@ -333,11 +353,17 @@ escaped process has been cleaned up.
333
353
 
334
354
  Complete receipts preserve configuration, usage, and termination evidence.
335
355
  Accepted effort is not proof of the server's effective reasoning effort.
356
+ DSH's `ready.reasoningEffort` echoes the request after successful initialization.
357
+ Known account sign-in/token errors and the exact account-signed-out hook map to
358
+ AUTH; account quota errors remain QUOTA. Original diagnostics and account
359
+ recovery guidance are preserved. The default preset has no API-key fallback.
336
360
  Unreported usage and dollar cost remain unknown; subscriptions are not recorded
337
361
  as free calls. The current DSH bridge does not support session resume or image
338
362
  and attachment inputs. Its protocol and implementation details are documented
339
363
  in [PROTOCOL.md](PROTOCOL.md) (Chinese).
340
364
 
365
+ From 0.28.3, a started DSH worker without a trustworthy terminal receipt, including a missing result or malformed stream, records unconfirmed SDK cleanup and ends the run without automatic retry or fallback. Reaping the outer worker does not replace an SDK shutdown receipt. Before resuming, confirm that the SDK-managed processes from that call have stopped.
366
+
341
367
  ## Upgrade and maintain the desktop entry
342
368
 
343
369
  The installer records an absolute Node/CLI launcher under
package/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  订阅预设迁移命令和 `permissions.mode` 从 **0.28.2** 开始提供。升级保留已有配置;只有用户明确应用迁移时才备份并更新模型与权限。
12
12
 
13
+ **0.28.3 的变化:**新增 `deepseek-account` 默认、账号错误处理与会话当前工作目录修复。0.28.2 仍使用 API 默认和会话最初目录,重装同版不会获得修复;升级保留已有配置,采用账号预设需要显式迁移。
14
+
13
15
  ## 提供什么
14
16
 
15
17
  | 部分 | 行为 |
@@ -62,23 +64,23 @@ Agent 可以加 `--json` 获取单个结构化结果。该命令全局安装本
62
64
 
63
65
  npm CLI 启动器自动解析同版本依赖中的 worker 绝对路径,并在用户未设置时注入 `FLYWHEEL_DSH_WORKER`。直接运行 Go 二进制时默认从 PATH 查找 `flywheel-dsh-worker`;也可显式设置 `FLYWHEEL_DSH_WORKER=/absolute/path/to/dist/worker.js`。自动 launcher 已记录安装时的桌面 `dsh` 入口,用户显式 `FLYWHEEL_DSH_BIN` 优先。`FLYWHEEL_DSH_PROFILE` 默认 `sdk`;可用另一个明确基于 SDK 的独立 profile,但不能是 `desktop`。
64
66
 
65
- 桌面有 adapter 不表示 SDK profile 也加载了它。SDK 的 DeepSeek 官方路由可自动挂载,其余 provider 必须在 **SDK profile** 中预先安装并完成该 provider 的授权配置。例如需原生 ChatGPT 路由时,先按该插件的文档检查并安装到对应 profile:
67
+ 标准 SDK profile 叠加 base,已经包含 DeepSeek 账号服务、账号 adapter 和 API-key adapter。标准桌面与 SDK 在同一 `DSH_HOME` 下默认共用账号授权存储,账号路径不要求复制 token 或额外 API key。自定义 home、凭证存储和 SDK 插件集需核对配置。第三方 provider 则要在 **SDK profile** 中加载并按各自流程授权,例如原生 ChatGPT 路由:
66
68
 
67
69
  ```sh
68
70
  dsh plugin --profile sdk add dsh-plugin-chatgpt@0.1.17
69
71
  ```
70
72
 
71
- 这条命令是配置示例,不会迁移桌面凭证,也不保证一个第三方插件版本自动适配所有宿主。保持用户选择的 `DSH_HOME` 与授权来源一致,按 provider 的正常登录流程配置;不要复制或打印 token。`initialize` 将严格检查请求的 provider、model、effort,未注册的 provider 会失败。
73
+ 这条命令是配置示例,不会复制凭证,也不保证一个第三方插件版本自动适配所有宿主。保持用户选择的 `DSH_HOME` 与授权来源一致,按 provider 的正常登录流程配置;不要复制或打印 token。`initialize` 检查请求的 provider、model、effort,未注册的 provider 会失败。DSH 桌面的账号默认模型选择不自动传给 SDK;Flywheel 必须明确请求所选 provider 与模型。
72
74
 
73
- 实际 Mac rc.5 的 DeepSeek 原生路由是:
75
+ 当前源码默认选择的 Mac rc.5 DeepSeek 账号路由是:
74
76
 
75
77
  ```text
76
- provider = deepseek-official
78
+ provider = deepseek-account
77
79
  model = deepseek-flash
78
80
  reasoningEffort = off | low | high | max
79
81
  ```
80
82
 
81
- `high` 是默认平衡档,**没有 `medium`**。其他 provider 使用自身的合法 effort。SDK README 的历史示例可能仍写 `deepseek-v4-flash`;以所用发行包与 adapter 的实际 catalog 为准。本桥接不替换模型 slug、不跨 provider 降级。
83
+ 本项目核对的 DSH rc.5 adapter 只接受 `off/low/high/max`,默认 high,**不接受 `medium` 或 `xhigh`**;这不同于 DeepSeek 通用 API 的兼容别名。`deepseek-official` 是另一个 API-key provider,只在用户明确选择 API 接入时配置,不复用账号 token。默认预设没有 API 后备,因此未登录、登录失效或额度不足不会自动转用 API;用户显式配置的后备链按原规则执行。其他 provider 使用各自合法 effort,GPT 支持 xhigh/max,Kimi K3.1 Pro 原生支持 low/high/max;理由与评估边界见[推理等级说明](https://github.com/ckanner/flywheel/blob/main/docs/model-policy.md)。SDK README 的历史示例可能仍写 `deepseek-v4-flash`;以所用发行包与 adapter 的实际 catalog 为准。本桥接不替换模型 slug、不跨 provider 降级。
82
84
 
83
85
  ## Codex Pro 插件执行通道
84
86
 
@@ -113,7 +115,7 @@ flywheel doctor --cwd /repo --task-dir /repo/docs/my-task --probe --json
113
115
  只调试单个 worker 时,仍可在已有且可访问的目标目录生成一条 probe 请求:
114
116
 
115
117
  ```sh
116
- node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1,requestId:"local-doctor",cwd:process.cwd(),provider:"deepseek-official",model:"deepseek-flash",reasoningEffort:"high",permissionMode:"read-only",prompt:"This text is not sent in doctor mode",timeoutMs:20000}))' \
118
+ node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1,requestId:"local-doctor",cwd:process.cwd(),provider:"deepseek-account",model:"deepseek-flash",reasoningEffort:"high",permissionMode:"read-only",prompt:"This text is not sent in doctor mode",timeoutMs:20000}))' \
117
119
  | flywheel-dsh-worker --doctor
118
120
  ```
119
121
 
@@ -123,7 +125,16 @@ node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1
123
125
 
124
126
  ## 桌面使用
125
127
 
126
- 打开一个 DSH 会话,在顶部“对话/轨迹”所在的视图栏选择 **Flywheel**。入口由 DSH 的原生视图栏管理;若未直接显示,检查该栏的更多视图入口。先在“对话”中让助手按 `flywheel-new` 准备任务说明与验收清单,审阅后由助手给出实际路径。“任务资料目录”就是这些文件所在的目录,例如 `/Users/me/project/docs/fix-login`,不是插件安装目录。“项目目录(可选)”默认取当前 DSH 会话目录,可留空;任务在另一份 worktree 时填对应的项目路径,例如 `/Users/me/project`。只看配置或做检查时,任务资料目录也可留空。按钮只执行以下 Go JSON 控制接口:
128
+ 正常入口是在 DSH 对话输入框调用 `/flywheel-new`,让助手准备任务说明与验收清单并给出实际路径;审阅后通过 `/flywheel-run` 开始工作。无需先填写面板。初始准备使用当前对话选择的模型,引擎启动后才使用 Flywheel 配置中的各阶段模型;两者不自动同步。
129
+
130
+ 要查看或直接控制已有任务,再在顶部“对话/轨迹”所在的视图栏选择 **Flywheel**;未直接显示时检查该栏的更多视图入口。
131
+
132
+ | 字段 | 用途 | 是否需要填写 |
133
+ |---|---|---|
134
+ | 任务资料目录 | Spec、packet、验收说明和任务记录所在的 `docs/<slug>/`,例如 `/Users/me/project/docs/fix-login` | 面板启动/刷新/停止需指定;配置与诊断可以留空。由准备 Skill 给出,无需自行编写这些文件才能使用入口 |
135
+ | 项目目录(可选) | 对应源码项目/任务集成 worktree,例如 `/Users/me/project` | 当前源码默认读取会话实际工作目录;只在选择另一项目或 worktree 时覆盖 |
136
+
137
+ 从 0.28.3 开始调用宿主 `workingDirectory.get(session)`,跟随会话中的目录切换;显式项目目录优先,getter 失败在派发前报错,不回退到会话最初的 `header.cwd`。0.28.2 仍取最初目录,切换 worktree 后请显式填写项目路径。面板不自动绑定 Skill 创建的任务;查看它时粘贴 Skill 返回的任务目录,再刷新。两个字段都是项目路径,不是插件安装目录。按钮只执行以下 Go JSON 控制接口:
127
138
 
128
139
  ```sh
129
140
  flywheel control status --task-dir /repo/docs/my-task --cwd /repo
@@ -139,7 +150,7 @@ flywheel control doctor --task-dir /repo/docs/my-task --cwd /repo --probe
139
150
  /flywheel {"action":"status","taskDir":"/repo/docs/my-task","cwd":"/repo"}
140
151
  ```
141
152
 
142
- 模型工具 `flywheel_control` 接受 `action`、`taskDir`、`cwd`,以及仅用于 doctor 的可选 `probe` 布尔值。config/doctor 可省 taskDir,cwd 可沿用会话目录;start/status/stop 必须指定任务。输入统一校验,使用 argv 调用,不拼接 shell 命令。打开视图不会启动任务或探测;视图只在点击时读取状态,不自动轮询,也不自动发现其他入口启动的任务。查看已有任务时,选择同一任务目录再刷新。
153
+ 模型工具 `flywheel_control` 接受 `action`、`taskDir`、`cwd`,以及仅用于 doctor 的可选 `probe` 布尔值。config/doctor 可省 taskDir,cwd 可沿用会话当前工作目录;start/status/stop 必须指定任务。`/flywheel {…}` 是直接人工控制命令,与 `/flywheel-new` 等 Skill 入口不同。输入统一校验,使用 argv 调用,不拼接 shell 命令。打开视图不会启动任务或探测;视图只在点击时读取状态,不自动轮询,也不自动发现其他入口启动的任务。
143
154
 
144
155
  “查看模型配置”展示实际配置来源、展开后的阶段绑定及 provider/channel/权限。“检查安装与配置”仅做本地预检,不启动 SDK;“检查 DSH 接入”额外临时启动 SDK、核对配置/目录/权限后退出。首次安装、升级或排障时按需使用,平时不必每次点击;两项都不向模型发送消息,也不验证账号、额度和实际回答能力。doctor 的合法退出码 1 会显示逐项诊断,超时、坏协议或不匹配的任务身份仍视为控制错误。`ok:true` 不代表账号、额度和真实推理通过。
145
156
 
@@ -167,6 +178,8 @@ flywheel control doctor --task-dir /repo/docs/my-task --cwd /repo --probe
167
178
 
168
179
  每个调用使用新的 SDK Session;provider/model/effort 在该进程中固定。v1 不支持 resume、图像/附件输入或 SDK 原生 output schema。文本与报告文件由 Go 的冻结合同和 JSON Schema 验收,不能把 SDK idle 当成验收完成。
169
180
 
181
+ SDK 接受模型与 effort 配置不代表远端服务证明了实际推理强度。`ready.reasoningEffort` 是初始化成功后的请求回显;只有实际任务评估才能判断更高 effort 是否改善交付。账号未登录和 token 失效精确归 AUTH,退出账号导致的已知取消事件同样归 AUTH;账号额度不足归 QUOTA。保留原始原因并提示恢复同一账号;默认预设不提供 API Key 后备。
182
+
170
183
  worker 支持 `read-only`、`workspace-write` 和显式 `danger-full-access`。新订阅预设通过 `permissions.mode:full-access` 为全部阶段选择完全权限,approval 固定为 `never`;Kimi print 使用原生 auto,不逐步请求用户点击允许。已有文件缺省仍保留 role-based,需通过[配置迁移](../../docs/configuration.md#将旧配置切换到已有的三种资源)显式采用新方案。受限模式由 Mac 上 DSH 的 Seatbelt bash runner 和 native fs fence 执行,workspace-write 范围是**实际工作目录加 DSH 规定的系统临时区**。full-access 如实记录无 OS 沙箱限制;宿主静态拒绝、账号与操作系统权限不被绕过。
171
184
 
172
185
  执行报告仍先写到 worktree 内的任务报告路径,由 Go 校验后归档到主任务目录。权限模式来自明确配置,不能因为报告写入失败而暗中扩大它;任务边界、冻结验收和候选身份检查保持有效。
@@ -179,7 +192,7 @@ worker 支持 `read-only`、`workspace-write` 和显式 `danger-full-access`。
179
192
 
180
193
  ## 生命周期与可验证范围
181
194
 
182
- 官方 rc.5 SDK 没有 per-turn cancel,所以使用每调用一个 SDK 进程。worker 请求 `shutdown` 并等待退出;SDK 自己排空其托管命令。Go 继续负责外层 deadline、取消及它拥有的进程组清扫。正常 SDK 关闭未被证明时结果失败,不能进入成功验收。Go 默认 5 秒 kill grace 应保留,以给 worker 清理窗口;不要配置短于桥接清理预算的值。
195
+ 官方 rc.5 SDK 没有 per-turn cancel,所以使用每调用一个 SDK 进程。worker 请求 `shutdown` 并等待退出;SDK 自己排空其托管命令。Go 继续负责外层 deadline、取消及它拥有的进程组清扫。正常 SDK 关闭未被证明时结果失败,不能进入成功验收。worker 已启动但终态回执缺失或协议损坏时会记录清理未确认并停止本次运行,不自动重试或降级;恢复前先确认该次调用的 SDK 托管进程已停止。Go 默认 5 秒 kill grace 应保留,以给 worker 清理窗口;不要配置短于桥接清理预算的值。
183
196
 
184
197
  测试覆盖离线 worker 全生命周期与事件折叠;桌面 command/tool 入口一致;构建后的浏览器模块在 React 与 jsdom 中运行插件自有控件、store handle 和 observable。组件夹具适配槽位桥接和 Remote,另用已发布的 rc.5 renderer、slots 与 Cordis 检验真实注册、会话绑定、声明收起/恢复及卸载;这两类测试都不验证真实宿主几何布局。还覆盖 Go 与 Node 的配置、诊断和失败契约,以及安装记录校验、用户配置优先和损坏入口拒绝。根目录的 npm 安装测试使用实际六包 tarball、隔离目录和 DSH 协议桩,验证完整安装流程。前轮还核对官方 npm rc.5 的真实 Host commands/tools 注册、SDK 初始化/关闭,以及当前 Linux 无可用沙箱时在 prompt 前 fail closed。前轮运行结果见[验证记录](../../docs/validation/2026-11-01-results.md)。
185
198
 
package/dist/bridge.js CHANGED
@@ -75,7 +75,10 @@ export class Runtime {
75
75
  });
76
76
  this.child.on('close', (code, signal) => {
77
77
  this.closed = true;
78
- if (this.buffer.length) this.fail(new BridgeError('TRANSPORT', 'DSH stdout ended in an incomplete frame'));
78
+ if (this.buffer.length) {
79
+ this.protocolFailure ??= new BridgeError('TRANSPORT', 'DSH stdout ended in an incomplete frame');
80
+ this.fail(this.protocolFailure);
81
+ }
79
82
  this.fail(new BridgeError('TRANSPORT', `DSH transport closed (exit=${code}, signal=${signal ?? 'none'})`));
80
83
  this.resolveExit(this.exit ?? { code, signal });
81
84
  });
@@ -265,7 +268,14 @@ export async function runWorker(input, { emit, env = process.env, signal, doctor
265
268
  settled = false;
266
269
  failure ??= { code: 'TRANSPORT', message: 'DSH runtime did not prove graceful shutdown of its managed processes' };
267
270
  }
268
- if (runtime.protocolFailure) failure ??= normalizedError(runtime.protocolFailure);
271
+ if (runtime.protocolFailure) {
272
+ // A provider result cannot attest a damaged SDK stream. Keep its
273
+ // diagnosis as context, while protocol and cleanup facts stay independent.
274
+ const protocolFailure = normalizedError(runtime.protocolFailure);
275
+ failure = failure && failure.message !== protocolFailure.message
276
+ ? { ...protocolFailure, message: `${protocolFailure.message}; preceding failure: ${failure.message}` }
277
+ : protocolFailure;
278
+ }
269
279
  } catch (error) {
270
280
  settled = false;
271
281
  failure = normalizedError(error);
package/dist/client.js CHANGED
@@ -103,7 +103,7 @@ const zh = {
103
103
  taskDir: '任务资料目录',
104
104
  taskHint: '填写助手准备任务时给出的完整路径,里面保存任务说明和验收清单;只查看配置或做检查时可留空',
105
105
  cwd: '项目目录(可选)',
106
- cwdHint: '默认使用当前 DSH 会话的项目目录,可留空。任务在其他项目或工作区时,填写对应的完整路径',
106
+ cwdHint: '留空时使用当前 DSH 会话的工作目录。若它不是本任务的项目根目录,填写助手给出的项目完整路径',
107
107
  pathInvalid: '请填写以 / 开头的完整路径',
108
108
  pathControl: '路径不能包含换行或空字符',
109
109
  taskRequired: '填写任务资料目录后,可刷新状态、启动或停止任务',
@@ -129,7 +129,7 @@ const zh = {
129
129
  feedback: '操作反馈',
130
130
  statusHeading: '任务状态',
131
131
  emptyHeading: '还没有任务状态',
132
- emptyBody: '先在“对话”里让助手按 flywheel-new 准备任务,审阅任务说明和验收标准,再填写助手给出的任务资料目录。已有任务可直接使用原目录',
132
+ emptyBody: '在“对话”输入 /flywheel-new 和需求即可准备任务,无需先填写本页。审阅后可用 /flywheel-run 和 /flywheel-status 加任务路径操作;如需在本页查看,再填写助手给出的任务资料目录。已有任务使用原目录',
133
133
  emptySelected: '点击“刷新状态”查看这个任务;打开本页不会自动启动任务',
134
134
  lastChecked: '上次确认:{time}',
135
135
  stale: '本次状态未确认,下方保留上次结果',
@@ -229,7 +229,7 @@ const en = {
229
229
  taskDir: 'Task files directory',
230
230
  taskHint: 'Use the full path supplied by the assistant when it prepared the task instructions and acceptance checks; leave blank to inspect configuration or run diagnostics',
231
231
  cwd: 'Project directory (optional)',
232
- cwdHint: 'Leave blank to use the project directory of this DSH session. If the task belongs to another project or worktree, enter that full path.',
232
+ cwdHint: 'Leave blank to use this DSH session’s current working directory. If that is not this task’s project root, enter the full project path supplied by the assistant.',
233
233
  pathInvalid: 'Enter a full path beginning with /',
234
234
  pathControl: 'Paths cannot contain line breaks or null characters',
235
235
  taskRequired: 'Enter the task files directory to refresh status, start or stop the task',
@@ -255,7 +255,7 @@ const en = {
255
255
  feedback: 'Operation feedback',
256
256
  statusHeading: 'Task status',
257
257
  emptyHeading: 'No task status yet',
258
- emptyBody: 'In Chat, ask the assistant to prepare a task with flywheel-new. Review its instructions and acceptance checks, then enter the task files directory it provides. For an existing task, use its original directory.',
258
+ emptyBody: 'In Chat, enter /flywheel-new and your requirement without filling in this view. After review, use /flywheel-run and /flywheel-status with the task path. To inspect it here, enter the task files directory the assistant provides. For an existing task, use its original directory.',
259
259
  emptySelected: 'Refresh status to inspect this task. Opening this view does not start it.',
260
260
  lastChecked: 'Last confirmed: {time}',
261
261
  stale: 'Current status is unconfirmed; the previous result is retained below',
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ControlNotDispatchedError, runControl } from './control.js';
2
2
  import { resolveFlywheelBinary } from './installation.js';
3
3
 
4
- export const inject = ['commands', 'tools'];
4
+ export const inject = ['commands', 'tools', 'workingDirectory'];
5
5
 
6
6
  export function apply(ctx, config = {}) {
7
7
  // Loading the desktop package accidentally into the SDK profile must not
@@ -16,9 +16,20 @@ export function apply(ctx, config = {}) {
16
16
  if (signal?.aborted) cancel();
17
17
  active.add(controller);
18
18
  try {
19
+ let defaultCwd;
20
+ try {
21
+ // The immutable session header records its original project. Read the
22
+ // public current-directory projection without ensure() recovery effects.
23
+ if (input?.cwd == null && agent?.session) {
24
+ defaultCwd = ctx.workingDirectory.get(agent.session);
25
+ }
26
+ } catch (error) {
27
+ // No control process has been launched if resolving the default fails.
28
+ throw new ControlNotDispatchedError(error);
29
+ }
19
30
  return await runControl(input, {
20
31
  binary,
21
- defaultCwd: agent?.session?.header?.cwd,
32
+ defaultCwd,
22
33
  timeoutMs: config.controlTimeoutMs ?? 60000,
23
34
  signal: controller.signal,
24
35
  });
@@ -70,7 +81,7 @@ export function apply(ctx, config = {}) {
70
81
  properties: {
71
82
  action: { type: 'string', enum: ['start', 'status', 'stop', 'config', 'doctor'] },
72
83
  taskDir: { type: 'string', description: 'Absolute existing task ledger directory; required for start/status/stop' },
73
- cwd: { type: 'string', description: 'Absolute target repository directory; defaults to this DSH session' },
84
+ cwd: { type: 'string', description: 'Absolute target repository directory; defaults to the current working directory of this DSH session' },
74
85
  probe: { type: 'boolean', description: 'Doctor only: explicitly enable SDK checks without inference; defaults to false' },
75
86
  },
76
87
  required: ['action'],
package/dist/protocol.js CHANGED
@@ -72,12 +72,25 @@ export function normalizedError(error) {
72
72
 
73
73
  export function modelErrorCode(code) {
74
74
  if (ERROR_CODES.has(code)) return code;
75
- if (code === 'MISSING_CREDENTIAL' || code === 'INVALID_CREDENTIAL') return 'AUTH';
75
+ if (code === 'MISSING_CREDENTIAL' || code === 'INVALID_CREDENTIAL' ||
76
+ code === 'ACCOUNT_SIGN_IN_REQUIRED' || code === 'ACCOUNT_TOKEN_INVALID') return 'AUTH';
76
77
  if (code === 'ACCOUNT_QUOTA') return 'QUOTA';
77
78
  if (code === 'NO_ADAPTER' || code === 'UNSUPPORTED_REASONING_EFFORT') return 'INVALID_REQUEST';
78
79
  return 'SERVER';
79
80
  }
80
81
 
82
+ const ACCOUNT_RECOVERY = new Map([
83
+ ['ACCOUNT_SIGN_IN_REQUIRED', 'Sign in to DeepSeek in DSH, then ensure the SDK runtime can access that account and the configured endpoint permits account authentication before retrying.'],
84
+ ['ACCOUNT_TOKEN_INVALID', 'The DeepSeek account login was rejected. Sign in again in DSH and ensure the SDK runtime can access the renewed login before retrying.'],
85
+ ['ACCOUNT_QUOTA', 'Check the DeepSeek account balance and usage; retry when account quota is available.'],
86
+ ]);
87
+
88
+ function modelErrorMessage(detail, fallback) {
89
+ const message = detail?.message ?? fallback;
90
+ const recovery = ACCOUNT_RECOVERY.get(detail?.code);
91
+ return recovery === undefined ? message : `${detail.code}: ${message}\n${recovery}`;
92
+ }
93
+
81
94
  /** Fold committed per-call records; streamed usage snapshots are never summed. */
82
95
  export class SessionFold {
83
96
  constructor(sessionId) {
@@ -164,9 +177,16 @@ export class SessionFold {
164
177
  if (!this.receipts.has(messageId)) throw new BridgeError('TRANSPORT', 'missing durable prompt receipt');
165
178
  if (!this.terminal) throw new BridgeError('TRANSPORT', 'idle without committed turn/end');
166
179
  if (this.terminal.kind !== 'completed') {
180
+ // DSH's account-tasks hook cancels the active account turn after sign-out,
181
+ // including when rejecting an expired token. Do not infer this from text
182
+ // or treat other cancellation hooks as authentication failures.
183
+ if (this.terminal.kind === 'aborted' && this.terminal.reason?.kind === 'hook' &&
184
+ this.terminal.reason.reason === 'deepseek-account/signed-out') {
185
+ throw new BridgeError('AUTH', 'DeepSeek account signed out (deepseek-account/signed-out). Sign in again in DSH and ensure the SDK runtime can access that account before retrying.');
186
+ }
167
187
  const detail = this.terminal.error;
168
188
  throw new BridgeError(modelErrorCode(detail?.code),
169
- detail?.message ?? `DSH turn ended as ${this.terminal.kind}`);
189
+ modelErrorMessage(detail, `DSH turn ended as ${this.terminal.kind}`));
170
190
  }
171
191
  if (!this.assistantCount || this.text.trim() === '') {
172
192
  throw new BridgeError('EMPTY_RESPONSE', 'DSH completed without assistant text');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckanner/dsh-plugin-flywheel",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "description": "Flywheel engine controls with owned DSH SDK and Codex Pro machine workers",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",