@ckanner/flywheel 0.22.1 → 0.28.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.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 ckanner. SPDX-License-Identifier: UNLICENSED
3
+ "use strict";
4
+
5
+ const { loadCodexProWorkerPath, runChild } = require("./runtime.js");
6
+
7
+ async function main() {
8
+ return runChild(process.execPath, [loadCodexProWorkerPath(), ...process.argv.slice(2)], {
9
+ label: "flywheel-codex-pro-worker",
10
+ });
11
+ }
12
+
13
+ main().then(code => { process.exitCode = code; }).catch(error => {
14
+ console.error(`flywheel-codex-pro-worker: ${error.message}`);
15
+ process.exitCode = 1;
16
+ });
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 ckanner. SPDX-License-Identifier: UNLICENSED
3
+ "use strict";
4
+
5
+ const { loadWorkerPath, runChild } = require("./runtime.js");
6
+
7
+ async function main() {
8
+ return runChild(process.execPath, [loadWorkerPath(), ...process.argv.slice(2)], {
9
+ label: "flywheel-dsh-worker",
10
+ });
11
+ }
12
+
13
+ main().then(code => { process.exitCode = code; }).catch(error => {
14
+ console.error(`flywheel-dsh-worker: ${error.message}`);
15
+ process.exitCode = 1;
16
+ });
package/bin/flywheel.js CHANGED
@@ -1,33 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright (c) 2026 ckanner. SPDX-License-Identifier: UNLICENSED
3
- //
4
- // npm bin shim(v0.22 平台包模式,esbuild 先例):按 platform/arch 解析
5
- // optionalDependencies 里的平台二进制包并透传执行——无 postinstall、无网络
6
- // 下载、无 checksum 步骤(npm 自身的包完整性承担校验)。安装即得二进制,
7
- // 维护者与分用户用法完全一致。
8
3
  "use strict";
9
4
 
10
- const { spawnSync } = require("child_process");
5
+ const { loadNativePath, loadWorkerPath, loadCodexProWorkerPath, assertSupportedNode, runChild } = require("./runtime.js");
11
6
 
12
- const PLATFORM_MAP = { darwin: "darwin", linux: "linux" };
13
- const ARCH_MAP = { arm64: "arm64", x64: "amd64" };
7
+ async function main() {
8
+ const args = process.argv.slice(2);
9
+ if (args[0] === "install" || args[0] === "setup") {
10
+ return require("../npm/install.js").main(args.slice(1));
11
+ }
12
+ assertSupportedNode();
14
13
 
15
- const platform = PLATFORM_MAP[process.platform];
16
- const arch = ARCH_MAP[process.arch];
17
- if (!platform || !arch) {
18
- console.error(`flywheel: unsupported platform ${process.platform}/${process.arch}(支持 darwin/linux × amd64/arm64)`);
19
- process.exit(1);
14
+ const env = { ...process.env };
15
+ // An npm dependency's bin is not necessarily on a desktop application's
16
+ // PATH. Resolve this installation's worker without changing an explicit
17
+ // user override or relying on the shell's global npm prefix.
18
+ if (env.FLYWHEEL_DSH_WORKER === undefined) {
19
+ env.FLYWHEEL_DSH_WORKER = loadWorkerPath();
20
+ }
21
+ if (env.FLYWHEEL_CODEX_PRO_WORKER === undefined) {
22
+ env.FLYWHEEL_CODEX_PRO_WORKER = loadCodexProWorkerPath();
23
+ }
24
+ return runChild(loadNativePath(), args, { env });
20
25
  }
21
26
 
22
- const pkg = `@ckanner/flywheel-${platform}-${arch}`;
23
- let bin;
24
- try {
25
- bin = require.resolve(`${pkg}/flywheel`);
26
- } catch (_) {
27
- console.error(`flywheel: 平台包 ${pkg} 未安装。npm 会按 os/cpu 自动选取 optionalDependencies——`);
28
- console.error(` 若被 --no-optional 跳过,请去掉该标志重装:npm i -g @ckanner/flywheel`);
29
- process.exit(1);
30
- }
31
-
32
- const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
33
- process.exit(result.status === null ? 1 : result.status);
27
+ main().then(code => { process.exitCode = code; }).catch(error => {
28
+ console.error(`flywheel: ${error.message}`);
29
+ process.exitCode = 1;
30
+ });
package/bin/runtime.js ADDED
@@ -0,0 +1,100 @@
1
+ // Copyright (c) 2026 ckanner. SPDX-License-Identifier: UNLICENSED
2
+ "use strict";
3
+
4
+ const { spawn } = require("node:child_process");
5
+ const { constants } = require("node:os");
6
+
7
+ const platforms = { darwin: "darwin", linux: "linux" };
8
+ const architectures = { arm64: "arm64", x64: "amd64" };
9
+ const forwardedSignals = ["SIGTERM", "SIGINT", "SIGHUP"];
10
+ const ownPackage = require("../package.json");
11
+
12
+ function packageEntry(name, entry, expectedVersion, missingMessage) {
13
+ let installed;
14
+ let entryPath;
15
+ try {
16
+ installed = require(`${name}/package.json`);
17
+ entryPath = require.resolve(`${name}/${entry}`);
18
+ } catch {
19
+ throw new Error(missingMessage);
20
+ }
21
+ // npm may legitimately hoist dependencies. Check the resolved package's
22
+ // identity, rather than assuming a particular machine's node_modules path.
23
+ // A partial installation must not pick up an incompatible parent package.
24
+ if (installed.name !== name) {
25
+ throw new Error(`package identity mismatch for ${name}: found ${installed.name}; reinstall @ckanner/flywheel`);
26
+ }
27
+ if (typeof expectedVersion !== "string" || installed.version !== expectedVersion) {
28
+ throw new Error(`version mismatch for ${name}: expected ${expectedVersion}, found ${installed.version}; reinstall @ckanner/flywheel`);
29
+ }
30
+ return entryPath;
31
+ }
32
+
33
+ function loadNativePath() {
34
+ const platform = platforms[process.platform];
35
+ const arch = architectures[process.arch];
36
+ if (!platform || !arch) {
37
+ throw new Error(`unsupported platform ${process.platform}/${process.arch} (supported: darwin/linux × amd64/arm64)`);
38
+ }
39
+ const name = `@ckanner/flywheel-${platform}-${arch}`;
40
+ return packageEntry(name, "flywheel", ownPackage.optionalDependencies?.[name],
41
+ `platform package ${name} is missing; reinstall @ckanner/flywheel with optional dependencies enabled`);
42
+ }
43
+
44
+ function loadWorkerPath() {
45
+ const name = "@ckanner/dsh-plugin-flywheel";
46
+ return packageEntry(name, "worker", ownPackage.dependencies?.[name],
47
+ "the matching @ckanner/dsh-plugin-flywheel worker is missing; reinstall @ckanner/flywheel");
48
+ }
49
+
50
+ function loadCodexProWorkerPath() {
51
+ const name = "@ckanner/dsh-plugin-flywheel";
52
+ return packageEntry(name, "codex-pro-worker", ownPackage.dependencies?.[name],
53
+ "the matching @ckanner/dsh-plugin-flywheel Codex Pro worker is missing; reinstall @ckanner/flywheel");
54
+ }
55
+
56
+ function assertSupportedNode(version = process.versions.node) {
57
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:$|-)/.exec(version);
58
+ if (!match || Number(match[1]) < 22 || (Number(match[1]) === 22 && Number(match[2]) < 18)) {
59
+ throw new Error("Node.js 22.18.0 or newer is required");
60
+ }
61
+ }
62
+
63
+ function signalExitCode(signal) {
64
+ return constants.signals[signal] ? 128 + constants.signals[signal] : 1;
65
+ }
66
+
67
+ /** Preserve the native owner's cancellation and cleanup lifetime through npm. */
68
+ function runChild(command, args, { env = process.env, cwd, label = "flywheel" } = {}) {
69
+ return new Promise(resolve => {
70
+ const child = spawn(command, args, { env, cwd, stdio: "inherit", shell: false, detached: false });
71
+ let cancelledBy;
72
+ let spawnFailed = false;
73
+ const listeners = new Map();
74
+ for (const signal of forwardedSignals) {
75
+ const listener = () => {
76
+ cancelledBy ??= signal;
77
+ // Go owns its worker groups. Keep this wrapper alive until Go confirms
78
+ // its own exit; killing only this PID would orphan that cleanup stack.
79
+ child.kill(signal);
80
+ };
81
+ listeners.set(signal, listener);
82
+ process.on(signal, listener);
83
+ }
84
+ child.once("error", error => {
85
+ spawnFailed = true;
86
+ console.error(`${label}: could not start ${command}: ${error.message}`);
87
+ });
88
+ child.once("close", (code, signal) => {
89
+ for (const [name, listener] of listeners) process.removeListener(name, listener);
90
+ if (spawnFailed) return resolve(1);
91
+ if (signal) return resolve(signalExitCode(signal));
92
+ // A child trapping termination as exit 0 still cannot turn cancellation
93
+ // into success. Preserve the native program's nonzero failure otherwise.
94
+ if (cancelledBy && code === 0) return resolve(signalExitCode(cancelledBy));
95
+ resolve(code ?? 1);
96
+ });
97
+ });
98
+ }
99
+
100
+ module.exports = { loadNativePath, loadWorkerPath, loadCodexProWorkerPath, assertSupportedNode, runChild };
@@ -0,0 +1,157 @@
1
+ # 模型 profiles、阶段绑定与预检
2
+
3
+ [English](en/configuration.md) · 简体中文
4
+
5
+ Flywheel 在启动时加载一份完整配置,再把模型 profile 展开成每个阶段的实际绑定。模型选择是显式配置,不由另一个模型替你决定;修改配置后需要重新启动引擎。
6
+
7
+ ## 1. 配置来源
8
+
9
+ 发现顺序为任务目录、项目 Git 根目录、全局文件、内嵌默认表,选择第一个存在的完整文件。全局默认路径为 `~/.config/flywheel/flywheel.config.json`;`FLYWHEEL_CONFIG` 可覆盖全局文件路径,但不能越过已经存在的任务/项目配置。当前不通过 `XDG_CONFIG_HOME` 改变该默认路径。
10
+
11
+ 高优先级文件不可读、JSON 无效或符号链接失效时直接报错,不悄悄使用其他账号或模型配置。不同文件的 profiles、loops、routes、channels 不逐字段合并。
12
+
13
+ ```sh
14
+ flywheel config inspect --cwd /repo --task-dir /repo/docs/task --json
15
+ ```
16
+
17
+ 输出 `source`、`scope`、物理 `cwd/taskDir` 和按阶段排列的 `bindings`。`scope` 为 task/project/global/embedded。它只解析配置,不物化全局文件,不读取账号 token,不启动模型,也不创建任务状态。省略任务时只从项目及更低层发现配置,不会误读运行命令所在的另一个目录。
18
+
19
+ 展示值包含 Flywheel 已确定的 driver 默认:DSH/Codex/Kimi 的空 thinking 为 high,pi/omp/zcode 为 max;Kimi/Codex/Zcode 的空 model 使用各自 driver 常量。pi/omp 未指定模型时仍留空,表示 Flywheel 不能从配置确定它。`residentSession` 只有 Plan+Kimi+明确启用时为 true,不把其他阶段忽略的请求显示为已生效。
20
+
21
+ ## 2. 四个概念各负责什么
22
+
23
+ | 字段 | 含义 | 示例 |
24
+ |---|---|---|
25
+ | `harness` | 实际工具循环与进程后端 | `dsh`、`kimi`、`codex-pro`;`codex` 为显式旧 CLI 路径 |
26
+ | `provider` | DSH SDK 已注册的 LLM adapter ID;仅 DSH 使用 | `deepseek-official`、`chatgpt` |
27
+ | `channel` | 配置中的凭证/后备/计量渠道名称 | `deepseek-sdk`、`chatgpt-sdk`、`kimi-subscription` |
28
+ | `profile` | 当前 Flywheel 配置内的一组模型字段引用 | `flash`、`gpt-review`、`kimi` |
29
+
30
+ Flywheel 的 model profile 与 DSH 的 `sdk`/`desktop` profile 是两件事。前者复用模型定义,后者决定宿主实际加载的插件和授权配置。channel 别名本身不会切换 DSH 账号;给同一个 provider 起两个 channel 名称不等于拥有两份独立订阅或额度。
31
+
32
+ DSH 的实际 provider 按 `binding.provider` → `channels[channel].provider` → 旧 `channel` 名解析。前两者都明确配置而不相等时拒绝加载。兼容旧内联配置的最后一项只适用于 DSH;Codex Pro/旧 Codex CLI 和 Kimi 使用自己的登录状态,不接受一个会被静默忽略的 provider。
33
+
34
+ ### Codex Pro 默认与旧配置迁移
35
+
36
+ 0.28.0 的内嵌 hard 路由使用插件路径。下面是需放入自己完整配置的绑定片段,不是可替换全部配置的文件:
37
+
38
+ ```json
39
+ {
40
+ "routes": {
41
+ "hard": {
42
+ "harness": "codex-pro",
43
+ "model": "gpt-6.1-sol",
44
+ "channel": "chatgpt-subscription",
45
+ "thinking": "high"
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ 完整 profiles 模板使用 `profiles.codex-pro`,hard 路由引用它。插件依赖 `dsh-subagent-codex-pro@0.8.0` 由 npm 安装;worker 直接导入其公开 `dsh-subagent-codex-pro/machine` API,内部仍使用 Codex app-server 和用户已有登录。它不是 DSH LLM provider,也不需要父模型调用 `subagent_codex_pro`。缺少插件或回执不符会失败,不自动回退 `codex exec`。
52
+
53
+ 已有配置不自动改写。要从旧路径迁移,在 `config inspect` 显示的有效文件中修改 inline harness 或其 profile,保留所选 model/thinking/channel,然后重启引擎。保留 `harness: "codex"` 则明确继续使用旧 CLI。`codex-pro` 必须提供 model 和 channel,不沿用旧 Codex driver 的隐式模型;`resident-session: true` 会在加载时拒绝。执行/评审每次创建 ephemeral 新会话,thinking 省略时为 high。
54
+
55
+ `FLYWHEEL_CODEX_PRO_WORKER` 可显式指定 worker;通常由 npm 启动器解析同版本依赖。`FLYWHEEL_CODEX_BIN` 指定插件内部的 Codex 程序,默认 PATH 中的 `codex`;`CODEX_HOME` 可选择既有配置与登录目录。自定义环境需对启动引擎的进程可见,安装器不会复制凭证。
56
+
57
+ 插件在运行前确认模型、cwd、approval 和沙箱模式;Flywheel 在完整结果返回后检查实际策略及进程终态。workspace-write 允许工作区及 Codex 标准临时目录;额外的工作区外 writable roots、网络启用或不能确认的策略不能进入成功验收。**该完整策略检查发生在运行后,不能阻止自定义 Codex 配置已允许的访问。** 要求完整策略在任务前被拒绝的环境,应等待上游公开 pre-turn guard;doctor 会持续提示这一边界。
58
+
59
+ 指标的 `codex-pro-receipt` 保存完整 machine receipt,包含请求、实际配置、thread/turn、进程、清理、usage 和失败事实。effort 的 `accepted: true` 不等于服务器证明了实际推理强度,`effectiveEffort` 保留 null。仅 `usage.status: "complete"` 的本次增量进入已知预算;partial/unknown 原样留在回执并计入未知调用。插件不提供实际美元成本,原始 cost 保持 unknown;Flywheel 只有在用户显式配置 pricing 且 usage 完整时才能另行估算,估算不等于订阅账单。
60
+
61
+ ## 3. 从完整示例开始
62
+
63
+ 查看默认内联表或可复用 profiles 表:
64
+
65
+ ```sh
66
+ flywheel template flywheel.config.example.json
67
+ flywheel template flywheel.profiles.example.json
68
+ ```
69
+
70
+ 确认目标没有需要保留的配置后,选择其中一份导出到项目根。已有配置先备份并审阅差异,避免 shell 重定向覆盖它。下面只是解释结构的片段;完整可用起点在第二份模板中:
71
+
72
+ ```json
73
+ {
74
+ "profiles": {
75
+ "flash": {
76
+ "harness": "dsh",
77
+ "provider": "deepseek-official",
78
+ "channel": "deepseek-sdk",
79
+ "model": "deepseek-flash",
80
+ "thinking": "high"
81
+ },
82
+ "gpt": {
83
+ "harness": "dsh",
84
+ "provider": "chatgpt",
85
+ "channel": "chatgpt-sdk",
86
+ "model": "gpt-6.1-sol",
87
+ "thinking": "high"
88
+ }
89
+ },
90
+ "loops": {
91
+ "review": { "profile": "gpt" },
92
+ "judge": { "profile": "gpt", "model": "gpt-6-astra" },
93
+ "intake": { "profile": "flash", "thinking": "low" }
94
+ },
95
+ "routes": {
96
+ "default": { "profile": "flash" }
97
+ },
98
+ "channels": {
99
+ "deepseek-sdk": { "harness": "dsh", "provider": "deepseek-official", "env-key": null },
100
+ "chatgpt-sdk": { "harness": "dsh", "provider": "chatgpt", "env-key": null }
101
+ }
102
+ }
103
+ ```
104
+
105
+ 这些模型名沿用本仓库的初始绑定,能否使用仍由实际账号和 adapter 决定。读取配置、看到模型名称、adapter 接受参数都不等于账号已获授权。DeepSeek 默认仅 Flash;profiles 不引入自动跨渠道付费回退。
106
+
107
+ ### 覆盖与错误规则
108
+
109
+ | 情况 | 行为 |
110
+ |---|---|
111
+ | 阶段省略某字段 | 继承所引用 profile 的字段 |
112
+ | 阶段显式写 `model`/`channel`/`thinking` 等 | 只覆盖该阶段,不修改共享 profile 或其他阶段 |
113
+ | `thinking: ""` | 使用对应 harness 的现有默认 effort,不重新继承 profile |
114
+ | `resident-session: false` | 明确关闭该阶段的恢复请求;不会丢失为缺省值 |
115
+ | `judge-panel: null` | 关闭第二意见,保持默认行为 |
116
+ | profile 定义缺 `harness/model/channel`,或这些字段为空/null/纯空白 | 加载失败 |
117
+ | profile 不存在、未知字段、错拼阶段名、不支持的 harness | 加载失败,不静默回落 |
118
+ | profile 再引用另一个 profile | 不支持继承链;加载失败 |
119
+ | `_note`、`_note_*` | 允许配置对象中的说明文字,不进入执行绑定 |
120
+
121
+ `resident-session` 是阶段策略,不能放进 profile。当前只有 Kimi 的 Plan 可恢复历史,且必须匹配实际 harness/model/channel/cwd/thinking;profile 名改了而实际值没变,不影响身份。其他执行、评审和 SDK 调用仍是新会话。
122
+
123
+ 旧内联绑定保留原有缺省行为,不强制迁移。`env-key: null`、`retry.jitter: 0`、`worktree.enabled: false` 等已有合法零值继续支持。显式切换 harness 或 channel 时,要一起核对继承的 provider;必要时用 `provider: ""` 清除显式值,再按新 channel 解析,不能保留另一个 adapter 的 ID。
124
+
125
+ ## 4. 后备渠道与记录
126
+
127
+ `channels[channel].fallback` 仍是人工配置的有界后备链,只有既有错误分类/重试策略允许时才使用。切换后依据新渠道重新选择 provider 和模型;不会继续沿用原渠道的 provider。后备渠道省略 harness 时继承当前 harness,不回到链起点的后端。
128
+
129
+ 指标分别保存 `harness`、`provider`、`channel`、`model`。provider 来自 driver 可确认的结果,未知时为 null;channel 继续描述配置渠道。usage 和美元成本未报告时保持 unknown,订阅不会被当成已知零成本。profiles 不改变计费规则,也不会绕过 hard budget。
130
+
131
+ ## 5. 预检范围和结果
132
+
133
+ ```sh
134
+ flywheel doctor --cwd /repo --json
135
+ flywheel doctor --cwd /repo --task-dir /repo/docs/task --json
136
+ flywheel doctor --cwd /repo --task-dir /repo/docs/task --probe --json
137
+ ```
138
+
139
+ 没有任务时检查全部配置绑定。有任务时校验 Spec/packet,仅把 Plan、Review、Judge、已启用的第二意见和实际 packet 路由列为必需;其他绑定显示 skipped。任务尚未生成 packet 时检查默认执行路由并提示规划后复查。mock driver 跳过真实工具检查,不冒充模型已验证。
140
+
141
+ 预检可提前运行,正式 `run`/`control start` 仍需至少一个已准备并审阅的合法 packet;没有 packet 的 warn 不表示引擎已经能从空任务开始。绑定中的 permissionMode 表示阶段要求,实际实施能力还取决于 harness,尤其 Kimi CLI 不能据此宣称拥有 OS 沙箱。
142
+
143
+ 本地检查确认命令可用、版本响应可靠,以及声明的环境变量存在且非空,不显示变量值。`--probe` 才启动 DSH worker 验证 adapter 接受配置、cwd、完整沙箱、approval policy 和 SDK 正常关闭。相同 provider、channel、model、effort、cwd 和权限的探测去重;不同账号别名或读写权限不能相互代替。整体验证预算 40 秒,另留进程清理窗口。
144
+
145
+ Codex Pro 本地检查额外实际导入公开 machine API,核对精确 0.8.0/协议 1、Node >=22.18.0 以及 Codex 命令版本;同一 worker 只查一次。`--probe` 不为此路径发送任务,实际模型、effort 与完整权限保持 unknown;它不会把成功导入插件当成账号、沙箱或推理能力已经验证。
146
+
147
+ | 状态 | 解释 |
148
+ |---|---|
149
+ | `pass` | 本项已取得对应证据;只对该项有效 |
150
+ | `fail` | 存在阻断项,整体 `ok:false` 且退出码 1 |
151
+ | `warn` | 限制或规划前状态,需要结合实际任务理解 |
152
+ | `unknown` | 未验证,例如账号授权、额度与真实模型服务能力 |
153
+ | `skipped` | 非必需绑定、mock、重复探测或前置检查失败等 |
154
+
155
+ 任何 probe 都不发送 `session/prompt`、不运行 L1/L2/L3、不写 Flywheel 状态或创建工作树;SDK 初始化可能访问 provider 目录并产生自己的会话/缓存文件。只读指 Flywheel 配置与任务状态,不表示整个宿主文件系统绝对无写入。
156
+
157
+ 桌面面板、`/flywheel` 和 `flywheel_control` 共用 `control config|doctor`,可省略 taskDir;只有 doctor 支持 `probe:true`。诊断阻断与传输失败分开显示。桌面使用的 PATH/profile/环境可能与终端不同,安装后应在面板里再检查一次。
@@ -0,0 +1,193 @@
1
+ # Model profiles, stage bindings, and diagnostics
2
+
3
+ [English README](../../README.en.md) · [中文](../configuration.md) · [Workflow](workflow.md)
4
+
5
+ Flywheel loads one complete configuration, expands its model profiles, and resolves each stage's binding before starting. Model selection is explicit configuration. Restart the engine after changing it. This guide describes the 0.28.0 implementation; existing user configuration is not automatically migrated during installation.
6
+
7
+ ## Configuration precedence
8
+
9
+ The discovery order is:
10
+
11
+ 1. Task directory: `flywheel.config.json`.
12
+ 2. Project Git root: `flywheel.config.json`.
13
+ 3. Global file: `~/.config/flywheel/flywheel.config.json`.
14
+ 4. Embedded defaults.
15
+
16
+ Flywheel selects the **first complete file**. It does not merge fields, profiles, loops, routes, or channels across those locations. `FLYWHEEL_CONFIG` replaces the global file path only; it does not override an existing task or project file. `XDG_CONFIG_HOME` does not change the current default path.
17
+
18
+ An unreadable higher-priority file, invalid JSON, or broken symlink is an error, not permission to use a different account's configuration. Inspect the selected source before editing:
19
+
20
+ ```sh
21
+ flywheel config inspect --cwd /absolute/path/to/repo --task-dir /absolute/path/to/repo/docs/my-feature --json
22
+ ```
23
+
24
+ The result includes `source`, `scope`, resolved physical paths, and stage `bindings`. Without `--task-dir`, discovery starts at the selected project and lower levels. Inspection does not create global configuration or task state, read account tokens, or invoke a model.
25
+
26
+ ## Fields and identities
27
+
28
+ | Field | Meaning | Example |
29
+ |---|---|---|
30
+ | `harness` | Process and tool-loop backend | `dsh`, `kimi`, `codex-pro`; `codex` is the explicit older CLI path |
31
+ | `provider` | Registered DSH SDK LLM adapter ID; applies to DSH bindings | `deepseek-official`, `chatgpt` |
32
+ | `channel` | A configuration label used for credentials, fallback, and accounting | `deepseek-sdk`, `chatgpt-sdk`, `kimi-subscription` |
33
+ | `model` | Exact model identifier passed to the selected backend | `deepseek-flash`, `kimi-code/k3.1-pro`, `gpt-6.1-sol` |
34
+ | `thinking` | Requested reasoning effort | `high` |
35
+ | `profile` | Reusable model fields inside this Flywheel configuration | `flash`, `gpt-review`, `codex-pro` |
36
+
37
+ A Flywheel model profile is different from a DSH `sdk` or `desktop` profile. The latter chooses the host's plugins and authorization configuration. A channel alias alone does not switch DSH accounts or create a separate subscription or quota pool.
38
+
39
+ For DSH, the provider resolves from `binding.provider`, then `channels[channel].provider`, then the legacy channel name. Explicit binding and channel provider values must agree. Kimi and Codex use their own login state; a DSH provider cannot be attached to those harnesses and silently ignored.
40
+
41
+ Only supported effort values should be used. The checked DSH Flash adapter accepts `off/low/high/max`; it has no `medium`. A configuration value names an intended binding, not evidence of account entitlement or measured model performance.
42
+
43
+ ## Start from a complete template
44
+
45
+ Inspect either bundled example:
46
+
47
+ ```sh
48
+ flywheel template flywheel.config.example.json
49
+ flywheel template flywheel.profiles.example.json
50
+ ```
51
+
52
+ The first uses inline bindings. The second is a complete configuration with reusable profiles. If you choose to create a project configuration, first confirm that no file needs preserving; then export one template from the target Git root. For example, Bash's noclobber option prevents replacing an existing regular file:
53
+
54
+ ```sh
55
+ (set -C; flywheel template flywheel.profiles.example.json > flywheel.config.json)
56
+ ```
57
+
58
+ Do not create a task file containing only a few profiles and expect it to inherit the rest from the project file. An illustrative **fragment**, to incorporate into a complete file, is:
59
+
60
+ ```json
61
+ {
62
+ "profiles": {
63
+ "flash": {
64
+ "harness": "dsh",
65
+ "provider": "deepseek-official",
66
+ "channel": "deepseek-sdk",
67
+ "model": "deepseek-flash",
68
+ "thinking": "high"
69
+ }
70
+ },
71
+ "routes": {
72
+ "default": { "profile": "flash" }
73
+ },
74
+ "loops": {
75
+ "intake": { "profile": "flash", "thinking": "low" }
76
+ },
77
+ "channels": {
78
+ "deepseek-sdk": {
79
+ "harness": "dsh",
80
+ "provider": "deepseek-official",
81
+ "env-key": null
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ `loops` binds Plan, Review, Judge, Intake, and Garden. `routes` selects the Execute backend for default/frontend/hard packets. `drivers` selects real or mock drivers; it is not a provider-name mapping. The full default routing table is in the [README](../../README.en.md#default-model-routes).
88
+
89
+ ### Profile override and validation rules
90
+
91
+ | Input | Effect |
92
+ |---|---|
93
+ | Stage omits a profile field | Inherit that field from the referenced profile |
94
+ | Stage explicitly supplies `model`, `thinking`, `channel`, etc. | Override only that stage |
95
+ | `thinking: ""` | Use the harness's default effort instead of re-inheriting the profile |
96
+ | `resident-session: false` | Explicitly disable the stage's resume request |
97
+ | `judge-panel: null` | Disable the optional second opinion |
98
+ | Missing/empty profile `harness`, `model`, or `channel` | Configuration error |
99
+ | Unknown profile, field, stage name, or harness | Configuration error |
100
+ | A profile references another profile | Unsupported; configuration error |
101
+ | `_note` or `_note_*` | Permitted explanatory metadata, excluded from execution bindings |
102
+
103
+ `resident-session` belongs on a stage, not in a profile. Currently only Kimi Plan can resume, and only when harness, model, channel, working directory, and thinking match. This restores session history, not a permanently running process. Execution and review calls start fresh sessions.
104
+
105
+ Legal existing zero values, including `env-key: null`, `retry.jitter: 0`, and `worktree.enabled: false`, remain meaningful. When changing a harness or channel, also inspect any inherited provider. Use `provider: ""` when necessary to clear an explicit value before resolving the newly selected DSH channel.
106
+
107
+ ## Codex Pro and migration
108
+
109
+ The 0.28.0 embedded hard route uses this binding. This is a **fragment**, not a complete replacement configuration:
110
+
111
+ ```json
112
+ {
113
+ "routes": {
114
+ "hard": {
115
+ "harness": "codex-pro",
116
+ "model": "gpt-6.1-sol",
117
+ "channel": "chatgpt-subscription",
118
+ "thinking": "high"
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ The complete profile template uses `profiles.codex-pro` and references it from `routes.hard`. The npm dependency is exactly `dsh-subagent-codex-pro@0.8.0`. The worker imports its public `dsh-subagent-codex-pro/machine` entry and calls `runMachine` using protocol 1. The plugin itself runs Codex app-server and uses the existing Codex login.
125
+
126
+ The external tool name `subagent_codex_pro` is not a DSH provider. This route does not create a parent DSH model to make a second delegation. Missing dependencies or invalid receipts fail; they do not trigger an implicit fallback to the older CLI driver.
127
+
128
+ To migrate an existing configuration, use `config inspect` to locate the effective file. Change the hard route's inline harness or referenced profile from `codex` to `codex-pro`, retain the intended model/channel/thinking, inspect the result, and restart. Keeping `harness: "codex"` explicitly retains the older `codex exec` behavior.
129
+
130
+ `codex-pro` requires nonempty model and channel values. It does not inherit the old Codex driver's implicit model. Omitted thinking defaults to `high`; `resident-session: true` is rejected. Each execute/review call uses a new ephemeral session.
131
+
132
+ ### Worker environment
133
+
134
+ | Variable | Purpose |
135
+ |---|---|
136
+ | `FLYWHEEL_DSH_WORKER` | Optional DSH worker override; the npm launcher normally resolves the matching dependency |
137
+ | `FLYWHEEL_DSH_BIN` | DSH command, otherwise `dsh` on PATH |
138
+ | `FLYWHEEL_DSH_PROFILE` | SDK profile, default `sdk`; it must not be `desktop` |
139
+ | `DSH_HOME` | Optional DSH configuration root, interpreted by DSH |
140
+ | `FLYWHEEL_KIMI_BIN` | Kimi CLI override, otherwise `kimi` on PATH |
141
+ | `FLYWHEEL_CODEX_PRO_WORKER` | Optional Codex Pro worker override; normally resolved by npm |
142
+ | `FLYWHEEL_CODEX_BIN` | Codex executable used by the plugin, otherwise `codex` on PATH |
143
+ | `CODEX_HOME` | Existing Codex configuration/login directory, interpreted by Codex |
144
+
145
+ These settings must be visible to the process starting the engine. The installer does not copy authentication data. The managed GUI launcher records executable paths and the installation-time tool PATH; a terminal export alone does not change an already running desktop process.
146
+
147
+ ### Permissions, receipts, and cleanup
148
+
149
+ The plugin checks the requested model, working directory, approval policy, and sandbox mode before a turn. Flywheel examines the complete returned policy afterward. Workspace-write permits the workspace and standard Codex temporary directories; extra external write roots, enabled network, or unconfirmed policy make the result ineligible for successful delivery.
150
+
151
+ **The complete policy check happens after execution. It cannot prevent access already allowed by custom Codex configuration.** An environment requiring a full policy rejection before any turn needs an upstream public pre-turn guard. Kimi's current CLI integration also provides no OS-level sandbox guarantee; a configured permission requirement should not be read as proof of its enforcement.
152
+
153
+ The `codex-pro-receipt` metric preserves the complete machine receipt, including requested and actual configuration, thread/turn IDs, process outcomes, cleanup, usage, and failures. An accepted effort request is not proof of effective server reasoning effort; `effectiveEffort` remains null.
154
+
155
+ An abnormal worker exit does not prove the plugin's separately managed processes stopped. If cleanup cannot be confirmed after startup, the engine records the evidence, escalates, and ends the run instead of automatically retrying or falling back. Before resuming, confirm the owned app-server/guardian processes have terminated. See [recovery](workflow.md#7-stop-and-recover-without-losing-evidence).
156
+
157
+ ## Fallback and budgets
158
+
159
+ Default profiles do not enable automatic paid fallback across channels. An explicitly configured `channels[channel].fallback` is a bounded chain and is used only when error classification and retry policy permit it. A fallback resolves provider and model for the newly selected channel; an omitted harness inherits the current harness.
160
+
161
+ Metrics retain harness, provider, channel, and model separately. Provider remains null if the driver cannot confirm it. Missing usage and dollar cost remain unknown, including subscription calls. Numeric aggregates are lower bounds of known usage, not a complete bill.
162
+
163
+ For Codex Pro, only complete usage for the current run enters known token accounting; partial/unknown usage remains in the receipt and counts as unknown calls. The plugin does not provide actual dollar billing. Explicit user pricing plus complete usage can produce an estimate, which remains distinct from the subscription bill.
164
+
165
+ The default token budget is soft, with a daily limit of 2,000,000 tokens. A soft budget reports uncertainty. A hard budget stops when unknown usage makes compliance unverifiable. Disabling enforcement does not turn off accounting or convert unknown values to zero.
166
+
167
+ Acceptance commands use a restricted environment. If the project requires additional tool variables such as `GOCACHE` or `GOPATH`, add their names to `verify.env-allowlist` and ensure they exist in the engine's environment. Do not put secret values into task contracts or committed configuration.
168
+
169
+ ## Diagnose without inference
170
+
171
+ ```sh
172
+ flywheel doctor --cwd /absolute/path/to/repo --json
173
+ flywheel doctor --cwd /absolute/path/to/repo --task-dir /absolute/path/to/repo/docs/my-feature --json
174
+ flywheel doctor --cwd /absolute/path/to/repo --task-dir /absolute/path/to/repo/docs/my-feature --probe --json
175
+ ```
176
+
177
+ Without a task, doctor checks all configured bindings. With a task, it validates the Spec/packets and requires Plan, Review, Judge, enabled second opinions, and routes used by the packets; other bindings can be skipped. With no packets yet, it checks the default execution route and reports that preparation must finish. A real run still needs at least one reviewed legal packet.
178
+
179
+ Local checks cover commands, versions, directories, configuration, and declared nonempty environment variables, without showing their values. `--probe` additionally initializes DSH SDK sessions, verifies accepted configuration, working directory, permissions, and normal shutdown. Identical probes are deduplicated only when their relevant identity and permissions match. The probe budget is 40 seconds plus bounded process cleanup time.
180
+
181
+ Codex Pro checks actually import the public API and verify exact version 0.8.0/protocol 1, Node compatibility, and the Codex executable. `--probe` does not send a task through this path: account/model/effort and full runtime policy remain unverified. A successful import is not a successful model call.
182
+
183
+ | Status | Meaning |
184
+ |---|---|
185
+ | `pass` | Evidence supports this individual check only |
186
+ | `fail` | A blocker; overall `ok:false` and exit code 1 |
187
+ | `warn` | A limitation or preparation state requiring interpretation |
188
+ | `unknown` | Not verified, such as entitlement, quota, or real inference |
189
+ | `skipped` | Not required, mock mode, duplicate probe, or an unmet prerequisite |
190
+
191
+ No probe sends an inference prompt, runs L1/L2/L3, creates a worktree, or writes Flywheel task state. SDK initialization may contact a provider's catalog and write its own cache/session files; read-only here is scoped to Flywheel state and configuration. Recheck in the desktop panel because its PATH/profile/environment may differ from the terminal.
192
+
193
+ Repository references: [inline defaults](https://github.com/ckanner/flywheel/blob/main/templates/flywheel.config.example.json), [complete profiles](https://github.com/ckanner/flywheel/blob/main/templates/flywheel.profiles.example.json), [design](https://github.com/ckanner/flywheel/blob/main/docs/design.md), and [worker protocol](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/PROTOCOL.md).