@ckanner/flywheel 0.32.1 → 0.33.0

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +5 -0
  4. package/THIRD_PARTY_NOTICES.md +42 -0
  5. package/bin/runtime.js +25 -31
  6. package/cordis.patch.yml +5 -0
  7. package/dist/bridge.mjs +421 -0
  8. package/dist/claude-code-pro-bridge.mjs +82 -0
  9. package/dist/claude-code-pro-worker.mjs +5 -0
  10. package/dist/client.js +4078 -0
  11. package/dist/codex-pro-bridge.mjs +75 -0
  12. package/dist/codex-pro-worker.mjs +5 -0
  13. package/dist/console-read.mjs +51 -0
  14. package/dist/control.mjs +218 -0
  15. package/dist/index.mjs +185 -0
  16. package/dist/installation.mjs +56 -0
  17. package/dist/machine-worker.mjs +241 -0
  18. package/dist/policy.mjs +91 -0
  19. package/dist/prewarm.mjs +136 -0
  20. package/dist/protocol.mjs +260 -0
  21. package/dist/task-association.mjs +127 -0
  22. package/dist/worker.mjs +52 -0
  23. package/dist/workspaces.mjs +42 -0
  24. package/docs/README.md +1 -1
  25. package/docs/README.zh-CN.md +1 -1
  26. package/docs/design.md +7 -4
  27. package/docs/en/configuration.md +1 -1
  28. package/docs/en/contributing.md +5 -6
  29. package/docs/en/flow-console.md +1 -1
  30. package/docs/en/getting-started.md +9 -2
  31. package/docs/en/releasing.md +132 -174
  32. package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
  33. package/docs/validation/2026-11-14-single-package.md +60 -0
  34. package/docs/validation/README.md +2 -0
  35. package/docs/zh-CN/configuration.md +1 -1
  36. package/docs/zh-CN/contributing.md +4 -5
  37. package/docs/zh-CN/flow-console.md +1 -1
  38. package/docs/zh-CN/getting-started.md +7 -3
  39. package/docs/zh-CN/releasing.md +97 -81
  40. package/native/darwin-amd64/flywheel +0 -0
  41. package/native/darwin-arm64/flywheel +0 -0
  42. package/native/linux-amd64/flywheel +0 -0
  43. package/native/linux-arm64/flywheel +0 -0
  44. package/native/manifest.json +22 -0
  45. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
  46. package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
  47. package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
  48. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
  49. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
  50. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
  51. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
  52. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
  53. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
  54. package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
  55. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
  56. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
  57. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
  58. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
  59. package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
  60. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
  61. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
  62. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
  63. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
  64. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
  65. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
  66. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
  67. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
  68. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
  69. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
  70. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
  71. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
  72. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
  73. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
  74. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
  75. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
  76. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
  77. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
  78. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
  79. package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
  80. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
  81. package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
  82. package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
  83. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
  84. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
  87. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
  88. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
  89. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
  90. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
  91. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
  92. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
  93. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
  94. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
  95. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
  96. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
  97. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
  98. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
  99. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
  100. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
  101. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
  102. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
  103. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
  104. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
  105. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
  106. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
  107. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
  108. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
  109. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
  110. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
  111. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
  112. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
  119. package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
  120. package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
  121. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
  122. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
  123. package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
  124. package/npm/install.js +59 -6
  125. package/package.json +74 -12
  126. package/plugins/dsh/LICENSE +21 -0
  127. package/plugins/dsh/PROTOCOL.md +607 -0
  128. package/plugins/dsh/README.en.md +4 -0
  129. package/plugins/dsh/README.md +342 -0
  130. package/plugins/dsh/README.zh-CN.md +279 -0
  131. package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
  132. package/plugins/dsh/vendor/README.md +17 -0
  133. package/plugins/dsh/vendor/codex-pro.json +16 -0
@@ -0,0 +1,260 @@
1
+ import { constants } from 'node:fs';
2
+ import { open, realpath, stat } from 'node:fs/promises';
3
+ import { dirname, isAbsolute } from 'node:path';
4
+
5
+ export const PROTOCOL = 1;
6
+ export const SESSION_PROTOCOL = 'dsh-sdk/session-resume/worker-v2';
7
+ export const MAX_FRAME_BYTES = 8 * 1024 * 1024;
8
+ export const ERROR_CODES = new Set([
9
+ 'AUTH', 'QUOTA', 'RATE_LIMIT', 'SERVER', 'TRANSPORT', 'TIMEOUT',
10
+ 'EMPTY_RESPONSE', 'INVALID_REQUEST', 'CONTEXT_WINDOW_EXCEEDED',
11
+ ]);
12
+
13
+ export class BridgeError extends Error {
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ }
18
+ }
19
+
20
+ export function record(value) {
21
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+
24
+ // The engine validates contract/report contents. The host validates the typed
25
+ // envelope before dispatch so an invalid command cannot become an unknown run.
26
+ export function validInvestigationCommand(value) {
27
+ if (!record(value) || !['start', 'run', 'status', 'report', 'cancel'].includes(value.action) ||
28
+ Object.keys(value).some(key => !['action', 'revision', 'contract', 'report'].includes(key))) return false;
29
+ if ('revision' in value && (!Number.isSafeInteger(value.revision) || value.revision < 0)) return false;
30
+ if (!['start', 'status'].includes(value.action) && !(value.revision > 0)) return false;
31
+ if ((value.action === 'start') !== ('contract' in value) ||
32
+ (value.action === 'report') !== ('report' in value)) return false;
33
+ return (!('contract' in value) || record(value.contract)) && (!('report' in value) || record(value.report));
34
+ }
35
+
36
+ export function nonempty(value, label, max = 512) {
37
+ if (typeof value !== 'string' || value.length > max || value.trim() === '' || value.includes('\0')) {
38
+ throw new BridgeError('INVALID_REQUEST', `${label} must be a non-empty string (max ${max})`);
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export async function directory(value, label) {
44
+ nonempty(value, label, 4096);
45
+ if (!isAbsolute(value)) throw new BridgeError('INVALID_REQUEST', `${label} must be absolute`);
46
+ const resolved = await realpath(value);
47
+ if (!(await stat(resolved)).isDirectory()) {
48
+ throw new BridgeError('INVALID_REQUEST', `${label} must be a directory`);
49
+ }
50
+ return resolved;
51
+ }
52
+
53
+ export async function validateStart(value) {
54
+ if (!record(value) || value.type !== 'start' || ![PROTOCOL, 2].includes(value.protocol)) {
55
+ throw new BridgeError('INVALID_REQUEST', 'expected one protocol 1 or 2 start request');
56
+ }
57
+ const allowed = new Set(['type', 'protocol', 'requestId', 'cwd', 'provider', 'model',
58
+ 'reasoningEffort', 'permissionMode', 'prompt', 'maxTokens', 'timeoutMs', 'workspaceCwd', 'sessionTitle']);
59
+ if (value.protocol === 2) {
60
+ allowed.add('sessionId');
61
+ allowed.add('persistent');
62
+ if (value.persistent !== true) throw new BridgeError('INVALID_REQUEST', 'protocol 2 requires explicit persistent history');
63
+ if (value.sessionId !== undefined) nonempty(value.sessionId, 'sessionId', 512);
64
+ }
65
+ for (const key of Object.keys(value)) {
66
+ if (!allowed.has(key)) throw new BridgeError('INVALID_REQUEST', `unsupported start field: ${key}`);
67
+ }
68
+ const requestId = nonempty(value.requestId, 'requestId', 128);
69
+ const provider = nonempty(value.provider, 'provider');
70
+ const model = nonempty(value.model, 'model');
71
+ const prompt = nonempty(value.prompt, 'prompt', MAX_FRAME_BYTES);
72
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
73
+ throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only, workspace-write or danger-full-access');
74
+ }
75
+ if (value.reasoningEffort !== undefined) nonempty(value.reasoningEffort, 'reasoningEffort', 64);
76
+ if (value.maxTokens !== undefined && (!Number.isSafeInteger(value.maxTokens) || value.maxTokens < 1)) {
77
+ throw new BridgeError('INVALID_REQUEST', 'maxTokens must be a positive integer');
78
+ }
79
+ const timeoutMs = value.timeoutMs ?? 30 * 60 * 1000;
80
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 24 * 60 * 60 * 1000) {
81
+ throw new BridgeError('INVALID_REQUEST', 'timeoutMs must be between 1 and 86400000');
82
+ }
83
+ if (value.sessionTitle !== undefined) nonempty(value.sessionTitle, 'sessionTitle', 512);
84
+ const request = { ...value, requestId, provider, model, prompt, timeoutMs, cwd: await directory(value.cwd, 'cwd'),
85
+ ...(value.workspaceCwd === undefined ? {} : { workspaceCwd: await directory(value.workspaceCwd, 'workspaceCwd') }) };
86
+ if (request.workspaceCwd && request.workspaceCwd !== request.cwd && request.permissionMode === 'workspace-write') {
87
+ throw new BridgeError('INVALID_REQUEST', 'DSH workspace-write binds its sandbox to the session project; separate execution roots require an explicitly compatible permission mode');
88
+ }
89
+ return request;
90
+ }
91
+
92
+ export async function readPolicyReceipt(path, identity) {
93
+ if (!isAbsolute(path) || await realpath(dirname(path)) !== dirname(path)) {
94
+ throw new BridgeError('INVALID_REQUEST', 'policy receipt directory is not physical');
95
+ }
96
+ const parent = await stat(dirname(path));
97
+ if (!parent.isDirectory() || (parent.mode & 0o077) !== 0) {
98
+ throw new BridgeError('INVALID_REQUEST', 'policy receipt directory is not private');
99
+ }
100
+ // A malicious/incorrect SDK must not stall the worker by substituting a FIFO.
101
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
102
+ try {
103
+ const info = await file.stat();
104
+ if (!info.isFile() || info.size > 65536 || (info.mode & 0o077) !== 0) {
105
+ throw new BridgeError('INVALID_REQUEST', 'policy receipt is not a bounded private regular file');
106
+ }
107
+ const buffer = Buffer.alloc(65537);
108
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
109
+ if (bytesRead !== info.size) throw new BridgeError('INVALID_REQUEST', 'policy receipt changed while being read');
110
+ const receipt = JSON.parse(buffer.subarray(0, bytesRead).toString('utf8'));
111
+ if (!record(receipt) || Object.entries(identity).some(([key, value]) => receipt[key] !== value)) {
112
+ throw new BridgeError('INVALID_REQUEST', 'policy receipt identity mismatch; prompt withheld');
113
+ }
114
+ return receipt;
115
+ } finally {
116
+ await file.close();
117
+ }
118
+ }
119
+
120
+ export function normalizedError(error) {
121
+ return {
122
+ code: ERROR_CODES.has(error?.code) ? error.code : 'TRANSPORT',
123
+ message: error instanceof Error ? error.message : String(error),
124
+ };
125
+ }
126
+
127
+ export function modelErrorCode(code) {
128
+ if (ERROR_CODES.has(code)) return code;
129
+ if (code === 'MISSING_CREDENTIAL' || code === 'INVALID_CREDENTIAL' ||
130
+ code === 'ACCOUNT_SIGN_IN_REQUIRED' || code === 'ACCOUNT_TOKEN_INVALID') return 'AUTH';
131
+ if (code === 'ACCOUNT_QUOTA') return 'QUOTA';
132
+ if (code === 'NO_ADAPTER' || code === 'UNSUPPORTED_REASONING_EFFORT') return 'INVALID_REQUEST';
133
+ return 'SERVER';
134
+ }
135
+
136
+ const ACCOUNT_RECOVERY = new Map([
137
+ ['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.'],
138
+ ['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.'],
139
+ ['ACCOUNT_QUOTA', 'Check the DeepSeek account balance and usage; retry when account quota is available.'],
140
+ ]);
141
+
142
+ function modelErrorMessage(detail, fallback) {
143
+ const message = detail?.message ?? fallback;
144
+ const recovery = ACCOUNT_RECOVERY.get(detail?.code);
145
+ return recovery === undefined ? message : `${detail.code}: ${message}\n${recovery}`;
146
+ }
147
+
148
+ /** Fold committed per-call records; streamed usage snapshots are never summed. */
149
+ export class SessionFold {
150
+ constructor(sessionId, { strict = false } = {}) {
151
+ this.sessionId = sessionId;
152
+ this.strict = strict;
153
+ this.seen = new Set();
154
+ this.policy = null;
155
+ this.beginCall();
156
+ }
157
+
158
+ // Resume may append model selection/policy facts, but those are not a paid
159
+ // invocation. Never inherit previous assistant output, terminal or usage.
160
+ beginCall() {
161
+ this.receipts = new Set();
162
+ this.text = '';
163
+ this.assistantCount = 0;
164
+ this.usageComplete = true;
165
+ this.usage = { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0,
166
+ cache_read_tokens: 0, cache_write_tokens: 0, total_tokens: 0 };
167
+ this.terminal = null;
168
+ this.turnId = '';
169
+ }
170
+
171
+ accept(params) {
172
+ if (!record(params) || params.sessionId !== this.sessionId) return false;
173
+ const event = params.event;
174
+ if (!record(event) || typeof event.type !== 'string' ||
175
+ !Number.isSafeInteger(event.seq) || event.seq < 0 || !record(event.data)) {
176
+ throw new BridgeError('TRANSPORT', 'malformed DSH session event');
177
+ }
178
+ if (this.seen.has(event.seq)) return false;
179
+ this.seen.add(event.seq);
180
+ const data = event.data;
181
+ if (event.type === 'agent/inbox/spliced' && Array.isArray(data.inserted)) {
182
+ for (const message of data.inserted) if (typeof message?.id === 'string') this.receipts.add(message.id);
183
+ }
184
+ if (event.type === 'assistant/message') {
185
+ if (!Array.isArray(data.message?.content) || !data.message.content.every(block =>
186
+ record(block) && typeof block.type === 'string' &&
187
+ (block.type !== 'text' || typeof block.text === 'string'))) {
188
+ throw new BridgeError('TRANSPORT', 'malformed committed assistant message');
189
+ }
190
+ this.assistantCount += 1;
191
+ this.text = data.message.content.filter(block => block.type === 'text').map(block => block.text).join('');
192
+ this.addUsage(data.usage);
193
+ }
194
+ if (event.type === 'turn/end') {
195
+ if (!record(data.reason) || typeof data.reason.kind !== 'string' ||
196
+ (this.strict && (!Number.isSafeInteger(data.turn) || data.turn < 0 ||
197
+ !['completed', 'aborted', 'blocked', 'error', 'max-tokens'].includes(data.reason.kind)))) {
198
+ throw new BridgeError('TRANSPORT', 'malformed DSH turn terminal');
199
+ }
200
+ this.terminal = data.reason;
201
+ this.turnId = Number.isSafeInteger(data.turn) ? String(data.turn) : '';
202
+ }
203
+ return true;
204
+ }
205
+
206
+ addUsage(usage) {
207
+ const valid = value => Number.isSafeInteger(value) && value >= 0;
208
+ const fields = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens', 'totalTokens'];
209
+ if (!record(usage) || !valid(usage.inputTokens) || !valid(usage.outputTokens) ||
210
+ fields.some(key => usage[key] !== undefined && !valid(usage[key]))) {
211
+ this.usageComplete = false;
212
+ return;
213
+ }
214
+ const cacheRead = usage.cacheReadTokens ?? 0;
215
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
216
+ const input = usage.inputTokens + cacheRead + cacheWrite;
217
+ const values = {
218
+ input_tokens: input, output_tokens: usage.outputTokens,
219
+ reasoning_tokens: usage.reasoningTokens ?? 0,
220
+ cache_read_tokens: cacheRead, cache_write_tokens: cacheWrite,
221
+ total_tokens: usage.totalTokens ?? input + usage.outputTokens,
222
+ };
223
+ for (const [key, value] of Object.entries(values)) {
224
+ if (!Number.isSafeInteger(this.usage[key] + value)) this.usageComplete = false;
225
+ else this.usage[key] += value;
226
+ }
227
+ }
228
+
229
+ verifiedPolicy(request) {
230
+ const policy = this.policy;
231
+ const fullAccess = request.permissionMode === 'danger-full-access';
232
+ if (!policy || policy.protocol !== PROTOCOL || policy.permissionMode !== request.permissionMode ||
233
+ policy.cwd !== (request.workspaceCwd ?? request.cwd) || policy.approvalPolicy !== 'never' ||
234
+ policy.confinedShell !== !fullAccess ||
235
+ (fullAccess ? policy.enforcement !== undefined : policy.enforcement !== 'full')) {
236
+ throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and permission policy; prompt withheld');
237
+ }
238
+ }
239
+
240
+ finish(messageId) {
241
+ if (!this.receipts.has(messageId)) throw new BridgeError('TRANSPORT', 'missing durable prompt receipt');
242
+ if (!this.terminal) throw new BridgeError('TRANSPORT', 'idle without committed turn/end');
243
+ if (this.terminal.kind !== 'completed') {
244
+ // DSH's account-tasks hook cancels the active account turn after sign-out,
245
+ // including when rejecting an expired token. Do not infer this from text
246
+ // or treat other cancellation hooks as authentication failures.
247
+ if (this.terminal.kind === 'aborted' && this.terminal.reason?.kind === 'hook' &&
248
+ this.terminal.reason.reason === 'deepseek-account/signed-out') {
249
+ 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.');
250
+ }
251
+ const detail = this.terminal.error;
252
+ throw new BridgeError(modelErrorCode(detail?.code),
253
+ modelErrorMessage(detail, `DSH turn ended as ${this.terminal.kind}`));
254
+ }
255
+ if (!this.assistantCount || this.text.trim() === '') {
256
+ throw new BridgeError('EMPTY_RESPONSE', 'DSH completed without assistant text');
257
+ }
258
+ return { text: this.text, usage: this.usageComplete ? this.usage : null };
259
+ }
260
+ }
@@ -0,0 +1,127 @@
1
+ import { z } from 'zod';
2
+ import { validInvestigationCommand } from './protocol.mjs';
3
+
4
+ // This read model folds existing public command/tool events. Third-party DSH
5
+ // plugins must not invent Session.append event types or write engine state.
6
+ export const taskProjectionKey = 'flywheelTask';
7
+ const modes = ['auto', 'quick', 'full'];
8
+ const taskActions = ['investigate', 'launch', 'start', 'status', 'stop', 'step', 'session', 'deliver'];
9
+ export const mutatesTask = action => ['investigate', 'launch', 'start', 'stop', 'step', 'session', 'deliver'].includes(action);
10
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ const path = value => typeof value === 'string' && value.startsWith('/') && !/[\0\r\n]/.test(value);
12
+ const parse = value => { try { return JSON.parse(value); } catch { return null; } };
13
+ const sameTask = (a, b) => a.taskDir === b.taskDir && a.cwd === b.cwd;
14
+ const operationSchema = z.object({
15
+ operationId: z.string(), sequence: z.number().int().nonnegative(),
16
+ action: z.enum(taskActions), taskDir: z.string(), cwd: z.string(),
17
+ });
18
+ const taskViewSchema = operationSchema.extend({
19
+ outcome: z.enum(['pending', 'confirmed', 'unknown']), message: z.string(),
20
+ });
21
+ const viewSchema = taskViewSchema.extend({ tasks: z.array(taskViewSchema).optional() }).nullable();
22
+ const stateSchema = z.object({
23
+ cwd: z.string(), inheritedEventCount: z.number().int().nonnegative(),
24
+ calls: z.record(z.string(), operationSchema), view: viewSchema, tasks: z.record(z.string(), taskViewSchema),
25
+ });
26
+
27
+ function request(input, cwd, operationId, sequence, launchOnly = false) {
28
+ if (!record(input)) return null;
29
+ const value = launchOnly ? { ...input, action: 'launch' } : input;
30
+ if (!taskActions.includes(value.action) || !path(value.taskDir) || !path(value.cwd ?? cwd) ||
31
+ Object.keys(input).some(key => !(launchOnly ? ['taskDir', 'cwd', 'mode'] : ['action', 'taskDir', 'cwd', 'mode', 'only', 'packet', 'revision', 'retryReason', 'session', 'delivery', 'investigation']).includes(key)) ||
32
+ ('mode' in value && (!['start', 'launch'].includes(value.action) || !modes.includes(value.mode)))) return null;
33
+ if (value.action === 'step' && (!['plan', 'execute', 'review', 'verify', 'integrate', 'final'].includes(value.only) || !Number.isSafeInteger(value.revision) || value.revision < 1)) return null;
34
+ if (value.action === 'investigate' ? !validInvestigationCommand(value.investigation) : 'investigation' in value) return null;
35
+ if (value.action === 'session' && (!record(value.session) || !['claim','send','return','reconcile','release'].includes(value.session.action) || !Number.isSafeInteger(value.session.revision) || value.session.revision < 1 || typeof value.session.key !== 'string')) return null;
36
+ if (value.action === 'deliver' && (!record(value.delivery) || value.delivery.confirmed !== true || !['create','merge'].includes(value.delivery.action))) return null;
37
+ return { operationId, sequence, action: value.action, taskDir: value.taskDir, cwd: value.cwd ?? cwd };
38
+ }
39
+
40
+ function begin(state, input, id, event, launchOnly = false) {
41
+ const operation = request(input, state.cwd, id, event.seq, launchOnly);
42
+ if (!operation) return state;
43
+ const unresolved = state.view && state.view.outcome !== 'confirmed';
44
+ // A second attempted mutation cannot erase the first uncertain target, even
45
+ // when a model calls a new tool before the earlier response reaches its view.
46
+ const binds = mutatesTask(operation.action) && !unresolved;
47
+ return { ...state, calls: { ...state.calls, [id]: operation },
48
+ view: binds ? { ...operation, outcome: 'pending', message: '', ...(Object.keys(state.tasks).length ? { tasks: Object.values(state.tasks) } : {}) } : state.view };
49
+ }
50
+
51
+ function settle(state, id, value, failed, error) {
52
+ const operation = state.calls[id];
53
+ if (!operation) return state;
54
+ const calls = { ...state.calls };
55
+ delete calls[id];
56
+ let view = state.view;
57
+ let tasks = state.tasks;
58
+ const owns = view?.operationId === id;
59
+ const successful = !failed && record(value) && value.protocol === 1 && value.ok === true &&
60
+ path(value.taskDir) && (value.cwd === undefined || path(value.cwd)) && typeof value.running === 'boolean';
61
+ if (successful && !(operation.action === 'status' && id.startsWith('command:') && (!view || !sameTask(operation, view)))) {
62
+ const task = { ...operation, taskDir: value.taskDir, cwd: value.cwd ?? operation.cwd, outcome: 'confirmed', message: '' };
63
+ const key = JSON.stringify([task.taskDir, task.cwd]);
64
+ if (!tasks[key] || tasks[key].sequence < operation.sequence) {
65
+ tasks = Object.fromEntries(Object.entries({ ...tasks, [key]: task }).sort((a,b) => b[1].sequence - a[1].sequence).slice(0,100));
66
+ }
67
+ // Compare the logged request, not lexical input against a realpath result.
68
+ // A later successful status explicitly reconciles the original target.
69
+ const reconciles = operation.action === 'status' && view && sameTask(operation, view) && operation.sequence > view.sequence;
70
+ if (owns || reconciles || (!view && operation.action === 'status') ||
71
+ (view?.outcome === 'confirmed' && operation.sequence > view.sequence && (['launch', 'start', 'investigate'].includes(operation.action) || operation.action === 'status' && id.startsWith('tool:')))) {
72
+ view = { ...operation, taskDir: value.taskDir, cwd: value.cwd ?? operation.cwd, outcome: 'confirmed', message: '' };
73
+ }
74
+ } else if (owns) {
75
+ const typed = record(value) && value.protocol === 1 && value.type === 'flywheel-control-error' &&
76
+ value.action === operation.action && ['not-dispatched', 'unknown'].includes(value.dispatch);
77
+ // Only host-produced structured proof can release an ambiguous mutation.
78
+ const notDispatched = typed && value.dispatch === 'not-dispatched' || error?.code === 'ABORTED_BEFORE_DISPATCH';
79
+ view = { ...view, outcome: notDispatched ? 'confirmed' : 'unknown',
80
+ message: typed && typeof value.message === 'string' ? value.message : '' };
81
+ }
82
+ if (view && tasks !== state.tasks) view = { ...view, tasks: Object.values(tasks) };
83
+ return { ...state, calls, view, tasks };
84
+ }
85
+
86
+ function contentValue(content) {
87
+ if (!Array.isArray(content)) return null;
88
+ const text = content.filter(block => block?.type === 'text').map(block => block.text).join('\n');
89
+ return parse(text);
90
+ }
91
+
92
+ export const taskAssociationProjection = {
93
+ key: taskProjectionKey,
94
+ stateVersion: 3,
95
+ stateSchema,
96
+ init: (header, inheritedEventCount) => ({
97
+ cwd: typeof header.cwd === 'string' ? header.cwd : '', inheritedEventCount,
98
+ calls: {}, view: null, tasks: {},
99
+ }),
100
+ apply(state, event) {
101
+ if (event.type === 'working-directory/change') return { ...state, cwd: event.data.cwd };
102
+ // A fork is a new conversation: inherited Flywheel tools are context, not
103
+ // permission to control the parent conversation's background task.
104
+ if (event.seq < state.inheritedEventCount) return state;
105
+ const data = event.data;
106
+ if (event.type === 'command/run' && data.name === 'flywheel') {
107
+ return begin(state, parse(data.args), 'command:' + data.commandId, event);
108
+ }
109
+ if (event.type === 'command/done') {
110
+ return settle(state, 'command:' + data.commandId, parse(data.text), data.kind !== 'success');
111
+ }
112
+ if (event.type === 'tool/call' && ['flywheel_launch', 'flywheel_control'].includes(data.name)) {
113
+ return begin(state, parse(data.arguments), 'tool:' + data.callId, event, data.name === 'flywheel_launch');
114
+ }
115
+ if (event.type === 'tool/result') {
116
+ return settle(state, 'tool:' + data.message.toolCallId, contentValue(data.message.content), data.message.isError, data.error);
117
+ }
118
+ if (event.type === 'tool/ptc-dispatch-start' && ['flywheel_launch', 'flywheel_control'].includes(data.name)) {
119
+ return begin(state, data.arguments, 'tool:' + data.subCallId, event, data.name === 'flywheel_launch');
120
+ }
121
+ if (event.type === 'tool/ptc-dispatch') {
122
+ return settle(state, 'tool:' + data.subCallId, contentValue(data.content), data.isError, data.error);
123
+ }
124
+ return state;
125
+ },
126
+ wire: { viewSchema, view: state => state.view },
127
+ };
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { runWorker } from './bridge.mjs';
3
+ import { prewarmInput } from './prewarm.mjs';
4
+ import { BridgeError, MAX_FRAME_BYTES, normalizedError } from './protocol.mjs';
5
+
6
+ const controller = new AbortController();
7
+ process.once('SIGTERM', () => controller.abort());
8
+ process.once('SIGINT', () => controller.abort());
9
+ process.once('SIGHUP', () => controller.abort());
10
+ process.stdout.on('error', () => controller.abort());
11
+ // The engine writes exactly one request then closes stdin. EOF after admission
12
+ // is normal and must not cancel an in-flight model turn.
13
+ async function readInput() {
14
+ const chunks = [];
15
+ let bytes = 0;
16
+ const admission = setTimeout(() => process.stdin.destroy(new BridgeError('TIMEOUT', 'no complete worker request within 10 seconds')), 10000);
17
+ try {
18
+ for await (const chunk of process.stdin) {
19
+ bytes += chunk.length;
20
+ if (bytes > MAX_FRAME_BYTES) throw new BridgeError('INVALID_REQUEST', 'worker request exceeds 8 MiB');
21
+ chunks.push(chunk);
22
+ }
23
+ const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
24
+ if (lines.length !== 1 || !lines[0]) throw new BridgeError('INVALID_REQUEST', 'expected exactly one JSON line then EOF');
25
+ try { return JSON.parse(lines[0]); } catch {
26
+ throw new BridgeError('INVALID_REQUEST', 'worker request is not valid JSON');
27
+ }
28
+ } finally {
29
+ clearTimeout(admission);
30
+ }
31
+ }
32
+
33
+ const emit = value => process.stdout.write(JSON.stringify(value) + '\n');
34
+ let warmInput;
35
+ try {
36
+ if (process.argv.slice(2).some(arg => !['--doctor', '--prewarm'].includes(arg)) || process.argv.length > 3) {
37
+ throw new BridgeError('INVALID_REQUEST', 'worker accepts only optional --doctor or --prewarm');
38
+ }
39
+ warmInput = process.argv.includes('--prewarm') ? prewarmInput(controller) : undefined;
40
+ const request = warmInput ? await warmInput.admission : await readInput();
41
+ const result = await runWorker(request, {
42
+ emit, signal: controller.signal, doctor: process.argv.includes('--doctor'),
43
+ stderr: chunk => process.stderr.write(chunk), prewarm: warmInput,
44
+ });
45
+ process.exitCode = !result || ['ok', 'closed'].includes(result.status) ? 0 : result.status === 'cancelled' ? 143 : 1;
46
+ } catch (error) {
47
+ emit({ protocol: 1, requestId: 'invalid-request', type: 'result', status: 'error', settled: true,
48
+ text: '', usage: null, costUsd: null, error: normalizedError(error) });
49
+ process.exitCode = 1;
50
+ } finally {
51
+ warmInput?.stop();
52
+ }
@@ -0,0 +1,42 @@
1
+ // Only the desktop Host owns Workspace mutations. SDK workers must never open
2
+ // a second writer on the desktop's workspace storage.
3
+ const workerId = /^flywheel-[a-f0-9-]{36}$/;
4
+
5
+ export async function reconcileWorkspaces(ctx, response) {
6
+ const registry = ctx.get?.('workspaceRegistry');
7
+ const persistence = ctx.get?.('sessionPersistence');
8
+ if (!registry || !persistence) return;
9
+ const project = response?.state?.project?.['workspace-cwd'];
10
+ if (project) await registry.create(project);
11
+ const groups = new Map(registry.list().map(workspace => [workspace.path, workspace]));
12
+ if (!groups.size) return;
13
+ // Public metadata listing, never a scan of project directories or session
14
+ // event bodies. attachSession independently validates the persisted header.
15
+ for (const snapshot of await persistence.list()) {
16
+ const header = snapshot.header;
17
+ if (!workerId.test(header.id)) continue;
18
+ const workspace = groups.get(header.cwd);
19
+ if (workspace && !workspace.sessionIds.includes(header.id)) {
20
+ await workspace.attachSession(header.id);
21
+ }
22
+ }
23
+ }
24
+
25
+ export function watchWorkspaces(ctx) {
26
+ // SDK profile/older compositions may omit Workspace entirely. Keep the
27
+ // controller available, and mount reconciliation when the public services exist.
28
+ ctx.inject?.(['workspaceRegistry', 'sessionPersistence'], (scope) => {
29
+ let busy = false;
30
+ const refresh = async () => {
31
+ if (busy) return;
32
+ busy = true;
33
+ try { await reconcileWorkspaces(scope); }
34
+ catch (error) { scope.logger.warn('Flywheel 工作区归属未同步:%s', error.message); }
35
+ finally { busy = false; }
36
+ };
37
+ const timer = setInterval(refresh, 30000);
38
+ timer.unref?.();
39
+ scope.on('dispose', () => clearInterval(timer));
40
+ return refresh();
41
+ });
42
+ }
package/docs/README.md CHANGED
@@ -31,7 +31,7 @@ execution separately.
31
31
  | --- | --- | --- |
32
32
  | Development setup, quality gates, and pull requests | [Contributing](en/contributing.md) | [贡献指南](zh-CN/contributing.md) |
33
33
  | Documentation ownership, bilingual updates, and review checklist | [Maintenance rules](en/contributing.md#keep-documentation-organized) | [文档维护规范](zh-CN/contributing.md#保持文档结构清晰) |
34
- | Build, verify, publish, and recover a six-package release | [Release runbook](en/releasing.md) | [发行手册](zh-CN/releasing.md) |
34
+ | Build, verify, publish, and recover a complete single-package release | [Release runbook](en/releasing.md) | [发行手册](zh-CN/releasing.md) |
35
35
  | DSH package, host compatibility, and worker setup | [Plugin README](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.md) | [插件说明](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.zh-CN.md) |
36
36
 
37
37
  The [architecture specification](design.md) is the maintained design contract
@@ -27,7 +27,7 @@
27
27
  | --- | --- | --- |
28
28
  | 开发环境、质量门禁和 PR 流程 | [贡献指南](zh-CN/contributing.md) | [Contributing](en/contributing.md) |
29
29
  | 文档归属、双语更新和审阅清单 | [文档维护规范](zh-CN/contributing.md#保持文档结构清晰) | [Maintenance rules](en/contributing.md#keep-documentation-organized) |
30
- | 六包构建、验证、发布和恢复 | [发行手册](zh-CN/releasing.md) | [Release runbook](en/releasing.md) |
30
+ | 单包构建、验证、发布和恢复 | [发行手册](zh-CN/releasing.md) | [Release runbook](en/releasing.md) |
31
31
  | DSH 包、宿主兼容和 worker 接入 | [插件说明](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.zh-CN.md) | [Plugin README](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.md) |
32
32
 
33
33
  [架构规范](design.md)是当前维护的设计契约,以中文编写。[Worker 协议](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/PROTOCOL.md)、[schema 与模板](https://github.com/ckanner/flywheel/tree/main/templates)及[仓库约定](https://github.com/ckanner/flywheel/blob/main/AGENTS.md)定义实现接口;版本变化见 [CHANGELOG](../CHANGELOG.md)。
package/docs/design.md CHANGED
@@ -628,6 +628,7 @@ Go 的 `make quality-gate` 执行 vet、gofmt 和 **逐函数 100% statement cov
628
628
  | P5 就绪队列与一次性 DSH 预热 | [实施计划](plans/2026-11-13-p5-ready-queue-prewarm.md) | [工程验证与收益边界](validation/2026-11-13-p5-ready-queue-prewarm.md) |
629
629
  | 历史影子评估(已退役) | [原 JEV 方案](plans/2026-11-10-jev-shadow.md) | [历史验收](validation/2026-11-10-jev-shadow.md) |
630
630
  | 0.30.2 发行与安装 | [发行流程](zh-CN/releasing.md) | [发行记录](validation/2026-11-11-release-0.30.2.md) |
631
+ | 0.33.0 单包分发 | [发行流程](zh-CN/releasing.md) | [工程验证](validation/2026-11-14-single-package.md) |
631
632
  | 0.31.0 发行与安装 | [发行流程](zh-CN/releasing.md) | [发行记录](validation/2026-11-11-release-0.31.0.md) |
632
633
 
633
634
  提案保留当时的背景和分期,验收记录保留实际结果;现行行为以本文、schema 和当前使用指南为准。新增实现应修改对应章节,发布事实写入 changelog 和验收记录,不在文档开头或结尾追加另一套现行说明。
@@ -785,19 +786,21 @@ DSH 插件按公开 API 的兼容系列声明 `>=0.2.1-rc.5 <0.3.0-0`;DSH 使
785
786
 
786
787
  ### 13.1 npm 安装与发行
787
788
 
788
- 正式分发以 npm 为入口:`npx -y @ckanner/flywheel@latest install` 显式协调同版本全局 CLI、worker、DSH 桌面插件与内嵌 skills。普通 `npm install` 不通过 postinstall 修改用户配置;安装器不要求源码 checkout 或本机 Go 编译器。四个平台包仍由 optionalDependencies 选择,插件是精确版本依赖,全部六包版本一致。安装不会替换用户的 DSH 桌面或代办模型账号登录。
789
+ 正式分发以 npm 为入口:`npx -y @ckanner/flywheel@latest install` 显式协调同版本全局 CLI、worker、DSH 桌面插件与内嵌 skills。普通 `npm install` 不通过 postinstall 修改用户配置;安装器不要求源码 checkout 或本机 Go 编译器。从 0.33 起只发布 `@ckanner/flywheel`:一个 tarball 内含 CLI、DSH Host/Client、worker 与四个平台 Go 二进制;skills 与模板继续内嵌在引擎中。`native/manifest.json` 固定版本、平台集合和各二进制摘要,启动器仅选择包内当前平台并核对完整性,不再下载或查找独立平台包。源码中的 `plugins/dsh` 仅为 private 构建/测试区。安装不会替换用户的 DSH 桌面或代办模型账号登录。
790
+
791
+ 桌面从拆包版本升级时,安装器先读取现有依赖,安装并确认新的完整包,再通过官方 DSH CLI 移除旧 `@ckanner/dsh-plugin-flywheel`,最后确认旧名不存在且新版本精确匹配。旧包移除失败时,仅在能确认旧包仍存在且替代包为本次新增的情况下回退;取消不启动新的回退子进程,局部状态在 JSON 报告中保留,重试先重新读取实际状态。自定义 file/git 来源的旧插件不自动迁移。升级前应退出 DSH;当前进程未重载不算新版已生效。全局配置、项目契约和历史调用账本不因包迁移而重写。
789
792
 
790
793
  npm shim 异步运行原生引擎,收到 TERM/INT/HUP 时转发给 Go 并等待退出;不能把仅 wrapper 退出当成子进程完成。worker 通过安装包公开入口解析绝对路径。桌面入口使用安装记录与绝对启动路径,避免依赖 GUI 恰好继承终端 PATH。用户显式配置优先,安装不覆盖已有 Flywheel 模型配置。
791
794
 
792
795
  skills 继续从同一 Go 二进制内嵌资源安装。每个受管快照保存内容与权限清单;升级前确认原内容未被用户修改,额外文件、未知标记或陌生目录保留并报冲突。先写完整 staging,再以 rename 切换并保留失败回滚;这是逐 skill 更新,不是全部目录的整体事务。旧文本标记只在目录与当前内嵌快照完全一致时迁移。`install-skills --json` 供安装器取得完整结果,部分失败不能报告整套成功。
793
796
 
794
- 发布以精确 HEAD 标签与干净候选为依据。构建一次六包候选,Linux/macOS 安装测试消费同一组包;已存在版本必须核对 integrity。平台包与插件先发布、meta 最后发布,精确版本从 registry 安装验证后才推进 latest,防止倒退。源码 PR CI 通过、npm pack 成功、registry 安装通过与用户 Mac 实机验收是不同证据,分别记录。
797
+ 发布以精确 HEAD 标签与干净候选为依据。构建一次完整单包候选(candidate format 2),Linux/macOS 安装测试消费同一个 tarball;候选验证同时核对四个平台二进制、公开入口、MIT 许可和内置 adapter。已存在版本必须核对 integrity;先发 candidate 标签,精确版本从 registry 安装验证后才推进 latest,防止倒退。源码 PR CI 通过、npm pack 成功、registry 安装通过与用户 Mac 实机验收是不同证据,分别记录。
795
798
 
796
- 历史 0.28.2 使用 DeepSeek API Key 默认,账号默认修复从 0.28.3 开始提供;这些版本差异保留在 changelog 和发行记录,升级仍须显式迁移已有配置,不重写已发布的包。DSH 插件精确依赖上游 `dsh-subagent-codex-pro@0.9.0`;该已审查源码候选以真实 npm tarball 锁定并 bundle 到插件,来源、许可证和摘要见 [vendor 记录](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/vendor/README.md)。Claude 0.4.0 维持可选 peer。旧发行失败与重试记录保留在 [npm 验证记录](validation/2026-11-01-npm-installation.md);源码、候选、CI、registry 和 Mac 实机证据分别记录,不混为一个“测试通过”。
799
+ 历史 0.28.2 使用 DeepSeek API Key 默认,账号默认修复从 0.28.3 开始提供;这些版本差异保留在 changelog 和发行记录,升级仍须显式迁移已有配置,不重写已发布的包。DSH 插件精确依赖上游 `dsh-subagent-codex-pro@0.9.0`;该已审查源码候选以真实 npm tarball 锁定并 bundle 到完整包,来源、许可证和摘要见 [vendor 记录](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/vendor/README.md)。Claude 0.4.0 维持可选 peer。旧发行失败与重试记录保留在 [npm 验证记录](validation/2026-11-01-npm-installation.md);源码、候选、CI、registry 和 Mac 实机证据分别记录,不混为一个“测试通过”。
797
800
 
798
801
  CI、候选构建、双平台验证和发布统一固定为 Node 24.22.1/npm 12.2.1,预检只需同版 Node。发行协议测试通过隔离的本地 registry 调用真实 `npm view`,覆盖 latest、integrity、缺失字段与错误响应。存在的字段必须返回字符串,兼容用单元素数组包裹;多结果拒绝,缺失 latest 单独处理,缺失 integrity 不能当成版本不存在。只有结构化 E404 表示版本不存在,取消、认证或网络错误仍阻止发布。npm 11/12 的 `pack --json` 兼容保持严格包身份检查。
799
802
 
800
- GitHub Actions 的 publish job 使用固定 Node/npm 发行工具链,支持 OIDC trusted publishing 及已有 `NPM_TOKEN` 后备。无 token 时显式使用空 user npmrc,避免 setup-node 的认证占位配置;不在日志或 GitHub 环境文件中写入凭证。六个包各自的 npm trusted publisher 都必须对应 `ckanner/flywheel` 的 `publish.yml`,并分别允许直接 publish 与 dist-tag;GitHub 登录或 `id-token: write` 本身不授予 npm 包写权限。维护者配置和失败恢复见 [发行指南](zh-CN/releasing.md)。
803
+ GitHub Actions 的 publish job 使用固定 Node/npm 工具链,仅通过 OIDC trusted publishing 认证;显式使用空 user npmrc 并清空认证占位值,不依赖存储 token 或浏览器逐包确认。只为完整包 `@ckanner/flywheel` 配置对应 `ckanner/flywheel`/`publish.yml` 的 npm Trusted Publisher,同时允许 publish 与 dist-tag。GitHub 登录或 `id-token: write` 本身不授予 npm 包写权限;新连接需实际 CI 发布来完成 npm 的验证。维护者配置和失败恢复见 [发行指南](zh-CN/releasing.md)。
801
804
 
802
805
  私有仓库的发行同样经过完整门禁。发布 job 在准备 npm 后,由官方 checkout 使用短暂的只读 GitHub 认证重新取得完整 refs,固定到已验证 source,再下载候选并检查精确 tag;后续不执行无凭证的远端 fetch。仓库可见性由 GitHub 事件显式传给 publisher,未知值拒绝发布。public 要求 npm provenance,private 显式关闭 npm 不支持的证明生成;这只记录 registry 的能力边界,不改变仓库可见性,也不取消本项目的 source/artifact integrity 校验。
803
806
 
@@ -542,7 +542,7 @@ Main-task fresh and scoped calls both use outer worker protocol 3. Fresh selects
542
542
 
543
543
  | Variable | Purpose |
544
544
  |---|---|
545
- | `FLYWHEEL_DSH_WORKER` | Optional DSH worker override; the npm launcher normally resolves the matching dependency |
545
+ | `FLYWHEEL_DSH_WORKER` | Optional DSH worker override; the npm launcher normally resolves the worker inside its own package |
546
546
  | `FLYWHEEL_DSH_BIN` | DSH command, otherwise `dsh` on PATH |
547
547
  | `FLYWHEEL_DSH_PROFILE` | SDK profile, default `sdk`; it must not be `desktop` |
548
548
  | `DSH_HOME` | Optional DSH configuration root, interpreted by DSH |
@@ -100,7 +100,7 @@ FLYWHEEL_NPM_PACKAGES_DIR=dist-npm npm run test:install
100
100
  |---|---|
101
101
  | Go engine | `make quality-gate`: vet, formatting, tests, and coverage |
102
102
  | DSH plugin or worker | Plugin checks and the applicable Go-to-Node bridges |
103
- | npm installation or release | Root npm tests, a six-package candidate build, and installation of those tarballs |
103
+ | npm installation or release | Root npm tests, a complete single-package candidate build, and installation of that tarball |
104
104
  | Documentation | Links, anchors, fences, runnable examples, translation consistency, and affected embedding/package contents |
105
105
 
106
106
  The Claude bridge needs the exact optional 0.4.0 machine API in the worker's
@@ -288,16 +288,15 @@ that limitation. Source merge and npm publication remain separate operations.
288
288
 
289
289
  ## Release changes
290
290
 
291
- Follow the [release guide](releasing.md). All six Flywheel packages share one exact
292
- version with exact internal dependencies and the pinned Codex Pro runtime. Build
291
+ Follow the [release guide](releasing.md). Flywheel publishes one complete package containing all native platforms and the
292
+ pinned Codex Pro runtime. The private DSH source harness is not published. Build
293
293
  once from final clean source and test those tarballs. Preserve the source commit,
294
294
  tree, snapshot hash, and package integrity values; the release tag must identify
295
295
  that source. Do not relabel a dirty candidate as a clean release or rebuild
296
296
  different bytes for a published version.
297
297
 
298
298
  Use the repository's release scripts for candidate checks, publication, registry
299
- verification, and promotion. Publish platform packages and the plugin before the
300
- meta package, test the exact registry version before moving `latest`, and verify
301
- all six tags and integrities. A partial publication resumes with the same
299
+ verification, and promotion. Publish the complete candidate, test the exact registry version before moving
300
+ `latest`, and verify the tag and integrity. A partial publication resumes with the same
302
301
  candidate. Manual publication requires its own explicit authorization and an
303
302
  accurate record of completed and unavailable checks.
@@ -3,7 +3,7 @@
3
3
  [Documentation](../README.md) · English · [简体中文](../zh-CN/flow-console.md)
4
4
 
5
5
  The DSH Flywheel view brings the current task, workflow stages, configuration, session
6
- interaction, and delivery into one place. Keep the CLI, platform packages, and DSH
6
+ interaction, and delivery into one place. Keep the CLI, bundled engines, and DSH
7
7
  plugin at the same version. For installation, see [Getting started](getting-started.md).
8
8
 
9
9
  ## Find a task without entering paths
@@ -5,10 +5,17 @@
5
5
  Install Flywheel, connect the backends you intend to use, then run a small task.
6
6
  This guide follows the current repository. Check the installed version and the npm
7
7
  version you choose; a merged change does not update an existing installation.
8
- Keep the CLI, platform packages, and DSH plugin at one version.
8
+ Keep the CLI, bundled engines, and DSH plugin at one version.
9
9
 
10
10
  For the complete path through task branches and PR review, see [Workflow](workflow.md).
11
11
 
12
+ From 0.33, `@ckanner/flywheel` is one complete package: CLI, DSH view/workers,
13
+ embedded skills and four platform engines. No separate platform package or first-run
14
+ engine download is needed. When upgrading from 0.32.x, quit DSH before running the
15
+ installer; it replaces the old desktop package registration and preserves your
16
+ configuration and histories. Reload DSH after setup. Existing old npm versions
17
+ remain available for historical installations.
18
+
12
19
  ## 1. Prepare the environment
13
20
 
14
21
  | Requirement | Purpose |
@@ -146,7 +153,7 @@ capabilities or merge full worker histories into the parent trajectory. See the
146
153
 
147
154
  Setup writes `~/.config/flywheel/npm-launcher` and an installation record containing
148
155
  absolute Node/CLI paths and the checked DSH entry. An explicit `flywheelBinary`
149
- overrides the managed launcher. Missing records can use legacy PATH lookup;
156
+ overrides the managed launcher. Without a record, the installed package uses its bundled CLI;
150
157
  invalid records or modified launchers fail visibly.
151
158
 
152
159
  After moving Node/the npm prefix or installing missing tools, rerun