@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.
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
@@ -0,0 +1,486 @@
1
+ /** The one production transition registry and its baseline-to-current delivery gate. */
2
+ import { latestStorageVersionState } from "../upgrade/recordVersions.js";
3
+ import { assertBaselineConsistency, baselineStorageVersionState } from "./baseline.js";
4
+ import { planMigration } from "./planner.js";
5
+ import { MigrationRegistry } from "./registry.js";
6
+ const FINAL_REVIEW_AGGREGATE_FROM_VERSION = 16;
7
+ const FINAL_REVIEW_AGGREGATE_TO_VERSION = 17;
8
+ const WORK_ITEM_FROM_VERSION = 6;
9
+ const WORK_ITEM_TO_VERSION = 7;
10
+ const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
11
+ const WORK_ITEM_GIT_SNAPSHOT_TO_VERSION = 8;
12
+ const WORK_ITEM_GROUP_HISTORY_FROM_VERSION = 8;
13
+ const WORK_ITEM_GROUP_HISTORY_TO_VERSION = 9;
14
+ const AGENT_RUN_FROM_VERSION = 5;
15
+ const AGENT_RUN_TO_VERSION = 6;
16
+ const REVIEW_ROUND_FROM_VERSION = 2;
17
+ const REVIEW_ROUND_TO_VERSION = 3;
18
+ const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
19
+ const REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION = 4;
20
+ const ACTIVE_RUN_POINTER_FROM_VERSION = 1;
21
+ const ACTIVE_RUN_POINTER_TO_VERSION = 2;
22
+ const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
23
+ const ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION = 3;
24
+ const MANAGED_WORKSPACE_FROM_VERSION = 1;
25
+ const MANAGED_WORKSPACE_TO_VERSION = 2;
26
+ /**
27
+ * Build the authoritative production graph. Transition intent and executable
28
+ * transforms are registered together here; compatible loading and offline
29
+ * migration consume this same graph.
30
+ */
31
+ export function createProductionStorageRegistry() {
32
+ assertBaselineConsistency();
33
+ const registry = new MigrationRegistry().registerOfflineMigration({
34
+ axis: "aggregate",
35
+ fromVersion: FINAL_REVIEW_AGGREGATE_FROM_VERSION,
36
+ toVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION,
37
+ preconditions: requireAggregateV16Snapshot,
38
+ transform: migrateAggregateV16ToV17,
39
+ declaredEffects: []
40
+ })
41
+ .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
42
+ .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
43
+ .registerOfflineMigration(workItemExecutionGroupHistoryStep())
44
+ .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_FROM_VERSION, AGENT_RUN_TO_VERSION, "agentRuns"))
45
+ .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
46
+ .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
47
+ .registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
48
+ .registerOfflineMigration(managedWorkspaceFamilyStep())
49
+ .registerOfflineMigration(activeRunPointerNamespaceStep());
50
+ assertRegistryCoversBaselineToCurrent(registry);
51
+ return registry;
52
+ }
53
+ function workItemExecutionGroupHistoryStep() {
54
+ return {
55
+ axis: "record",
56
+ recordKind: "workItem",
57
+ fromVersion: WORK_ITEM_GROUP_HISTORY_FROM_VERSION,
58
+ toVersion: WORK_ITEM_GROUP_HISTORY_TO_VERSION,
59
+ preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "workItem", WORK_ITEM_GROUP_HISTORY_FROM_VERSION, "workItems"),
60
+ transform: migrateWorkItemExecutionGroupHistory,
61
+ declaredEffects: []
62
+ };
63
+ }
64
+ /**
65
+ * ManagedWorkspace is stored once in the owner-keyed workspace map and copied
66
+ * into Candidates, AgentRuns, and ReviewRounds as immutable lifecycle
67
+ * evidence. Those copies are the same record family, not independent parent
68
+ * records: advancing only the map leaves an otherwise legal historical Home
69
+ * unreadable by the strict current parser.
70
+ */
71
+ function managedWorkspaceFamilyStep() {
72
+ return {
73
+ axis: "record",
74
+ recordKind: "managedWorkspace",
75
+ fromVersion: MANAGED_WORKSPACE_FROM_VERSION,
76
+ toVersion: MANAGED_WORKSPACE_TO_VERSION,
77
+ preconditions: requireLegacyManagedWorkspaceFamily,
78
+ transform: migrateManagedWorkspaceFamily,
79
+ declaredEffects: []
80
+ };
81
+ }
82
+ function requireLegacyManagedWorkspaceFamily(snapshot) {
83
+ requireRecordFamilyVersion(snapshot, "managedWorkspace", MANAGED_WORKSPACE_FROM_VERSION, "managedWorkspaces");
84
+ visitEmbeddedManagedWorkspaces(snapshot, (workspace, label) => {
85
+ requireManagedWorkspaceVersion(workspace, MANAGED_WORKSPACE_FROM_VERSION, label);
86
+ return workspace;
87
+ });
88
+ }
89
+ function migrateManagedWorkspaceFamily(snapshot) {
90
+ requireLegacyManagedWorkspaceFamily(snapshot);
91
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
92
+ const schemaManifest = {
93
+ ...snapshot.schemaManifest,
94
+ recordVersions: {
95
+ ...manifestVersions,
96
+ managedWorkspace: MANAGED_WORKSPACE_TO_VERSION
97
+ }
98
+ };
99
+ if (snapshot.state === null)
100
+ return { schemaManifest, state: null };
101
+ const migrated = visitEmbeddedManagedWorkspaces(snapshot, (workspace) => ({ ...workspace, schemaVersion: MANAGED_WORKSPACE_TO_VERSION }));
102
+ return { schemaManifest, state: migrated.state };
103
+ }
104
+ /**
105
+ * Rewrite every persisted ManagedWorkspace occurrence while preserving the
106
+ * surrounding record bytes. The direct map is handled by the ordinary
107
+ * record-family migration; this traversal covers the immutable embedded
108
+ * snapshots which the strict validators also treat as ManagedWorkspace.
109
+ */
110
+ function visitEmbeddedManagedWorkspaces(snapshot, visit) {
111
+ if (snapshot.state === null)
112
+ return snapshot;
113
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
114
+ const nextTasks = {};
115
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
116
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
117
+ const nextTask = { ...task };
118
+ if (task.managedWorkspaces !== undefined) {
119
+ const workspaces = asObject(task.managedWorkspaces, `managedWorkspace map ${taskId}`);
120
+ nextTask.managedWorkspaces = Object.fromEntries(Object.entries(workspaces).map(([recordId, rawWorkspace]) => [
121
+ recordId,
122
+ visit(asObject(rawWorkspace, `managedWorkspace ${taskId}/${recordId}`), `managedWorkspace ${taskId}/${recordId}`)
123
+ ]));
124
+ }
125
+ if (task.workItems !== undefined) {
126
+ const workItems = asObject(task.workItems, `workItem map ${taskId}`);
127
+ nextTask.workItems = Object.fromEntries(Object.entries(workItems).map(([workItemId, rawWorkItem]) => {
128
+ const workItem = asObject(rawWorkItem, `workItem ${taskId}/${workItemId}`);
129
+ if (workItem.candidates === undefined)
130
+ return [workItemId, { ...workItem }];
131
+ if (!Array.isArray(workItem.candidates)) {
132
+ throw new Error(`workItem ${taskId}/${workItemId} candidates must be an array.`);
133
+ }
134
+ return [workItemId, {
135
+ ...workItem,
136
+ candidates: workItem.candidates.map((rawCandidate, index) => {
137
+ const candidate = asObject(rawCandidate, `Candidate ${taskId}/${workItemId}/${index}`);
138
+ return migrateOptionalManagedWorkspace(candidate, `Candidate ${taskId}/${workItemId}/${index}`, visit);
139
+ })
140
+ }];
141
+ }));
142
+ }
143
+ for (const mapKey of ["agentRuns", "reviewRounds"]) {
144
+ if (task[mapKey] === undefined)
145
+ continue;
146
+ const records = asObject(task[mapKey], `${mapKey} map ${taskId}`);
147
+ nextTask[mapKey] = Object.fromEntries(Object.entries(records).map(([recordId, rawRecord]) => [
148
+ recordId,
149
+ migrateOptionalManagedWorkspace(asObject(rawRecord, `${mapKey} ${taskId}/${recordId}`), `${mapKey} ${taskId}/${recordId}`, visit)
150
+ ]));
151
+ }
152
+ nextTasks[taskId] = nextTask;
153
+ }
154
+ return {
155
+ schemaManifest: snapshot.schemaManifest,
156
+ state: { ...snapshot.state, tasks: nextTasks }
157
+ };
158
+ }
159
+ function migrateOptionalManagedWorkspace(record, label, visit) {
160
+ if (record.workspace === undefined)
161
+ return { ...record };
162
+ return {
163
+ ...record,
164
+ workspace: visit(asObject(record.workspace, `${label} workspace`), `${label} workspace`)
165
+ };
166
+ }
167
+ function requireManagedWorkspaceVersion(workspace, version, label) {
168
+ if (workspace.schemaVersion !== version) {
169
+ throw new Error(`${label} must use schemaVersion ${version}.`);
170
+ }
171
+ }
172
+ function migrateWorkItemExecutionGroupHistory(snapshot) {
173
+ requireRecordFamilyVersion(snapshot, "workItem", WORK_ITEM_GROUP_HISTORY_FROM_VERSION, "workItems");
174
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
175
+ const schemaManifest = {
176
+ ...snapshot.schemaManifest,
177
+ recordVersions: { ...manifestVersions, workItem: WORK_ITEM_GROUP_HISTORY_TO_VERSION }
178
+ };
179
+ if (snapshot.state === null)
180
+ return { schemaManifest, state: null };
181
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
182
+ const nextTasks = {};
183
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
184
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
185
+ if (task.workItems === undefined) {
186
+ nextTasks[taskId] = { ...task };
187
+ continue;
188
+ }
189
+ const workItems = asObject(task.workItems, `workItem map ${taskId}`);
190
+ const nextWorkItems = {};
191
+ for (const [recordId, rawRecord] of Object.entries(workItems)) {
192
+ const record = asObject(rawRecord, `workItem ${taskId}/${recordId}`);
193
+ const { executionGroup, ...rest } = record;
194
+ if (executionGroup !== undefined
195
+ && (executionGroup === null || typeof executionGroup !== "object" || Array.isArray(executionGroup))) {
196
+ throw new Error(`workItem ${taskId}/${recordId} executionGroup must be an object.`);
197
+ }
198
+ const groupId = executionGroup === undefined
199
+ ? undefined
200
+ : asObject(executionGroup, `ExecutionGroup ${taskId}/${recordId}`).id;
201
+ if (groupId !== undefined && (typeof groupId !== "string" || groupId.trim().length === 0)) {
202
+ throw new Error(`workItem ${taskId}/${recordId} executionGroup id is invalid.`);
203
+ }
204
+ nextWorkItems[recordId] = {
205
+ ...rest,
206
+ schemaVersion: WORK_ITEM_GROUP_HISTORY_TO_VERSION,
207
+ executionGroups: executionGroup === undefined ? [] : [executionGroup],
208
+ ...(groupId === undefined ? {} : { currentExecutionGroupId: groupId })
209
+ };
210
+ }
211
+ nextTasks[taskId] = { ...task, workItems: nextWorkItems };
212
+ }
213
+ return {
214
+ schemaManifest,
215
+ state: { ...snapshot.state, tasks: nextTasks }
216
+ };
217
+ }
218
+ /**
219
+ * Move the legacy `lane:<group>:<lane>` keys into a namespace that cannot be
220
+ * mistaken for a legal Role identity. A legacy key is only rewritten when
221
+ * its pointed Run proves the lane lineage; otherwise a legal Role with that
222
+ * exact name is retained. Ambiguous or malformed lane-looking records fail
223
+ * closed instead of guessing which active Run the old bytes meant.
224
+ */
225
+ function activeRunPointerNamespaceStep() {
226
+ return {
227
+ axis: "record",
228
+ recordKind: "activeRunPointer",
229
+ fromVersion: ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION,
230
+ toVersion: ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION,
231
+ preconditions: (snapshot) => requireActiveRunPointerNamespaceVersion(snapshot),
232
+ transform: (snapshot) => migrateActiveRunPointerNamespace(snapshot),
233
+ declaredEffects: []
234
+ };
235
+ }
236
+ function requireActiveRunPointerNamespaceVersion(snapshot) {
237
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
238
+ if (manifestVersions.activeRunPointer !== ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION) {
239
+ throw new Error(`Record activeRunPointer migration requires manifest version ${ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION}.`);
240
+ }
241
+ if (snapshot.state === null)
242
+ return;
243
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
244
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
245
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
246
+ const activeRuns = task.activeRuns;
247
+ if (activeRuns === undefined)
248
+ continue;
249
+ const pointers = asObject(activeRuns, `activeRunPointer map ${taskId}`);
250
+ for (const [key, rawPointer] of Object.entries(pointers)) {
251
+ const pointer = asObject(rawPointer, `Active run ${taskId}/${key}`);
252
+ if (pointer.schemaVersion !== ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION) {
253
+ throw new Error(`Active run ${taskId}/${key} must use schemaVersion ${ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION}.`);
254
+ }
255
+ if (typeof pointer.runId !== "string" || pointer.runId.trim().length === 0) {
256
+ throw new Error(`Active run ${taskId}/${key} has an invalid runId.`);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ function migrateActiveRunPointerNamespace(snapshot) {
262
+ requireActiveRunPointerNamespaceVersion(snapshot);
263
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
264
+ const schemaManifest = {
265
+ ...snapshot.schemaManifest,
266
+ recordVersions: {
267
+ ...manifestVersions,
268
+ activeRunPointer: ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION
269
+ }
270
+ };
271
+ if (snapshot.state === null)
272
+ return { schemaManifest, state: null };
273
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
274
+ const nextTasks = {};
275
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
276
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
277
+ const rawActiveRuns = task.activeRuns;
278
+ if (rawActiveRuns === undefined) {
279
+ nextTasks[taskId] = { ...task };
280
+ continue;
281
+ }
282
+ const activeRuns = asObject(rawActiveRuns, `activeRunPointer map ${taskId}`);
283
+ const rawAgentRuns = asObject(task.agentRuns, `agentRun map ${taskId}`);
284
+ const nextActiveRuns = {};
285
+ for (const [key, rawPointer] of Object.entries(activeRuns)) {
286
+ const pointer = asObject(rawPointer, `Active run ${taskId}/${key}`);
287
+ const runId = typeof pointer.runId === "string" ? pointer.runId : "";
288
+ const run = asObject(rawAgentRuns[runId], `Agent run ${taskId}/${runId}`);
289
+ const lane = legacyLaneKeyParts(key);
290
+ const migratedPointer = {
291
+ ...pointer,
292
+ schemaVersion: ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION
293
+ };
294
+ const addPointer = (nextKey) => {
295
+ if (nextActiveRuns[nextKey] !== undefined) {
296
+ throw new Error(`Active run pointer key collides after migration: ${taskId}/${nextKey}.`);
297
+ }
298
+ nextActiveRuns[nextKey] = migratedPointer;
299
+ };
300
+ if (lane !== null) {
301
+ const laneBacked = run.executionGroupId === lane.executionGroupId
302
+ && run.executionLaneId === lane.executionLaneId;
303
+ // The prior writer used the same map key for a legal Role and a Lane
304
+ // when the Role itself happened to contain the `lane:g:l` shape. The
305
+ // v3 namespace must retain both identities instead of rejecting the
306
+ // record or silently dropping the Role pointer.
307
+ const roleBacked = run.roleName === key;
308
+ if (!laneBacked && !roleBacked) {
309
+ throw new Error(`Active run pointer lineage is invalid: ${taskId}/${key}.`);
310
+ }
311
+ if (roleBacked)
312
+ addPointer(key);
313
+ if (laneBacked) {
314
+ addPointer(executionLaneNamespaceKey(lane.executionGroupId, lane.executionLaneId));
315
+ }
316
+ }
317
+ else if (key.startsWith("lane:")) {
318
+ // A malformed lane-looking key is allowed only when it is a legal
319
+ // legacy Role pointer. It must not be silently reinterpreted.
320
+ if (run.roleName !== key) {
321
+ throw new Error(`Active run pointer key is malformed: ${taskId}/${key}.`);
322
+ }
323
+ addPointer(key);
324
+ }
325
+ else {
326
+ // Role pointers remain keyed by the Role even when the pointed Run
327
+ // also carries execution lineage (a shape emitted by the prior
328
+ // writer). The lane namespace above preserves that second identity.
329
+ if (run.roleName !== key) {
330
+ throw new Error(`Active run Role pointer is invalid: ${taskId}/${key}.`);
331
+ }
332
+ addPointer(key);
333
+ }
334
+ }
335
+ nextTasks[taskId] = { ...task, activeRuns: nextActiveRuns };
336
+ }
337
+ return {
338
+ schemaManifest,
339
+ state: { ...snapshot.state, tasks: nextTasks }
340
+ };
341
+ }
342
+ function legacyLaneKeyParts(key) {
343
+ const match = /^lane:([^:]+):([^:]+)$/u.exec(key);
344
+ if (match === null)
345
+ return null;
346
+ return { executionGroupId: match[1], executionLaneId: match[2] };
347
+ }
348
+ function executionLaneNamespaceKey(executionGroupId, executionLaneId) {
349
+ return `/execution-lane/${encodeLaneKeyPart(executionGroupId)}:${encodeLaneKeyPart(executionLaneId)}`;
350
+ }
351
+ function encodeLaneKeyPart(value) {
352
+ return encodeURIComponent(value).replace(/:/gu, "%3A");
353
+ }
354
+ /**
355
+ * Upgrade one nested Task record family without guessing fields or repairing
356
+ * malformed state. The new execution lineage fields are optional, so legal
357
+ * old single-lane records retain their direct shape; the next dispatch creates
358
+ * the unified one-lane Group explicitly. The version transition is still
359
+ * durable and centralized, which keeps old Homes out of the strict current
360
+ * parser until this step has run.
361
+ */
362
+ function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
363
+ return {
364
+ axis: "record",
365
+ recordKind,
366
+ fromVersion,
367
+ toVersion,
368
+ preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, recordKind, fromVersion, taskMapKey),
369
+ transform: (snapshot) => migrateRecordFamily(snapshot, recordKind, fromVersion, toVersion, taskMapKey),
370
+ declaredEffects: []
371
+ };
372
+ }
373
+ /**
374
+ * The snapshot boundary is nested inside WorkItem/ReviewRound ExecutionGroup
375
+ * results. The parent record versions make that persisted shape explicit;
376
+ * this adjacent step intentionally performs no field rewrite, preserving old
377
+ * records while preventing a pre-v8/pre-v4 Home from entering the strict
378
+ * current parser without the declared transition.
379
+ */
380
+ function requireRecordFamilyVersion(snapshot, recordKind, fromVersion, taskMapKey) {
381
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
382
+ if (manifestVersions[recordKind] !== fromVersion) {
383
+ throw new Error(`Record ${recordKind} migration requires manifest version ${fromVersion}.`);
384
+ }
385
+ if (snapshot.state === null)
386
+ return;
387
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
388
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
389
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
390
+ const records = task[taskMapKey];
391
+ if (records === undefined)
392
+ continue;
393
+ const map = asObject(records, `${recordKind} map ${taskId}`);
394
+ for (const [recordId, rawRecord] of Object.entries(map)) {
395
+ const record = asObject(rawRecord, `${recordKind} ${taskId}/${recordId}`);
396
+ if (record.schemaVersion !== fromVersion) {
397
+ throw new Error(`Record ${recordKind} ${taskId}/${recordId} must use schemaVersion ${fromVersion}.`);
398
+ }
399
+ }
400
+ }
401
+ }
402
+ function migrateRecordFamily(snapshot, recordKind, fromVersion, toVersion, taskMapKey) {
403
+ // Keep the same source-shape checks in the transform so a direct caller
404
+ // cannot bypass the migration's precondition contract.
405
+ requireRecordFamilyVersion(snapshot, recordKind, fromVersion, taskMapKey);
406
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
407
+ const schemaManifest = {
408
+ ...snapshot.schemaManifest,
409
+ recordVersions: {
410
+ ...manifestVersions,
411
+ [recordKind]: toVersion
412
+ }
413
+ };
414
+ if (snapshot.state === null)
415
+ return { schemaManifest, state: null };
416
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
417
+ const nextTasks = {};
418
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
419
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
420
+ const records = task[taskMapKey];
421
+ if (records === undefined) {
422
+ nextTasks[taskId] = { ...task };
423
+ continue;
424
+ }
425
+ const map = asObject(records, `${recordKind} map ${taskId}`);
426
+ const nextMap = {};
427
+ for (const [recordId, rawRecord] of Object.entries(map)) {
428
+ nextMap[recordId] = {
429
+ ...asObject(rawRecord, `${recordKind} ${taskId}/${recordId}`),
430
+ schemaVersion: toVersion
431
+ };
432
+ }
433
+ nextTasks[taskId] = { ...task, [taskMapKey]: nextMap };
434
+ }
435
+ return {
436
+ schemaManifest,
437
+ state: { ...snapshot.state, tasks: nextTasks }
438
+ };
439
+ }
440
+ function asObject(value, label) {
441
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
442
+ throw new Error(`${label} must be an object.`);
443
+ }
444
+ return value;
445
+ }
446
+ /** Historical public spelling retained as an alias to the single graph. */
447
+ export const createProductionRegistry = createProductionStorageRegistry;
448
+ /** Advance the aggregate identity after proving manifest/root agreement at v16. */
449
+ function migrateAggregateV16ToV17(snapshot) {
450
+ return {
451
+ schemaManifest: {
452
+ ...snapshot.schemaManifest,
453
+ aggregateSchemaVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION
454
+ },
455
+ state: snapshot.state === null
456
+ ? null
457
+ : {
458
+ ...snapshot.state,
459
+ schemaVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION
460
+ }
461
+ };
462
+ }
463
+ function requireAggregateV16Snapshot(snapshot) {
464
+ if (snapshot.schemaManifest.aggregateSchemaVersion
465
+ !== FINAL_REVIEW_AGGREGATE_FROM_VERSION) {
466
+ throw new Error("Aggregate 16->17 migration requires schema.json aggregateSchemaVersion 16.");
467
+ }
468
+ if (snapshot.state !== null
469
+ && snapshot.state.schemaVersion !== FINAL_REVIEW_AGGREGATE_FROM_VERSION) {
470
+ throw new Error("Aggregate 16->17 migration requires state.json schemaVersion 16 to match schema.json.");
471
+ }
472
+ }
473
+ /**
474
+ * A version bump is deliverable only when the shared planner resolves the full
475
+ * adjacent path. This also covers target-only record families as explicit 0->1
476
+ * introductions and rejects offline declarations without executable steps.
477
+ */
478
+ export function assertRegistryCoversBaselineToCurrent(registry, baseline = baselineStorageVersionState(), current = latestStorageVersionState()) {
479
+ const plan = planMigration(registry, baseline, current);
480
+ if (plan.kind !== "blocked")
481
+ return;
482
+ const coordinate = plan.blocker.axis === "record"
483
+ ? `record/${plan.blocker.recordKind ?? "?"}`
484
+ : plan.blocker.axis;
485
+ throw new Error(`Storage migration delivery gate failed for ${coordinate}: ${plan.blocker.message}`);
486
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The central migration registry.
3
+ *
4
+ * Declarations and offline transforms are discoverable strictly by
5
+ * `axis + (recordKind) + fromVersion`. There is no version-magnitude or semver
6
+ * guessing: only an explicitly declared chain of adjacent transitions is
7
+ * usable. A fresh registry is EMPTY, so every older version fails closed until
8
+ * its compatible normalization or offline migration is authored explicitly.
9
+ */
10
+ export class MigrationRegistryError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "MigrationRegistryError";
14
+ }
15
+ }
16
+ /**
17
+ * Explicit key-field delimiter: the ASCII Unit Separator (U+001F), built with
18
+ * `String.fromCharCode` so this source file stays pure printable ASCII (no raw
19
+ * control byte embedded in the text). The Unit Separator never appears in an
20
+ * `axis` literal, a `recordKind`, or a decimal version, so the three key fields
21
+ * cannot run together and the composite key is unambiguous.
22
+ */
23
+ const KEY_SEPARATOR = String.fromCharCode(0x1f);
24
+ /**
25
+ * Composite lookup key. The record axis includes its `recordKind`; the layout
26
+ * and aggregate axes use a fixed sentinel so a single map serves all axes.
27
+ */
28
+ function stepKey(axis, recordKind, fromVersion) {
29
+ const family = axis === "record" ? requireRecordKind(recordKind) : "-";
30
+ return `${axis}${KEY_SEPARATOR}${family}${KEY_SEPARATOR}${fromVersion}`;
31
+ }
32
+ function requireRecordKind(recordKind) {
33
+ if (typeof recordKind !== "string" || recordKind.trim().length === 0) {
34
+ throw new MigrationRegistryError("A record-axis migration step must declare a non-empty recordKind.");
35
+ }
36
+ return recordKind;
37
+ }
38
+ /**
39
+ * A discoverable set of adjacent migration steps. Register at most one step per
40
+ * `(axis, recordKind, fromVersion)`; lookup returns only that exact adjacent
41
+ * step (or `undefined`) — it never infers a step from version ordering.
42
+ */
43
+ export class MigrationRegistry {
44
+ #steps = new Map();
45
+ #declarations = new Map();
46
+ /** Backward-compatible spelling: explicitly registers one offline transition and its transform. */
47
+ register(step) {
48
+ this.#validateMigrationStep(step);
49
+ const key = stepKey(step.axis, step.recordKind, step.fromVersion);
50
+ this.#assertDeclarationAvailable(step, key);
51
+ this.#assertStepAvailable(step, key);
52
+ // Publish the declaration and transform together only after every check has
53
+ // passed. A rejected combined registration must not leave a newly-runnable
54
+ // half update behind in the registry.
55
+ this.#declarations.set(key, { ...step, kind: "offline-migration" });
56
+ this.#steps.set(key, step);
57
+ return this;
58
+ }
59
+ /** Explicitly register the declaration and transform for an offline change. */
60
+ registerOfflineMigration(step) {
61
+ return this.register(step);
62
+ }
63
+ /** Declare an offline change. A missing transform remains fail-closed. */
64
+ declareOfflineMigration(declaration) {
65
+ this.#validateCoordinates(declaration);
66
+ this.#registerDeclaration({ ...declaration, kind: "offline-migration" });
67
+ return this;
68
+ }
69
+ /** Declare an online-compatible normalization with deterministic defaults. */
70
+ registerCompatible(step) {
71
+ const runtimeAxis = step.axis;
72
+ if (runtimeAxis !== "record") {
73
+ throw new MigrationRegistryError("A compatible transition must use the record axis; layout and aggregate changes require offline migration.");
74
+ }
75
+ this.#validateCoordinates(step);
76
+ if (!Array.isArray(step.defaults)
77
+ || step.defaults.length === 0
78
+ || step.defaults.some((value) => typeof value !== "string" || value.trim().length === 0)) {
79
+ throw new MigrationRegistryError("A compatible transition must declare at least one deterministic default or rebuild rule.");
80
+ }
81
+ if (typeof step.normalize !== "function" || typeof step.validateSource !== "function") {
82
+ throw new MigrationRegistryError("A compatible transition must provide a strict source validator and normalizer.");
83
+ }
84
+ this.#registerDeclaration({ ...step, kind: "compatible" });
85
+ return this;
86
+ }
87
+ /** Register a transform without inferring its operational declaration. */
88
+ registerMigrationStep(step) {
89
+ this.#validateMigrationStep(step);
90
+ const key = stepKey(step.axis, step.recordKind, step.fromVersion);
91
+ this.#assertStepAvailable(step, key);
92
+ this.#steps.set(key, step);
93
+ return this;
94
+ }
95
+ #validateMigrationStep(step) {
96
+ this.#validateCoordinates(step);
97
+ if (!Array.isArray(step.declaredEffects)
98
+ || typeof step.transform !== "function"
99
+ || typeof step.preconditions !== "function") {
100
+ throw new MigrationRegistryError("A migration step must provide its transform contract.");
101
+ }
102
+ }
103
+ #assertStepAvailable(step, key) {
104
+ if (!this.#steps.has(key))
105
+ return;
106
+ throw new MigrationRegistryError(`A migration step is already registered for ${step.axis}` +
107
+ `${step.recordKind ? `/${step.recordKind}` : ""} ${step.fromVersion}->${step.toVersion}.`);
108
+ }
109
+ #assertDeclarationAvailable(declaration, key) {
110
+ if (!this.#declarations.has(key))
111
+ return;
112
+ throw new MigrationRegistryError(`A transition is already registered for ${declaration.axis}` +
113
+ `${declaration.recordKind ? `/${declaration.recordKind}` : ""} ` +
114
+ `${declaration.fromVersion}->${declaration.toVersion}.`);
115
+ }
116
+ #validateCoordinates(step) {
117
+ if (step.axis === "record") {
118
+ requireRecordKind(step.recordKind);
119
+ }
120
+ else if (step.recordKind !== undefined) {
121
+ throw new MigrationRegistryError(`A ${step.axis}-axis transition must not declare a recordKind.`);
122
+ }
123
+ if (!Number.isSafeInteger(step.fromVersion) || step.fromVersion < 0) {
124
+ throw new MigrationRegistryError(`Transition fromVersion must be a non-negative integer: ${String(step.fromVersion)}.`);
125
+ }
126
+ if (step.fromVersion === 0) {
127
+ if (step.axis !== "record" || step.introduction !== true || step.toVersion !== 1) {
128
+ throw new MigrationRegistryError("Only an explicit record-family introduction may use the 0->1 transition.");
129
+ }
130
+ }
131
+ else if (step.introduction === true) {
132
+ throw new MigrationRegistryError("A record-family introduction must be the explicit 0->1 transition.");
133
+ }
134
+ if (step.toVersion !== step.fromVersion + 1) {
135
+ throw new MigrationRegistryError(`Transitions must be adjacent and one-directional: ${step.axis}` +
136
+ `${step.recordKind ? `/${step.recordKind}` : ""} ${step.fromVersion}->${step.toVersion} ` +
137
+ `(expected ${step.fromVersion}->${step.fromVersion + 1}).`);
138
+ }
139
+ }
140
+ #registerDeclaration(declaration) {
141
+ const key = stepKey(declaration.axis, declaration.recordKind, declaration.fromVersion);
142
+ this.#assertDeclarationAvailable(declaration, key);
143
+ this.#declarations.set(key, declaration);
144
+ }
145
+ /** Look up the exact adjacent step advancing `fromVersion -> fromVersion + 1`. */
146
+ lookup(axis, recordKind, fromVersion) {
147
+ return this.#steps.get(stepKey(axis, recordKind, fromVersion));
148
+ }
149
+ /** Look up the exact compatible/offline declaration for one adjacent change. */
150
+ lookupDeclaration(axis, recordKind, fromVersion) {
151
+ return this.#declarations.get(stepKey(axis, recordKind, fromVersion));
152
+ }
153
+ /** Total number of declared adjacent transitions (0 for an EMPTY registry). */
154
+ get size() {
155
+ return this.#declarations.size;
156
+ }
157
+ /** True when neither declarations nor offline transforms are registered. */
158
+ isEmpty() {
159
+ return this.#declarations.size === 0 && this.#steps.size === 0;
160
+ }
161
+ }
162
+ /**
163
+ * Create a fresh, EMPTY registry for isolated planning or callers that
164
+ * authorize no transitions. Production wiring builds its explicit graph in a
165
+ * separate factory.
166
+ */
167
+ export function createEmptyRegistry() {
168
+ return new MigrationRegistry();
169
+ }