@zq-silk/yui 0.15.8 → 0.15.11

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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -0,0 +1,266 @@
1
+ <p align="right"><a href="./release-workflow.md">English</a> | <strong>简体中文</strong></p>
2
+
3
+ # 获授权的发布操作
4
+
5
+ 发布工作流是一段被显式选择、获授权的外部发布效果序列——pull request、CI 确认、
6
+ 合并、版本 tag、npm 发布、全新安装冒烟、CLI 更新、Controller 替换、Project 迁移和
7
+ 后置验证。它是一个专用的外部效果设施,而不是 Yui 的 Task 规划或 Agent 执行模型。
8
+ Agent 选择一个预先声明的计划,设施从持久状态驱动该计划:每次转换都在下一次外部调用
9
+ 之前持久化,因此崩溃、超时或被撤销的 grant 都不会让发布陷入猜测。
10
+
11
+ 两个 Task 级记录族支撑它:
12
+
13
+ - **CapabilityGrant**(`capability-grant-N`)——权威。一个具名的授权者把 grant
14
+ 限定到若干动作、参数边界、一个过期时间、一个使用次数和一个不可逆上限。
15
+ - **ReleaseWorkflow**(`release-workflow-N`)——计划及其进展:一个确切来源(仓库 +
16
+ 钉住的 commit,可选一个 artifact)、一份不可变的有序步骤计划,以及每步一条持久记录。
17
+
18
+ 引擎(`src/release/releaseWorkflowEngine.ts`)是一个纯库;`yui task workflow` 和
19
+ `yui task grant` 命令驱动它。每个外部系统都位于 `ReleaseWorkflowPorts`
20
+ (`src/release/releaseWorkflowPorts.ts`)之后,因此整个工作流可以用确定性的 fake
21
+ 测试,不产生任何真实的 GitHub、npm、git、Controller 或进程副作用。
22
+
23
+ ## 授权模型
24
+
25
+ 每一次(再)提交一个步骤,都在外部调用**之前**通过 `checkGrant(grant, request, now)`
26
+ (`src/grant/capabilityGrant.ts`)。步骤 kind 就是 grant 动作:一个 grant 列出它授权的
27
+ 步骤 kind,例如 `--action npm-publish --action version-tag`。该判定是 fail-closed
28
+ 的——每个拒绝都带一个机器可读原因并停止这次运行:
29
+
30
+ | 原因 | 含义 |
31
+ | --- | --- |
32
+ | `grant-missing` | 没有 grant 记录绑定到该工作流(引擎级)。 |
33
+ | `grant-revoked` | 该 grant 已被 operator 撤销。 |
34
+ | `grant-expired` | 墙钟已过该 grant 的 `expiresAt`。 |
35
+ | `grant-uses-exhausted` | 该 grant 的 `maxUses` 已被消耗。 |
36
+ | `grant-action-not-allowed` | 步骤 kind 不在该 grant 的动作中。 |
37
+ | `grant-parameter-missing` | 步骤缺少一个受约束参数。 |
38
+ | `grant-parameter-value-not-allowed` | 一个受约束参数的取值越界。 |
39
+ | `grant-irreversibility-exceeds-ceiling` | 该步骤比 grant 的上限更不可逆。 |
40
+
41
+ 附加规则:
42
+
43
+ - **每次获授权提交消耗一次。** 引擎在成功判定与外部调用之间记录一次 grant 使用,
44
+ 因此一个 `maxUses` grant 会在那次将要超额的尝试上 fail closed。
45
+ - **不可逆步骤需要一个已确认的前缀。** 一个标记为 `irreversible` 的步骤额外要求此前
46
+ 每个步骤都是 `succeeded`;否则该步骤以 `prerequisite-not-confirmed` 失败并停止运行。
47
+ 这正是让 `npm-publish` 不会跟在一个失败的 PR 之后运行的机制。
48
+ - **拒绝会被记录。** 当一个待处理步骤被拒绝时,引擎会启动并把该步骤失败,日志里带上
49
+ 这次拒绝,因此 `workflow status` 能准确显示授权在哪里停下。
50
+ - **重新绑定。** 一个被撤销、过期或过窄的 grant 不会让工作流走进死胡同。签发一个新
51
+ grant 并用 `yui task workflow resume <task> <workflow> --grant <new-grant>` 恢复;
52
+ 跨越这次重新绑定,计划、来源和所有已确认的步骤证据都不可变。
53
+
54
+ ## 稳定的 Task-final Review 合同
55
+
56
+ 兼容的 CLI 包更新和 Controller 替换不改变一个活动 Task 的 final-review 能力。受管
57
+ Session 使用普通的 `yui` 命令,兼容性由协议和存储身份检查,一个替换 Leader 呈现由
58
+ 持久 Task 证据已确立的合同。不需要任何版本感知的 Operator 动作。Candidate 和
59
+ Task-final 的 ReviewRound 记录必须全部携带那唯一的合同。冲突的记录 fail closed;
60
+ 不存在重新绑定事件、恢复命令或第二套合同状态机。
61
+
62
+ ## 常驻 Agent Host 升级兼容
63
+
64
+ 存活 Host 保持原 Endpoint 实现,先把准确的 Session/attempt/nativeTurn 事实写入现有
65
+ 持久 Inbox,再联系 Controller。只有当前 Controller 解析 Run 归属、校验权限、工作区、
66
+ 生命周期与历史关联并提交结果。Inbox 文件不代表接受;提交后才确认消费,断线或丢失
67
+ ACK 不会重放用户输入或模型工作。
68
+
69
+ 启动事实从已兑现的 launch payload 取得准确 Run 身份,不把 Run 固定到长期 Session
70
+ 环境。即使冻结的 Run 工作区不同于 Role 默认值,登记前的证据也能保留;后续活动和
71
+ 终态仍只按各自的原生输入身份解析归属。
72
+
73
+ 支持边界为控制协议 `yui-agent-host/v5`、事件来源协议 `yui-agent-host-events/v1`、
74
+ Controller RPC 版本 4。声明 `storage=controller-owned` 的 Host 不打开 Home 数据库,
75
+ 包括进程归属、原生账号位置和执行环境校验。Home 22 声明 Inbox 的新增来源字段,
76
+ 不改写有效历史事实与业务记录。后续版本要么保留该线协议,要么在修改存储前拒绝不兼容
77
+ 的存活生产者。刷新 Session CLI wrapper 或新 `doctor` 成功都不能证明旧 Host 兼容。
78
+
79
+ `upgrade`、`update` 的目标版本预检及 release activation 独立检查存活 Host。
80
+ 没有此能力的 legacy Host(包括 idle)以及兼容响应不确定的 Host 都阻止采用;
81
+ 在既有隔离/静默交接边界再次检查,先于迁移或发布切换。检查不会 kill、替换或 reload
82
+ Host。先让原有工作结束并保留原始未决输入/结果证据,再由获授权的 Operator 在安全
83
+ 边界选择 Session 替换或清理,之后重试。安装新代码不能修改已加载的 legacy Host 内存,
84
+ 也不能回收它从未持久投递过的终态。
85
+
86
+ `task role status`、`task role list` 和 `task role session inspect` 在持久 Run 状态旁
87
+ 显示 Host 上报观测。原生结果待落库或已知上报故障需要关注,但不代表 Provider 失败、
88
+ Run 已结束或业务已验收。Host 诊断只检查自己已投递文件的身份是否存在,不解析或隔离
89
+ 其他生产者的新格式事件。
90
+
91
+ ## CLI 与 Controller 发布边界
92
+
93
+ 全局 `yui` 命令是稳定的用户与受管 Session 接口。对普通命令,它不跟随
94
+ `runtime/active-release.json`:那个指针选择的是 Controller 发布,而不是 CLI 包。这让
95
+ CLI、Operator Session 和 Controller 替换保持兼容,而不必把每个命令钉在一个不可变构建上。
96
+
97
+ 一个源码 checkout 或以其他方式未经验证的本地 CLI 不是这个已发布接口。当 `YUI_HOME`
98
+ 已经命名了一个活动发布时,这样的 CLI 会在打开存储之前失败,并报告它的构建/来源、
99
+ 持久的 Home 身份以及它的调用类别。`make install-local` 仍默认使用 checkout 隔离的
100
+ `output/dev/home`;把那个 launcher 显式指向一个发布拥有的 Home 会被拒绝。
101
+
102
+ 一个显式的 `yui release activate <release-id|build-id>` 是唯一的例外。全局 CLI 校验
103
+ 已安装的目标发布及其匹配的冒烟回执,然后把未改动的激活参数委派给那个目标的
104
+ `dist/cli.js`。因此目标发布拥有完整的交接协议和超时层级。一次无目标的激活、help、
105
+ `--json` 以及其他每个命令都留在全局 CLI 上。激活不新增另一条普通 CLI 路由路径。
106
+
107
+ ## 步骤目录
108
+
109
+ 计划是一个固定、预先声明的操作子集。每个计划条目有一个 id(工作流内唯一)、一个
110
+ kind、可选 params,以及一个可选的不可逆级别(`none` | `reversible` | `irreversible`)。
111
+
112
+ | Kind | 外部效果 | 权威身份 |
113
+ | --- | --- | --- |
114
+ | `pr-create-or-reuse` | 创建发布 PR,或复用一个针对该 head 的开放 PR。 | `pull-request` 号 |
115
+ | `ci-confirm` | 读取该来源 ref 的 CI 结论;仅在 `success` 时成功。 | — |
116
+ | `merge` | 合并具名 PR(默认 squash)。 | — |
117
+ | `version-tag` | 创建并推送带注释的版本 tag。 | `git-tag` 名 |
118
+ | `npm-publish` | 把 tarball 发布到 registry。 | `npm-package` 版本 |
119
+ | `fresh-install-smoke` | 从 registry 安装并运行已发布的包。 | — |
120
+ | `cli-update` | 通过既有更新编排器更新 Yui CLI/Controller home。 | `controller-home` |
121
+ | `controller-replace` | 停止并重启 file-task Controller。 | — |
122
+ | `project-migrate` | 通过既有的 project 命令运行 Project 迁移。 | — |
123
+ | `post-verify` | 运行一个任意的验证命令。 | — |
124
+
125
+ 步骤可以引用更早的证据:一个 param 值为 `$externalId:<step-id>` 时,会在运行时解析为
126
+ 被引用步骤已确认的 external id,因此一个 `merge` 步骤可以消费 `pr` 步骤产生的 PR 号,
127
+ 而 operator 事先并不需要知道它。对一个未确认步骤的引用会让运行失败,而不是猜测。
128
+
129
+ ## 恢复与 resume 语义
130
+
131
+ 一次运行总是从 **resume 游标**开始:第一个状态非终态(`succeeded` 或 `skipped`)的
132
+ 计划步骤。没有“从头再来”——已确认的步骤绝不重跑。
133
+
134
+ 因为每次状态转换都在下一次外部调用之前持久化,所以任意一点的进程退出都是可恢复的:
135
+ 重新调用 `run`(或 `resume`),引擎就从第一个未确认步骤继续。`--max-steps <n>` 限定
136
+ 单次运行;一次在工作流中途耗尽预算的运行返回 `budget-exhausted`,下一次调用继续。
137
+
138
+ 在途步骤通过**权威身份查询**解决,绝不盲目重新提交:
139
+
140
+ - 一个留在 `running` 或 `unknown` 的步骤,先按其记录的 `externalIdentity` 查询。
141
+ - `exists` → 该步骤到达 `succeeded`,且**没有第二次提交**(`unknown` 被确认,
142
+ `running` 被完成)。
143
+ - `unknown` → 运行以结果 `unknown` 停止;在其命运不可知期间,该步骤绝不被重新提交。
144
+ - `absent` → 效果从未落地,因此该步骤被重试(一个 `running` 步骤会记录这次恢复尝试)。
145
+ - 一个**没有** external identity 的 `running` 步骤在记录提交结果之前就崩溃了。一个
146
+ 不可逆步骤无论如何都通过端口查询(适配器咨询其持久幂等存储):`exists` 不经第二次
147
+ 提交确认该步骤,`unknown` 以 `unconfirmed` 停止,只有权威的 `absent` 才恰好重试该
148
+ 步骤一次。一个可逆步骤总是落到重试,并沿用同一个幂等键。
149
+ - 一次**没有** external identity 的超时把该步骤标记为 `unknown`(unconfirmed),因此
150
+ 它绝不被盲目重新提交;在 resume 时它以 `unconfirmed` fail closed。
151
+ - 一个 `failed` 步骤在下一次运行时被重试;它的 `attempts` 计数和日志按尝试增长。
152
+
153
+ 运行结果:`succeeded`、`failed`、`unknown`、`unauthorized`、`unconfirmed`、
154
+ `budget-exhausted`。每个都带一个机器可读的 `stopReason`(例如 `unknown:publish`、
155
+ `unauthorized:grant-revoked`、`budget-exhausted:verify`)以及该次运行尝试过的步骤 id
156
+ 列表。
157
+
158
+ ## 幂等键合同
159
+
160
+ 每个步骤的幂等键在**创建时预先声明**且永不改变:
161
+
162
+ ```text
163
+ <taskId>/<workflowId>/<stepId>
164
+ ```
165
+
166
+ 该键被传给该步骤的每一次 `executeStep` 调用,包括在一次确认为 absent 的超时之后的
167
+ 重试。端口合同要求 `executeStep` 在同一键下是幂等的:一次重试尝试不得产生第二次
168
+ 副作用。引擎侧的合同更严格——它绝不为一个已标记 `unknown` 的步骤调用 `executeStep`,
169
+ 而是按记录的身份重新查询。fake 记录每一个键,因此测试套件直接证明至多一次执行。
170
+
171
+ ## Operator 指南
172
+
173
+ Session 权威依据当前持久绑定检查。Telemetry 按 Role/AgentRun 分组,进程 owner 使用
174
+ PID/start 身份。存储变更遵循[唯一的显式升级边界](sqlite-control-plane-design.zh-CN.md);
175
+ 普通命令绝不改写 Home schema。
176
+
177
+ grant 的签发与撤销是不可逆权威操作。它们需要当前已登记的全局 Operator 对话。它的
178
+ 原生 session ID 必须与持久的活动 session 绑定匹配:Codex 命令在存在时使用
179
+ `CODEX_THREAD_ID`,否则使用 `YUI_NATIVE_SESSION_ID`;Claude 使用 `YUI_NATIVE_SESSION_ID`。
180
+ Host generation 和启动时的 Agent 标签不是调用者身份。通过另一个入口恢复同一段对话
181
+ 不撤销其权威。一个未登记、被替换或已结束的对话没有这种权威。一个受管的 Task Agent
182
+ 不能自签发或自撤销 grant,清空子进程环境也不赋予用户权威。被记录的授权者/撤销者
183
+ 绑定到那个 Operator session(`operator:<agent-id>`);不存在可伪造的 `--granter`/`--by`
184
+ 标签。
185
+
186
+ ```sh
187
+ # 1. Operator session 为发布链签发权威。
188
+ yui task grant issue task-15 \
189
+ --action pr-create-or-reuse --action npm-publish --action post-verify \
190
+ --irreversibility-ceiling irreversible
191
+
192
+ # 2. 针对确切来源和预先声明的计划创建工作流。
193
+ # npm-publish 步骤需要一个内容寻址的来源 artifact:不可变的工作流来源以后
194
+ # 永远无法再获得它,因此没有 --source-artifact 的计划在创建时即被拒绝。
195
+ yui task workflow create task-15 \
196
+ --grant capability-grant-1 \
197
+ --source-repo acme/widget --source-commit abc1234deadbeef0000000000000000000000000 \
198
+ --source-artifact widget-1.0.0.tgz@sha512-<base64-integrity> \
199
+ --step pr:pr-create-or-reuse \
200
+ --step publish:npm-publish --step-irreversibility publish=irreversible \
201
+ --step-param publish:tarball=./dist/widget-1.0.0.tgz \
202
+ --step verify:post-verify --step-param verify:command='yui --version'
203
+
204
+ # 3. 运行(或 resume)并检查。
205
+ yui task workflow run task-15 release-workflow-1
206
+ yui task workflow resume task-15 release-workflow-1 [--grant capability-grant-2] [--max-steps 1]
207
+ yui task workflow status task-15 release-workflow-1
208
+
209
+ # 4. 随时撤销权威;下一个步骤以 unauthorized 停止。
210
+ yui task grant revoke task-15 capability-grant-1
211
+ ```
212
+
213
+ `workflow status` 渲染每个步骤的状态、尝试次数和已确认的 external id,因此 operator
214
+ 能准确看到一次发布在哪里停下以及为什么。
215
+
216
+ ## 真实资源边界
217
+
218
+ 真实执行只通过 `yui` CLI 发生,它接上真实适配器(`createReleaseWorkflowPorts`)。该
219
+ 适配器是既有原子操作——`gh`、`npm`、`git`、CLI 更新编排器、Controller stop/restart 和
220
+ `project migrate`——之上的一层薄壳,并且只在一个人类授权者签发了对每个步骤都通过
221
+ `checkGrant` 的显式 CapabilityGrant 时才运行。一个本地测试请求绝不替代那份权威。
222
+
223
+ 由 tag 触发的 `publish.yml` 工作流是唯一维护的发布冒烟。它复用那个通过了 core CI 的
224
+ 确切 commit,只增加发布所需的产物装配、全新安装和 provenance 检查。
225
+
226
+ 该工作流通过 npm Trusted Publishing(OIDC)认证,因此发布身份存在于 tag 之外的两处:
227
+ `repository`、`bugs` 和 `homepage` 由 `assemble-runtime-package.mjs` 从源 `package.json`
228
+ 逐字复制进已发布 manifest,而该包的 npm Trusted Publisher 条目点明 GitHub owner、
229
+ 仓库、工作流文件和环境。npm 在接受 provenance 之前会区分大小写地将 `repository.url`
230
+ 与正在构建的仓库比较。因此重命名或转移 GitHub 仓库时,必须连同重命名一起更新这些 URL
231
+ 和 npm Trusted Publisher 条目;否则下一个 tag 会走到 `npm publish` 并在那里失败——此时
232
+ tag 和已过门的构建都已成功。
233
+
234
+ ## 适配器安全加固
235
+
236
+ 真实适配器(`createReleaseWorkflowPorts`)在引擎的 grant 检查之外再施加额外防护:
237
+
238
+ - **Tarball 选项注入。** 一个看起来像选项的 tarball 路径(以 `-` 开头)在任何子进程
239
+ ——`tar -xOf` manifest 检视和 `npm publish`——看到它之前就被拒绝,因此一个精心构造的
240
+ 路径永远不会被当作 flag 解释。
241
+ - **Tarball TOCTOU。** 在校验冻结的 `source.artifact.integrity` 之后,被校验的字节被
242
+ 快照到一个工作流私有、只读的临时文件。`tar -xOf` manifest 检视和 `npm publish` 都
243
+ 读取该快照,而不是活的 tarball 路径,因此校验之后对原文件的替换不能改变所发布的内容。
244
+ 步骤完成时移除该快照。
245
+ - **钉住的外部命令。** 适配器在构造时通过 `resolveExecutable` 把它 shell 调用的外部
246
+ 命令(`gh`、`git`、`npm`、`tar`、`sh`)解析为绝对路径,只走一次调用者的 `PATH`。每次
247
+ 子进程调用都使用解析后的路径,因此之后的 `PATH` 变化(或被操纵的工作目录)不能把一次
248
+ 发布效果重定向到另一个二进制。一个无法解析的命令返回一个合成失败(exit 127),不调用
249
+ 任何二进制。
250
+ - **钉住的 cli-update 激活目标。** 在不可逆的更新效果之前,适配器把确切的激活目标——
251
+ Home 加上全局 npm 前缀(`bin/yui`)——持久化到 Home 下的一个持久文件
252
+ (`release/cli-update-identity/<idempotency-key>.json`)。一次硬退出的恢复查询(一个
253
+ 没有记录身份的步骤)读取这个文件并调用那个钉住的目标;如果该文件不存在(进程在预效果
254
+ 持久化之前退出),查询返回 `unknown`,而不是从 resume 调用者的 `npm prefix --global`
255
+ 或 `PATH` 推导目标,因此 resume 环境中的另一个安装不能替这个步骤背书。
256
+ - **Controller 生命周期校验。** 一次 `cli-update` 恢复查询会证明替换后的 Controller
257
+ 确实拥有目标 Home:它运行 `yui --json controller status`(`YUI_HOME` 钉在记录的 Home
258
+ 上),并要求一个 `current` controller 资源,其 `yuiHome` 解析到那个 Home,然后运行
259
+ `yui --json controller identity`,并要求已认证的 Controller 身份与已激活的产物匹配:
260
+ Node.js 可执行路径、由钉住的全局二进制派生的确切 Controller 入口点,以及包版本。仅有
261
+ 二进制健康(doctor、`--version`)绝不确认这次交接,任何不可证明的状态都返回 `unknown`。
262
+ 这适用于带身份的查询和硬退出查询(一个没有记录身份的步骤)两者。
263
+ - **npm integrity 比较。** 一次 `npm-publish` 恢复查询不止步于已发布版本:它通过
264
+ `npm view <pkg>@<version> dist.integrity` 取 `dist.integrity`,并与冻结的
265
+ `source.artifact.integrity` 逐字节比较。匹配则确认该步骤;同一版本但字节不同是一个
266
+ 冲突,返回 `unknown`(绝不确认,绝不重新发布);一个缺失的版本是 `absent`。
@@ -1,3 +1,5 @@
1
+ <p align="right"><strong>English</strong> | <a href="./roles-and-configuration.zh-CN.md">简体中文</a></p>
2
+
1
3
  # Roles, Profiles and execution configuration
2
4
 
3
5
  ## Responsibilities
@@ -0,0 +1,96 @@
1
+ <p align="right"><a href="./roles-and-configuration.md">English</a> | <strong>简体中文</strong></p>
2
+
3
+ # Role、Profile 与执行配置
4
+
5
+ ## 职责
6
+
7
+ Agent 选择一个执行组件、连接方案和启动环境。Role 选择一个活动的 Agent 绑定和
8
+ 可移植行为。每个绑定保留独立的运行时选项。一个 Role 可以持有多个绑定,而不产生
9
+ 并行写者,也不把一个绑定的凭据/配置共享给另一个。
10
+
11
+ Task Role 是 Task 局部的;global Role 提供已配置的默认值和全局对话。Role
12
+ 身份/配置不是可写的运行时状态。Session 与 Provider 观察描述实际活动。
13
+
14
+ 一次显式的 Task-final Review 使用既有的 Task 局部 Reviewer Role,不要求存在同名
15
+ 的 global Role。只有当请求的 Task Role 不存在时,global Role 才作为创建模板。
16
+ 可用性、producer 分离和冻结的审查候选仍然适用。
17
+
18
+ ## Profile
19
+
20
+ Agent Profile 把可移植行为(指令、Skill 和访问意图)与运行时意图组合在一起。
21
+ 运行时要么沿用当前 Global Worker 绑定,要么显式选择一个 Agent 并可选 model 和
22
+ effort。`config profile reset` 提供 `worker`、`explorer`、`implementer` 和
23
+ `reviewer`。
24
+
25
+ 从 Profile 创建 Task Role 会冻结其解析后的行为和绑定。之后对 Profile 或 Global
26
+ Worker 的编辑不会改写既有的 Task Role。重新套用一个 Profile 是一次显式配置变更。
27
+ 所选 Agent 必须与目标绑定匹配;显式的 Role 选项覆盖对应的模板字段。Profile
28
+ 不是 Session、工作区 owner 或资源 grant。
29
+
30
+ 原生子代继承其父 Agent 和权限。Profile 可以引导它们的行为;model/effort 覆盖
31
+ 需要真实的原生工具支持。它们不获得 Yui Role、独立 Assignment 或更大范围。
32
+
33
+ ## 期望、生效与观察
34
+
35
+ 期望设置是下一次启动的意图。AgentRun 和 Session 捕获生效启动:Agent/组件、
36
+ 协议、model、effort、权限策略、工作区/环境、Role 上下文以及 planning/delivery
37
+ 权限。
38
+
39
+ 对运行中配置的检查单独报告 Agent 实际声明的内容。unsupported 和 unknown 都是
40
+ 显式的;一个被接受的 setter 若没有回报当前值,并不算已观察到的匹配。读取配置
41
+ 不会修改 Agent 以让观察与期望一致。
42
+
43
+ Worker 绑定变更保留活动 Assignment 的 Agent 和生效快照;之后的显式派发使用当前
44
+ 选择。Leader 替换撤销上一条管理入口,但不改写 Worker Assignment。更改期望配置
45
+ 不会热改原生 Session。一次显式的 `task role session new` 请求会在选择新 Session
46
+ 之前处理旧运行时清理;它不要求先手动结算 Run 状态。一个有用的 Session 可以复用,
47
+ 但它绝不是 Task 上下文的唯一持有者。
48
+
49
+ ## 权限与 Project 上下文
50
+
51
+ Provider 权限策略、Profile 访问意图和 Project 写范围是不同的合同。Provider 旁路
52
+ 不授予对另一个 Project 的写入。受管工作区 owner、精确 Assignment 和资源 grant
53
+ 落实 Yui 操作;宽泛的原生权限不是 OS 沙箱。
54
+
55
+ Yui 提供其通用 Role Skill 和 Context 指针。Project Skill 仍是由 Agent 原生发现的
56
+ 普通 Project 文件。Project Knowledge 维护在 `YUI_HOME` 下;把仓库材料复制进 prompt
57
+ 并不使其成为权威 Knowledge。
58
+
59
+ ## 原生认证
60
+
61
+ 账号配置比 Session 活得更久。Yui 保留 `HOME` 和所选的 `CLAUDE_CONFIG_DIR`;
62
+ 新建/恢复的 Session 不会复制、清除或伪造原生登录、key 批准或 onboarding 记录。
63
+
64
+ 对 Claude Code,标准的 API-key、base-URL、bearer/OAuth、model-alias 以及原生
65
+ provider 选择相关环境变量只转发给 Claude。这些值留在 Controller 可替换的运行时
66
+ 环境和子进程中,不进入 Task/Role 记录。取消某个来源并刷新 Controller 环境,即可
67
+ 在后续启动中移除它。其他自定义凭据变量仍使用显式的 Agent 环境绑定或原生用户设置;
68
+ Yui 不继承整个 shell 环境,也不推断云凭据。
69
+
70
+ Claude 自身加载原生设置,并按其生效配置在 API key、既有 helper、登录凭据、profile
71
+ 和云认证之间做选择。Yui 不注入 `apiKeyHelper`、不复制凭据文件,也不覆盖原生认证
72
+ 优先级。显式的 `--settings` 路径和 settings 来源选择被原样透传。
73
+
74
+ 全新的原生配置仍可能需要 Claude 的初始化、key、工作区和安全确认,包括访问初始化
75
+ 服务。隔离 `YUI_HOME` 或替换 Session 都不要求一个全新的原生账号目录。受管的 Task
76
+ 执行使用 Claude 的非交互 stream-json 路径,并沿用同样的原生配置归属。
77
+
78
+ ## 命令
79
+
80
+ ```sh
81
+ yui config agent capabilities <agent-id>
82
+ yui config role show <global-role>
83
+ yui config profile show <profile>
84
+ yui task role add <task> <role> --profile <profile>
85
+ yui task role show <task> <role>
86
+ yui task role update <task> <role> --environment <preparation-id>
87
+ yui task role update <task> <role> --managed-environment
88
+ yui task role session inspect <task> <role>
89
+ yui task role session new <task> <role> --reason "<why a fresh Session is useful>"
90
+ ```
91
+
92
+ 创建 Role 时,显式的 Agent 设置需要 `--agent`。更新时,省略 `--agent` 针对活动
93
+ 绑定;一个具名绑定会被更新但不被激活。`task role bind` 更改选择。在更改期望设置
94
+ 之前,活动 Session 需要该命令的显式确认。
95
+
96
+ 关于原生配置和实现限制,参见 [Provider Runtime](provider-runtime.zh-CN.md)。
@@ -1,3 +1,5 @@
1
+ <p align="right"><strong>English</strong> | <a href="./sqlite-control-plane-design.zh-CN.md">简体中文</a></p>
2
+
1
3
  # SQLite control-plane storage
2
4
 
3
5
  Yui has one authoritative product Store: `YUI_HOME/yui.db` in WAL mode. The
@@ -67,10 +69,232 @@ Every persistent schema or payload change appends one immutable, contiguous
67
69
  storage migration. The CLI publishes both `storageVersion` and
68
70
  `minimumStorageVersion`; every valid Home in that inclusive range can upgrade
69
71
  directly to the current version without installing intermediate releases.
70
- The current source declares storage version **18**, with minimum supported
72
+ The current source declares storage version **25**, with minimum supported
71
73
  migration version **1**, in `src/storage/storageVersions.ts`. Homes below that
72
74
  floor are not migration inputs and remain untouched.
73
75
  The target binary's `upgrade --update-preflight` and `--update-apply` result
74
76
  shapes and parent-owned handover-lock proof remain backward compatible with
75
77
  every updater released from storage version 1 onward, so an old source CLI can
76
78
  still drive a much newer target's complete migration chain.
79
+
80
+ ## Unified Home layout
81
+
82
+ Every Yui self-managed directory lives under the single canonical `YUI_HOME`
83
+ (default `~/.yui`; an explicit `YUI_HOME` is honoured verbatim). `YUI_HOME` is
84
+ never inferred from the current working directory and never substituted with a
85
+ username. `src/storage/homeLayout.ts` is the one authority that derives each
86
+ managed root from Home:
87
+
88
+ | Root | Path | Holds |
89
+ |---|---|---|
90
+ | Managed worktrees | `<home>/workspaces/tasks/<taskId>/<owner>/<projectDirectory>` | Actual Task/WorkItem/Review/Integration Git directories, addressed by the bound Project directory. Owners are `main`, `work-items/<id>`, `reviews/<id>`, `integrations/<id>` and `execution-lanes/<group>/<lane>`. |
91
+ | Read-only context views | Within the same owner directory | Regenerable symlinks to read-only Project context only; writable entries are actual Git directories, not links. |
92
+ | Global Role workspace | `<home>/workspaces/global` | Default cwd for Yui-auto-created Global Roles (the `yui setup` Operator/Leader and ad-hoc Global Roles added without an explicit `--workspace`). A plain cwd, not a managed Git workspace. |
93
+ | Task provider runtimes | `<home>/runtime/task-runtimes` | Task provider data/cache/tmp; also the planning cwd at `…/planning/<taskId>`. |
94
+ | Integration runtimes | `<home>/runtime/integration-runtimes` | The integration check's provider data/cache/tmp (a separate partition from Task runtimes). |
95
+ | Update staging | `<home>/runtime/update-staging` | `yui update`'s side-by-side package install (an upgrade artifact). |
96
+ | Release workflow scratch | `<home>/runtime/release-workflow` | The release workflow's smoke-install dir and verified publish-snapshot tarball (release artifacts). |
97
+ | Storage backups | `<home>/backups` | Pre-upgrade DB backups (the fenced upgrade's rollback anchor). |
98
+
99
+ Published migrations 1–23 remain unchanged, including Task artifacts in local
100
+ Git (19), Integration continuation (20), force-archive evidence (21), and
101
+ Controller-owned Host ingress (22), and unified message input control (23).
102
+ The two offline layout steps are now 23→24 (`unify-home-layout`) and
103
+ 24→25 (`collapse-worktree-layout`). Version 24's
104
+ `workspaces/worktree` directory is an intermediate layout, not a second live
105
+ root at version 25. A single upgrade applies the full pending chain.
106
+
107
+ Stop this Home's writers and take a backup before upgrading. The layout steps
108
+ copy and verify the registered Git trees, repair only the copies' links, and
109
+ preserve old sources for manual recovery. Version 25 replaces registered
110
+ Task-view symlinks with real writable directories; unrelated Task scratch is
111
+ retained. Read-only context remains a view and can be promoted to a writable
112
+ worktree when WorkItem scope expands. Do not delete the old sources until the
113
+ new layout is verified; a failed upgrade requires manual residue cleanup and
114
+ backup recovery, not automatic resume.
115
+
116
+ Both runtime partitions (`runtime/task-runtimes`, `runtime/integration-runtimes`)
117
+ are the ONLY Home subtrees a provider runtime root is allowed to overlap; a
118
+ runtime root overlapping any other part of Home (the database, `workspaces/`,
119
+ `projects/`) is still rejected by `assertTaskRuntimeIsolationPreflight`, so
120
+ unifying the root does not weaken control-data or cross-owner isolation.
121
+
122
+ `defaultWorkspace` is a user-facing cwd for external Project input only; it is
123
+ **not** a second authority for internal managed paths, and is intentionally not
124
+ an input to `homeLayout.ts`. A Yui-auto-created Global Role that carries no
125
+ user-chosen cwd no longer falls back to it (or to `process.cwd()`): `yui setup`'s
126
+ built-in Operator/Leader and `yui role add` without `--workspace` now default to
127
+ the Home-internal `managedGlobalRoleWorkspace(home)` (`<home>/workspaces/global`),
128
+ and `setup` no longer fabricates an external Home-sibling `workspace/` — a
129
+ `default-workspace` is persisted only if the user configured one. A user who
130
+ *names* an external directory (explicit `--workspace`, or a configured
131
+ `default-workspace`) keeps external-resource semantics; the outside-Home guard
132
+ still applies to it. The "planning/global cwd" that criterion 1 places under Home
133
+ is thus both the *disposable runtime cwd Yui materializes itself* — the Draft
134
+ planning cwd (`planningRuntimeCwd`, under `runtime/task-runtimes/planning`) — and
135
+ the auto-created Global Role cwd above; only an operator's *explicitly named*
136
+ external directory stays outside by design.
137
+
138
+ Only genuine short-path IPC socket ENDPOINTS remain outside Home, and only
139
+ because a Unix-domain `sockaddr_un` path has a small fixed length budget that a
140
+ deep Home path would exceed. Each is a single socket path, never a data/cache/tmp
141
+ root:
142
+
143
+ - the Controller socket (`/tmp/yui-<uid>/<homeId>.sock`),
144
+ - the tmux server socket (`/tmp/tmux-<uid>` via the tmux namespace),
145
+ - the Agent Host socket (`/tmp/yui-<uid>/agent-host/…sock`), and
146
+ - the integration check's tmux socket dir (`/tmp/yi-<uid>-<digest>`), bound only
147
+ into `TMUX_TMPDIR`.
148
+
149
+ The integration check's ordinary runtime state is **not** an exception: its
150
+ provider data, cache, and temp roots live in the Home partition above
151
+ (`runtime/integration-runtimes`); `TMPDIR`/`TMP`/`TEMP` point there, and only
152
+ `TMUX_TMPDIR` is redirected to the short `/tmp` socket dir.
153
+
154
+ ## Migration 23 → 24: unify managed paths under Home
155
+
156
+ Historically the managed worktrees lived under the out-of-Home
157
+ `defaultWorkspace` (`<ws>/worktree`, `<ws>/tasks`) and the provider runtimes
158
+ under a string-built Home sibling (`<home>.task-runtimes`). The one forward
159
+ migration `unify-home-layout` (`src/storage/migrations/unifyHomeLayout.ts`)
160
+ brings that content under Home and rewrites the persisted absolute pointers the
161
+ runtime dereferences as live, without re-cloning Git content or renaming the
162
+ path-independent Git refs. It runs as the migration's `migrateData` step inside
163
+ the upgrade transaction, so schema and data advance atomically or roll back
164
+ together.
165
+
166
+ **Exactly one tree is physically relocated: the managed Git worktree tree.** It
167
+ is the sole subtree that holds durable, non-regenerable content (committed **and**
168
+ uncommitted work), so it alone is copied on disk. Everything else that "moves"
169
+ moves only by pointer:
170
+
171
+ - the per-Task symlink views (`<ws>/tasks`) are regenerable — the pointer is
172
+ rewritten and `ensureWorkspaceView` rebuilds the links at the next launch;
173
+ - the provider runtimes (`<home>.task-runtimes`) are disposable — the pointer is
174
+ rewritten and the roots are recreated at the next launch.
175
+
176
+ The worktree copy is **non-destructive and verified** (see *Recovery and
177
+ rollback*): the source is copied (never renamed away), the replica's content
178
+ digest is checked against the source, and only a verified replica is atomically
179
+ published. The original worktree tree is **preserved** as the rollback anchor;
180
+ removing it is a later, authorized, post-restart cleanup step, never part of this
181
+ transaction.
182
+
183
+ The pointer rewrite is **surgical, not a table sweep** — only records the runtime
184
+ treats as live launch pointers are touched:
185
+
186
+ - `managed_workspaces` — the authoritative registry (`path` column, payload
187
+ `root`, every `entries[].path`). Every surviving row is live (dispositioned
188
+ rows are deleted at cleanup).
189
+ - active (`status='active'`) `turns` — **both** `run.effective.workspace` (the
190
+ actual OS launch cwd source) and the `run.workspace` mirror, rewritten together
191
+ because `validateRun` requires them to stay identical; a run's
192
+ `.result.systemEvidence.workspaceSnapshot` is frozen Git evidence and is left
193
+ byte-for-byte intact.
194
+ - `role_session_sets` / `global_role_session_sets` — each live session's
195
+ `effective.workspace` in the `sessions` map; terminal sessions in `history` are
196
+ preserved.
197
+ - `review_rounds` — the mirrored workspace (only while its `managed_workspaces`
198
+ owner row still exists) and each OPEN execution lane; an orphaned mirror or a
199
+ terminal lane is frozen evidence and is preserved.
200
+ - `work_items` — each OPEN execution lane inside `executionGroups`; candidate
201
+ snapshots (`work_item_candidates`) are frozen and preserved.
202
+ - `task_roles.workspace` — the live launch cwd, including a Draft's planning Role
203
+ under the old runtime sibling (never self-healed until activation).
204
+ - `task_records.cwd` — self-heals on the next `prepareTaskWorkspace`, but is
205
+ rewritten defensively to close the stale-read window.
206
+
207
+ Everything else is preserved on purpose: `context_snapshots`, terminal `turns`
208
+ (with their system evidence), terminal sessions, `work_item_candidates`, terminal
209
+ execution lanes, terminal `durable_jobs`, `events`, and reports are frozen
210
+ history. `resource_registry` is re-discovered from disk; `projects.path` is an
211
+ external, user-owned checkout.
212
+
213
+ The migration is applied **offline** and is **fail-closed and pre-checkable**.
214
+ It is run by the standalone `yui upgrade` boundary AFTER the operator has stopped
215
+ this Home's Controller, Agent Host, and any execution/Job writers; it does not
216
+ orchestrate that shutdown, coordinate an online write-stop, or migrate a live
217
+ Session. It keeps only the minimal preconditions it can implement directly:
218
+
219
+ - It **refuses** if a queued or running `durable_jobs` step is bound to a tree
220
+ about to relocate. A durable Job's runner is detached and could outlive an
221
+ incompletely stopped Controller, so moving that tree would risk an in-flight
222
+ silent move; this is the one residual runtime signal the offline migration
223
+ still guards. Let the Job drain or cancel it, then re-run the upgrade.
224
+ (`active_turns` is steady state, not an in-flight signal, and is deliberately
225
+ not consulted.)
226
+ - It **refuses** if a relocation target already exists at all — it is either a
227
+ foreign directory or residue from a failed prior run, and the offline migration
228
+ never adopts a pre-existing target. Confirm the source is intact, then move or
229
+ remove the target and re-run the upgrade.
230
+ - Every refusal is surfaced as a **collected, read-only pre-check**: `yui
231
+ upgrade --dry-run` and the updater's `--update-preflight` run the same plan and
232
+ the same blocking conditions execute would throw on, opening the DB read-only
233
+ and reporting each independent blocker as `{reason, detail}` (blocked outcome)
234
+ without mutating the Home — a genuine pre-check, not a best-effort guess.
235
+ - A Home already in the unified layout (or a fresh Home with nothing to relocate)
236
+ is a **no-op**.
237
+
238
+ ### Recovery and rollback
239
+
240
+ The migration keeps **no recovery manifest and no resumable state machine** — it
241
+ is a one-time offline transform, not an interruptible online orchestration. The
242
+ worktree relocation is **copy → digest-verify → atomic-publish → preserve-source**:
243
+
244
+ 1. the relocation target must not already exist; a pre-existing target is refused
245
+ up front (foreign directory or failed-run residue — never adopted);
246
+ 2. the source is copied into a same-filesystem staging dir (`<to>.incoming`),
247
+ never renamed away;
248
+ 3. a content-addressed inventory digest of the replica is compared to the source
249
+ — a mismatch deletes the staging copy and aborts (nothing published, source
250
+ intact);
251
+ 4. only a verified replica is `rename`d into the final target (atomic on one
252
+ filesystem);
253
+ 5. the original source tree is left in place as the rollback anchor.
254
+
255
+ There is **no automatic idempotent recovery**. Because a pre-existing target is
256
+ always refused, a run interrupted after a partial publish does not silently
257
+ resume or adopt the partial tree on the next attempt: the operator inspects the
258
+ preserved source, removes the incomplete target (and any `<to>.incoming`
259
+ staging), and re-runs the upgrade from a clean state. The `--dry-run` /
260
+ `--update-preflight` pre-check surfaces exactly this `target-conflict` before the
261
+ apply transaction is entered, so the residue is reported, not discovered
262
+ mid-migration.
263
+
264
+ After the copy, the worktrees are reconnected. `git worktree repair` chases the
265
+ absolute pointer files inside a worktree, so running it on a verbatim copy whose
266
+ pointers still address the OLD source would rewrite the OLD source's `.git`
267
+ files and corrupt the rollback anchor. The migration therefore **relinks first**:
268
+ it deterministically repoints, in the NEW copy only, the two cross-reference
269
+ pointer files (a linked worktree's `.git` stub and each
270
+ `main/.git/worktrees/<name>/gitdir`) from OLD to NEW, and only THEN runs `git
271
+ worktree repair` from each main clone at its new path as a belt-and-braces
272
+ reconciliation now confined to the new tree. This keeps the preserved source a
273
+ fully independent, working Git: its `.git` is byte-for-byte unchanged and it
274
+ still resolves HEAD/index/status after the migration (verified empirically on a
275
+ private disposable Home). **A repair failure is fatal** — it aborts the migration
276
+ so the transaction rolls back rather than advancing the version over unrepaired
277
+ worktrees.
278
+
279
+ Because the data step runs inside the upgrade transaction, any throw rolls the
280
+ schema back to its original version; the fenced upgrade orchestrator additionally takes a
281
+ `database.backup()` and restores it on failure. Recovery from a failed run is
282
+ **manual, not automatic**: because the source is never removed and the copy is
283
+ digest-verified before publish, the preserved source is always intact, so the
284
+ operator clears any partial target and re-runs the upgrade. No re-run can lose or
285
+ corrupt the original content, but the tool does not itself resume an interrupted
286
+ move set.
287
+
288
+ **Old-source cleanup** is intentionally deferred and out of band: after a
289
+ successful upgrade the old external `worktree`, `tasks`, and `<home>.task-runtimes`
290
+ roots are left **in place** (not emptied) until an operator-authorized cleanup
291
+ removes them. This keeps a full rollback anchor available across the first
292
+ restart.
293
+
294
+ **Rollback limits:** once the Controller restarts against the unified layout and
295
+ begins writing new records under Home, restoring the pre-upgrade DB backup no
296
+ longer matches the newly written on-disk state. Until that first post-upgrade
297
+ write, the preserved old source plus the DB backup are a complete rollback pair;
298
+ after it, the supported recovery is forward (the layout is already unified), not a
299
+ downgrade to the split layout. Verify an upgrade only on a private, disposable
300
+ Home before applying it to a shared environment.