@zq-silk/yui 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/agentConfigurationPicker.js +1 -1
  4. package/dist/cli/commandCatalog.js +251 -13
  5. package/dist/cli/updateOrchestrator.js +8 -0
  6. package/dist/cli/updatePorts.js +76 -22
  7. package/dist/cli.js +264 -20
  8. package/dist/commands/configCommands.js +83 -9
  9. package/dist/commands/controllerCommands.js +103 -0
  10. package/dist/commands/deliveryGuardPreflight.js +35 -0
  11. package/dist/commands/durableJobCommands.js +231 -0
  12. package/dist/commands/executionAuditCommands.js +193 -0
  13. package/dist/commands/grantCommands.js +374 -0
  14. package/dist/commands/projectCommands.js +119 -81
  15. package/dist/commands/releaseCommands.js +444 -0
  16. package/dist/commands/resourcesCommands.js +274 -0
  17. package/dist/commands/sessionCommands.js +104 -0
  18. package/dist/commands/taskActor.js +117 -0
  19. package/dist/commands/taskChangeSetCommands.js +60 -0
  20. package/dist/commands/taskCommands.js +610 -201
  21. package/dist/commands/taskCompletionGate.js +78 -1
  22. package/dist/commands/taskContextCommand.js +24 -6
  23. package/dist/commands/taskInputCommands.js +1 -1
  24. package/dist/commands/taskIntegrationCommands.js +136 -33
  25. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  26. package/dist/commands/taskNextActionCommand.js +85 -0
  27. package/dist/commands/taskOverlapCommands.js +120 -0
  28. package/dist/commands/taskOverviewCommand.js +36 -8
  29. package/dist/commands/telemetryCommands.js +330 -0
  30. package/dist/commands/workflowCommands.js +415 -0
  31. package/dist/config/yuiConfig.js +60 -0
  32. package/dist/controller/clientRuntime.js +42 -1
  33. package/dist/controller/controller.js +413 -61
  34. package/dist/controller/controllerMain.js +25 -2
  35. package/dist/controller/domainIdentity.js +16 -8
  36. package/dist/controller/ephemeralResourceReaper.js +2 -1
  37. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  38. package/dist/controller/handoverCandidate.js +168 -0
  39. package/dist/controller/jobClient.js +102 -0
  40. package/dist/controller/jobControl.js +613 -0
  41. package/dist/controller/jobSupervisor.js +498 -0
  42. package/dist/controller/providerHookRunFence.js +34 -5
  43. package/dist/controller/resourceCleanupLinux.js +18 -9
  44. package/dist/controller/resourceInventoryLinux.js +90 -39
  45. package/dist/controller/resourceInventoryRpc.js +85 -0
  46. package/dist/controller/resourceInventoryWorker.js +50 -0
  47. package/dist/controller/runtime.js +238 -22
  48. package/dist/controller/runtimeEventInbox.js +234 -57
  49. package/dist/controller/runtimeEventProcessor.js +549 -42
  50. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  51. package/dist/core/boundedRpc.js +475 -0
  52. package/dist/core/controllerServer.js +416 -27
  53. package/dist/core/controllerTelemetry.js +167 -0
  54. package/dist/doctor/doctor.js +113 -16
  55. package/dist/domain/validation.js +9 -0
  56. package/dist/execution/executionGroup.js +40 -3
  57. package/dist/executor/agentExecutor.js +6 -3
  58. package/dist/executor/effectiveLaunch.js +52 -0
  59. package/dist/executor/executorRegistry.js +50 -0
  60. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  61. package/dist/grant/capabilityGrant.js +282 -0
  62. package/dist/integration/changeSet.js +16 -3
  63. package/dist/integration/changeSetManifest.js +46 -0
  64. package/dist/integration/gitIntegrationService.js +528 -147
  65. package/dist/integration/integrationAttempt.js +54 -5
  66. package/dist/integration/integrationQueueEntry.js +221 -0
  67. package/dist/integration/integrationQueueService.js +955 -0
  68. package/dist/integration/manifestTags.js +99 -0
  69. package/dist/integration/overlapDiagnostics.js +211 -0
  70. package/dist/job/durableJob.js +449 -0
  71. package/dist/job/jobRunner.js +350 -0
  72. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  73. package/dist/lifecycle/providerErrorClass.js +126 -0
  74. package/dist/message/message.js +16 -3
  75. package/dist/observability/executionAudit.js +545 -0
  76. package/dist/observability/faultClassification.js +160 -0
  77. package/dist/observability/runtimeIdentity.js +367 -0
  78. package/dist/release/fakeReleasePorts.js +55 -0
  79. package/dist/release/releaseHandover.js +475 -0
  80. package/dist/release/releaseIdempotencyStore.js +165 -0
  81. package/dist/release/releaseWorkflow.js +459 -0
  82. package/dist/release/releaseWorkflowEngine.js +688 -0
  83. package/dist/release/releaseWorkflowPorts.js +1720 -0
  84. package/dist/release/runtimeRelease.js +495 -0
  85. package/dist/release/workflowFileLock.js +218 -0
  86. package/dist/repository/gitWorkspace.js +177 -1
  87. package/dist/repository/projectMaintenanceLock.js +315 -0
  88. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  89. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  90. package/dist/resources/autoResourceGc.js +116 -0
  91. package/dist/resources/liveReferences.js +574 -0
  92. package/dist/resources/resourceDiscovery.js +477 -0
  93. package/dist/resources/resourceGc.js +645 -0
  94. package/dist/resources/resourceRegistrar.js +256 -0
  95. package/dist/resources/resourceRegistry.js +150 -0
  96. package/dist/resources/resourceRegistryStore.js +41 -0
  97. package/dist/resources/resourceTypes.js +42 -0
  98. package/dist/resources/sqliteResourceRegistry.js +111 -0
  99. package/dist/review/reviewConfig.js +10 -0
  100. package/dist/review/reviewFinding.js +240 -0
  101. package/dist/review/reviewFindingLedger.js +545 -0
  102. package/dist/review/reviewOutcomeClassifier.js +61 -0
  103. package/dist/review/reviewRound.js +56 -4
  104. package/dist/run/agentRun.js +80 -4
  105. package/dist/run/providerRetry.js +84 -0
  106. package/dist/run/providerRetryConfig.js +63 -0
  107. package/dist/run/yieldReceipt.js +65 -0
  108. package/dist/runtime/exactControlPlane.js +79 -2
  109. package/dist/runtime/index.js +4 -0
  110. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  111. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  112. package/dist/runtime/sessionReconciliation.js +93 -0
  113. package/dist/runtime/sessionTerminationGuard.js +211 -0
  114. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  115. package/dist/runtime/tmuxAdapters.js +34 -1
  116. package/dist/scheduler/actionability.js +155 -0
  117. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  118. package/dist/scheduler/activeTaskProgress.js +60 -0
  119. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  120. package/dist/scheduler/roleRunStall.js +135 -29
  121. package/dist/scheduler/taskExecutionProjection.js +11 -0
  122. package/dist/setup/setupCommand.js +27 -4
  123. package/dist/storage/compatibleTaskStore.js +112 -5
  124. package/dist/storage/migration/productionRegistry.js +769 -1
  125. package/dist/storage/persistenceWorker.js +194 -0
  126. package/dist/storage/sqliteSchema.js +705 -0
  127. package/dist/storage/sqliteStore.js +1695 -0
  128. package/dist/storage/storageVersions.js +9 -2
  129. package/dist/storage/storeRpc.js +298 -0
  130. package/dist/storage/taskStore.js +982 -21
  131. package/dist/storage/upgrade/homeClassification.js +157 -12
  132. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  133. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  134. package/dist/storage/upgrade/recordVersions.js +10 -1
  135. package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
  136. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  137. package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
  138. package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
  139. package/dist/task/deliveryGuard.js +226 -0
  140. package/dist/task/nextAction.js +343 -0
  141. package/dist/task/repairWave.js +137 -0
  142. package/dist/task/taskRecordReference.js +6 -1
  143. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  144. package/dist/telemetry/telemetryCompaction.js +251 -0
  145. package/dist/telemetry/telemetryConfig.js +64 -0
  146. package/dist/telemetry/telemetryRouter.js +32 -0
  147. package/dist/telemetry/telemetryStore.js +19 -0
  148. package/dist/telemetry/telemetryWiring.js +33 -0
  149. package/dist/tmux/tmuxManager.js +20 -1
  150. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  151. package/dist/verification/gateArtifact.js +216 -0
  152. package/dist/verification/gateArtifactStore.js +87 -0
  153. package/dist/verification/verificationGateService.js +414 -0
  154. package/dist/verification/verificationPlan.js +308 -0
  155. package/dist/workspace/gitChangeSetCapture.js +12 -2
  156. package/dist/workspace/workItemChangeSetManager.js +60 -3
  157. package/package.json +2 -1
@@ -8,6 +8,8 @@ const FINAL_REVIEW_AGGREGATE_FROM_VERSION = 16;
8
8
  const FINAL_REVIEW_AGGREGATE_TO_VERSION = 17;
9
9
  const HOME_IDENTITY_AGGREGATE_FROM_VERSION = 17;
10
10
  const HOME_IDENTITY_AGGREGATE_TO_VERSION = 18;
11
+ const SQLITE_LAYOUT_FROM_VERSION = 6;
12
+ const SQLITE_LAYOUT_TO_VERSION = 7;
11
13
  const PROJECT_FROM_VERSION = 2;
12
14
  const PROJECT_TO_VERSION = 3;
13
15
  const TASK_FROM_VERSION = 3;
@@ -20,6 +22,15 @@ const WORK_ITEM_GROUP_HISTORY_FROM_VERSION = 8;
20
22
  const WORK_ITEM_GROUP_HISTORY_TO_VERSION = 9;
21
23
  const AGENT_RUN_FROM_VERSION = 5;
22
24
  const AGENT_RUN_TO_VERSION = 6;
25
+ /**
26
+ * v7 combines the optional Issue 04 `providerRetry`/`yieldReceipt` fields and
27
+ * Issue 05 Leader actionability fields. All are optional, so the transition is
28
+ * a version-only rewrite; legacy v6 records without them remain valid.
29
+ */
30
+ const AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION = 6;
31
+ const AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION = 7;
32
+ const MESSAGE_WAKE_POLICY_FROM_VERSION = 2;
33
+ const MESSAGE_WAKE_POLICY_TO_VERSION = 3;
23
34
  const REVIEW_ROUND_FROM_VERSION = 2;
24
35
  const REVIEW_ROUND_TO_VERSION = 3;
25
36
  const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
@@ -30,6 +41,22 @@ const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
30
41
  const ACTIVE_RUN_POINTER_NAMESPACE_TO_VERSION = 3;
31
42
  const MANAGED_WORKSPACE_FROM_VERSION = 1;
32
43
  const MANAGED_WORKSPACE_TO_VERSION = 2;
44
+ const CHANGE_SET_MANIFEST_FROM_VERSION = 2;
45
+ const CHANGE_SET_MANIFEST_TO_VERSION = 3;
46
+ const INTEGRATION_ATTEMPT_FROM_VERSION = 2;
47
+ const INTEGRATION_ATTEMPT_TO_VERSION = 3;
48
+ const INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION = 3;
49
+ const INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION = 4;
50
+ const INTEGRATION_QUEUE_FROM_VERSION = 0;
51
+ const INTEGRATION_QUEUE_TO_VERSION = 1;
52
+ const STORED_TASK_DURABLE_JOBS_FROM_VERSION = 14;
53
+ const STORED_TASK_DURABLE_JOBS_TO_VERSION = 15;
54
+ const STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION = 15;
55
+ const STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION = 16;
56
+ const CAPABILITY_GRANT_FROM_VERSION = 0;
57
+ const CAPABILITY_GRANT_TO_VERSION = 1;
58
+ const RELEASE_WORKFLOW_FROM_VERSION = 0;
59
+ const RELEASE_WORKFLOW_TO_VERSION = 1;
33
60
  /**
34
61
  * Build the authoritative production graph. Transition intent and executable
35
62
  * transforms are registered together here; compatible loading and offline
@@ -38,6 +65,14 @@ const MANAGED_WORKSPACE_TO_VERSION = 2;
38
65
  export function createProductionStorageRegistry() {
39
66
  assertBaselineConsistency();
40
67
  const registry = new MigrationRegistry().registerOfflineMigration({
68
+ axis: "layout",
69
+ fromVersion: SQLITE_LAYOUT_FROM_VERSION,
70
+ toVersion: SQLITE_LAYOUT_TO_VERSION,
71
+ preconditions: requireLayoutV6Snapshot,
72
+ transform: migrateLayoutV6ToV7,
73
+ declaredEffects: []
74
+ })
75
+ .registerOfflineMigration({
41
76
  axis: "aggregate",
42
77
  fromVersion: FINAL_REVIEW_AGGREGATE_FROM_VERSION,
43
78
  toVersion: FINAL_REVIEW_AGGREGATE_TO_VERSION,
@@ -59,11 +94,22 @@ export function createProductionStorageRegistry() {
59
94
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
60
95
  .registerOfflineMigration(workItemExecutionGroupHistoryStep())
61
96
  .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_FROM_VERSION, AGENT_RUN_TO_VERSION, "agentRuns"))
97
+ .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION, AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION, "agentRuns"))
98
+ .registerOfflineMigration(messageWakePolicyStep())
62
99
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
63
100
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
64
101
  .registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
65
102
  .registerOfflineMigration(managedWorkspaceFamilyStep())
66
- .registerOfflineMigration(activeRunPointerNamespaceStep());
103
+ .registerOfflineMigration(activeRunPointerNamespaceStep())
104
+ .registerOfflineMigration(changeSetManifestStep())
105
+ .registerOfflineMigration(integrationAttemptSupersededStep())
106
+ .registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION, INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION, "integrationAttempts"))
107
+ .registerOfflineMigration(integrationQueueIntroductionStep())
108
+ .registerCompatible(storedTaskDurableJobsStep())
109
+ .registerCompatible(storedTaskJobCallerKeyHashesStep())
110
+ .registerCompatible(durableJobIntroductionStep())
111
+ .registerOfflineMigration(capabilityGrantIntroductionStep())
112
+ .registerOfflineMigration(releaseWorkflowIntroductionStep());
67
113
  assertRegistryCoversBaselineToCurrent(registry);
68
114
  return registry;
69
115
  }
@@ -78,6 +124,80 @@ function workItemExecutionGroupHistoryStep() {
78
124
  declaredEffects: []
79
125
  };
80
126
  }
127
+ /**
128
+ * Task Messages gain an optional `wakePolicy` field (Issue 05). The field is
129
+ * absent on every older record, so this adjacent step only advances the
130
+ * family version; the strict current parser treats the omitted field as
131
+ * "no explicit policy" and preserves the existing routing.
132
+ */
133
+ function messageWakePolicyStep() {
134
+ return {
135
+ axis: "record",
136
+ recordKind: "message",
137
+ fromVersion: MESSAGE_WAKE_POLICY_FROM_VERSION,
138
+ toVersion: MESSAGE_WAKE_POLICY_TO_VERSION,
139
+ preconditions: (snapshot) => requireMessageFamilyVersion(snapshot, MESSAGE_WAKE_POLICY_FROM_VERSION),
140
+ transform: migrateMessageWakePolicy,
141
+ declaredEffects: []
142
+ };
143
+ }
144
+ function requireMessageFamilyVersion(snapshot, version) {
145
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
146
+ if (manifestVersions.message !== version) {
147
+ throw new Error(`Message migration requires manifest version ${version}.`);
148
+ }
149
+ if (snapshot.state === null)
150
+ return;
151
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
152
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
153
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
154
+ const messages = task.messages;
155
+ if (messages === undefined)
156
+ continue;
157
+ const map = asObject(messages, `message map ${taskId}`);
158
+ for (const [messageId, rawMessage] of Object.entries(map)) {
159
+ const message = asObject(rawMessage, `message ${taskId}/${messageId}`);
160
+ if (message.schemaVersion !== version) {
161
+ throw new Error(`Message ${taskId}/${messageId} must use schemaVersion ${version}.`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ function migrateMessageWakePolicy(snapshot) {
167
+ requireMessageFamilyVersion(snapshot, MESSAGE_WAKE_POLICY_FROM_VERSION);
168
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
169
+ const schemaManifest = {
170
+ ...snapshot.schemaManifest,
171
+ recordVersions: {
172
+ ...manifestVersions,
173
+ message: MESSAGE_WAKE_POLICY_TO_VERSION
174
+ }
175
+ };
176
+ if (snapshot.state === null)
177
+ return { schemaManifest, state: null };
178
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
179
+ const nextTasks = {};
180
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
181
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
182
+ if (task.messages === undefined) {
183
+ nextTasks[taskId] = { ...task };
184
+ continue;
185
+ }
186
+ const messages = asObject(task.messages, `message map ${taskId}`);
187
+ const nextMessages = {};
188
+ for (const [messageId, rawMessage] of Object.entries(messages)) {
189
+ nextMessages[messageId] = {
190
+ ...asObject(rawMessage, `message ${taskId}/${messageId}`),
191
+ schemaVersion: MESSAGE_WAKE_POLICY_TO_VERSION
192
+ };
193
+ }
194
+ nextTasks[taskId] = { ...task, messages: nextMessages };
195
+ }
196
+ return {
197
+ schemaManifest,
198
+ state: { ...snapshot.state, tasks: nextTasks }
199
+ };
200
+ }
81
201
  /**
82
202
  * ManagedWorkspace is stored once in the owner-keyed workspace map and copied
83
203
  * into Candidates, AgentRuns, and ReviewRounds as immutable lifecycle
@@ -356,6 +476,215 @@ function migrateActiveRunPointerNamespace(snapshot) {
356
476
  state: { ...snapshot.state, tasks: nextTasks }
357
477
  };
358
478
  }
479
+ /**
480
+ * ChangeSet v3 adds the optional integration manifest (tags, deleted paths,
481
+ * target ref, evidence references). The manifest is optional, so the
482
+ * transition only rewrites the record version; legacy records without a
483
+ * manifest remain valid and keep integrating.
484
+ */
485
+ function changeSetManifestStep() {
486
+ return {
487
+ axis: "record",
488
+ recordKind: "changeSet",
489
+ fromVersion: CHANGE_SET_MANIFEST_FROM_VERSION,
490
+ toVersion: CHANGE_SET_MANIFEST_TO_VERSION,
491
+ preconditions: requireChangeSetManifestVersion,
492
+ transform: migrateChangeSetManifest,
493
+ declaredEffects: []
494
+ };
495
+ }
496
+ function requireChangeSetManifestVersion(snapshot) {
497
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
498
+ if (manifestVersions.changeSet !== CHANGE_SET_MANIFEST_FROM_VERSION) {
499
+ throw new Error(`Record changeSet migration requires manifest version ${CHANGE_SET_MANIFEST_FROM_VERSION}.`);
500
+ }
501
+ if (snapshot.state === null)
502
+ return;
503
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
504
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
505
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
506
+ if (task.changeSets === undefined)
507
+ continue;
508
+ const changeSets = asObject(task.changeSets, `changeSet map ${taskId}`);
509
+ for (const [changeSetId, rawRecord] of Object.entries(changeSets)) {
510
+ const record = asObject(rawRecord, `changeSet ${taskId}/${changeSetId}`);
511
+ if (record.schemaVersion !== CHANGE_SET_MANIFEST_FROM_VERSION) {
512
+ throw new Error(`changeSet ${taskId}/${changeSetId} must use schemaVersion ${CHANGE_SET_MANIFEST_FROM_VERSION}.`);
513
+ }
514
+ }
515
+ }
516
+ }
517
+ function migrateChangeSetManifest(snapshot) {
518
+ requireChangeSetManifestVersion(snapshot);
519
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
520
+ const schemaManifest = {
521
+ ...snapshot.schemaManifest,
522
+ recordVersions: {
523
+ ...manifestVersions,
524
+ changeSet: CHANGE_SET_MANIFEST_TO_VERSION
525
+ }
526
+ };
527
+ if (snapshot.state === null)
528
+ return { schemaManifest, state: null };
529
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
530
+ const nextTasks = {};
531
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
532
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
533
+ if (task.changeSets === undefined) {
534
+ nextTasks[taskId] = { ...task };
535
+ continue;
536
+ }
537
+ const changeSets = asObject(task.changeSets, `changeSet map ${taskId}`);
538
+ const nextChangeSets = {};
539
+ for (const [changeSetId, rawRecord] of Object.entries(changeSets)) {
540
+ const record = asObject(rawRecord, `changeSet ${taskId}/${changeSetId}`);
541
+ nextChangeSets[changeSetId] = {
542
+ ...record,
543
+ schemaVersion: CHANGE_SET_MANIFEST_TO_VERSION
544
+ };
545
+ }
546
+ nextTasks[taskId] = { ...task, changeSets: nextChangeSets };
547
+ }
548
+ return {
549
+ schemaManifest,
550
+ state: { ...snapshot.state, tasks: nextTasks }
551
+ };
552
+ }
553
+ /**
554
+ * IntegrationAttempt v2->v3 adds the "superseded" terminal status for
555
+ * committed Integrations that are obsolete. The migration preserves all
556
+ * existing fields and advances the schema version; the new status is
557
+ * opt-in, so old records remain valid until explicitly superseded.
558
+ */
559
+ function integrationAttemptSupersededStep() {
560
+ return {
561
+ axis: "record",
562
+ recordKind: "integrationAttempt",
563
+ fromVersion: INTEGRATION_ATTEMPT_FROM_VERSION,
564
+ toVersion: INTEGRATION_ATTEMPT_TO_VERSION,
565
+ preconditions: requireIntegrationAttemptV2,
566
+ transform: migrateIntegrationAttemptV2ToV3,
567
+ declaredEffects: []
568
+ };
569
+ }
570
+ function requireIntegrationAttemptV2(snapshot) {
571
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
572
+ if (manifestVersions.integrationAttempt !== INTEGRATION_ATTEMPT_FROM_VERSION) {
573
+ throw new Error(`Record integrationAttempt migration requires manifest version ${INTEGRATION_ATTEMPT_FROM_VERSION}.`);
574
+ }
575
+ if (snapshot.state === null)
576
+ return;
577
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
578
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
579
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
580
+ if (task.integrationAttempts === undefined)
581
+ continue;
582
+ const attempts = asObject(task.integrationAttempts, `integrationAttempt map ${taskId}`);
583
+ for (const [attemptId, rawRecord] of Object.entries(attempts)) {
584
+ const record = asObject(rawRecord, `integrationAttempt ${taskId}/${attemptId}`);
585
+ if (record.schemaVersion !== INTEGRATION_ATTEMPT_FROM_VERSION) {
586
+ throw new Error(`integrationAttempt ${taskId}/${attemptId} must use schemaVersion ${INTEGRATION_ATTEMPT_FROM_VERSION}.`);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ function migrateIntegrationAttemptV2ToV3(snapshot) {
592
+ requireIntegrationAttemptV2(snapshot);
593
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
594
+ const schemaManifest = {
595
+ ...snapshot.schemaManifest,
596
+ recordVersions: {
597
+ ...manifestVersions,
598
+ integrationAttempt: INTEGRATION_ATTEMPT_TO_VERSION
599
+ }
600
+ };
601
+ if (snapshot.state === null)
602
+ return { schemaManifest, state: null };
603
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
604
+ const nextTasks = {};
605
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
606
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
607
+ if (task.integrationAttempts === undefined) {
608
+ nextTasks[taskId] = { ...task };
609
+ continue;
610
+ }
611
+ const attempts = asObject(task.integrationAttempts, `integrationAttempt map ${taskId}`);
612
+ const nextAttempts = {};
613
+ for (const [attemptId, rawRecord] of Object.entries(attempts)) {
614
+ const record = asObject(rawRecord, `integrationAttempt ${taskId}/${attemptId}`);
615
+ nextAttempts[attemptId] = {
616
+ ...record,
617
+ schemaVersion: INTEGRATION_ATTEMPT_TO_VERSION
618
+ };
619
+ }
620
+ nextTasks[taskId] = { ...task, integrationAttempts: nextAttempts };
621
+ }
622
+ return {
623
+ schemaManifest,
624
+ state: { ...snapshot.state, tasks: nextTasks }
625
+ };
626
+ }
627
+ /**
628
+ * The integration queue is a post-baseline per-Task record family. Its
629
+ * explicit 0->1 introduction adds the empty map and its id high-water mark to
630
+ * every Task aggregate and the family to the record manifest; old Homes keep
631
+ * integrating without it until the queue is first used.
632
+ */
633
+ function integrationQueueIntroductionStep() {
634
+ return {
635
+ axis: "record",
636
+ recordKind: "integrationQueue",
637
+ fromVersion: INTEGRATION_QUEUE_FROM_VERSION,
638
+ toVersion: INTEGRATION_QUEUE_TO_VERSION,
639
+ introduction: true,
640
+ preconditions: requireIntegrationQueueIntroduction,
641
+ transform: introduceIntegrationQueue,
642
+ declaredEffects: []
643
+ };
644
+ }
645
+ function requireIntegrationQueueIntroduction(snapshot) {
646
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
647
+ if (manifestVersions.integrationQueue !== undefined) {
648
+ throw new Error("integrationQueue is already introduced in the record manifest.");
649
+ }
650
+ if (snapshot.state === null)
651
+ return;
652
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
653
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
654
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
655
+ if (task.integrationQueue !== undefined) {
656
+ throw new Error(`Task aggregate already carries an integrationQueue map: ${taskId}.`);
657
+ }
658
+ }
659
+ }
660
+ function introduceIntegrationQueue(snapshot) {
661
+ requireIntegrationQueueIntroduction(snapshot);
662
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
663
+ const schemaManifest = {
664
+ ...snapshot.schemaManifest,
665
+ recordVersions: {
666
+ ...manifestVersions,
667
+ integrationQueue: INTEGRATION_QUEUE_TO_VERSION
668
+ }
669
+ };
670
+ if (snapshot.state === null)
671
+ return { schemaManifest, state: null };
672
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
673
+ const nextTasks = {};
674
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
675
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
676
+ const highWaterMarks = asObject(task.idHighWaterMarks ?? {}, `Task id high-water marks ${taskId}`);
677
+ nextTasks[taskId] = {
678
+ ...task,
679
+ idHighWaterMarks: { ...highWaterMarks, integrationQueue: 0 },
680
+ integrationQueue: {}
681
+ };
682
+ }
683
+ return {
684
+ schemaManifest,
685
+ state: { ...snapshot.state, tasks: nextTasks }
686
+ };
687
+ }
359
688
  function legacyLaneKeyParts(key) {
360
689
  const match = /^lane:([^:]+):([^:]+)$/u.exec(key);
361
690
  if (match === null)
@@ -368,6 +697,274 @@ function executionLaneNamespaceKey(executionGroupId, executionLaneId) {
368
697
  function encodeLaneKeyPart(value) {
369
698
  return encodeURIComponent(value).replace(/:/gu, "%3A");
370
699
  }
700
+ /**
701
+ * The StoredTask aggregate gains the `durableJobs` record family. Every v14
702
+ * task defaults to an empty map; the compatible normalizer adds it in place
703
+ * without rewriting any other field. The strict source validator rejects
704
+ * unknown fields so a pre-v15 Home cannot smuggle an unrecognized shape.
705
+ */
706
+ function storedTaskDurableJobsStep() {
707
+ return {
708
+ kind: "compatible",
709
+ axis: "record",
710
+ recordKind: "storedTask",
711
+ fromVersion: STORED_TASK_DURABLE_JOBS_FROM_VERSION,
712
+ toVersion: STORED_TASK_DURABLE_JOBS_TO_VERSION,
713
+ defaults: [
714
+ "durableJobs defaults to an empty map on every Task aggregate",
715
+ "idHighWaterMarks.durableJob defaults to 0 on every Task aggregate"
716
+ ],
717
+ validateSource: (snapshot) => requireStoredTaskV14Shape(snapshot),
718
+ normalize: (snapshot) => normalizeStoredTaskV14ToV15(snapshot)
719
+ };
720
+ }
721
+ const STORED_TASK_V14_FIELDS = [
722
+ "schemaVersion",
723
+ "task",
724
+ "idHighWaterMarks",
725
+ "brief",
726
+ "changeSets",
727
+ "integrationAttempts",
728
+ "integrationQueue",
729
+ // durableJobs may already be present on a v14 task when the Home was
730
+ // written by a build that had the family before the storedTask version
731
+ // advanced (or after a state.json→SQLite repair round-trip, which
732
+ // reconstructs every current family map). The 14→15 normalizer preserves
733
+ // existing records instead of overwriting them with an empty map.
734
+ "durableJobs",
735
+ // jobCallerKeyHashes is introduced at v16 but the state.json→SQLite repair
736
+ // round-trip reconstructs every current family map, so a v14 task may carry
737
+ // an empty map after the repair. The 14→15 normalizer preserves it.
738
+ "jobCallerKeyHashes",
739
+ "capabilityGrants",
740
+ "releaseWorkflows",
741
+ "roles",
742
+ "managedWorkspaces",
743
+ "roleSessionSets",
744
+ "workItems",
745
+ "agentRuns",
746
+ "reviewRounds",
747
+ "activeRuns",
748
+ "messages",
749
+ "inputRequests",
750
+ "decisions",
751
+ "milestones",
752
+ "events",
753
+ "leaderFailure",
754
+ "operatorNotification"
755
+ ];
756
+ function requireStoredTaskV14Shape(snapshot) {
757
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
758
+ if (manifestVersions.storedTask !== STORED_TASK_DURABLE_JOBS_FROM_VERSION) {
759
+ throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_DURABLE_JOBS_FROM_VERSION}.`);
760
+ }
761
+ if (snapshot.state === null)
762
+ return;
763
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
764
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
765
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
766
+ if (task.schemaVersion !== STORED_TASK_DURABLE_JOBS_FROM_VERSION) {
767
+ throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_DURABLE_JOBS_FROM_VERSION}.`);
768
+ }
769
+ const allowed = new Set(STORED_TASK_V14_FIELDS);
770
+ const unknown = Object.keys(task).find((key) => !allowed.has(key));
771
+ if (unknown !== undefined) {
772
+ throw new Error(`Task aggregate ${taskId} has an unknown v14 field: ${unknown}.`);
773
+ }
774
+ }
775
+ }
776
+ function normalizeStoredTaskV14ToV15(snapshot) {
777
+ requireStoredTaskV14Shape(snapshot);
778
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
779
+ const schemaManifest = {
780
+ ...snapshot.schemaManifest,
781
+ recordVersions: {
782
+ ...manifestVersions,
783
+ storedTask: STORED_TASK_DURABLE_JOBS_TO_VERSION
784
+ }
785
+ };
786
+ if (snapshot.state === null)
787
+ return { schemaManifest, state: null };
788
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
789
+ const nextTasks = {};
790
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
791
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
792
+ // The v14 high-water marks have no durableJob family; the strict current
793
+ // parser requires every family key, so the normalizer supplies the zero
794
+ // default in the same pass as the empty durableJobs map.
795
+ const highWaterMarks = asObject(task.idHighWaterMarks, `Task aggregate ${taskId} idHighWaterMarks`);
796
+ const existingJobs = task.durableJobs === undefined
797
+ ? {}
798
+ : asObject(task.durableJobs, `Task aggregate ${taskId} durableJobs`);
799
+ const existingJobMark = highWaterMarks.durableJob;
800
+ nextTasks[taskId] = {
801
+ ...task,
802
+ schemaVersion: STORED_TASK_DURABLE_JOBS_TO_VERSION,
803
+ idHighWaterMarks: {
804
+ ...highWaterMarks,
805
+ durableJob: typeof existingJobMark === "number" ? existingJobMark : 0
806
+ },
807
+ durableJobs: existingJobs
808
+ };
809
+ }
810
+ return {
811
+ schemaManifest,
812
+ state: { ...snapshot.state, tasks: nextTasks }
813
+ };
814
+ }
815
+ /**
816
+ * rr13: The StoredTask aggregate gains the `jobCallerKeyHashes` map. Every v15
817
+ * task defaults to an empty map; the compatible normalizer adds it in place
818
+ * without rewriting any other field. A v15 task must not already carry the
819
+ * field. No legacy fallback: a Session whose hash is absent is rejected at the
820
+ * Controller boundary (fail-closed).
821
+ */
822
+ function storedTaskJobCallerKeyHashesStep() {
823
+ return {
824
+ kind: "compatible",
825
+ axis: "record",
826
+ recordKind: "storedTask",
827
+ fromVersion: STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION,
828
+ toVersion: STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION,
829
+ defaults: [
830
+ "jobCallerKeyHashes defaults to an empty map on every Task aggregate"
831
+ ],
832
+ validateSource: (snapshot) => requireStoredTaskV15Shape(snapshot),
833
+ normalize: (snapshot) => normalizeStoredTaskV15ToV16(snapshot)
834
+ };
835
+ }
836
+ const STORED_TASK_V15_FIELDS = [
837
+ "schemaVersion",
838
+ "task",
839
+ "idHighWaterMarks",
840
+ "brief",
841
+ "changeSets",
842
+ "integrationAttempts",
843
+ "integrationQueue",
844
+ "durableJobs",
845
+ // May already be present after a state.json→SQLite repair round-trip (the
846
+ // reverse reader reconstructs every current family map). The 15→16
847
+ // normalizer preserves existing hashes instead of overwriting them.
848
+ "jobCallerKeyHashes",
849
+ "capabilityGrants",
850
+ "releaseWorkflows",
851
+ "roles",
852
+ "managedWorkspaces",
853
+ "roleSessionSets",
854
+ "workItems",
855
+ "agentRuns",
856
+ "reviewRounds",
857
+ "activeRuns",
858
+ "messages",
859
+ "inputRequests",
860
+ "decisions",
861
+ "milestones",
862
+ "events",
863
+ "leaderFailure",
864
+ "operatorNotification"
865
+ ];
866
+ function requireStoredTaskV15Shape(snapshot) {
867
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
868
+ if (manifestVersions.storedTask !== STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION) {
869
+ throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION}.`);
870
+ }
871
+ if (snapshot.state === null)
872
+ return;
873
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
874
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
875
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
876
+ if (task.schemaVersion !== STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION) {
877
+ throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION}.`);
878
+ }
879
+ const allowed = new Set(STORED_TASK_V15_FIELDS);
880
+ const unknown = Object.keys(task).find((key) => !allowed.has(key));
881
+ if (unknown !== undefined) {
882
+ throw new Error(`Task aggregate ${taskId} has an unknown v15 field: ${unknown}.`);
883
+ }
884
+ }
885
+ }
886
+ function normalizeStoredTaskV15ToV16(snapshot) {
887
+ requireStoredTaskV15Shape(snapshot);
888
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
889
+ const schemaManifest = {
890
+ ...snapshot.schemaManifest,
891
+ recordVersions: {
892
+ ...manifestVersions,
893
+ storedTask: STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION
894
+ }
895
+ };
896
+ if (snapshot.state === null)
897
+ return { schemaManifest, state: null };
898
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
899
+ const nextTasks = {};
900
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
901
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
902
+ const existingHashes = task.jobCallerKeyHashes === undefined
903
+ ? {}
904
+ : asObject(task.jobCallerKeyHashes, `Task aggregate ${taskId} jobCallerKeyHashes`);
905
+ nextTasks[taskId] = {
906
+ ...task,
907
+ schemaVersion: STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION,
908
+ jobCallerKeyHashes: existingHashes
909
+ };
910
+ }
911
+ return {
912
+ schemaManifest,
913
+ state: { ...snapshot.state, tasks: nextTasks }
914
+ };
915
+ }
916
+ /**
917
+ * The durableJob record family is introduced alongside the StoredTask v15
918
+ * aggregate. A pre-baseline Home has no durableJobs map and no manifest entry;
919
+ * the introduction declares the family at version 1. The StoredTask 14->15
920
+ * compatible step supplies the empty map; this step only advances the manifest
921
+ * record version so the planner can resolve the 0->1 boundary.
922
+ */
923
+ function durableJobIntroductionStep() {
924
+ return {
925
+ kind: "compatible",
926
+ axis: "record",
927
+ recordKind: "durableJob",
928
+ fromVersion: 0,
929
+ toVersion: 1,
930
+ introduction: true,
931
+ defaults: ["durableJob family is introduced as an empty map on every Task"],
932
+ validateSource: (snapshot) => {
933
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
934
+ const declared = manifestVersions.durableJob;
935
+ if (declared !== undefined && declared !== 0) {
936
+ throw new Error(`Record durableJob introduction requires manifest version 0 or absent, found ${String(declared)}.`);
937
+ }
938
+ if (snapshot.state === null)
939
+ return;
940
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
941
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
942
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
943
+ if (task.durableJobs !== undefined) {
944
+ const jobs = asObject(task.durableJobs, `durableJobs map ${taskId}`);
945
+ if (Object.keys(jobs).length > 0) {
946
+ throw new Error(`Task aggregate ${taskId} must not carry durableJobs before introduction.`);
947
+ }
948
+ }
949
+ }
950
+ },
951
+ normalize: (snapshot) => {
952
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
953
+ return {
954
+ schemaManifest: {
955
+ ...snapshot.schemaManifest,
956
+ recordVersions: {
957
+ ...manifestVersions,
958
+ durableJob: 1
959
+ }
960
+ },
961
+ state: snapshot.state === null
962
+ ? null
963
+ : { ...snapshot.state }
964
+ };
965
+ }
966
+ };
967
+ }
371
968
  /**
372
969
  * Upgrade one nested Task record family without guessing fields or repairing
373
970
  * malformed state. The new execution lineage fields are optional, so legal
@@ -376,6 +973,154 @@ function encodeLaneKeyPart(value) {
376
973
  * durable and centralized, which keeps old Homes out of the strict current
377
974
  * parser until this step has run.
378
975
  */
976
+ /**
977
+ * The post-baseline introduction of the task-scoped CapabilityGrant family.
978
+ * A pre-introduction Home names no capabilityGrant manifest version and its
979
+ * Task aggregates carry no `capabilityGrants` map; this explicit 0->1 step
980
+ * adds the empty family to every Task (plus the matching high-water mark) so
981
+ * the strict current parser can open the migrated state.
982
+ */
983
+ function capabilityGrantIntroductionStep() {
984
+ return {
985
+ axis: "record",
986
+ recordKind: "capabilityGrant",
987
+ fromVersion: CAPABILITY_GRANT_FROM_VERSION,
988
+ toVersion: CAPABILITY_GRANT_TO_VERSION,
989
+ introduction: true,
990
+ preconditions: requireCapabilityGrantIntroduction,
991
+ transform: introduceCapabilityGrantFamily,
992
+ declaredEffects: []
993
+ };
994
+ }
995
+ function requireCapabilityGrantIntroduction(snapshot) {
996
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
997
+ const namedVersion = manifestVersions.capabilityGrant;
998
+ if (namedVersion !== undefined && namedVersion !== CAPABILITY_GRANT_FROM_VERSION) {
999
+ throw new Error(`Record capabilityGrant introduction requires an absent manifest version or ${CAPABILITY_GRANT_FROM_VERSION}.`);
1000
+ }
1001
+ if (snapshot.state === null)
1002
+ return;
1003
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1004
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1005
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1006
+ if (task.capabilityGrants !== undefined) {
1007
+ const grants = asObject(task.capabilityGrants, `capabilityGrant map ${taskId}`);
1008
+ if (Object.keys(grants).length > 0) {
1009
+ throw new Error(`Capability grant introduction found existing records: ${taskId}.`);
1010
+ }
1011
+ }
1012
+ const marks = task.idHighWaterMarks;
1013
+ if (marks !== undefined) {
1014
+ const highWaterMarks = asObject(marks, `Task id high-water marks ${taskId}`);
1015
+ const mark = highWaterMarks.capabilityGrant;
1016
+ if (mark !== undefined && mark !== CAPABILITY_GRANT_FROM_VERSION) {
1017
+ throw new Error(`Capability grant introduction found a high-water mark: ${taskId}.`);
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ function introduceCapabilityGrantFamily(snapshot) {
1023
+ requireCapabilityGrantIntroduction(snapshot);
1024
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1025
+ const schemaManifest = {
1026
+ ...snapshot.schemaManifest,
1027
+ recordVersions: {
1028
+ ...manifestVersions,
1029
+ capabilityGrant: CAPABILITY_GRANT_TO_VERSION
1030
+ }
1031
+ };
1032
+ if (snapshot.state === null)
1033
+ return { schemaManifest, state: null };
1034
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1035
+ const nextTasks = {};
1036
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1037
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1038
+ const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1039
+ nextTasks[taskId] = {
1040
+ ...task,
1041
+ idHighWaterMarks: { ...marks, capabilityGrant: CAPABILITY_GRANT_FROM_VERSION },
1042
+ capabilityGrants: {}
1043
+ };
1044
+ }
1045
+ return {
1046
+ schemaManifest,
1047
+ state: { ...snapshot.state, tasks: nextTasks }
1048
+ };
1049
+ }
1050
+ /**
1051
+ * The post-baseline introduction of the task-scoped ReleaseWorkflow family.
1052
+ * A pre-introduction Home names no releaseWorkflow manifest version and its
1053
+ * Task aggregates carry no `releaseWorkflows` map; this explicit 0->1 step
1054
+ * adds the empty family to every Task (plus the matching high-water mark) so
1055
+ * the strict current parser can open the migrated state.
1056
+ */
1057
+ function releaseWorkflowIntroductionStep() {
1058
+ return {
1059
+ axis: "record",
1060
+ recordKind: "releaseWorkflow",
1061
+ fromVersion: RELEASE_WORKFLOW_FROM_VERSION,
1062
+ toVersion: RELEASE_WORKFLOW_TO_VERSION,
1063
+ introduction: true,
1064
+ preconditions: requireReleaseWorkflowIntroduction,
1065
+ transform: introduceReleaseWorkflowFamily,
1066
+ declaredEffects: []
1067
+ };
1068
+ }
1069
+ function requireReleaseWorkflowIntroduction(snapshot) {
1070
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1071
+ const namedVersion = manifestVersions.releaseWorkflow;
1072
+ if (namedVersion !== undefined && namedVersion !== RELEASE_WORKFLOW_FROM_VERSION) {
1073
+ throw new Error(`Record releaseWorkflow introduction requires an absent manifest version or ${RELEASE_WORKFLOW_FROM_VERSION}.`);
1074
+ }
1075
+ if (snapshot.state === null)
1076
+ return;
1077
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1078
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1079
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1080
+ if (task.releaseWorkflows !== undefined) {
1081
+ const workflows = asObject(task.releaseWorkflows, `releaseWorkflow map ${taskId}`);
1082
+ if (Object.keys(workflows).length > 0) {
1083
+ throw new Error(`Release workflow introduction found existing records: ${taskId}.`);
1084
+ }
1085
+ }
1086
+ const marks = task.idHighWaterMarks;
1087
+ if (marks !== undefined) {
1088
+ const highWaterMarks = asObject(marks, `Task id high-water marks ${taskId}`);
1089
+ const mark = highWaterMarks.releaseWorkflow;
1090
+ if (mark !== undefined && mark !== RELEASE_WORKFLOW_FROM_VERSION) {
1091
+ throw new Error(`Release workflow introduction found a high-water mark: ${taskId}.`);
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+ function introduceReleaseWorkflowFamily(snapshot) {
1097
+ requireReleaseWorkflowIntroduction(snapshot);
1098
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1099
+ const schemaManifest = {
1100
+ ...snapshot.schemaManifest,
1101
+ recordVersions: {
1102
+ ...manifestVersions,
1103
+ releaseWorkflow: RELEASE_WORKFLOW_TO_VERSION
1104
+ }
1105
+ };
1106
+ if (snapshot.state === null)
1107
+ return { schemaManifest, state: null };
1108
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1109
+ const nextTasks = {};
1110
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1111
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1112
+ const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1113
+ nextTasks[taskId] = {
1114
+ ...task,
1115
+ idHighWaterMarks: { ...marks, releaseWorkflow: RELEASE_WORKFLOW_FROM_VERSION },
1116
+ releaseWorkflows: {}
1117
+ };
1118
+ }
1119
+ return {
1120
+ schemaManifest,
1121
+ state: { ...snapshot.state, tasks: nextTasks }
1122
+ };
1123
+ }
379
1124
  function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
380
1125
  return {
381
1126
  axis: "record",
@@ -462,6 +1207,29 @@ function asObject(value, label) {
462
1207
  }
463
1208
  /** Historical public spelling retained as an alias to the single graph. */
464
1209
  export const createProductionRegistry = createProductionStorageRegistry;
1210
+ /**
1211
+ * Layout 6 -> 7 is the state.json -> SQLite WAL transition (task-21 §8). The
1212
+ * transform only advances the manifest's layout version; the staged SQLite
1213
+ * database is populated by the migration target's `writeFreshOutput`, and the
1214
+ * `state.json` document is retained read-only for rollback. The precondition
1215
+ * requires a layout-6 manifest so a future layout cannot be silently regressed.
1216
+ */
1217
+ function requireLayoutV6Snapshot(snapshot) {
1218
+ if (snapshot.schemaManifest.storageVersion !== SQLITE_LAYOUT_FROM_VERSION) {
1219
+ throw new Error(`Layout ${SQLITE_LAYOUT_FROM_VERSION}->${SQLITE_LAYOUT_TO_VERSION} migration requires ` +
1220
+ `schema.json storageVersion ${SQLITE_LAYOUT_FROM_VERSION}.`);
1221
+ }
1222
+ }
1223
+ function migrateLayoutV6ToV7(snapshot) {
1224
+ requireLayoutV6Snapshot(snapshot);
1225
+ return {
1226
+ schemaManifest: {
1227
+ ...snapshot.schemaManifest,
1228
+ storageVersion: SQLITE_LAYOUT_TO_VERSION
1229
+ },
1230
+ state: snapshot.state
1231
+ };
1232
+ }
465
1233
  /** Advance the aggregate identity after proving manifest/root agreement at v16. */
466
1234
  function migrateAggregateV16ToV17(snapshot) {
467
1235
  return {