DSH / Atlas
2026-07-31implementedfeaturearchived 2026-08-03

experimental subcommands gate behind `--experimental` or `DSH_EXPERIMENTAL=1`

实验性子命令由 `--experimental` 或 `DSH_EXPERIMENTAL=1` 把守

The `meta` and `upgrade` entry points carried their experimental status in their names: `dsh experimental-meta` and `dsh experimental-upgrade`. The prefix made every invocation verbose, and renaming a command at stabilization would break every reference to it — muscle memory, scripts, and docs alike. The status belongs in an opt-in gate, not in the name.

English

Problem

The meta and upgrade entry points carried their experimental status in their names: dsh experimental-meta and dsh experimental-upgrade. The prefix made every invocation verbose, and renaming a command at stabilization would break every reference to it — muscle memory, scripts, and docs alike. The status belongs in an opt-in gate, not in the name.

Decision

dsh experimental-meta is dsh meta and dsh experimental-upgrade is dsh upgrade. Each runs only when the invocation passes its --experimental flag or the environment carries DSH_EXPERIMENTAL=1; otherwise the command fails loud on stderr with exit 1, naming both opt-ins. Per the pre-release stance, the old names are gone with no aliases, and args.spec.ts pins their rejection.

The gate has two halves with one owner each. The per-invocation half is a Commander --experimental option on each experimental subcommand, checked inside its action after the leaked-parent-option rejection. The environment half is a boolean parseDshArgs parameter: bin.ts reads process.env.DSH_EXPERIMENTAL === '1' at the process boundary (after loadEnv, so a project .env can set it) and passes the result down, so the parser's environment dependency is explicit in its signature and the tests need no env mutation. 1 is the only enabling value — the variable is an explicit opt-in, not a truthiness check.

Stabilizing a command later means deleting its --experimental option and requireExperimental call; the name does not move.

Testing

args.spec.ts pins both admit paths, bare-name rejection, old-name rejection, and leaked-option rejection under the env opt-in. built-bin.e2e.ts proves the assembled entry end to end: the gate diagnostic on stderr with exit 1, and that --experimental, DSH_EXPERIMENTAL=1, but not DSH_EXPERIMENTAL=0, reach the TUI's piped-stdio refusal — the next gate past this one. Both gated commands were also verified interactively in tmux: dsh meta --experimental and DSH_EXPERIMENTAL=1 dsh meta boot the TUI over the checkout, and DSH_EXPERIMENTAL=1 dsh upgrade seeds the dsh-upgrade skill.

Alternatives considered

Keep the experimental- name prefix. Rejected by the user's direction: the prefix taxes every invocation, and stabilization would be a breaking rename instead of deleting a gate.

A parent-level --experimental flag (dsh --experimental meta). Rejected: the default surface is deliberately option-only with enablePositionalOptions, so parent options that leak across the subcommand boundary are treated as mistyped invocations. A parent flag consumed only by two subcommands would be exactly the leaked-option shape the adapter rejects everywhere else.

Read process.env inside parseDshArgs. Rejected: the repo validates at the process boundary and keeps typed seams pure; tests would have to mutate and restore process.env around each case.

Accept any non-empty DSH_EXPERIMENTAL. Rejected: the telemetry switch prefers off-by-mistake for a privacy control, but an experimental gate is an acknowledgement — DSH_EXPERIMENTAL=0 must not enable the commands it names.

Consequences

Daily invocations shorten to dsh meta --experimental and dsh upgrade --experimental, and a developer who sets DSH_EXPERIMENTAL=1 in their environment gets the bare dsh meta/dsh upgrade. dsh --help marks both commands (experimental). The gate costs one extra flag or env var until a command stabilizes, at which point the gate is deleted and the name is already final.

中文

Problem

metaupgrade 两个入口把实验性状态写在名字里:dsh experimental-metadsh experimental-upgrade。前缀让每次调用都变得冗长,而在稳定时重命名命令会破坏对它的所有引用——肌肉记忆、脚本与文档皆然。这种状态应当由一个显式选择加入的门槛承载,而不是由名字承载。

Decision

dsh experimental-meta 改为 dsh metadsh experimental-upgrade 改为 dsh upgrade。二者只有在调用时传入各自的 --experimental 标志、或环境中带有 DSH_EXPERIMENTAL=1 时才会运行;否则命令在 stderr 上明确报错并以退出码 1 结束,同时指明两种选择加入方式。依据发布前立场,旧名称已移除且没有别名,args.spec.ts 钉住了对它们的拒绝。

该门槛分为两半,各有其归属。按调用的一半是每个实验性子命令上的 Commander --experimental 选项,在其 action 内、泄漏父级选项的拒绝之后检查。环境的一半是 parseDshArgs 的一个布尔参数:bin.ts 在进程边界读取 process.env.DSH_EXPERIMENTAL === '1'(在 loadEnv 之后,因此项目 .env 也可以设置它)并向下传递结果,因此解析器对环境的依赖显式体现在签名中,测试也无需改动环境变量。1 是唯一的启用值——该变量是显式的选择加入,而不是真值判断。

之后要稳定某个命令,只需删除它的 --experimental 选项和 requireExperimental 调用;名字不再变动。

Testing

args.spec.ts 钉住两条准入路径、裸名称拒绝、旧名称拒绝,以及在环境选择加入下对泄漏选项的拒绝。built-bin.e2e.ts 端到端地证明组装后的入口:stderr 上的门槛诊断与退出码 1,以及 --experimentalDSH_EXPERIMENTAL=1(而非 DSH_EXPERIMENTAL=0)会到达 TUI 的管道 stdio 拒绝——即此门之后的下一道关卡。两个被把守的命令还在 tmux 中做了交互式验证:dsh meta --experimentalDSH_EXPERIMENTAL=1 dsh meta 以检出目录为 workspace 启动 TUI,DSH_EXPERIMENTAL=1 dsh upgrade 播种 dsh-upgrade skill。

Alternatives considered

保留 experimental- 名称前缀。 按用户的指示拒绝:前缀让每次调用都付出代价,稳定时也会变成破坏性的重命名,而不是删除一个门槛。

父级 --experimental 标志(dsh --experimental meta)。 拒绝:默认界面刻意保持纯选项形式并启用 enablePositionalOptions,跨子命令边界泄漏的父级选项都被视为拼错的调用。一个只被两个子命令消费的父级标志,恰恰就是适配器在其他所有地方都拒绝的泄漏选项形态。

parseDshArgs 内部读取 process.env 拒绝:本仓库在进程边界做验证,并保持类型化接缝的纯粹性;否则测试必须在每个用例前后修改并恢复 process.env

接受任何非空的 DSH_EXPERIMENTAL 拒绝:遥测开关作为隐私控制倾向于误关而非误开,但实验性门槛是一种确认——DSH_EXPERIMENTAL=0 绝不能启用它所指名的命令。

Consequences

日常调用缩短为 dsh meta --experimentaldsh upgrade --experimental;在环境中设置了 DSH_EXPERIMENTAL=1 的开发者可以直接使用 dsh meta/dsh upgradedsh --help 将这两个命令标注为 (experimental)。在命令稳定之前,门槛的代价是一个额外的标志或环境变量;稳定时删除门槛即可,名字已是最终形态。