@zq-silk/yui 0.2.0 → 0.4.1

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
package/ARCHITECTURE.md CHANGED
@@ -1,141 +1,611 @@
1
- # Yui architecture
2
-
3
- Yui is a single-user local control plane. FileTaskStore is the one authority for Yui state, tmux is the one authority for Agent terminal/process interaction, and Git is the authority for repositories and worktrees.
4
-
5
- ## Components
6
-
7
- ```mermaid
8
- flowchart LR
9
- CLI[yui CLI] --> F[(schema.json + state.json)]
10
- CLI -->|private Unix socket| C[Controller]
11
- C --> F
12
- C --> G[Git worktrees]
13
- C --> T[tmux]
14
- T --> A[Codex / Claude]
15
- ```
16
-
17
- - The CLI owns parsing, interactive selection, setup/completion, and foreground attach.
18
- - FileTaskStore owns all persisted domain records and atomic mutations.
19
- - One background Controller per `YUI_HOME` owns automatic Git/tmux effects.
20
- - tmux receives all automated input and exclusively owns interactive terminal input after attach.
21
- - Native Agent transcript stores remain outside Yui. Only explicit messages, inputs, Run state, and summaries enter `state.json`.
22
-
23
- The Controller socket uses a private discovery file, random token, strict JSON-line protocol, and local file permissions. It is transport, not a second persistence system.
24
-
25
- ## Persistent layout
26
-
27
- ```text
28
- YUI_HOME/
29
- schema.json
30
- state.json
31
- .state.lock
32
- runtime/
33
- controller.json
34
- controller.sock
35
- worktrees/
36
- <task-id>/
37
- <role-name>/
38
- ```
39
-
40
- `schema.json` records storage-layout version 5, aggregate-schema version 2, and a reserved `activeGeneration` pointer. `state.json` is one aggregate containing:
41
-
42
- - configuration and completion installation records;
43
- - configured Agents;
44
- - Repositories;
45
- - global Roles and their per-Agent session sets;
46
- - Tasks, Task Roles, RoleWorkspaces, messages, WorkItems, AgentRuns, append-only events, Task Briefs, Decisions, and Milestones;
47
- - pending Leader wakes, Leader failures, and Operator notifications.
48
-
49
- Every persisted domain record has its own schema version and is validated when read. Unsupported aggregate or record shapes fail explicitly; Yui does not silently repair them.
50
-
51
- Writes acquire a cross-process lock, reread the latest aggregate, apply the mutation once, and commit one replacement. The durable write path creates a mode-`0600` temporary file, flushes it, renames it over `state.json`, and flushes the containing directory. Compound workflow operations use the same transaction callback and produce one aggregate write.
52
-
53
- The layout and aggregate migration registries are intentionally empty in this release. Their boundaries validate complete sequential plans before applying any mutation. A reserved generation pointer allows a later layout to write and validate a new immutable generation before atomically switching the manifest; generation storage is not implemented in version 5.
54
-
55
- ## Domain model and invariants
56
-
57
- - A Task is `draft`, `active`, `completed`, or `archived`. Completion is a reversible execution fence; archive is terminal.
58
- - Creating a Task also creates its Leader Role.
59
- - Repository-backed active Tasks use one deterministic worktree per Role at `<YUI_HOME>/worktrees/<task-id>/<role-name>`.
60
- - Common Role names map directly to `yui/<task-id>/<role-name>` branches; names that are not valid Git ref segments use a deterministic encoded branch segment without changing their worktree directory.
61
- - `Task.cwd` marks the Task worktree root; each Task Role workspace agrees with its persisted RoleWorkspace path.
62
- - A Role may bind multiple Agents but has one active Agent.
63
- - Each `(Role, Agent)` binding has its own native session record. Switching preserves dormant sessions.
64
- - A Role has at most one active AgentRun.
65
- - A WorkItem has at most one active Run.
66
- - A Worker yield atomically completes its Run/WorkItem, appends its summary, and merges a Leader wake.
67
- - A Leader yield never creates a self-wake, but it releases any already-pending wake for the next Controller pass.
68
- - Completing a Task requires no active Worker Run or running WorkItem, clears pending wakes and recovery failures, and rejects later execution until an explicit reopen.
69
- - A Leader control Run may atomically yield itself while completing the Task. Reopen returns the Task to active and queues one `task-reopened` wake.
70
- - Completed Tasks retain their Role sessions and worktrees; archived Tasks stop tmux and clean only clean worktrees.
71
- - Archived Tasks reject new messages, Roles, work, dispatch, enter, and recovery actions.
72
-
73
- FileTaskStore validates cross-record references after every transaction, including Repository ownership, Task/Role ownership, active-run pointers, and session-set ownership.
74
-
75
- ## Controller pass
76
-
77
- The Controller runs a non-overlapping full reconciliation pass every 30 seconds by default. `reconciliationIntervalSeconds` may be set from 5 to 300 in Yui config. Durable state changes request an immediate pass through the Controller socket, and concurrent scan requests coalesce into one follow-up pass.
78
-
79
- `controller restart` stops only this process and waits for its private socket/discovery state to disappear before starting the currently installed runtime. tmux sessions are external durable runtime state and are never stopped by Controller restart.
1
+ # Yui Architecture
2
+
3
+ Yui is a local control plane for durable work across Projects and native Agent
4
+ runtimes. The user talks to one Operator. The Operator routes each request to
5
+ the right Project and Task; that Task's Leader owns decomposition, execution
6
+ choice, review, integration, and completion.
7
+
8
+ ## One work model
9
+
10
+ `WorkItem` is the only bounded unit of work. It holds the objective, acceptance
11
+ criteria, dependencies, assigned Task Role when applicable, lifecycle, and a
12
+ compact reviewed result.
13
+
14
+ A Leader chooses one of three execution paths for each WorkItem:
15
+
16
+ 1. **Direct**: the Leader executes a roleless WorkItem.
17
+ 2. **Native subagent**: the Leader creates a child through its current Agent
18
+ conversation. The child inherits the Leader Agent and is not a Yui entity.
19
+ 3. **Task Role AgentRun**: Yui dispatches a Role-bound WorkItem to a
20
+ Task-managed native Agent Session.
21
+
22
+ There is no Yui subagent launcher, child-session record, or second bounded-work
23
+ model. Direct work and native subagents use the WorkItem lifecycle. Managed
24
+ independent execution additionally records an AgentRun.
25
+
26
+ ## Profiles, Roles, and Agents
27
+
28
+ - `Agent` selects a supported adapter such as Codex or Claude and defines its
29
+ launch context.
30
+ - `WorkerProfile` is a versioned, provider-neutral behavior template containing
31
+ instructions, Skills, a read/write behavior intent, and optional model/effort hints.
32
+ - `TaskRole` is a mutable Worker instance inside one Task. Applying a Profile
33
+ copies its portable behavior. Its versioned desired launch configuration is
34
+ next-launch-only. The Role may bind multiple Agents; every binding retains
35
+ independent runtime configuration.
36
+ - `AgentRun` records one managed dispatch and an immutable effective snapshot:
37
+ actual Agent, adapter, model, effort, Profile behavior intent, exact writable
38
+ Projects, provider permission strategy and native options, workspace, Role
39
+ context, and source desired revision. A native Role Session stores the same snapshot; running processes
40
+ are never hot-mutated by later Role edits.
41
+ - A `WorkItemCandidate` is the explicit result currently awaiting Leader
42
+ acceptance. It snapshots the WorkItem revision, summary, and either a
43
+ yielded execution Run or a Leader-managed direct source.
44
+ - `ReviewRound` records review of one candidate under the same WorkItem and
45
+ references that immutable candidate. It is not another WorkItem.
46
+
47
+ Adding another Agent requires an explicit adapter implementation. Profiles do
48
+ not choose adapters, own Sessions, or carry credentials.
49
+
50
+ For a native subagent, the Leader must choose and read an explicit
51
+ WorkerProfile, using `worker` when no specialist fits. The Leader includes the
52
+ Profile instructions, Skills, behavior intent, workspace boundary, validation expectations, and
53
+ supported model/effort hints in the child brief. Task Role Agent bindings are
54
+ ignored because the child inherits the Leader Agent. The reviewed WorkItem
55
+ summary records the actual Profile revision, inherited or confirmed model and
56
+ effort, round, result, and checks.
57
+
58
+ ## Lifecycle and acceptance
59
+
60
+ Direct and native-subagent work follows:
80
61
 
81
62
  ```text
82
- prepare active workspaces
83
- -> stop archived Task tmux sessions
84
- -> clean archived workspaces when clean
85
- -> deliver queued Role Runs
86
- -> reconcile exited active Role Runs
87
- -> dispatch pending Leader wakes
63
+ todo -> running -> done | failed
64
+ -> awaiting Leader decision (when review is configured)
88
65
  ```
89
66
 
90
- Repository preparation precedes delivery. A Repository path and base ref are validated by Git. Each Role derives the path `<YUI_HOME>/worktrees/<task-id>/<role-name>` and branch `yui/<task-id>/<role-name>`. The minimal RoleWorkspace record retains its Repository, path, branch, base ref, and starting commit; it is not a ref ledger. Existing worktrees must resolve to the expected path, branch, and Git common directory.
91
-
92
- Archive stops tmux before worktree cleanup. Each clean Role worktree is removed idempotently and recorded independently. A dirty Role worktree and its RoleWorkspace record are preserved; they are never force-removed. A Git failure is isolated to its Task so other Task reconciliation continues.
93
-
94
- ## Durable wake and Run behavior
95
-
96
- Task activation/reopen, an Operator/user message, Worker yield, and exited Role failure can merge a `PendingWakeup`. Reasons are de-duplicated while request count and first/last timestamps remain durable. Completed Tasks never dispatch a pending wake.
97
-
98
- If the Leader is busy, the Controller does not touch tmux and leaves the wake pending. When idle, it prepares the fixed Role session, then atomically claims the unchanged wake as a durable, not-yet-delivered Leader AgentRun before any tmux input. The claim clears that wake; later requests form a new pending wake. A confirmed receipt marks the Run delivered. A send failure fails the claim and restores its wake, while a Controller crash can resume the same Run with the same `agent-run:<run-id>` receipt.
99
-
100
- A dispatched Worker WorkItem creates a durable AgentRun before any terminal effect. The Controller is the only automatic delivery path. Delivery uses `agent-run:<run-id>` as its receipt and persists `deliveredAt` plus successful session/Role state after tmux confirms the send. Completion and yield reject a Run whose delivery is still pending.
101
-
102
- If an active Role's tmux window disappears before yield, the Controller fails the AgentRun and running WorkItem, clears the active-run pointer, stops its session record, and merges a failure wake for the Leader. A failed Leader recovery records `LeaderFailure` plus `OperatorNotification`; `jobs retry leader-recovery:<task-id>` clears those records and queues a recovery wake.
103
-
104
- `jobs list` is a compatibility projection over pending wakes and recovery failures. There is no generic Job table or retry queue.
105
-
106
- ## tmux ownership and delivery
107
-
108
- Foreground attach is a hard terminal handoff:
109
-
110
- 1. close any readline interface;
111
- 2. leave raw mode;
112
- 3. pause Yui stdin;
113
- 4. run `tmux attach-session` synchronously with inherited stdio.
114
-
115
- Yui does not read stdin, draw UI, or relay bytes while attached.
116
-
117
- Automatic delivery never reads stdin. It requires an adapter-specific readiness probe: Codex and Claude have separate composer markers. Before waiting for readiness, the Controller checks for an existing pane receipt, so a busy Agent does not cause a retry scan to block. Receipt check/write, literal input, and Enter execute in one tmux server command queue.
118
-
119
- ## Native session identity
120
-
121
- Claude receives a preallocated session ID at new launch and resumes that fixed ID later.
122
-
123
- Codex discovers its thread ID at runtime. Managed launches add a structured Codex `notify` argv configuration. After each completed turn, Codex invokes:
67
+ Task Role work follows:
124
68
 
125
69
  ```text
126
- yui internal session-notify <codex-json-payload>
70
+ todo -> running -> awaiting Leader review
71
+ | accept -> done
72
+ | reject -> failed -> redispatch -> running
127
73
  ```
128
74
 
129
- The hidden command validates the payload and Yui provenance environment, then records the fixed task/global Role session through the Controller. No session-binding text is placed in a model prompt.
130
-
131
- ## Deliberate exclusions
132
-
133
- This version does not restore:
134
-
135
- - backup/restore, import/export, trash, or general maintenance commands;
136
- - native storage extensions, derived indexes, or recovery journals;
137
- - runtime claims, leases, fencing generations, permission fingerprints, or identity ledgers;
138
- - inactivity TTL, cooldown, review-time, recurring schedules, or offline resolution;
139
- - Web APIs, Web UI, or remote multi-user coordination.
140
-
141
- Those systems are not required for the retained single-user workflow. Future storage-schema migration is the one explicit extension boundary kept in the design.
75
+ Worker yield ends the AgentRun and submits its result for review. It never
76
+ accepts the WorkItem. The Leader checks semantics, evidence, and Git state,
77
+ then accepts or rejects with bounded feedback. A rejected isolated WorkItem
78
+ keeps its workspace so the next Run can repair the same result.
79
+
80
+ An optional global review rule names one existing Global Role and chooses
81
+ `always`, `leader`, or `final`. It is a live default for every Task; each
82
+ WorkItem Candidate snapshots the effective legacy rule when submitted.
83
+ Every result awaiting acceptance is stored as an explicit WorkItem candidate.
84
+ `always` dispatches a review AgentRun for every candidate, whether it comes
85
+ from a yielded execution Run or a Leader-managed direct result; `leader`
86
+ leaves every candidate for the Leader to accept directly or review explicitly.
87
+ `final` keeps WorkItem acceptance and Integration independent, then `task
88
+ complete` queues one fresh Task-scoped ReviewRound over the frozen committed
89
+ heads of every bound Project. A changed integrated head queues a new round;
90
+ the previous report remains evidence. This final Reviewer evaluates the whole
91
+ Task, so normal delivery does not pay for a complete review of every WorkItem.
92
+ Review Runs complete only their exact ReviewRound, leave the WorkItem awaiting
93
+ acceptance, and never trigger another review or append a Candidate. Successful
94
+ and failed review attempts both wake the Leader and remain evidence for
95
+ judgment, not a machine verdict. The ReviewRound stores its frozen Candidate
96
+ base, isolated workspace provenance, complete free-form report, optional
97
+ structured checks, and optional diagnostic commit. The Leader may route that evidence to the original Worker, but Yui
98
+ never merges it automatically.
99
+
100
+ Roles describe Agent capability, but they do not own repository workspaces. A
101
+ `ManagedWorkspace` is keyed by its durable owner (`Task`, `WorkItem`,
102
+ `ReviewRound`, or `IntegrationAttempt`); an AgentRun carries only a launch
103
+ snapshot. Review workspaces are fresh writable copies at the Candidate's
104
+ frozen commit, so diagnostics cannot redirect Develop or become a ChangeSet
105
+ source. Each ReviewRound has an independent lifecycle and explicit cleanup.
106
+
107
+ Dependencies are enforced at dispatch. A Role cannot have overlapping active
108
+ Runs, and terminal Task state fences new messages, dispatches, retries, and
109
+ late results until explicitly reopened.
110
+
111
+ ## Project workspaces and integration
112
+
113
+ Stable Project checkouts are read-only references. Task identity follows one
114
+ bounded outcome rather than Project count. A Task binds zero or more Projects,
115
+ records an independent base ref for each binding, and owns one workspace root
116
+ containing a managed main worktree for each binding. The
117
+ `<workspace>/tasks/<task>/main` root is a logical multi-Project container, not a
118
+ Git repository. Each Project child (for example
119
+ `<workspace>/tasks/<task>/main/yui`) is the supported Git cwd and points to
120
+ `<workspace>/worktree/<project>/<task>/main`; Git commands run in that child.
121
+ For a single-Project workspace, the native Agent starts in that Project's
122
+ managed worktree so its project configuration and Skills are discovered
123
+ natively. For a multi-Project workspace, the Agent starts at this root and Yui
124
+ registers every Project worktree through the provider's native
125
+ additional-directory mechanism. The active Leader may append a Project when the
126
+ same outcome expands; replacing an existing binding is not a scope-repair
127
+ mechanism.
128
+
129
+ A WorkItem can read the full Task workspace but has an explicit Project write
130
+ scope. Isolation creates a second root with independent worktrees for writable
131
+ Projects and Task-main context for the rest. The managed dispatch and
132
+ `yui-worker` Skill name both sets explicitly; the Agent must modify only the
133
+ writable set. Provider permission is binding configuration: every managed Role
134
+ defaults to `bypass`, while `default` and `configured` preserve provider-native
135
+ behavior. Provider permission and Profile access intent do not grant Project
136
+ writes. A normal source write requires an exact WorkItem write scope and
137
+ matching managed workspace; a review write instead
138
+ requires an exact ReviewRound owner and frozen Candidate base. Profiles and
139
+ Skills constrain behavior even when provider prompts are bypassed. Provider
140
+ permissions remain Session-wide rather than Project-specific, so the durable
141
+ workspace owner and exact Project scope remain the authorization boundary.
142
+ Scope is monotonic. A Worker cannot expand it directly: it reports the need,
143
+ and the Leader either adds Projects to the existing scope, creates another
144
+ WorkItem, or adds the Project to the Task.
145
+
146
+ An isolated result is handled in this order:
147
+
148
+ 1. the Worker yields;
149
+ 2. the Leader reviews semantics and evidence;
150
+ 3. Yui captures each writable Project HEAD as an immutable Project ChangeSet;
151
+ 4. each Project integration applies its latest reviewed ChangeSet in a candidate worktree;
152
+ 5. configured checks run;
153
+ 6. compare-and-swap advances the target only if its HEAD is unchanged;
154
+ 7. the Leader accepts the WorkItem;
155
+ 8. clean Integration, ReviewRound, and WorkItem resources are explicitly
156
+ removed.
157
+
158
+ The context contract is layered: Yui Core owns durable identity, lifecycle,
159
+ access, and workspace safety; generic role Skills own portable orchestration;
160
+ Project Policy/Knowledge and Agent-native Skills versioned in each Project own
161
+ project-specific engineering rules; and the Task Contract owns the requested
162
+ outcome. Yui injects only its own generic Role Skills. It never scans or copies
163
+ Project Skills into managed context; the selected Agent discovers them through
164
+ its native project mechanism. Execution and review select their generic Skill
165
+ by durable Run purpose. A Reviewer finding routes to the original
166
+ Worker while open, a small Repair WorkItem when closed, Leader/Integration for
167
+ merge or local fixes, and an architecture WorkItem only for a genuinely
168
+ cross-cutting design change.
169
+
170
+ Capture at the same HEAD reuses the existing ChangeSet. A repaired HEAD creates
171
+ a new candidate; only the latest reviewed candidate may satisfy acceptance.
172
+ An isolated WorkItem cannot be accepted or a Task completed while any writable
173
+ Project's latest result is uncaptured or unintegrated. Workspace roots are
174
+ multi-Project; ChangeSets and Integration Attempts remain single-Project Git
175
+ boundaries.
176
+
177
+ Conflicts store a compact report and block. The Leader chooses rejection or
178
+ manual resolution in the retained candidate worktree. Failed checks, rejected
179
+ results, conflicts, target movement, and abandoned work never advance the
180
+ target. Full check output is streamed to cleanable artifact files; durable
181
+ records retain compact evidence.
182
+
183
+ ## Durable context
184
+
185
+ Native transcripts remain native to their Agent. Yui persists only the control
186
+ and knowledge needed to resume and audit work:
187
+
188
+ - Task Brief: objective, boundaries, cross-Project technical approach, current
189
+ focus, and Leader summary;
190
+ - Decisions: material choices and supersession;
191
+ - Milestones: independently useful phase outcomes;
192
+ - Project Knowledge: stable facts reusable across Tasks;
193
+ - WorkItems, Roles, AgentRuns, Messages, InputRequests, Events, ChangeSets, and
194
+ integration evidence.
195
+
196
+ The Leader updates the Brief before every yield, records material choices as
197
+ Decisions, records phase outcomes as Milestones, and promotes only cross-Task
198
+ stable facts to Project Knowledge. `task context` is the consolidated recovery
199
+ read; launches and wake messages carry record pointers rather than copied
200
+ context.
201
+
202
+ ## Runtime ownership
203
+
204
+ tmux owns native Agent terminals. The Controller owns mailbox delivery,
205
+ wakeups, Role liveness, reconciliation, and read-only Web observation. Operator
206
+ and Leader Sessions are fixed Task/global Roles; Task Worker Sessions are
207
+ selected through Role Agent bindings.
208
+
209
+ Role desired revisions and Run/Session effective snapshots keep configuration
210
+ history explicit. Resume compares the complete effective snapshot and
211
+ workspace compatibility rather than revision alone. Desired drift is expected
212
+ while an old process is running and becomes effective only on a later launch;
213
+ control-plane wakes continue through the live Session's actual snapshot, and
214
+ fresh replacement archives the stopped snapshot instead of rewriting it.
215
+ Mailbox generations, reservations, liveness, native Turn Hooks, and exact yield
216
+ remain the control-plane authority; configuration snapshots do not replace any
217
+ of those completion fences. Lifecycle code uses structured Hook data, persisted identities, tmux
218
+ process state, receipts, and pane fences. It never parses Agent terminal glyphs,
219
+ progress text, trust dialogs, or final prose to infer readiness or success.
220
+
221
+ All durable writes use process locking and atomic replacement. Storage validates
222
+ record identity, legal transitions, dependency cycles, cross-record ownership,
223
+ immutable Git evidence, and current Controller protocol compatibility. Worktree
224
+ cleanup revalidates ownership and fails safely when concurrent state changes;
225
+ manual retry is the recovery boundary rather than another durable state
226
+ machine.
227
+
228
+ Storage compatibility is modeled on three independent, monotonic version axes:
229
+ `layout` (on-disk `schema.json`, `state.json`, locks), `aggregate` (the
230
+ authoritative document), and `record` — a `recordKind -> version` map so each
231
+ record family versions on its own. A centralized compatibility framework
232
+ (registry → planner → compatible loader or migration engine) is generic and
233
+ domain-free: the engine is parameterized over an injected `MigrationTarget` and
234
+ never hardcodes a Yui record list. Compatibility is decided **only** by explicit
235
+ adjacent transition declarations, never by version magnitude or semver.
236
+
237
+ The registry separates transition intent from executable transformation. A
238
+ `compatible` declaration is legal only on a single `record` axis and carries
239
+ three obligations: deterministic named defaults, `validateSource` for the exact
240
+ old shape (including rejection of unknown fields), and a normalizer that returns
241
+ a fresh value in the next/current domain model. Layout, aggregate, identity or
242
+ reference meaning, record splits/merges, and transactional semantic changes must
243
+ be declared `offline-migration`; that declaration is runnable only when the
244
+ matching adjacent migration step is registered. A transform without a
245
+ declaration is `missing-declaration`; an offline declaration without a step is
246
+ `missing-step`. Both fail closed. The planner chooses `compatible` only if every
247
+ hop on every changed axis is compatible; one offline hop selects the migration
248
+ engine. Future versions and damaged structures remain unsupported. The
249
+ production registry contains the explicit aggregate `16→17` offline transition;
250
+ no historical record-family normalization is implicitly authorized. A frozen
251
+ post-baseline descriptor snapshot (versions and locators) plus the shared planner
252
+ form the delivery gate: any current-axis advance, locator drift, or new target
253
+ family must have its full declared path before the registry can be constructed.
254
+
255
+ `doctor`, staged `update` preflight, ordinary store opening, and `upgrade` share
256
+ the same classification: **current** (`USABLE`), **compatible-old**
257
+ (`COMPATIBLE`), **migration-required** (`MIGRATABLE`), or **unsupported**
258
+ (`NEEDS_NEW_VERSION`/`CORRUPTED`). The legacy uppercase verdict remains an
259
+ internal/result compatibility label; the product meaning is the four-state
260
+ vocabulary above.
261
+
262
+ The staged updater uses a dedicated internal preflight contract, not the user's
263
+ `upgrade --dry-run`. It stops after four-state classification for current Homes,
264
+ strict source/current-model validation for compatible-old Homes, or the
265
+ authoritative offline inventory for migration-required Homes. It does not create
266
+ a migration target, copy or back up the Home, place a fence, touch Controller
267
+ lifecycle, or claim staged-output validation, so the exact old Controller may
268
+ still be running. After this preflight is clear, the update parent captures and
269
+ stops that exact Controller PID; only then does staged activation run the full
270
+ stage, loader validation, and atomic switch. Machine results carry one explicit
271
+ `update-preflight` outcome plus a consistent current/compatible/migration-required
272
+ status; malformed or contradictory combinations fail closed.
273
+
274
+ The three axes are genuinely independent, including the record axis. The
275
+ durable `schema.json#/recordVersions` map is authoritative for each persisted
276
+ family version; a family absent from that map is explicit pre-introduction
277
+ version `0`, even when its `state.json` locator is empty. Raw `state.json` is
278
+ traversed only to cross-check the manifest against persisted records, never to
279
+ infer that a missing target family is current. The planner can advance version
280
+ `0` only through an explicitly marked record-family `0->1` introduction; a
281
+ missing declaration or offline transform fails closed. For compatible-old,
282
+ `openCompatibleFileTaskStore` normalizes a fresh
283
+ in-memory snapshot hop by hop, validates the resulting current state with the
284
+ same strict graph gate, and exposes only the current domain model. Commits use
285
+ the existing current `FileTaskStore` writer, so the first write emits only
286
+ current records and advances the durable manifest to the same current versions;
287
+ there is no dual write, no preservation of unknown old fields, and no old writer
288
+ permitted against a newly written Home. For current Homes the
289
+ ordinary strict loader remains the direct path. CORRUPTED is reserved for real
290
+ structural JSON damage: an unparseable `state.json`, a container whose shape does
291
+ not match its locator, a record with a missing/invalid `schemaVersion`, or a
292
+ reference graph that fails the appropriate strict gate.
293
+
294
+ `yui upgrade` is the transactional entry point only for
295
+ **migration-required** Homes. Before constructing a migration target or touching
296
+ the Controller, fence, binary, staging directory, or Home, both dry-run and
297
+ execute re-read an authoritative offline inventory. The blocking facts are an
298
+ active AgentRun, an in-flight Run, a live native Session, a native Session whose
299
+ health cannot be determined, pending turn-completion ownership, a lifecycle
300
+ mailbox, or a durable inbox event. Stopped/history-only Sessions, an idle Role
301
+ with no native process, and an open Input alone are non-blocking. Every blocker
302
+ returns the count plus the available Task/Role/Run/native-session/launch identity
303
+ and reason, asserts the scene is unchanged, and names `yui update` as the user
304
+ re-run boundary. The inspection never kills, resets, rebinds, retries, or drains
305
+ anything. An unreadable inventory is unknown activity and fails closed.
306
+
307
+ Once that inventory is clear (including on the user's later re-run after a
308
+ block), execute mode places an **admission fence** honored at every authoritative
309
+ write choke point, so baseline CLI writers and the Controller (which mutate
310
+ through the same store) refuse to begin a new write while an upgrade owns the
311
+ Home; the fencing process itself is exempt. Durable runtime-inbox `publish`
312
+ participates in a separate,
313
+ shared sibling coordination boundary: `<home>.upgrade-coordination.lock` lives
314
+ outside the Home and serializes the complete inbox write with the final
315
+ snapshot/copy/two-step switch. A publish acquires that lock, then checks the
316
+ fence and any unresolved `<home>.upgrade-switch.json` marker before its
317
+ temp/link/fsync sequence. Upgrade acquires the same lock after Controller drain,
318
+ proves both runtime lanes, re-pins under `.state.lock`, stages the complete
319
+ Home, and holds the coordination lock through `home -> backup` and
320
+ `staging -> home`. A hook that passed admission before the fence therefore either
321
+ finishes under the lock and is copied into promoted Home, or waits and receives a
322
+ structured `UpgradeFenceError` that permits re-delivery; it cannot be silently
323
+ dropped into backup-only storage. With no fence, normal hook behavior is unchanged
324
+ apart from this shared serialization point. **Fence acquisition is a single atomic
325
+ `O_CREAT|O_EXCL` create** — the kernel guarantees exactly one of any number of
326
+ concurrent upgraders wins that create, so there is no check-then-write window in
327
+ which two upgraders both believe they acquired; a loser either re-enters (it
328
+ already owns the fence), reclaims a *provably-dead* owner's stale fence and
329
+ retries, or fails closed for a live/undeterminable owner. **Stale-fence reclaim
330
+ is itself atomic (compare-and-delete under a `mkdir` critical section):** the
331
+ reclaim re-reads the fence bytes under the lock and deletes *only* the exact
332
+ dead-owner bytes it observed, so a racer that slipped a fresh live fence into the
333
+ same path between the observe and the delete is never clobbered — closing the
334
+ reclaim TOCTOU that could otherwise let two entrants both acquire. **That
335
+ critical-section lock is itself crash-recoverable** (mirroring the storage lock's
336
+ dead-owner reclaim): it records its owner pid, and a lock left behind by a
337
+ crashed holder is reclaimed by a later entrant once its owner is provably dead
338
+ (or it is older than a small age bound), so a mid-reclaim crash can never
339
+ permanently orphan the lock and strand admission (R2-F4). When a reclaim cannot
340
+ be proven complete, `assertHomeWritable` re-verifies and refuses rather than
341
+ falsely reporting the home writable, and a dead-owner fence is never left
342
+ indefinitely stranding writers. There is no lease or multi-round negotiation.
343
+ The coordination lock uses the same bounded crash-recovery rule as other Home
344
+ locks: it records an owner PID, waits only a bounded interval, and atomically
345
+ renames aside a lock whose owner is provably dead (or whose owner-less directory
346
+ is older than the conservative acquisition window). A live or undeterminable
347
+ holder fails closed; a switch-progress marker blocks hook admission when the
348
+ Home is missing or uninitialized (including a malformed marker), while a stale
349
+ marker beside an intact Home is ignored after filesystem corroboration. Lock
350
+ ordering is one-way — coordination lock, then `.state.lock`; inbox writers
351
+ never acquire `.state.lock` — so the cutover cannot deadlock on a reverse order.
352
+ The fence is enforced by every writer built from this release forward (its check
353
+ lives in the shared store-commit path); it cannot retroactively bind an
354
+ already-installed older binary, so cross-release
355
+ concurrency is instead handled by the quiesce step and the recommendation to
356
+ stop all Yui activity for the home before upgrading. It then drains the
357
+ Controller with the public `controller.stop`/shutdownAndDrain (never a broad
358
+ kill, never a TTL or idle heuristic), fails closed if any foreign writer, live
359
+ Controller, or held `.state.lock` remains, and proves BOTH durable runtime lanes
360
+ empty — the aggregate `state.json` runtime-lifecycle mailboxes AND the durable
361
+ runtime inbox `runtime/inbox/*` (authoritative not-yet-applied native-hook
362
+ events; per task-1 / message-8 §3, either non-empty is a `drain-incomplete`
363
+ blocker). The inbox is proven empty **read-only** (a plain directory scan for
364
+ committed `*.json` events, in-progress `.tmp-*` writes, and quarantined
365
+ `runtime/inbox-invalid` entries) — never via the inbox's own `list()`, which
366
+ would quarantine as a side effect, so the check never mutates the source; an
367
+ unreadable inbox directory fails closed. This matters because the no-Controller
368
+ / stale-event path reaches quiesce with inbox entries still on disk, and an
369
+ atomic switch must never silently drop them. The read-only quiesce proof is
370
+ performed only after acquiring the shared coordination lock; an admitted hook
371
+ that was still completing cannot cross that lock, and a hook that waits sees the
372
+ fence and fails explicitly. The cutover then re-pins the committed revision
373
+ under the write lock after the drain (avoiding a
374
+ check-then-migrate race), migrates the immutable source into a fresh staged home,
375
+ validates it
376
+ through the real `FileTaskStore` loader gate (record parse + reference graph),
377
+ then atomically switches into place with a timestamped backup and a post-switch
378
+ health check. Any blocked or failed step leaves the authoritative home
379
+ byte-for-byte unchanged and reports the exact stage and recovery action;
380
+ User-facing `--dry-run` runs through the validation gate and reports success only
381
+ when the migration engine itself returns its exact `dry-run` evidence. A live
382
+ runtime or any other earlier engine result remains a blocker; it is never wrapped
383
+ as validated. Successful dry-run discards the staged output without switching.
384
+ The aggregate `16→17` transition is the only production offline path in this
385
+ release; compatible record-family normalization remains explicitly declaration-gated.
386
+
387
+ **Uninitialized home is an actionable blocker, not a no-op.** An
388
+ uninitialized home (never `yui setup`) has no storage to migrate. The classifier
389
+ reports it as USABLE (nothing is *wrong* with it, so `doctor` may present it
390
+ as-is), but the *upgrade* path would otherwise collapse that verdict into a
391
+ silent no-op against a home that was never set up. Upgrade therefore returns a
392
+ structured `uninitialized` blocker ("run `yui setup`") — never an unclassified
393
+ runtime error and never a false success.
394
+
395
+ **Complete home content preservation contract.** A migration only *transforms*
396
+ `schema.json` + `state.json`, but the atomic switch replaces the **whole** home
397
+ directory (`home -> backup`, `staging -> home`). Staging that held only those two
398
+ files would silently drop everything else the real home persists — `runtime/`
399
+ discovery, `runtime/inbox/*` (AUTHORITATIVE, not-yet-applied events), `cache/`,
400
+ `artifacts/`. The chosen contract (implemented in `writeFreshOutput`) is that
401
+ **staging carries a complete copy of the home**: every other entry (any depth:
402
+ dirs, files, symlinks) is copied verbatim, and only `schema.json`/`state.json`
403
+ are overwritten with their migrated bytes. So the switch preserves all
404
+ authoritative and rebuildable content — and the timestamped backup retains the
405
+ original of everything too. The transient `.state.lock` is the one exception: a
406
+ lock is per-instance coordination state, never authoritative content, so it is
407
+ not promoted into the migrated home. The staging directory is required to live
408
+ *outside* the home (an in-home staging layout is refused at construction), so the
409
+ copy never excludes a home entry merely because it shares the staging directory's
410
+ name — a real home entry named `home.upgrade-staging` is preserved like any other.
411
+
412
+ **Partial (two-step) switch is reported honestly, never as "unchanged".** The
413
+ atomic switch is two renames — `home -> backup`, then `staging -> home` — with one
414
+ non-atomic window between them, tracked by a durable sibling progress marker
415
+ (`<home>.upgrade-switch.json`) whose phase distinguishes *not-started* /
416
+ *backing-up* / *promoting* / *interrupted* / *complete*. The invariant that drives
417
+ error handling: **before** the first rename commits the home is intact and any
418
+ failure is a clean pre-switch error ("source unchanged", which is true);
419
+ **after** it commits, *every* subsequent operation — the post-rename fsync, the
420
+ `promoting` marker write, the promote rename, and the post-promote fsync/marker
421
+ clear — is phase-aware, so an fsync or marker failure can never escape as a plain
422
+ error that the engine would render as "source unchanged". On any pre-promotion
423
+ failure the code attempts an automatic rollback (`backup -> home`); when that
424
+ succeeds the original is restored and the failure is reported with the home
425
+ genuinely unchanged. **Only if the rollback also fails** is the switch left
426
+ partially applied: the marker records `interrupted`, the engine surfaces a
427
+ distinct `switch-ambiguous` outcome, and the upgrade blocks at a dedicated
428
+ `switch-ambiguous` stage that states the home is **not** intact and prints the
429
+ exact `mv "<backup>" "<home>"` recovery. A failure of the *post-promotion*
430
+ fsync/marker-clear, by contrast, does **not** fail the switch — the new home is
431
+ already in place and correct, and those steps are best-effort durability, so a
432
+ good migrated home is never rolled back. No completion receipt is written for an
433
+ interrupted switch (it did not commit); the `interrupted` marker is the durable
434
+ signal.
435
+
436
+ **Crash-window recovery keys off the marker plus filesystem evidence.** A process
437
+ that dies mid-switch leaves a durable marker (`backing-up`, `promoting`, or
438
+ `interrupted`), with the original at the backup and the home path missing. `yui
439
+ update`'s probe treats a marker of **any** phase as an interrupted switch **only
440
+ when the filesystem still corroborates it** — the backup exists AND the home is
441
+ missing/uninitialized — and then prints the exact backup-restore path, never a
442
+ generic "most likely did not commit, retry/setup" that would send the operator to
443
+ re-initialize a missing home. Crucially this evidence gate applies to the
444
+ `interrupted` phase too (R2-F3): a stale `interrupted` marker left over after a
445
+ manual recovery — the home already restored, or the backup already removed — is
446
+ **not** trusted to emit a restore path; the probe ignores the stale marker and
447
+ reconciles against the real on-disk state instead. A pre-start marker whose home
448
+ is still intact (or that has no usable backup) is likewise not treated as
449
+ interrupted: there is nothing to recover.
450
+
451
+ **Quiesce fails closed on any undeterminable signal.** The `.state.lock` is
452
+ acquired mkdir-first with its `owner` file written a moment later, so a lock
453
+ directory that exists but whose owner is missing, empty, non-integer, or
454
+ unreadable is *not* proof of "no writer" — it may be a writer mid-acquisition.
455
+ Quiesce therefore treats such a lock as **unknown-active** and refuses to proceed
456
+ (reporting an `active-runtime` blocker); only a lock whose owner is clearly
457
+ readable *and* names a dead PID is reclaimable. A `runtime/controller.json` that
458
+ exists but is malformed/unparseable is treated the same way — a live Controller
459
+ cannot be ruled out, so it fails closed rather than being read as "no
460
+ controller". A lock or discovery file that is provably absent is the only "no
461
+ runtime" case.
462
+
463
+ `yui update` stages the published package side by side (never replacing the live
464
+ install first) and runs that staged binary's read-only classification against the
465
+ Home. Current and compatible-old Homes take the **fast path**: no Home target is
466
+ constructed, copied, backed up, renamed, or replayed, and no Provider Session is
467
+ waited on. The parent captures the exact executable/argv/version identity of the
468
+ old Controller, stops it once with authenticated lifecycle control, promotes the
469
+ same staged artifact, validates the activated binary and compatible loader, then
470
+ starts and authenticates the replacement Controller. Existing managed Sessions
471
+ retain their frozen executable/CLI path, Home, control digest, and exact
472
+ Task/Run/launch/native-Session fence; neither binary promotion nor Controller
473
+ replacement retargets them through PATH. The managed continuity preflight treats
474
+ package-version drift alone as expected for that in-place path, but keeps
475
+ protocol, layout, aggregate, path, Home, digest, and runtime identity strict.
476
+ This lets the old Session record progress and yield through the replacement
477
+ Controller. It does not authorize migration-required storage: that path still
478
+ requires the offline inventory to prove zero live Sessions. A compatible Home
479
+ remains byte-for-byte old until an ordinary new-CLI commit; that first
480
+ current-only write is also the no-auto-downgrade boundary.
481
+
482
+ Migration-required Homes take the **offline path**. Staged preflight applies the
483
+ offline inventory before the parent stops the Controller, and storage activation
484
+ rechecks it before the child may fence, stage, or mutate the Home. Execute then
485
+ closes pre-admitted writers through `.state.lock` while the fence is held and
486
+ rechecks the same inventory once more before staging. A newly active Run or
487
+ native Session therefore blocks the race window and the parent restores the exact
488
+ captured Controller identity on a clean pre-switch refusal. Only a clear user
489
+ invocation proceeds through the existing complete-Home migration, backup,
490
+ validation, and switch. Neither path writes Task Messages as a heartbeat or
491
+ performs background automatic upgrades.
492
+
493
+ **Same-artifact promotion:** the version resolved at stage time is pinned, and
494
+ binary activation installs that exact `@zq-silk/yui@<version>` — never a second
495
+ bare `@latest` that could resolve to a different build than the one that passed
496
+ preflight. **Only a CONCRETE version is accepted** (R3-F1): the resolver
497
+ requires a semver-shaped `X.Y.Z` (optional pre-release/build suffix) — a dist-tag
498
+ sentinel like `latest`, an empty/malformed value, or a version probe that does
499
+ not come back in a valid `{ ok:true, data }` envelope at exit 0 all yield "no
500
+ version", and the stage then FAILS closed (the live install is untouched, fully
501
+ recoverable) rather than splicing a `latest` sentinel into an activation spec.
502
+ **Verify the activated binary:** the post-update health check runs the
503
+ *actually-activated* global binary (resolved via `npm prefix -g`), not the
504
+ staging path, and **requires** its reported version to be concrete and equal to
505
+ the staged version — a missing, unparseable, or mismatched version fails closed
506
+ (never skipped), so a build whose identity cannot be positively confirmed is
507
+ never trusted.
508
+
509
+ **A success envelope is required before any outcome is trusted.** Every
510
+ interpretation of a spawned staged-binary result first requires a valid
511
+ `{ ok: true, data: <object> }` success envelope (R3-F3). The parser guards the
512
+ top-level shape *before* reading any field: a body that parses to `null`, an
513
+ array, or a primitive (`JSON.parse("null")`/`"[]"`/`"5"` all succeed) is rejected
514
+ as no-envelope rather than crashing on a `.ok` access (R4-F1); likewise an
515
+ `ok:false` error envelope, a non-object `data`, unparseable output, a kill, or a
516
+ transport error is unresolved — preflight treats it as **blocked**, activation as
517
+ **ambiguous**, and a version probe as "no version". The `runUpdate` orchestrator
518
+ also wraps the preflight/activation port calls so an unexpected throw becomes a
519
+ blocked preflight / ambiguous activation, never an uncaught error that could hide
520
+ a committed switch. Only then does the outcome/exit consistency rule apply: a
521
+ *success-class* outcome (`upgraded`, `compatible`, `already-current`, or a
522
+ `dry-run` preflight) is trusted **only when the process also
523
+ exited 0**. A contradiction — stdout says `upgraded` but the process exited
524
+ non-zero — means the child's own contract was violated mid-flight, so it is
525
+ treated as **ambiguous** (activation) or **blocked** (preflight), never a false
526
+ success. Blocker-class outcomes are exempt: `yui upgrade` deliberately exits
527
+ non-zero (5) for a clean `blocked`, so a non-zero exit there is expected and
528
+ consistent. A parseable result with **no** recognized outcome is likewise never
529
+ read as success.
530
+
531
+ **Post-verify parses the doctor machine-readable result before the exit status.**
532
+ The post-update health check validates the structured `yui --json doctor` verdict
533
+ FIRST, then the exit status (R2-F2) — because `--json doctor` deliberately exits
534
+ non-zero on unhealthy storage, so keying off the exit first would reduce a precise
535
+ "storage unsupported/corrupted" verdict to a generic "exited with status N".
536
+ Storage is healthy only when ALL hold: a valid `{ ok: true, data: { checks,
537
+ storage } }` success envelope, **every expected storage check present exactly once
538
+ and `ok`** (a missing, duplicated, or malformed check fails closed — the `healthy`
539
+ flag is never trusted over the authoritative checks array, R3-F2), a
540
+ `storage.blocking` that is **a well-formed array of check-shaped objects** (a
541
+ missing field, a non-array value, or a malformed element fails closed rather than
542
+ being silently coerced to an empty array, R4-F2), `storage.healthy === true` with
543
+ no blocking checks, AND exit 0. A parseable-
544
+ but-unhealthy result (typically exit 5) throws a precise, recovery-oriented
545
+ blocker; an unparseable, non-success, or self-contradictory envelope (e.g.
546
+ `healthy: true` alongside a non-`ok` storage check, or `ok: false`) fails closed —
547
+ an unverifiable health check must never pass silently. The `--json` doctor path
548
+ additionally exits non-zero when storage is unhealthy, so even a naive exit-code
549
+ consumer fails closed; text-mode `doctor` keeps its existing presentation.
550
+
551
+ **Activation ambiguity.** Storage activation runs in a spawned staged-binary
552
+ child. If that child is killed (SIGTERM/OOM) or crashes *after* the atomic switch
553
+ commits but *before* it prints its result JSON, the parent cannot tell "nothing
554
+ happened" from "storage already switched". This is reported as a distinct
555
+ **ambiguous** outcome — never a false "recoverable/unchanged". The switch writes
556
+ a durable completion **receipt** at a sibling path (`<home>.upgrade-receipt.json`)
557
+ the instant it commits, and clears it only on a clean, fully-verified return; so
558
+ its presence proves the switch committed even when stdout was lost. On an
559
+ ambiguous result the orchestrator probes the receipt + timestamped backup +
560
+ current schema and prints precise manual-recovery steps (verify with `yui doctor`;
561
+ restore the named backup with `mv` if needed), and the CLI exits non-zero with a
562
+ dedicated code so the ambiguity is never mistaken for success.
563
+
564
+ **A receipt is only trusted when it genuinely corresponds to the current home
565
+ AND its backup.** A leftover receipt from a prior attempt is not unconditional
566
+ proof that *this* attempt's switch committed, and existence alone is not
567
+ correspondence (R3-F6). Before using a receipt for a recovery decision, the probe
568
+ requires the current protocol's correlating fields and a real backup: it is
569
+ rejected (the caller re-probes the real on-disk state instead) when it lacks a
570
+ `homePath` (a legacy/degraded marker), names a **different home**, lacks a
571
+ `backupPath`, names a backup that is **not this home's expected
572
+ `<home>.backup-*` timestamped sibling** (unrelated/foreign evidence), or whose
573
+ backup is **absent or not a real directory** (already restored or cleaned). A
574
+ non-corresponding receipt reads as "not switched" so recovery advice is never
575
+ derived from stale, legacy, or unrelated evidence.
576
+
577
+ **Rollback boundary (narrowed):** the managed Session launcher is an in-place
578
+ forwarder to the currently activated CLI, not a versioned package pointer, so
579
+ this release still makes no binary+Home dual-resource atomicity claim. It
580
+ guarantees isolated staging (a stage/preflight failure leaves binary and Home
581
+ unchanged), a no-Home-mutation fast path, a recoverable atomic storage switch on
582
+ the offline path (timestamped backup, restorable until the new version resumes
583
+ writes), and no auto-downgrade after writes resume. The offline path's single
584
+ non-atomic window — storage switched, binary promotion then failing — is surfaced
585
+ with the exact backup-restore recovery, and the version-gated axes make the old
586
+ binary fail-close on the new Home rather than misread it. This release exercises
587
+ the contracts only against isolated synthetic Homes; the production registry
588
+ contains the aggregate `16→17` offline transition, but this Task does not run a
589
+ migration against any real Home.
590
+
591
+ **Cross-Task schema scheduling.** Storage schema work is not globally serialized.
592
+ Any module or Task may advance a storage version axis (`layout`, `aggregate`, or
593
+ a `record` family) on its own isolated branch without waiting for another Task's
594
+ schema change to land — branches do not block each other. The cost of that
595
+ parallelism is assigned, by design, to whichever branch integrates later: the
596
+ later-integrating branch is responsible for rebasing onto the latest project
597
+ head, resolving all schema and code conflicts, re-advancing whatever schema
598
+ versions and record-version-map entries the rebase requires, rebuilding and
599
+ re-validating the real wiring, and fully re-running the isolated migration/upgrade
600
+ E2E and its documentation. This rework-and-reconcile duty belongs to the later
601
+ integrator; it is a deliberate scheduling trade-off (authorized by the user) that
602
+ avoids cross-Task blocking rather than an accident to be repaired ad hoc.
603
+ Concretely, the current manifest descriptor map is re-derived against the newest
604
+ head, while the post-baseline descriptor snapshot remains frozen. If another
605
+ Task lands a record-schema change, the integrating branch must reconcile both:
606
+ existing-family advances need a complete adjacent path, and a new target family
607
+ needs an explicit `0->1` introduction before re-testing to convergence.
608
+
609
+ The Web control room is loopback-only and never receives Controller socket
610
+ credentials. It presents durable records and native terminal access without
611
+ becoming a second source of truth.