feat: step-code 终端 AI 编码助手探索分支架构快照(v0.1.2) - #107
Open
li-xiu-qi wants to merge 164 commits into
Open
Conversation
- .step-code/agents/*.md 的 model 字段支持 [models.<别名>] 解析 - 命中别名时独立构造 provider,携带对应 base_url/api_key/capabilities/maxContextSize - 自定义子 agent 角色(非 general/explore)自动进入主 agent system prompt - --resume/-r//resume 恢复会话时,若存储模型别名失效自动回退 config.model
- 新增表格块渲染:按显示宽度对齐列宽,宽字符按 2 列计算 - 修复裸 URL / 自链接被打印两遍的问题 - 新增 markdownTable 回归测试
- 说明子 agent model 字段推荐写 Step 系列模型别名 - 补充自定义角色覆盖优先级、进入主 agent system prompt 的机制 - AGENTS.md 已具备能力列表同步子 agent 跨渠道解析、自定义角色可见性、恢复回退 - 移除别家 CLI/模型作为默认示例,统一 Step 官方口径
- 版本号对齐 0.1.0 - 安装说明以源码 clone + pnpm build 为主路径 - 补充 npm 全局安装(v0.1.0 起)作为后续注册 npm registry 后的方式 - package.json description 改为 Step 官方口径 - .env.example api_key 配置位置同步多渠道模型配置
- 交互文档补充 Ctrl+B 前台任务转后台说明 - 快速开始文档 provider 示例从 [providers.default] 改为 [providers.stepfun]
- 记录子 agent 跨渠道解析、自定义角色可见性、恢复回退 - 记录 UI 修复与文档口径统一 - 版本号 0.1.0
- 包含 provider 边界层、TUI 组件、工具循环、skill/MCP/插件系统、 后台任务、自主目标、定时任务、动态工作流等完整实现 - 基于上游 step-code-explore 空分支从零构建
- 安装文档明确当前主要方式是源码 clone + pnpm build + pnpm link --global - npm install -g step-code 改为 v0.1.0 起计划注册 npm 后可用 - 同步中英文 docs 与 step-code-install skill
- 搜索请求发到当前会话渠道 base_url 拼 /step_plan/v1/search,用当前渠道 key - 主会话必须走阶跃渠道,否则搜索 404 / 鉴权失败 - 子 agent 的搜索跟随主会话渠道,不跟随子 agent 自己的 model 别名 - 404 / 连接失败的排查指引同步进错误分流说明
模型思考块只回签名、不回思考正文时(content_block_start[thinking] → signature_delta → content_block_stop,零思考增量),过去 UI 完全没有信号, 只剩通用忙碌态 spinner,与请求卡死无从区分。 现在思考块的开始与结束作为事件独立上抛(thinking_start / thinking_end), 状态行在整个思考块期间显示「思考中…」(无正文时只有这一行标题), 思考块结束即收起。同时把忙碌态 spinner 的随机状态词从思考类换成中性 「处理中/推进中/…」,思考态由「思考中…」独家表达。 合并自 PeronGH 的 PR #1,整合到目标分支已演进的双语 docs 与新 CHANGELOG 结构中:docs/interactive.md(旧版单文件)已弃用,说明并入 docs/zh|en/interactive.md 的「推理过程显示」段。 Co-authored-by: Peron <peron_ap@icloud.com> Closes #1
初始化三位贡献者:li-xiu-qi(项目作者与维护者)、PeronGH(无痕思考修复)、 ZouR-Ma(step-code-explore 分支探索的支持与指导)。约定:外部 PR 因目标分支 结构调整需改写提交时,以 Co-authored-by 保留原作者身份并在本文件登记贡献。
web_search / web_image_search 此前复用主会话模型渠道的 base_url + api_key, 主会话切到非阶跃渠道时搜索请求打到错误地址 404。搜索是阶跃平台专属能力, 与主会话用哪家模型在业务上无关,因此独立配置。 - config.toml 新增 [search] 段:三层结构([search] 通用兜底、[search.web] 覆盖内容搜索、[search.image] 覆盖文搜图),字段全可选 - endpoint 解析优先级:专用段 → 通用段 → 主会话渠道;独立配置 url 视为 精确意图不裁剪,兜底主会话时沿用旧的 /v1 归一化(向后兼容) - 独立配置只给 url 没给 key 时 key 回退主会话 apiKey - ctx.searchConfig 注入主会话(main.tsx)、子 agent(runner.ts,跟随主会话 不跟随子 agent model 别名)、/reload 热重载(App.tsx 两处) - 文档:configuration.md 中英 [search] 段说明,tools.md 中英 endpoint 解析 与 api/plan 双通道说明(内容搜索 api+plan 双通道,文搜图仅 plan) - 单测 tests/configSearch.test.ts 13 个全过
防漂移测试(updateConfigDrift)发现新增 [search] 顶层键未同步三处: - doctor.ts CONFIG_TOP_LEVEL_KEYS 补 'search' - updateConfig skill 内嵌 schema 表补 [search] 段(url/key + [search.web]/[search.image]) 该测试是 update-config skill 自包含路线的漂移保险,加键不加表即变红。 顺带验证 questionPrompt 单跑 16/16 通过,全量下的那个失败是 TUI known flaky,与本改动无关。
修复一处不一致:package.json / src/version.ts 仍是 0.4.0,而 docs 与 step-code-install skill 已写 v0.1.0、tag 也已打 v0.1.0。 根因:早前改过这三处,但后续为整理分支历史、基于上游空分支重建 step-code-explore 时,git checkout <branch> -- . 拉的是改版本号之前的文件状态, 把那次改动覆盖了。tests/version.test.ts 只断言 package.json 与 version.ts 互相一致(同为 0.4.0 也算一致),因此未变红、未被发现。 - package.json version → 0.1.0 - src/version.ts VERSION → 0.1.0 - CHANGELOG [Unreleased] → [0.1.0] - 2026-08-02,正文改为从零重构首个探索版本 - tests/session/debugBundle.test.ts 硬编码断言同步 全量 pnpm test 1841 passed / 145 files,typecheck 通过。
Esc / Ctrl+C 中断后,历史里只剩一段残文,模型无法区分「自己出错」 与「用户主动叫停」,下一轮常出现道歉或重复解释上一轮。 runAgent 在 stopReason === 'aborted' 时追加一条 origin: 'injection' 的 user 消息说明中断事实。选 injection 而非新事件类型:该 origin 已有 五处消费点(UI 隐藏、压缩排除、undo 跳过、fork 边界、通知同类), 零新增基础设施;runAgent 又是 aborted 的单点出口,覆盖全部中断场景。
表格此前按内容自然宽度渲染、超宽交给终端硬折行,导致宽表溢出屏幕、 单元格不折行、中文列把其它列挤出后整体错位。 Markdown 组件加可选 width prop,由 App/MessageList/ExpandViewer 透传 终端列数;不在组件内取 process.stdout.columns,保持纯函数可测。 列宽分配两遍扫描(自然宽度 + 最长词兜底,30 列封顶),超出可用宽度时 按 grow potential 比例收缩、余数逐列补齐。单元格走 StyledSegment 线性化 后 greedy wrap——Ink 环境无法对 React 节点做 ANSI 折行,必须拆成 「提取样式」与「按宽折行」两步;CJK 逐字成断点,拉丁串按空格聚合。 边框开销按 3n+1 预留并渲染完整外框,两者必须配套:只画列间中缝会让 每行恒比终端窄 4 列。width 为 undefined 时走原自然宽度逻辑;可用宽度 不足每列 1 字符时回退原始 markdown。 新增 5 例覆盖无 width / 宽度充足 / 比例收缩 / 极窄回退 / 宽字符分配。
0.1.0 起每版发布 SEA 可执行文件与 npm tarball 两个产物,安装主路径从 「源码 clone + 构建」改为「从 Release 下载」,源码安装降为需要最新特性时的备选。 docs/zh 与 docs/en 的 installation、skills/step-code-install/SKILL.md 三处的安装/升级/卸载/故障排查全部同步;npm 全局安装仍标注为注册后可用。 package.json 加 prepack 钩子:npm pack 时自动 build,保证 tarball 内 dist/ 完整,装完即用,不依赖使用方本地构建。
机制早已完备(并行、后台、resume、跨渠道),但提示词里委派只有一行, 模型很少主动派生。本轮补齐三处缺口:工具描述过薄、内置角色描述从未 进提示词、委派纪律无并行意识。 - spawn_agent 描述扩为三段式(~180 → ~900 字符):写 prompt 四条、 不该派生的三类情形、派生后守则。后台 anti-pattern 并入 schema describe - AgentDefinition 增可选 whenToUse(兼容 when_to_use 蛇形写法), 与 description 分工:一个答「是什么」,一个答「何时选」 - subagentListing 改为列全部角色(含内置 general/explore,此前被显式过滤), 拼接 whenToUse,加 skillListing 同款三级预算降级 - 内置两角色 prompt 重写:补结果契约段;explore 要求给路径行号、 查不到明说、区分事实与推断;general 补范围超限如实说明 - 子 agent 结果加状态头 subagent/status/session,error 附 resume 提示。 格式化放工具层,SubagentResult.summary 保持纯净不污染落盘 - 删除 describeAgents 死代码(全仓无调用方,职能已由 subagentListing 覆盖) 角色清单刻意走 system prompt 而非工具描述:清单来自运行时扫描 markdown, 塞进 tools block 会让每次 .step-code/agents/ 变动 bust 整块 prompt cache, 代价不值得。
若干处注释只写了「做了什么」,没写「为什么这么做」,改动时容易误判意图。 这次把设计约束补进注释,并统一几处易误读的表述: - projector.ts:说明消息序列修复的触发条件与不可跳过的原因。 - workingTips.ts / duration.ts:补上文案与时长格式的取值依据。 - AgentGroup.tsx:说明子 agent 分组的折叠规则。 - debugBundle.ts / logger.ts:注释内直接给出设计要点,去掉指向仓库外文件的 失效引用(读者拿不到那些路径,指过去只是噪音)。 - App.tsx / HistoryPanel.tsx / OffsetViewport.tsx:模块间复用统一写作 「沿用 / 复用」,「照抄」易被读成机械复制,与实际的按需借用不符。 - compaction-summary-gate.test.ts:断言里的示例路径改为与环境无关的 D:/work/demo-repo,原先写的是开发机真实目录,换台机器读起来没有意义。
[compaction] model 此前只作为模型 id 透传给主会话 provider 的 stream({ model }),
只能在同一渠道内换模型。想让摘要走另一条渠道(不同 base_url / api_key / 协议)做不到:
跨渠道模型 id 发给主渠道端点会稳定 400,压缩每次失败等于上下文兜底失效。
现在该字段除模型 id 外还接受 [models.<别名>]:命中别名时走 resolveModelEntry 的
渠道/密钥回落链,用 createProvider 建独立 provider,摘要请求打该别名自己的端点。
未配置或裸模型 id 的行为与之前完全一致。
别名渠道构造失败时整体放弃覆盖、回退主会话模型,而不是退化成把跨渠道 id 发给主渠道——
后者是稳定失败,能压缩比压缩失败重要。
- src/provider/compaction.ts:新增 resolveCompactionBinding,四条分支 + 按别名缓存实例
- loop.ts:RunAgentOptions 加 compactionProvider,循环内压缩与溢出保命压缩两处消费
- runner.ts:子 agent 透传绑定
- main.tsx / App.tsx:组合根解析绑定;/reload 清缓存并按新配置重解
- compact.ts 零改动(它只在一处调 provider.stream,换实例在调用点做即可)
测试 7 例覆盖四条分支、缓存复用与降级路径。文档中英双语同步,内置 update-config
skill 的 schema 表一并更新(防漂移测试盯着)。
全量跑测试时总有 1 个用例超时,但每次挂的都不是同一个——三次分别挂在 promptInput 的两个不同用例和 markdownBodyRepro 上,跨文件漂移,而这些用例 单独跑全部稳定通过。这是调度竞争的特征,不是用例本身的逻辑缺陷。 成因:项目此前没有任何 vitest 配置,用的是默认 5000ms 超时,而 Ink 测试要 真实渲染组件树、靠 delay() 等待异步 flush。全套件 167 处硬等待累计 5.3 秒; 单个用例内也会累积,promptInput 那个逐字符删除 21 字符占位符的用例每步 delay(25),仅硬等待就 525ms,单跑约 1.2s。单跑余量看着够,16 核并发下互相 抢 CPU,放大 3 倍即撞线。 新增 vitest.config.ts 把 testTimeout / hookTimeout 提到 20s。没有去改那 167 处 delay——那是大规模无关重构,且硬等待本身是 Ink 异步渲染的合理写法, 真正缺的是与之匹配的超时配置。 放宽超时不会掩盖死锁或死循环,那类问题一样会超时,只是多等 15 秒;而假阳性 会训练人忽略红灯,代价更高。 验证:连续三次全量跑 1865/1865 全绿,墙钟 45-52s 与此前持平, 说明只抬高了上限、未拖慢正常路径。
react 与 react-reconciler 的 CJS 入口按 require 那一刻的 NODE_ENV 分流成 production /
development 两套构建,两者必须落在同一套。错配时 reconciler 调度静默失效——render()
正常返回、根组件函数从未被调用、stdout 零字节、不抛任何异常,TUI 表现为启动即空白屏,
而 -p 非交互模式与全部单元测试照常全绿。
两轮修复,方向相反,都必要:
① bin 入口换成不含 JSX、无任何静态 import 的引导文件 src/main.ts(先设 NODE_ENV 再
await import('./cli.js'));原 main.tsx 改名 cli.tsx 内容不变;bin 仍指 dist/main.js,
分发链路不变。加固三条:进程级首帧冒烟测试(经两轮反向验证)、结构断言、bundle
打包期 esbuild define 静态折叠。
② 删除 src/env.ts。第一轮把它保留为「非 bin 入口的兜底」,注释写明「救不了 react 自身」
但判定为可接受残留。实测证明它不是救不了,而是主动制造错配:在 bin 路径被引导文件
抢先、在 bundle 路径被 define 折叠,两条都是 no-op;唯一真正生效的场合是直跑
cli.tsx / dist/cli.js,而那里 react 已被 tsc 注入的 jsx-runtime 抢跑定型为 dev,
它把 reconciler 单独掰到 prod,于是把「两包一致走 dev、完全可用」变成「错配、静默卡死」。
实测六组入口矩阵,唯一变量为入口路径与 NODE_ENV,外部一律 env -u NODE_ENV 清除环境值,
判据为 stdout 首 2000 字节,并用 require hook 逐包记录实际加载的构建文件:
node dist/cli.js 修复前 react=dev / reconciler=prod → 0 字节;修复后同为 dev → 2000
tsx src/cli.tsx 同上(与 dist/cli.js 同因)
node dist/main.js 两包同为 production → 2000
tsx src/main.ts 两包同为 production → 2000
dist-bundle/step.mjs define 折叠 → 2000(首次构建验证:此前 dist-bundle 是 8-01 旧产物,
define 加进构建脚本后一直没重新打包,那层加固从未被实际验证过)
NODE_ENV=development tsx src/cli.tsx 两包同为 dev → 2000(证明「不设即一致」可用)
护栏方向随之反转:原有两例要求「cli.tsx 首个 import 是 env.js」,守的是一个会导致卡死的
约束。现为 9 例——保留引导文件的四条不变量(无静态 import、赋值早于动态 import、bin 与
dev 脚本指向),新增三条反向断言(cli.tsx 不设 NODE_ENV / 不 import 兜底模块 / src/env.ts
不存在)与一条 build-bundle.mjs 必须含 define。
验证:全量 2071/2071、160 个测试文件全过、typecheck 0、npm run build:bundle 通过、
措辞合规检查通过。firstFrameSmoke 确认真的执行而非因 dist/ 缺失被静默跳过。
注:本次为两个并行会话在共用工作区上的合并提交,AGENTS.md 与 CONTRIBUTING.md 的改动
含另一会话写的入口硬约束章节与调试章节;AGENTS.md 另含两处措辞合规修正(一处点名了外部
产品,一处把私有设计仓路径写进了公开仓)。
两个耦合的缺陷,都让压缩在长会话里失去兜底作用。 一、检查点只挂在 tool_use 一个分支 回合有六种结局,压缩只在其中一种末尾评估,于是三条路径完全绕过:纯对话轮(end_turn)、 用户 Esc 中断(aborted 直接 return)、以及新一轮 run 的第一个回合(循环顶部无检查)。 第三条影响最大——用户每次新提问都开新 run,上一轮结束时若已过线,新 run 的第一个请求 必然带着超限上下文发出;若模型实际窗口比配置的 max_context_size 更宽,接口不报 overflow, 这个状态不会自愈。现改为循环顶部「发请求前预检」,口径为 lastUsage.total + 尾部估算, 并删掉 tool_use 分支的重复调用(两处口径等价,留两处会在同一位置判两遍、连发两次摘要)。 二、压缩饱和时反复烧摘要请求 加了预检之后暴露出来的:当需要保留的最近消息本身就超预算时,每回合都压、每回合都压不到 阈值以下,于是每轮白发一次摘要调用。现加 compactionSaturated 标记,只在「确实压过了仍 超限」时置位——压不动(历史太短)不置位。这条区分是实测踩出来的:一开始两种都置位, 导致长 prompt 的子 agent 第一个回合即被判饱和、整个 run 不再压缩。饱和后停止本轮自动 压缩并明确提示用户改用 /compact 或 /new。 三、overflow 保命压缩不刷新状态栏 循环内压缩既发 notice 又发 usage,overflow 分支只发 notice——同一认知在同一文件里只 落实了一半,用户看到「已压缩并重试」而占用数字不动,像是压缩没生效。已补齐。 四、压缩不可中断(本项来自并行会话,与上述在 loop.ts / compact.ts 上耦合,故同批提交) fullCompact 要等一次完整摘要请求,长历史实测可达数十秒,此前这段时间 Esc 完全无效, 手动 /compact 甚至没挂 abortRef。现将 signal 透传到摘要模型调用。中断的状态安全性建立 在既有事实上:fullCompact 对入参 messages 只读,新序列先在局部算完、由调用方 replaceMessages 一次性生效,因此中断只要发生在返回前,历史必然停在压缩前的完整状态, 不存在压缩到一半的中间态。顺带修了 overflow 分支的 abort 检查位置——原先放在 !acted 分支内,micro 恰好有收益时会绕过,用户先看到压缩提示、要等下一回合才真正停下;现在 中断优先于任何压缩结果判定。 砍了什么:不做「估算超限即拒发请求」的 100% 硬闸门(已有 overflow 恢复路径兜底,且 估算有约 6% 误差,硬闸门会把误判变成「明明能发却被拦」);不改精确 tokenizer;不做 异步预热压缩。 验证:typecheck 0;新增 6 例预检用例 + 7 例中断用例;全量 2071/2071。 仍未收敛,不要当已解决:落盘数据解释不了状态栏上那个远超上限的占用数字(最后一次压缩后 按估算复算与显示值差约 48 倍)。已排除落盘裁剪、缓存重复计算、估算低估三种解释,剩余 可能需运行时插桩才能定论。本次修的四处缺陷各自独立成立且有回归测试,但不构成那个数字的 完整解释。
li-xiu-qi
force-pushed
the
step-code-explore
branch
from
August 7, 2026 20:12
b31d396 to
b6da1c1
Compare
- Release 资产新增固定名 step-code.tgz 副本,latest/download 永久链接可装最新版 - 删除 CI dist-branch job 与 make-dist-branch.mjs(分支已在远程删除) - 安装文档/快速上手/README(中英)与 step-code-install skill 对齐为四种安装方式 - 单文件可执行与 tarball 段落启用真实下载链接,去除「未发布」标注
- 新增 selectVisibleTodos:进行中全保留,最新一条已完成做进度上下文, 剩余名额按原顺序填待办,待办不足时从最近回补已完成 - 替换 slice(0,5) 硬切:已完成在清单前部堆积时不再把进行中/待办挤出可视区 - 折叠行从 +N more 改为带隐藏条目状态分布(如 +3(1 已完成 · 2 待办)) - i18n 新增 todo.status.* 三个 key(中英),todo.more 改含 detail 占位 - 测试补 8 个用例:裁剪优先级 6 个 + 渲染 2 个
li-xiu-qi
force-pushed
the
step-code-explore
branch
2 times, most recently
from
August 8, 2026 12:23
77c33a3 to
39651cf
Compare
…审阅收编 - 模式与工具:team_init/plan/spawn/merge/teardown/send/inbox/status 八工具 + /team 命令(init/status/exit/teardown),session 级开关,快照随会话落盘恢复 - 隔离:每任务独立 git worktree(.teams/worktrees/,写 .git/info/exclude 不动 .gitignore);写文件 per-worker 硬拦(runner 层 wrapWriteGuard);协调者在模式活跃时不能写代码 - 门控:build 任务范围两两互斥;deps 未全 merged 系统拒绝启动(代码强制,不靠自觉);依赖满足自动解锁 - 收编六道门:门〇空 diff 拒绝(防 worker 直提主仓绕过流程)/ 已审阅 / tip 未移动 / 依赖全并 / 无范围外文件 / --no-ff;冲突自动 merge --abort 救回;成功后自动清理干净 worktree(dirty 保留并提示) - 生命周期:closedAt 落盘标记(resume 不复活,init 重进清标记保留状态);teardown quit_only 应急强退;lost 联动(BackgroundManager onLost → 仅 active 任务标 blocked,可 respawn 重派);completed 可 rework 返工 - 可指定基准:--repo 指挥其他仓、--base 指定基准分支(缺省当前分支,收编校验 checkout 防合错) - 引导:内置 team skill(参数组合/反面教材/工作期用法/收编后验证/dist 错位运维知识)+ drift 锚点测试强制同步;init 报错意图识别(dir 误指 git 仓时引导改用 repo) - TUI:状态栏 team 徽标、/team 忙时即时执行;subagent 请求支持 cwd/writeAllowRoot - 测试:team 系列 30+ 用例(真实临时 git 仓跑 worktree/merge/门控/信箱/teardown)
…建成功但漏了同步,别名原样发给服务端导致 404 model_invalid;状态栏正常是因 displayName 走别名查表的另一路径
…先、同级新触发在前;状态栏 bg 徽章带最近任务名;submit 加 fromQueue 守卫,队列 drain 与空闲通知注入不再误清用户正在编辑的草稿
li-xiu-qi
force-pushed
the
step-code-explore
branch
from
August 8, 2026 12:31
39651cf to
dc3e1ec
Compare
模型在 tool_use 分支陷入「看到相同结果 → 做出相同反应」的稳态时(每轮
assistant 消息与工具结果逐字节相同、零信息增量),此前只能烧到
maxIterations=500 大断路器才被拦下。本提交在 loop.ts 的 tool_use 分支
挂上确定性检测:
- 新增 src/agent/roundLoop.ts(纯函数,无 IO):fingerprintRound 取
messages 尾部 assistant + tool_result 对拼稳定指纹(排除每轮必变的
调用 id 与消息 ts;工具结果参与指纹,合法轮询结果在变则不触发);
createRoundLoopDetector 维护 streak,连续相同第 3 轮 warn、第 4 轮 stop
- loop.ts 接线:warn 注入 user 消息 + yield notice(不打断,给一次机会);
stop 走 notice + turn_done 收尾(模型行为问题不是系统故障,不走 error)
- i18n 新增 loop.roundLoop.{warn,inject,stop} 中英双语
- tests/agent/roundLoop.test.ts 穷举:指纹各成分参与度、streak 清零、
合法轮询不误伤、is_error 参与、warn/stop 的 runAgent 级集成断言
- subagent.test.ts 压缩估算用例的工具参数逐轮区分:完全同构序列正是
本检测要拦的形态,会在压缩触发前被硬停,测不到压缩路径
- CHANGELOG 补条目
完整设计见内部产品设计文档(跨回合零进展检测设计)。
…e 相等时 > 漏判索引过期(fork Windows CI 的 cleanup ttl_days 随机失败根因,upstream 同内容全绿佐证 flake);测试用 utimesSync 把直改 mtime 拨到未来消除时序依赖;主 SessionStore 同源同修
li-xiu-qi
force-pushed
the
step-code-explore
branch
from
August 8, 2026 14:18
209531d to
436c9b5
Compare
真实事故驱动:一个会话在同一条 bash grep 上原地复读 497 轮,直到撞 500 轮硬上限 才被中止,期间用户侧没有任何提示。回放该会话(1070 条消息 / 518 个回合对)确认 指纹算法本身有效(最大连续相同 streak 达 497,远超阈值),但暴露两个缺口: 一、round-loop 只认「与上一轮相同」。单槽 lastFingerprint 一旦不等就清零 streak, 于是周期≥2 的交替循环(A→B→A→B)每步都与上一步不同,streak 永远停在 1,检测一次 都不会触发;而「读 A 发现要看 B,读 B 又回头看 A」这种两态摆动在工具链路里比原地 复读更常见。改为大小 8 的滑动窗口 + 窗口内出现次数计数:出现 4 次及以上判 stop, 恰好 3 次判 warn。周期 1 的行为与原实现逐条一致,原有用例全部保留守住回归。 二、500 轮硬上限是唯一真正生效过的防线,且中间完全静默、撞线才硬停。现在在 maxIterations 的 50% / 80% 两档各注入一次自查提醒(notice + injection,照 roundLoop 的现成范式),阈值按比例计算而非硬编码,每档一轮交互只触发一次。提醒挂在循环顶部 而非 tool_use 分支——挂分支内会被 roundLoop.stop 的提前 return 吞掉。 i18n 同步:roundLoop 文案措辞从「连续」改为窗口语义,新增 turnWarning.mid / late 双语。
bash 工具此前对写操作零检查:write_file / edit_file 受 runner.ts 的 wrapWriteGuard 限制在 allowRoot 内,而 bash 的重定向、mv、rm、sed -i 一律直通,安全边界完全寄托在 模型自觉上(AGENTS.md 与 team skill 都只是把这一点写成告知)。 新增 checkBashWrite(command, cwd, allowRoot) 纯函数,引号感知地拆段后三档判定: - A 档,目标可解析且越界 → 拒绝。相对与绝对路径统一 resolve 后再判定,路径遍历 由 resolve 自身处理,不用正则去理解 ..;边界判定避开兄弟目录前缀陷阱。 - B 档,有写入迹象但目标不可解析 → 拒绝,并要求改写成 allowRoot 内的显式路径。 覆盖变量、命令替换、eval、python -c / node -e / perl -e 内联写入、以及命令内 cd 越界后的相对路径。这里不做 fail-open:放行等于没拦,而模型收到明确反馈会改写。 - C 档,无写入迹象 → 放行。动态路径判定附加「该段确有写入迹象」门槛,否则 ls $HOME、grep $PAT f 这类只读命令会被误拦,误拦比漏拦更伤可用性。 生效边界:allowRoot 缺省或为空时一律放行。主 agent 没有 allowRoot,必须保持能往 仓库外写文件(既定行为);只有 team worker 场景才受约束。 本 commit 只落纯函数与单测(93 例,含误报防线;Windows 语义的路径断言按平台跳过),尚未接入 bash.ts 的 execute()。接线、ctx 传参与用户可见文案另做。 已知残留缺口:>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法不覆盖。
li-xiu-qi
force-pushed
the
step-code-explore
branch
from
August 9, 2026 10:40
2a56230 to
25b893a
Compare
AgentGroup 的 backgroundHint 只挂在非 busy 的完整渲染路径(AgentGroup.tsx:215), 而 agentGroupRows 在 busy 时 return 1、面板压缩成单行摘要。前台派生子 agent 时主 agent 必然 busy,于是这条提示在真正需要它的场景永远不显示;只有后台派生且主 agent 空闲时 才出现——那时任务早已在后台,提示没有意义。 不是回归:Ctrl+B 的按键处理(App.tsx:1150)与文案 key 一直都在,缺的是 busy 摘要行 上的入口提示。 修法是在 busy 摘要行按 running > 0 追加一条紧凑提示,风格与已有的「· /tasks 查看全部」 一致,因此新增 agentGroup.busyBackgroundHint 而不复用带括号的 backgroundHint。 摘要行本身是 wrap="truncate",追加文本不会增加行数,agentGroupRows 仍返回 1—— AgentGroup.tsx:101 的注释记着一次「加 backgroundHint 导致帧高超预算越过 rows-1 红线」 的事故,这次刻意避开同一个坑,并在测试里断言行数不变。 补两条回归守卫:有 running 时含提示且行数不变、全部 done 时不含提示。
现象:「↓/j 选择下一条,tail 预览跟着切换」这条用例在 CI 上间歇失败,断言期望预览显示(ta),实际是(tb)。 先前把它归因为「等待不够」并把固定 delay 换成 vi.waitFor,归因错了——换完之后 windows CI 仍挂在同一条断言。真因是列表顺序本身不确定:TasksViewer 的排序是「运行中优先,同级按 startedAt 倒序」(TasksViewer.tsx:165-170),用例里两个任务同为 running,谁排在前完全由 startedAt 决定;而 makeTask 的默认 startedAt 是各自现取 new Date(),于是顺序取决于「两次调用是否落在同一毫秒」。落在同一毫秒时稳定排序保持声明顺序(ta 在前,凑巧满足断言),一旦跨过时钟 tick,tb 变新排到最前,初始选中项就成了 tb。 修法是显式给两个任务 startedAt 并让 ta 比 tb 新(偏移 50ms,两条仍算刚启动,时长显示不受影响)。诊断与修复都用注入法验证过:把 makeTask 的默认值改成「tb 晚 1 秒」以模拟 CI 条件,修复前这条用例失败、其余 19 条全过(与 CI 表现一致),修复后同一模拟下 20 条全过——说明对时钟的依赖被真正消除,不是碰巧通过。 vi.waitFor 保留:预览区确实要等一次 ink re-render 才出现,固定 20ms 在 CI 负载高时不够。它解决的是「等得够不够」,与排序不确定是两个独立问题,注释里已写清以免下次再误判。
设计文档「首次运行引导设计」第 20-21 行要求 select 步骤有四个选项,第四项是「查看文档,稍后手动配置」,i18n 的 firstRun.optionDocs 中英文案齐备,但该出口在实现上被三处叠加吞掉,实际彻底不可达(用户想跳过配置只剩 Esc):渲染只 map PROVIDER_OPTIONS(3 项 provider),第四项从未画出;方向键取模用 PROVIDER_OPTIONS.length,光标最大到索引 2,画出来也选不中;数字键路由里 idx === 3 分支写在 const option = PROVIDER_OPTIONS[idx]! 与 if (!option) return 之后,PROVIDER_OPTIONS[3] 恒为 undefined,按 4 被守卫提前 return,分支是死代码。修法对应三处:追加文档出口渲染行、取模改用 SELECT_OPTION_COUNT(渠道数 + 1)、把出口判断提到取值守卫之前。 选项 label 原为硬编码中文字面量('StepFun Plan 订阅' 等),i18n 里配套的 firstRun.optionPlan/optionApi/optionCustom 三条从未被用——后果不是提示缺失,而是英文用户在向导里看到中文。改为在 ProviderOption 存 labelKey、渲染时 t():PROVIDER_OPTIONS 是模块级常量,若在其中直接调 t() 会在 import 时求值把语言固化,运行时 /lang 切换不生效。硬编码的配置文档链接提示一并接线为新增的 firstRun.docsNotice。 顺带清理普查出的 26 条零引用文案(zh/en 双表对称删除,tsc 保证键集一致):app.goal/permission/provider/lang/plugin.arg.* 与 app.skill.list/none 共 22 条系重构遗留,实际替代者是 commands.ts 的 cmd.<命令>.sub.<子命令>;agentGroup.status.running/queued 在状态改用彩点后失效(done/error 仍在用);firstRun.optionPaste 属旧流程遗留、optionManual 与 optionDocs 语义重复。app.image.unsupported 同为零引用但保留并加注释:全仓没有任何 supportsImage/vision/multimodal 能力判断,该文案是「贴图给非多模态模型静默无提示」这个缺口的唯一线索。
li-xiu-qi
force-pushed
the
step-code-explore
branch
from
August 9, 2026 15:02
745bcde to
bfd7e7b
Compare
此前 system prompt 里没有任何时间信息,模型只能拿训练截止日期当「现在」,后果是搜索时用错年份、写文档与 commit 时写错日期、判断「最新版本」基于过时认知,且它意识不到自己在猜。 为什么是「启动快照 + 诚实标注」而不是逐轮刷新真实时间:system 整块打 cache_control(provider/prepare.ts 的 buildSystemBlocks),逐轮改写会让 system 断点连同其后的 tools 与历史断点一起失效,等于每轮按全上下文重算 input token。所以这里放弃「保持准确」,改为明说它是启动快照、可能过时数小时、真要准确时间就去跑 date——换来 system 永久静态,精度反而可以给到分钟而不必压到天。 跨天是唯一不可容忍的过时(日期错一天会让写文档、判断时效系统性出错),用一条 injection 消息修正,复用 loop 既有注入通道(与 turnWarning、后台通知同形)。baseline 取「最后一条消息的本地日期」而非局部状态变量:runLoop 每个 prompt 回合重新调用,局部变量在回合边界就重置了,而跨天几乎总发生在回合之间——用局部变量等于永远检测不到。注入的提醒自身成为最后一条消息、ts 即今天,故天然只注入一次,无需 warned 标记;resume 后同样成立。 时间一律用本地时区并显式标注 UTC 偏移与 IANA 名。直接给 toISOString() 会错日期:实测 new Date(2026,7,9,7,30).toISOString() 在东八区得到 2026-08-08T23:30:00Z,每天 00:00 到 08:00 报给模型的日期都比本地早一天,而模型无法自行修正(它不知道用户在哪个时区)。crossedLocalMidnight 另加坏时间戳防御:Invalid Date 的日期键是 NaN-NaN-NaN,与今天必然不等,不拦掉会导致每轮都注入一次「日期已变更」。 三处注入点:主 agent(systemPrompt.ts,now 可注入以保证测试不随真实日期漂移)、子 agent(runner.ts,联网调查最依赖当前日期)、--print 纯净模式。25 个新用例:nowContext 20 个覆盖时区换算、半小时时区、跨午夜判定与坏 ts(时区靠运行时改 process.env.TZ 切换,其中 UTC 对照组兼作方法自验——本机是 UTC+8,若切换在 vitest 里不生效该组必然失败);loopDateChange 5 个覆盖注入接线本身,判定函数写对而接线接错的情形纯函数测不出来,并用「临时把注入条件短路」反向验证过——禁用后期望注入的 3 条失败、期望不注入的 2 条仍过。
bashWriteGuard 的三档判定纯函数与 93 个单测早已落地,但没有任何调用方:wrapWriteGuard 只拦 write_file/edit_file,bash 直接透传给 base。后果是 worker 一句重定向或 cp 就能写到工作间外,范围互斥只剩 team_merge 合并时的 diff 检查兜着——已经发生过踩穿。 接线点选在 wrapWriteGuard 而非原计划的 bash.ts execute():前者本就是 per-worker 写隔离的位置、已单测、已在 runner 里接到子 agent 的 hooks 上,改动只是加一个分支;接在 execute() 则要把 allowRoot 一路穿进 ToolContext,而这对非 team 场景没有意义。hook 层拦截还有个好处是能把守卫给出的 reason 直接回给模型,比工具层抛错更容易让它自己改对。 拦 bash 曾被担心会拦死 git(worker 要在工作间里提交),实测不成立:守卫只看命令行里的显式写入语法(重定向、cp/mv/rm/tee/sed -i/dd/truncate),git 子命令与 npm/npx 一律判为无写入迹象放行。接线前用 21 条 worker 典型命令量过误报面,唯一被误拦的是 ls -la > /dev/null——/dev/null 会被解析成绝对路径、天然落在 allowRoot 外,而这是最常见的丢弃输出写法,拦下去会卡死正常命令。故本 commit 一并加丢弃型设备白名单(/dev/null、/dev/stdout、/dev/stderr、/dev/tty、NUL、/dev/fd/N、/proc/self/fd/N),并留一条用例锁住「白名单不是所有 /dev/*」——dd of=/dev/sda 仍按越界拦。 测试:守卫层加 6 例(99 例)覆盖特殊设备与块设备对照;接线层加 5 例覆盖两端——git 提交/跑测试/丢弃输出必须放行,越界重定向/拷贝/删除与动态路径必须拦。两侧都做了反向验证:把 bash 分支短路后,期望拦截的 2 例失败、期望放行的 3 例仍过,确认用例测的是接线本身而不是恒真。 已知边界(未变):tee FILE < input 形态下取错 token 而漏判(tee FILE 与 cmd | tee -a FILE 能正确拦);>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法仍不覆盖。这些形态的越界写仍由 team_merge 的事后 diff 兜住。
li-xiu-qi
force-pushed
the
step-code-explore
branch
from
August 9, 2026 15:30
bfd7e7b to
6e72f42
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
本 PR 是 step-code 终端 AI 编码助手的架构探索分支当前快照,从空分支起步、从 0 到 1 搭建完整架构,当前挂牌 v0.1.2(其后演进第三批,见下节)。与上游 main 分支差异极大(436 个文件,+84,586 行):
3075d49线性演进至今,保留完整的 worktree 功能分支合并拓扑Closes #103
核心架构
_index.json索引缓存、恢复与回退、调试包导出、Wire 日志、Token 统计架构级特性
/agents只读下钻子会话step doctor config可校验0.1.1 → 0.1.2 第二批演进
/resume只列主会话,新增/agents归属到会话上下文;子会话索引缓存根治/resume卡顿/new/fork换绑后台任务目录;edit_file截断 diff 完整落盘/think/provider切换写回配置;[tools.web]缓存阈值可配置完整演进记录见 CHANGELOG.md。
v0.1.2 之后第三批演进(2026-08-08)
测试与质量
文档
与上游 main 的关系
本分支是独立探索,未合并上游 main 的后续变更。架构选择(如 Ink React、动态工作流、子 agent 编排)与上游可能有根本差异,建议作为新架构提案评审,而非增量功能合并。
分支:
li-xiu-qi:step-code-explore→stepfun-ai:step-code-exploreCommit 数: 150
文件变更: 436 个(+84,586 行)