@zq-silk/yui 0.2.0 → 0.4.2
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.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
package/ARCHITECTURE.md
CHANGED
|
@@ -1,141 +1,611 @@
|
|
|
1
|
-
# Yui
|
|
2
|
-
|
|
3
|
-
Yui is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
todo -> running -> awaiting Leader review
|
|
71
|
+
| accept -> done
|
|
72
|
+
| reject -> failed -> redispatch -> running
|
|
127
73
|
```
|
|
128
74
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|