Skip to content

Unify default local state under .loopx with explicit migration - #4915

Open
LIHUA919 wants to merge 22 commits into
loopx-project:mainfrom
LIHUA919:codex/issue-4800-local-state-paths
Open

LIHUA919 wants to merge 22 commits into
loopx-project:mainfrom
LIHUA919:codex/issue-4800-local-state-paths

Conversation

@LIHUA919

@LIHUA919 LIHUA919 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fresh LoopX installations previously split local state between $HOME/.codex/loopx and project .codex/goals. This change defaults the global runtime to $HOME/.loopx and new project Goals to .loopx/goals, while existing registry-declared legacy routes remain usable until an explicit migration. If both default global registries exist, implicit selection fails instead of creating two writable authorities.

loopx migrate-local-state is dry-run first and binds execution to a content-derived plan_id. It verifies a private backup, moves registered state, rewrites project registries through their codec, and writes a rollback receipt. Preview and execution reject symlink/junction/reparse-point ancestors of Goal destinations, backup paths, and project registries. Custom Goal state_file routes stay in place, but their project registry still receives the same ancestor checks before backup, rewrite, and rollback. Synthetic negative tests verify that linked ancestors and route changes cannot write outside the declared project or backup directory.

The selected route now carries through doctor, CLI and generated commands, SSH/LaunchAgent and Codex App scheduler paths, extension activation state, and DSH GoalBar source revisions. Public guidance and examples use the fresh default. After integrating current main, chat assets follow its Git-external delivery builder; the bundled example is rebuilt and verified without restoring retired tracked assets.

Validation at 9bb022e98125d083d4294c4b3deb241ad940bd9a:

  • 192 related Python tests and 16 TypeScript tests passed on this exact head, covering local-state migration, project registry routes and I/O census, source-session lifecycle, Todo summary integration from current main, and adjacent CLI and chat-bundle paths. The I/O manifest validates 235 sites with zero unclassified direct sites.
  • Ruff, control-plane TypeScript typecheck, git diff --check, and the manifest generator check passed. The DSH plugin quality, package, and local runtime phases passed after preparing its dependencies; quality included 188 plugin tests.
  • loopx canary premerge --from-git-diff executed 19 selected automatic checks: 19 passed and zero failed. Its gate remains manual_review_required for one benchmark-sensitive manual hold; this validation does not grant merge authority.
  • No live user state was migrated. Cross-platform CI and maintainer re-review are tracked on this exact head; the PR remains for the maintainer to merge.

Fixes #4800.

Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
(cherry picked from commit c6208daea8b4def86cbdf63dc35a31faaf0a5046)
Signed-off-by: Lihua <1017343802@qq.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查对象:#4915 @ 4e332dd(exact head)。

  1. 问题与边界:把新安装默认状态统一到 .loopx、旧安装保留原注册路径并只在显式 plan_id 下迁移,是合理的交付边界。迁移前预览、内容绑定、私有备份、冲突拒绝和回滚都对保护原始数据有价值;本次没有对任何真实 Goal 执行迁移。

  2. 关键路径:migrate-local-state 会在 loopx/local_state_migration.py:291 将整个旧 runtime root 改名到 ~/.loopx,因此 extensions/state.json 也会一同迁走;core CLI 通过 paths.select_default_runtime_root 选择新/旧路由,DSH GoalBar 会按 registry 的 state_file 读取。

  3. 阻断发现 [P1]:扩展 CLI 的默认读取没有跟随上述路由。loopx/extensions/runtime.py:69-75 的 default_extension_state_file(None) 仍固定返回 ~/.codex/loopx/extensions/state.json;普通 loopx extension list/enable 在 loopx/cli.py:492-495 与 cli_commands/extension.py:64-68 传入的正是 None。迁移成功后,扩展启用状态已在 ~/.loopx/extensions/state.json,但 list 会以 ok=true 静默返回空列表,enable 等操作还可能重新创建旧路径。隔离临时目录复现:迁移后路径有 1 个扩展,隐式默认读到 0 个。请让扩展默认读取/写入复用同一个 selected runtime route,并增加 execute/rollback 后普通 extension list 的回归测试;不应只靠文档要求操作者每次显式传 --runtime-root。同类 governed-capability 默认目录也可一并核对(目前未发现其无参生产调用,非独立阻断)。

  4. 验证:tests/test_local_state_migration.py 8/8;project-registry + refresh-isolation 74/74;DSH GoalBar focused vitest 17/17、host TypeScript typecheck 通过;git diff --check 通过。pnpm 完整依赖校验受私有 registry 两个可选二进制 404 阻塞,已安装的直接 vitest 可运行;按该 Goal 的 wait_for_ci=false 没有轮询远端 CI。最关键的扩展迁移反例未被现有 8 个迁移测试覆盖。

  5. 结论与后续:REQUEST_CHANGES。修复后请更新 exact head,再验证旧路由、全新 .loopx 路由、显式迁移与回滚下扩展状态的一致性。面向下一次改动,最小的相关重构是只保留 paths.py 一个默认 runtime 选择器,避免扩展子系统重复持有路径权威;不需要扩大成新的迁移框架。

English verdict: REQUEST_CHANGES — the migration moves extension activation state to the new runtime root, but ordinary extension commands still read the old hard-coded default and silently report no extensions.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

评审精确 head 4e332dd8689ebd7ba891d37b5b6b01af1e743a8a。新安装将全局与项目 Goal 的本地状态默认收敛到 .loopx,现有注册继续沿用声明过的旧路径,显式迁移则需要可预览、可备份、可回滚,避免改默认值时暗中搬动用户数据。

改动思路

路径选择由注册状态与默认路由决定;migrate-local-state 先生成内容绑定的 plan,再在停工条件下执行备份、状态迁移、注册改写并留下 rollback receipt。doctor、生成的 CLI 命令、SSH/LaunchAgent、Codex App scheduler bridge、DSH GoalBar、文档和打包 chat 示例跟随选中的路径。这些方向合理,迁移的读写端却必须共同遵守同一个路径选择权威。

具体改动

PR 修改了全局 registry 与项目 Goal 的默认目录,增加冲突/符号链接/源变更检查、备份与回滚,并更新上述调用者和示例。迁移正向与回滚的 8 个聚焦测试在当前 head 通过,diff whitespace 检查通过。仍有一处遗漏:loopx/extensions/runtime.py 的 default_extension_state_file(None) 仍固定返回旧的 .codex/loopx/extensions/state.json,普通 loopx extension list/enable 未给 --runtime-root 时会使用它。

对主干的风险

这是数据路径迁移,不应在默认命令静默制造第二份状态。执行迁移会把原有 extensions/state.json 随旧 runtime root 搬到 .loopx,但普通 extension 命令随后会读取旧目录:已有启用的 extension 被显示为空;enable 还可能重新写出旧目录状态。合成隔离状态复现了“隐式列表 0 个、显式迁移目录 1 个”的差异,未触碰用户活动状态。请让 extension 的默认状态文件走统一的 select_default_runtime_root,并补 execute/rollback 后不带 --runtime-root 的普通 CLI list/readback 回归。现有备份机制不能替代这个调用链修复;维护者还需独立确认跨平台与迁移门槛。

我的整体评价

REQUEST_CHANGES。 迁移事务本身及多处路径消费者已有扎实覆盖,但 extension 这个实际入口仍破坏迁移后的默认读写一致性,属于合并阻塞项。相关 refactor 应保持在现有路径选择 owner,不需要新增第二套路由规则;修复后重新核对精确 head。

English verdict: REQUEST_CHANGES - exact head 4e332dd still routes implicit extension state to the legacy runtime root after migration. Eight migration tests and diff check passed, but a synthetic implicit list/readback counterexample remains; unify the default route and add execute/rollback CLI regression coverage.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

复审精确 head 5798e39dabe8f70e496e022b6b161c9e66f0a56b。#4800 的目标是让新安装只使用 .loopx 默认状态路径,同时让既有注册继续沿用声明过的旧路径;只有操作者预览并执行迁移时才移动原始数据。上轮指出的扩展启用状态分叉已修复:普通 extension list 在合成迁移及回滚前后均跟随选中的 runtime。

改动思路

paths.py 统一选择 fresh/current/legacy 路由;migrate-local-state 将 registry 声明、源内容摘要和目标路径绑定为 plan_id,执行前备份并校验,再移动 Goal/runtime、改写 registry,留下可验证的回滚收据。项目 CLI、扩展状态、doctor、生成的 host 命令、DSH GoalBar、安装示例与中英文文档跟随该路由。方向与边界合理,但迁移预检也必须证明目标路径的每一层都仍处于所声明的项目目录内。

具体改动

PR 共改动 114 个文件(+1524/-295):核心是 local_state_migration.py 的预览/执行/回滚、paths.py 的选择器和 registry 路径,余下主要是调用者、示例、文档及聚焦测试。extensions/runtime.py 在本 head 改用共享选择器;新的 CLI 回归覆盖 legacy、fresh、execute、rollback 与双 registry 冲突。

关键代码讲解

  • select_default_runtime_root 只根据 registry 路由选择默认 runtime,不暗中搬迁旧数据。
  • plan_local_state_migration 对 Goal 源目录和目标叶子做检查、计算计划摘要,却漏检中间的 .loopx/goals 目录。
  • migrate_local_state 在接受计划后调用 new.parent.mkdir 和 old.rename(new);如果中间目录是符号链接,这两个操作会沿链接把 Goal 目录放到项目外。
  • default_extension_state_file 现已复用默认路由选择器,上轮扩展状态遗漏不再是阻断项。

对主干的风险

[P1] 迁移预检未拒绝目标路径中间层的符号链接。 在隔离的合成项目里,使 .loopx/goals 指向项目外的空目录,目标 Goal 叶子仍不存在:预览返回 ok=true,执行返回 migrated,而 ACTIVE_GOAL_STATE.md 实际落在链接目标。这与迁移指南所承诺的 symlink-boundary 校验不符,也可能在成功收据下悄悄改变原始数据的物理位置。请在预览中逐层拒绝符号链接或验证 canonical containment,并在真正 rename 前重验;补一条负例,断言外部目录没有写入。现有备份/回滚能帮助事故恢复,不能替代准入保护。

本轮 145 个迁移、扩展和项目 registry 聚焦测试通过;git diff --check 通过。上述真实文件系统负例仍失败。按 Goal 的 wait_for_ci=false 未轮询远端 CI,也未迁移任何活动 Goal。未来相关重构保持一个 paths.py 路由 owner,并在现有迁移模块补窄边界检查即可,不需另建框架。

语义与 CI 对齐

这是共享本地状态路径契约的有意变更;扩展入口现与选择器对齐,但“预览检查符号链接边界”的当前文档承诺被目标祖先链接反例违反。CI 不是该确定性安全缺口的替代证据。

我的整体评价

REQUEST_CHANGES。 统一默认路径、保留旧注册以及显式可回滚迁移是一块有用且成形的交付;上轮扩展路径问题也已闭合。但此 head 仍允许一个预检通过、执行成功、数据移出项目的场景。修复目标祖先检查并在新 exact head 上跑通负例后再复审;批准代码与实际迁移授权仍是两件事。

English verdict: REQUEST_CHANGES - head 5798e39dabe8f70e496e022b6b161c9e66f0a56b fixes the prior extension-route issue, but a symlinked Goal destination ancestor passes preview and moves state outside the project. 145 focused tests pass; the synthetic filesystem counterexample does not.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

复审 #4915 精确 head bda7a5bfb1ae991b58d42cd96d0c0c05926fa226。#4800 要把新安装的全局与项目 Goal 默认状态收敛到 .loopx,同时保留已注册旧路径;搬动原始数据只能经操作者显式预览、备份、执行和回滚。上轮指出的 extension 默认路由及 Goal 目标祖先符号链接问题,在本 head 的代码和测试中已得到针对性修复。

改动思路

paths.py 统一选择 fresh/legacy/conflict 路由,普通 CLI、extension、项目 prompt、doctor、生成的 host 命令、DSH GoalBar 和打包示例跟随所选路由。migrate-local-state 则以源内容和路径生成 plan_id,先复制并校验私有备份,再移动 Goal/runtime、经严格 codec 改写项目 registry,留下迁移及回滚收据。迁移保持离线、显式;没有对任何活动 Goal 做试迁。

具体改动

整份 PR 相对当前 base 涉及 115 个文件(+1686/-304):核心是迁移与路径选择,其余主要是调用者、双语文档、示例和回归测试。最新四文件增量加入了 Goal 目标路径逐级检查、移动前复验,并让迁移和 project prompt 使用严格项目 registry codec。

关键代码讲解

  • default_runtime_route 选择新旧默认根;双 registry 并存时拒绝隐式选择,不自行搬迁。
  • _require_goal_destination 逐层拒绝 Goal 目标祖先链接,预览与真正 rename 前均调用,关闭上轮反例。
  • plan_local_state_migration 检查声明、摘要和目标冲突,但在第 223–227 行只检查备份目录叶子,没有检查其父目录。
  • migrate_local_state 在第 273–280 行以 parents=True 建立并复制完整备份;若备份父目录是符号链接,会沿链接写入外部物理目录。

对主干的风险

[P1] 默认备份路径仍可被父级符号链接重定向。 在隔离合成项目中,使 source.parent/loopx-local-state-backups 指向项目外的空目录,预览返回 ok=true,按精确 plan_id 执行返回 migrated,外部目录却出现包含 ACTIVE_GOAL_STATE.md 的完整备份。收据只显示词法路径;成功状态不能证明私有状态留在声明的备份位置。请在预览和复制前检查默认及显式备份路径的祖先/规范化边界,补“外部目录无写入”的负例。修复后仍应保留原始备份与回滚校验,而不是用它们代替准入。

94 个迁移、registry codec/IO census 聚焦测试通过;Ruff、control-plane TypeScript typecheck、git diff --check 通过。风险 canary 的 10 个 catalog 检查与 public-boundary 检查通过,8 个 risk checks 中 3 个 DSH 检查因当前环境无法取得可选平台依赖而失败;另有一个 benchmark-sensitive 人工 hold。不能把这次 canary 记为全绿。未迁移真实用户状态,按本 Goal 的 wait_for_ci=false 未轮询远端 CI。

语义与 CI 对齐

这是共享本地状态与私有备份路径契约的有意变化。严格 codec 和 Goal 目标边界现与文档对齐;备份祖先反例仍违背迁移指南的私有备份/符号链接边界。环境导致的 DSH 校验缺口需在可用依赖环境补跑,不能用其余绿灯抵消确定性的迁移边界缺陷。

我的整体评价

REQUEST_CHANGES。 统一路由与显式可回滚迁移是有用且成形的交付,跨入口的较大改动量有其原因;此前两个阻断项也已闭合。但本 head 仍会在成功收据下把原始状态的备份写到预期位置之外。相关的未来向重构应继续让 paths.py 管默认路由、项目 codec 管序列化,在现有迁移模块中复用一处窄的目标祖先校验,不另建路径权威。修复备份边界并补齐 DSH 环境验证后再按新 exact head 复审;代码批准本身也不等于授权迁移或合并。

English verdict: REQUEST_CHANGES - exact head bda7a5b fixes earlier extension and Goal-target issues, but a symlinked default backup parent still redirects the full private snapshot while preview and execute report success. 94 focused tests pass; three DSH risk checks remain environment-blocked.

Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机
将既有本地默认状态迁入 .loopx,并以显式预览、备份和回滚保护原始数据;这个方向有价值,本轮是对新 head f2d3998 的全 PR 复审。

改动思路
最新提交补上了备份目录祖先 symlink/junction 的检查,之前的备份重定向问题得到覆盖。但项目 registry 的规范路径只检查末端文件是否为 symlink,没有检查其 .loopx 祖先。

具体改动
[P1] 在 loopx/local_state_migration.py:205-213,若某 Goal 使用合法的自定义 state_file,且项目 .loopx 目录是指向项目外目录的符号链接,_state_route 不产生 Goal move,因而不会调用 Goal 目的路径的祖先检查。隔离真实文件系统复现:preview 返回 ok=true、goal_directory_count=0;以该 plan_id 执行后返回 migrated,并改写链接目标中的 registry.json。请在预览和实际项目 registry 写入前拒绝 symlink/junction/reparse-point 祖先,并补一个自定义 state_file 的负例,断言预览/执行均无外部写入。仅检查 registry.json 本身不够。

对主干的风险
此路径会在操作者以为只迁移已声明项目状态时改写项目外 registry,违背迁移数据边界。93 个聚焦测试及独立安装 smoke 通过,但未覆盖这一分支。标准 canary 另有本机 DSH 依赖源不可用及手动 hold,不能据此宣称全绿;这里的阻断结论来自独立可复现的真实文件系统反例,未迁移任何现有 Goal。

我的整体评价
请求修改;保留目前备份祖先修复,补齐项目 registry 祖先边界后,再用同一负例和聚焦迁移/回滚测试复核。相邻边界的前瞻性收敛应复用现有路径验证,而不是增加第二套 registry 决策规则。
English verdict: REQUEST_CHANGES

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

复审 #4915 精确 head f2d3998f3e686bdf39c5f4521855b06f7b4dec8f,按 #4800 的目标判断整份变更:新安装应统一使用 .loopx,已注册旧状态保持可用,原始数据只能经明确预览、私有备份、执行和回滚迁移。本轮提交修补了上一轮指出的 backup 祖先链接,但迁移准入仍有一个独立的 project registry 路径缺口。

改动思路

paths.py 统一选择 fresh/current/legacy/conflict 路由;migrate-local-state 用源内容与路径绑定 plan_id,停工后备份、迁移 Goal/runtime、严格改写项目 registry,再留下可回滚收据。project CLI/prompt、extension、doctor、生成的 host 命令、macOS 启动脚本、Codex App scheduler、DSH GoalBar、打包 chat 示例和双语文档跟随被选择或声明的路径。这个完整链路是必要的;任何真实写入路径都必须服从同一祖先边界,不能只验证有 Goal 目录移动的分支。

具体改动

相对本 PR base,115 个文件约 +1835/-304:核心是 local_state_migration.py 与 paths.py,其余主要是上述调用者、文档、fixture 和 526 行聚焦迁移测试。最新增量对默认/显式 backup 的父目录做 symlink/junction 检查,并在复制前复验;这关闭了上轮的备份重定向反例。

关键代码讲解

  • default_runtime_route(loopx/paths.py)识别新旧单一路由或双 registry 冲突,不在普通读取中搬迁数据。
  • _state_route(loopx/local_state_migration.py:123)对自定义 state_file 返回“不移动 Goal”;此时后续不会调用 Goal 目标祖先校验。
  • plan_local_state_migration(同文件约 177–255 行)检查项目 registry 叶子本身是否为 symlink,却没有检查 .loopx 父目录。
  • migrate_local_state(同文件约 283–390 行)依计划备份并改写项目 registry;当父目录是链接时,项目外文件实际被改写。
  • computeGoalBarSourceRevision(packages/dsh-loopx-plugin/src/goalbar/read-model.ts)改为读取 registry 声明的 Goal 状态路径,而非固定旧默认目录。

对主干的风险

[P1] 自定义 Goal 路由绕过 registry 祖先边界。 我在隔离的真实文件系统中构造合法自定义 state_file,将项目 .loopx 指向项目外目录,保持 registry.json 叶子是普通文件。当前 head 预览 ok=true、goal_directory_count=0;以精确 plan_id 执行后返回 migrated,但外部 registry 字节发生变化。原始 runtime 被移动,收据成功,物理路径却违背迁移指南“检查每个 source registry/symlink boundary”的承诺。请在预览和真正写项目 registry 前检查其每层祖先(含 junction/reparse point),补自定义 state_file 的负例,断言预览/执行拒绝且外部目录没有写入。上轮修复的 backup 和 Goal 目的路径检查应保留。

[P1] 当前主干仍无法集成。 git merge-tree --write-tree origin/main HEAD 对 eb16c54 报 loopx/web/chat/asset-retention.json、index.html 内容冲突及打包 JS 的 rename/rename 冲突。请重基并由当前源重新构建 chat bundle,保留主干新资产,再在新精确 head 跑打包/保留清单 smoke;不能只选择一侧生成文件。

本轮源码树的迁移测试 18/18、项目 registry/Windows 安装测试 65 通过且 4 项平台跳过;Ruff 与 diff 检查通过。上述真实文件系统负例失败,merge-tree 失败;未迁移任何活动 Goal,按 Goal 契约未轮询远端 CI。手动 benchmark-sensitive hold 与跨平台复核仍属维护者的独立门槛,不能被本地绿灯替代。

语义与 CI 对齐

#4800 的持久路径契约要求“预览不写、执行不越界、只保留一个权威状态”。新默认和旧注册兼容路径已有测试,但链接祖先反例是当前契约的具体违反;打包资产冲突则意味着当前主干上的最终交付尚不存在。两处均需在新 exact head 重新验证。

我的整体评价

REQUEST_CHANGES。 统一默认路径和显式可回滚迁移是一块有用、比例虽大但由多入口迁移驱动的完整交付;上一轮 backup 修复有效。现有阻断项仍可能让“成功”迁移触碰声明范围外的原始数据,且当前主干合并冲突。相邻的前瞻性收敛应复用现有路径校验/registry codec,而不是新增第二套路由权威;修复两项后重新评审,不执行真实用户状态迁移。

English verdict: REQUEST_CHANGES - exact head f2d3998 still rewrites a project-external registry through a symlinked .loopx ancestor for a custom state_file, and current main has packaged-chat merge conflicts. 18 migration tests and 65 project/Windows tests passed, but the synthetic real-filesystem counterexample and merge-tree failed.

Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
(cherry picked from commit f0de8eab81c20c0d6a22675c222f05bebfde649d)
Signed-off-by: Lihua <1017343802@qq.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review conclusion: REQUEST_CHANGES — exact head 9bb022e98125d083d4294c4b3deb241ad940bd9a

Two blocking findings

  1. [P1] The GoalBar revision reader rejects a supported persisted project registry. registeredActiveStatePath at packages/dsh-loopx-plugin/src/goalbar/read-model.ts:108-117 parses the registry as an object with top-level goals. A valid strict registry is [header, payload], with payload.goals; the Python registry loader and this PR's test_strict_project_registry_keeps_legacy_route_and_wire_format accept and preserve it. With an exact Goal/agent binding, computeGoalBarSourceRevision now throws GoalBarSourceRevisionError: project registry has no Goal list. A review-only Vitest fixture using real temporary files and a canonical payload_sha256 reproduced the failure on this head, while the existing 17 DSH tests pass. Decode and verify both supported wire forms, choose the payload's Goal list, and keep hashing the original bytes. Add strict-envelope revision and stable service readback tests, including an unmigrated legacy route.

  2. [P1] The Goal destination guard misses Windows junction/reparse ancestors. _require_goal_destination at loopx/local_state_migration.py:160-174 checks each ancestor with is_symlink() only, whereas the adjacent _is_redirected_path deliberately detects is_junction() and reparse attributes. A targeted predicate simulation showed that the helper detects a redirecting ancestor while the Goal destination guard accepts it. That is a preflight safety gap for a Goal rename through a Windows junction; I have not run a physical Windows junction test, so the filesystem effect remains to be confirmed there. Use the existing redirected-path guard for destination and source ancestors, then add a real Windows junction-ancestor rejection test for preview and execute.

动机

Issue #4800 targets a real split in fresh local state: global ~/.codex/loopx and project .codex/goals. The desired outcome is one .loopx default without silently moving existing state; legacy registered routes must keep working until an explicit migration. This is a worthwhile cohesive slice, but that legacy readback promise is not yet proven across the shipped DSH consumer.

改动思路

The PR centralizes fresh path selection and adds explicit migration preview, backup, execute, and rollback. The project registry and its declared state_file remain the authority; GoalBar should derive its revision from that persisted contract. Python and TypeScript are separate runtimes, but they consume the same registry wire format. The new TypeScript structural check does not yet match the existing Python codec. For filesystem safety, the migration module already has a reusable redirected-path classifier, so a second narrower ancestor rule is unnecessary.

具体改动

The full base-to-head PR changes 111 files (+2,001/-293): runtime/CLI path selectors and migration, generated commands and DSH readback, tests, and public docs. Prior feedback about explicit migration and strict registry retention is addressed on the Python path; this review considers the whole current head, not just those fixes.

关键代码讲解

  • local_state_migration._require_goal_destination validates Goal rename targets, but its ancestor loop omits junction/reparse detection.
  • read-model.registeredActiveStatePath resolves the declared active state_file, but assumes goals is at the JSON top level.
  • read-model.computeGoalBarSourceRevision newly calls that parser for an exact Goal binding, making the supported envelope failure observable in GoalBar's stable-read path.

对主干的风险

The strongest regression is an existing strict-envelope project opening GoalBar without opting into migration: the new parser throws before it can hash the registered state. The Windows redirect guard is a separate preflight safety gap. I ran uv run --extra test python -m pytest tests/test_local_state_migration.py -q (22 passed), the existing DSH read-model suite (17 passed), selected Ruff and git diff --check (passed), the strict-envelope DSH counterexample (failed as above), and a simulated junction predicate probe (guard accepted it). No physical Windows test was available; remote CI was not polled, per this Goal's review configuration.

语义与 CI 对齐

The current obligation is explicit migration with legacy registered-state readback before opt-in. This head's DSH parser violates that persisted codec contract. The minimum repair is codec-aware GoalBar parsing plus the junction guard; rerun the Python migration suite, the existing DSH suite, a strict-envelope revision/service fixture, and a real Windows junction migration test. Green existing suites alone do not establish this cross-language parity.

我的整体评价

REQUEST_CHANGES for this unchanged exact head. The new default and explicit migration are directionally justified, and the tested Python path is strong; the two negative boundaries above must be repaired before the main branch can safely take the whole change. A small related refactor is to reuse _is_redirected_path in the Goal destination guard and share one representative strict-registry fixture across the Python/DSH contract. The review is not a CI or merge-readiness claim.

English verdict: REQUEST_CHANGES — preserve strict-envelope GoalBar readback and reject redirected Goal migration ancestors.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: 统一默认本地状态路径为 .loopx,并提供从 .codex 的显式迁移

2 participants