@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
@@ -1,14 +1,22 @@
1
- import { existsSync, mkdirSync } from "node:fs";
2
- import { delimiter, isAbsolute, join, resolve } from "node:path";
1
+ import { existsSync, mkdirSync, realpathSync } from "node:fs";
2
+ import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
+ import { isDeepStrictEqual } from "node:util";
4
5
  import { configuredAgentToDefinition, createConfiguredAgent } from "../agent/agent.js";
6
+ import { selectAgentPermission, selectAgentModelAndEffort } from "../cli/agentConfigurationPicker.js";
5
7
  import { runCompletionWizard } from "../completion/completionWizard.js";
6
8
  import { usageError } from "../errors/cliError.js";
9
+ import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
10
+ import { AgentConfigurationCatalogService } from "../executor/agentConfigurationCatalog.js";
7
11
  import { defaultTableWidth, renderTable } from "../output/table.js";
8
- import { createGlobalRole, createRoleAgentBinding } from "../role/role.js";
9
- import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
10
- import { ensureYuiHome, FileTaskStore, resolveYuiHome } from "../storage/taskStore.js";
11
- import { ensureStorageSchema } from "../storage/storageSchema.js";
12
+ import { resolveTimeZone } from "../output/timePresentation.js";
13
+ import { createGlobalRole, createRoleAgentBinding, updateGlobalRole } from "../role/role.js";
14
+ import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE, SYSTEM_WORKER_ROLE } from "../role/systemRoles.js";
15
+ import { DEFAULT_REVIEWER_ROLE } from "../review/reviewConfig.js";
16
+ import { builtinAgentProfileInputs, createAgentProfile } from "../profile/agentProfile.js";
17
+ import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
18
+ import { ensureYuiHome, resolveYuiHome } from "../storage/taskStore.js";
19
+ import { initializeCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
12
20
  const BUILTIN_AGENTS = Object.freeze([
13
21
  Object.freeze({
14
22
  id: "codex",
@@ -36,15 +44,40 @@ export async function runSetupCommand(args, env, executor, io = {}) {
36
44
  const question = createSetupQuestion(readline, io);
37
45
  const home = resolveYuiHome(env);
38
46
  ensureYuiHome(home);
39
- ensureStorageSchema(home);
40
- const store = new FileTaskStore(home);
41
- const result = await configureYui(store, home, env, question, io);
47
+ const store = initializeCompatibleFileTaskStore(home);
48
+ const catalogs = new AgentConfigurationCatalogService(home, { environment: env });
49
+ const result = await configureYui(store, home, env, question, setupSelectionIo(question, io), catalogs, io);
42
50
  const lines = [
43
51
  "Yui home initialized.",
44
52
  `Agents configured: ${result.agentIds.join(", ")}.`,
45
53
  `Default Agent: ${result.defaultAgentId}.`,
46
54
  `Operator Agent: ${result.operatorAgentId}.`,
47
- `Operator workspace: ${result.workspace}.`
55
+ `Leader model: ${result.leaderConfig.model ?? "CLI default"}.`,
56
+ `Leader reasoning effort: ${result.leaderConfig.effort ?? "CLI default"}.`,
57
+ `Leader permission: ${result.leaderConfig.permission.strategy}.`,
58
+ `Operator model: ${result.operatorConfig.model ?? "CLI default"}.`,
59
+ `Operator reasoning effort: ${result.operatorConfig.effort ?? "CLI default"}.`,
60
+ `Operator permission: ${result.operatorConfig.permission.strategy}.`,
61
+ `Worker Agent: ${result.workerAgentId}.`,
62
+ `Worker configuration: ${result.workerReusesLeader
63
+ ? "Reused Leader configuration"
64
+ : "Configured separately"}.`,
65
+ `Worker model: ${result.workerConfig.model ?? "CLI default"}.`,
66
+ `Worker reasoning effort: ${result.workerConfig.effort ?? "CLI default"}.`,
67
+ `Worker permission: ${result.workerConfig.permission.strategy}.`,
68
+ ...(result.reviewerInitialized
69
+ ? [
70
+ `Reviewer Agent: ${result.reviewerAgentId}.`,
71
+ `Reviewer model: ${result.reviewerConfig?.model ?? "CLI default"}.`,
72
+ `Reviewer reasoning effort: ${result.reviewerConfig?.effort ?? "CLI default"}.`,
73
+ `Reviewer permission: ${result.reviewerConfig?.permission.strategy}.`,
74
+ ...(result.reviewPolicy === undefined
75
+ ? ["Review policy: disabled."]
76
+ : [`Review policy: ${result.reviewPolicy.roleName} (${result.reviewPolicy.trigger}).`])
77
+ ]
78
+ : []),
79
+ `Project workspace: ${result.workspace}.`,
80
+ `Time zone: ${resolveTimeZone(store.getConfig().timeZone)}.`
48
81
  ];
49
82
  if (dependency === undefined || dependency === "tmux") {
50
83
  lines.push(...await setupTmux(env, executor, question));
@@ -63,7 +96,13 @@ export function validateSetupInvocation(args, io = {}) {
63
96
  if (!shouldPrompt(io))
64
97
  throw setupRequiresInteractiveError();
65
98
  }
66
- async function configureYui(store, home, env, question, io) {
99
+ async function configureYui(store, home, env, question, selectionIo, catalogs, io) {
100
+ const initialConfig = store.getConfig();
101
+ const freshHome = store.listGlobalRoles().length === 0
102
+ && store.listConfiguredAgents().length === 0
103
+ && initialConfig.defaultAgent === undefined
104
+ && initialConfig.defaultWorkspace === undefined
105
+ && initialConfig.review === undefined;
67
106
  const candidates = availableAgentChoices(store, env);
68
107
  if (candidates.length === 0) {
69
108
  throw usageError("No supported Agent CLI was found. Install Codex or Claude, then run setup again.");
@@ -83,45 +122,210 @@ async function configureYui(store, home, env, question, io) {
83
122
  ]), tableWidth(io))}\n`);
84
123
  const selected = parseAgentSetSelection(await question(`Choose Agents by number or name, comma-separated [all: ${candidates.map(({ id }) => id).join(", ")}]: `), candidates);
85
124
  const now = new Date();
86
- const persisted = selected.map((choice) => persistAgent(store, choice, now));
87
- const configuredIds = new Set(persisted.map(({ id }) => id));
125
+ const prepared = selected.map((choice) => prepareAgent(store, choice, now));
126
+ const configuredIds = new Set(prepared.map(({ id }) => id));
88
127
  const config = store.getConfig();
89
- const defaultFallback = configuredIds.has(config.defaultAgent ?? "")
128
+ const defaultFallback = prepared.some(({ id }) => id === config.defaultAgent)
90
129
  ? config.defaultAgent
91
- : persisted[0]?.id;
92
- if (defaultFallback === undefined)
93
- throw usageError("At least one Agent must be selected.");
94
- const defaultAgentId = parseSingleAgentSelection(await question(`Choose default Agent [${defaultFallback}]: `), persisted, defaultFallback);
130
+ : prepared[0].id;
131
+ const defaultAgentId = parseSingleAgentSelection(await question(`Choose default Agent [${defaultFallback}]: `), prepared, defaultFallback);
95
132
  const currentOperatorAgent = store.getGlobalRole(SYSTEM_OPERATOR_ROLE)?.activeAgentId;
96
133
  const operatorFallback = configuredIds.has(currentOperatorAgent ?? "")
97
134
  ? currentOperatorAgent
98
135
  : defaultAgentId;
99
- const operatorAgentId = parseSingleAgentSelection(await question(`Choose Operator Agent [${operatorFallback}]: `), persisted, operatorFallback);
100
- const suggestedWorkspace = config.defaultWorkspace?.trim() || join(home, "workspace");
101
- const workspaceAnswer = (await question(`Operator workspace [${suggestedWorkspace}]: `)).trim();
102
- const workspace = resolveWorkspace(workspaceAnswer || suggestedWorkspace);
103
- const defaultAgent = persisted.find(({ id }) => id === defaultAgentId);
104
- const operatorAgent = persisted.find(({ id }) => id === operatorAgentId);
136
+ const operatorAgentId = parseSingleAgentSelection(await question(`Choose Operator Agent [${operatorFallback}]: `), prepared, operatorFallback);
137
+ const existingReviewer = store.getGlobalRole(DEFAULT_REVIEWER_ROLE);
138
+ const reviewerInitialized = freshHome || existingReviewer !== null;
139
+ let reviewerAgentId;
140
+ let reviewerConfig;
141
+ if (reviewerInitialized) {
142
+ const reviewerFallback = configuredIds.has(existingReviewer?.activeAgentId ?? "")
143
+ ? existingReviewer.activeAgentId
144
+ : defaultAgentId;
145
+ reviewerAgentId = parseSingleAgentSelection(await question(`Choose Reviewer Agent [${reviewerFallback}]: `), prepared, reviewerFallback);
146
+ const reviewerAgent = prepared.find(({ id }) => id === reviewerAgentId);
147
+ if (reviewerAgent === undefined) {
148
+ throw usageError("Selected Reviewer Agent is no longer available.");
149
+ }
150
+ reviewerConfig = await promptRoleAgentConfig("Reviewer", reviewerAgent, existingReviewer, home, selectionIo, catalogs, true);
151
+ }
152
+ const defaultAgent = prepared.find(({ id }) => id === defaultAgentId);
153
+ const operatorAgent = prepared.find(({ id }) => id === operatorAgentId);
105
154
  if (defaultAgent === undefined || operatorAgent === undefined) {
106
155
  throw usageError("Selected setup Agent is no longer available.");
107
156
  }
108
- assertSystemRoleCompatible(store, SYSTEM_OPERATOR_ROLE, operatorAgent, workspace);
109
- assertSystemRoleCompatible(store, SYSTEM_LEADER_ROLE, defaultAgent, workspace);
110
- mkdirSync(workspace, { recursive: true, mode: 0o700 });
111
- store.saveConfig({
112
- ...store.getConfig(),
113
- defaultAgent: defaultAgentId,
114
- defaultWorkspace: workspace
157
+ const leaderConfig = await promptRoleAgentConfig("Leader", defaultAgent, store.getGlobalRole(SYSTEM_LEADER_ROLE), home, selectionIo, catalogs);
158
+ const operatorConfig = await promptRoleAgentConfig("Operator", operatorAgent, store.getGlobalRole(SYSTEM_OPERATOR_ROLE), home, selectionIo, catalogs);
159
+ const existingWorker = store.getGlobalRole(SYSTEM_WORKER_ROLE);
160
+ const workerModeFallback = workerConfigurationModeFallback(existingWorker, defaultAgentId, leaderConfig);
161
+ io.output?.write("\nWorker is the default Agent configuration copied into Task Roles such as "
162
+ + "investigator and implementer. Each Task Role gets its own Session.\n");
163
+ const workerReusesLeader = parseWorkerConfigurationMode(await question(`Choose Worker configuration (reuse Leader/configure separately) [${workerModeFallback === "reuse-leader" ? "reuse Leader" : "configure separately"}]: `), workerModeFallback) === "reuse-leader";
164
+ let workerAgentId = defaultAgentId;
165
+ let workerConfig = structuredClone(leaderConfig);
166
+ if (!workerReusesLeader) {
167
+ const existingWorkerAgent = existingWorker?.activeAgentId;
168
+ const workerFallback = configuredIds.has(existingWorkerAgent ?? "")
169
+ ? existingWorkerAgent
170
+ : defaultAgentId;
171
+ workerAgentId = parseSingleAgentSelection(await question(`Choose Worker Agent [${workerFallback}]: `), prepared, workerFallback);
172
+ const workerAgent = prepared.find(({ id }) => id === workerAgentId);
173
+ if (workerAgent === undefined) {
174
+ throw usageError("Selected Worker Agent is no longer available.");
175
+ }
176
+ workerConfig = await promptRoleAgentConfig("Worker", workerAgent, existingWorker, home, selectionIo, catalogs);
177
+ }
178
+ const suggestedWorkspace = config.defaultWorkspace?.trim()
179
+ || join(dirname(resolve(home)), "workspace");
180
+ const workspaceAnswer = (await question(`Project workspace for stable checkouts and managed worktrees [${suggestedWorkspace}]: `)).trim();
181
+ const workspace = resolveWorkspace(workspaceAnswer || suggestedWorkspace, home);
182
+ if (config.defaultWorkspace !== undefined
183
+ && resolve(config.defaultWorkspace) !== workspace) {
184
+ throw usageError(`Project workspace is fixed after setup: ${resolve(config.defaultWorkspace)}.`);
185
+ }
186
+ store.transaction((tx) => {
187
+ for (const agent of prepared)
188
+ tx.saveConfiguredAgent(agent);
189
+ const latestDefaultAgent = requireSetupAgent(tx, defaultAgentId);
190
+ const latestOperatorAgent = requireSetupAgent(tx, operatorAgentId);
191
+ const latestWorkerAgent = requireSetupAgent(tx, workerAgentId);
192
+ const operatorRole = prepareSystemRole(tx, SYSTEM_OPERATOR_ROLE, latestOperatorAgent, workspace, now, operatorConfig);
193
+ const leaderRole = prepareSystemRole(tx, SYSTEM_LEADER_ROLE, latestDefaultAgent, workspace, now, leaderConfig);
194
+ const workerRole = prepareSystemRole(tx, SYSTEM_WORKER_ROLE, latestWorkerAgent, workspace, now, workerConfig);
195
+ const reviewerRole = reviewerInitialized
196
+ && reviewerAgentId !== undefined
197
+ && reviewerConfig !== undefined
198
+ ? prepareSystemRole(tx, DEFAULT_REVIEWER_ROLE, requireSetupAgent(tx, reviewerAgentId), workspace, now, reviewerConfig, freshHome ? reviewerRoleProfile() : undefined)
199
+ : null;
200
+ const latest = tx.getConfig();
201
+ tx.saveConfig({
202
+ ...latest,
203
+ defaultAgent: defaultAgentId,
204
+ defaultWorkspace: workspace,
205
+ timeZone: resolveTimeZone(latest.timeZone),
206
+ ...(freshHome
207
+ ? { review: { roleName: DEFAULT_REVIEWER_ROLE, trigger: "final" } }
208
+ : {})
209
+ });
210
+ if (operatorRole !== null)
211
+ savePreparedSystemRole(tx, operatorRole, now);
212
+ if (leaderRole !== null)
213
+ savePreparedSystemRole(tx, leaderRole, now);
214
+ if (workerRole !== null)
215
+ savePreparedSystemRole(tx, workerRole, now);
216
+ if (reviewerRole !== null)
217
+ savePreparedSystemRole(tx, reviewerRole, now);
218
+ seedBuiltinProfiles(tx, now);
115
219
  });
116
- ensureSystemRole(store, SYSTEM_OPERATOR_ROLE, operatorAgent, workspace, now);
117
- ensureSystemRole(store, SYSTEM_LEADER_ROLE, defaultAgent, workspace, now);
118
220
  return {
119
- agentIds: persisted.map(({ id }) => id),
221
+ agentIds: prepared.map(({ id }) => id),
120
222
  defaultAgentId,
121
223
  operatorAgentId,
224
+ leaderConfig,
225
+ operatorConfig,
226
+ workerAgentId,
227
+ workerConfig,
228
+ workerReusesLeader,
229
+ reviewerInitialized,
230
+ ...(reviewerAgentId === undefined ? {} : { reviewerAgentId }),
231
+ ...(reviewerConfig === undefined ? {} : { reviewerConfig }),
232
+ ...(freshHome
233
+ ? { reviewPolicy: { roleName: DEFAULT_REVIEWER_ROLE, trigger: "final" } }
234
+ : initialConfig.review === undefined ? {} : { reviewPolicy: initialConfig.review }),
122
235
  workspace
123
236
  };
124
237
  }
238
+ function reviewerRoleProfile() {
239
+ const reviewer = builtinAgentProfileInputs().find(({ id }) => id === "reviewer");
240
+ if (reviewer === undefined)
241
+ return {};
242
+ return {
243
+ ...(reviewer.description === undefined ? {} : { description: reviewer.description }),
244
+ ...(reviewer.instructions === undefined ? {} : { systemPrompt: reviewer.instructions }),
245
+ ...(reviewer.skills === undefined ? {} : { skills: [...reviewer.skills] })
246
+ };
247
+ }
248
+ function workerConfigurationModeFallback(existing, leaderAgentId, leaderConfig) {
249
+ if (existing === null)
250
+ return "reuse-leader";
251
+ return existing.activeAgentId === leaderAgentId
252
+ && isDeepStrictEqual(existing.agentBindings[leaderAgentId]?.config, leaderConfig)
253
+ ? "reuse-leader"
254
+ : "configure-separately";
255
+ }
256
+ function parseWorkerConfigurationMode(answer, fallback) {
257
+ const value = answer.trim().toLowerCase();
258
+ if (value.length === 0)
259
+ return fallback;
260
+ if (["1", "reuse", "reuse leader", "leader"].includes(value))
261
+ return "reuse-leader";
262
+ if (["2", "configure", "configure separately", "separate"].includes(value)) {
263
+ return "configure-separately";
264
+ }
265
+ throw usageError("Choose reuse Leader or configure separately for Worker.");
266
+ }
267
+ function savePreparedSystemRole(store, role, now) {
268
+ const sessions = store.getGlobalRoleSessionSet(role.name);
269
+ const activeSession = sessions?.sessions[sessions.activeAgentId];
270
+ if (sessions === null
271
+ || sessions.activeAgentId === role.activeAgentId
272
+ || (activeSession !== undefined && activeSession.status !== "stopped")) {
273
+ store.saveGlobalRole(role);
274
+ return;
275
+ }
276
+ store.saveGlobalRoleWithSessionSet(role, {
277
+ ...sessions,
278
+ activeAgentId: role.activeAgentId,
279
+ updatedAt: now.toISOString()
280
+ });
281
+ }
282
+ function seedBuiltinProfiles(store, now) {
283
+ for (const desired of builtinAgentProfileInputs()) {
284
+ const existing = store.getAgentProfile(desired.id);
285
+ if (existing === null)
286
+ store.saveAgentProfile(createAgentProfile(desired, now));
287
+ }
288
+ }
289
+ async function promptRoleAgentConfig(label, agent, existingRole, cwd, io, catalogs, selectPermission = false) {
290
+ const existing = existingRole?.activeAgentId === agent.id
291
+ ? existingRole.agentBindings[agent.id]?.config
292
+ : undefined;
293
+ io.write(`\n${label} Agent configuration: ${agent.id}\n`);
294
+ const resolved = await catalogs.resolve({
295
+ agent,
296
+ cwd,
297
+ ...(existing === undefined ? {} : { config: existing })
298
+ });
299
+ const selection = await selectAgentModelAndEffort(resolved, io, {
300
+ currentModel: existing?.model,
301
+ currentEffort: existing?.effort
302
+ });
303
+ if (selection.kind === "cancelled") {
304
+ throw usageError(`${label} Agent configuration was cancelled.`);
305
+ }
306
+ const candidate = structuredClone(existing ?? defaultRoleAgentConfig(agent.adapterId));
307
+ if (selection.model === undefined)
308
+ delete candidate.model;
309
+ else
310
+ candidate.model = selection.model;
311
+ if (selection.effort === undefined)
312
+ delete candidate.effort;
313
+ else
314
+ candidate.effort = selection.effort;
315
+ if (selectPermission) {
316
+ const permission = await selectAgentPermission(resolved, io, candidate.permission);
317
+ if (permission.kind === "cancelled") {
318
+ throw usageError(`${label} permission configuration was cancelled.`);
319
+ }
320
+ candidate.permission = permission.permission;
321
+ }
322
+ try {
323
+ return resolveAgentAdapter(agent.adapterId).canonicalizeConfig(candidate);
324
+ }
325
+ catch (error) {
326
+ throw usageError(error instanceof Error ? error.message : String(error));
327
+ }
328
+ }
125
329
  function availableAgentChoices(store, env) {
126
330
  const existing = new Map(store.listConfiguredAgents().map((agent) => [agent.id, agent]));
127
331
  return BUILTIN_AGENTS.flatMap((builtin) => {
@@ -136,7 +340,16 @@ function availableAgentChoices(store, env) {
136
340
  }];
137
341
  });
138
342
  }
139
- function persistAgent(store, choice, now) {
343
+ function setupSelectionIo(question, io) {
344
+ return {
345
+ interactive: true,
346
+ json: false,
347
+ width: tableWidth(io),
348
+ write: (value) => { io.output.write(value); },
349
+ question
350
+ };
351
+ }
352
+ function prepareAgent(store, choice, now) {
140
353
  const existing = store.getConfiguredAgent(choice.id);
141
354
  if (existing !== null
142
355
  && existing.adapterId === choice.adapterId
@@ -144,24 +357,49 @@ function persistAgent(store, choice, now) {
144
357
  return existing;
145
358
  }
146
359
  const agent = createConfiguredAgent(choice.id, choice.adapterId, choice.command, existing?.baseArgs ?? [], existing?.environment ?? [], now);
147
- store.saveConfiguredAgent(agent);
148
360
  return agent;
149
361
  }
150
- function ensureSystemRole(store, name, agent, workspace, now) {
151
- const definition = configuredAgentToDefinition(agent);
152
- const existing = store.getGlobalRole(name);
153
- if (existing !== null)
154
- return;
155
- store.saveGlobalRole(createGlobalRole(name, [createRoleAgentBinding(definition)], definition.id, workspace, now));
362
+ function requireSetupAgent(store, agentId) {
363
+ const agent = store.getConfiguredAgent(agentId);
364
+ if (agent === null)
365
+ throw usageError(`Configured Agent not found: ${agentId}.`);
366
+ return agent;
156
367
  }
157
- function assertSystemRoleCompatible(store, name, agent, workspace) {
368
+ function prepareSystemRole(store, name, agent, workspace, now, config, profile) {
158
369
  const existing = store.getGlobalRole(name);
159
- if (existing === null)
160
- return;
161
- if (existing.activeAgentId === agent.id && existing.workspace === workspace)
162
- return;
163
- throw usageError(`Global Role ${name} is already configured with Agent ${existing.activeAgentId} `
164
- + `and workspace ${existing.workspace}. Stop its Session and use role update before changing it.`);
370
+ if (existing !== null) {
371
+ const definition = configuredAgentToDefinition(agent);
372
+ const binding = createRoleAgentBinding(definition, config);
373
+ if (name === "operator" && !Object.hasOwn(existing.agentBindings, agent.id)) {
374
+ const sameAdapter = Object.values(existing.agentBindings).find((candidate) => candidate.adapterId === agent.adapterId);
375
+ if (sameAdapter !== undefined) {
376
+ throw usageError(`${name} already has a ${agent.adapterId} Agent: ${sameAdapter.agentId}. `
377
+ + "Update that Agent's configuration, or activate another adapter and "
378
+ + "unbind it before selecting this Agent.");
379
+ }
380
+ }
381
+ if (existing.activeAgentId === agent.id
382
+ && existing.workspace === workspace
383
+ && isDeepStrictEqual(existing.agentBindings[agent.id], binding)) {
384
+ return null;
385
+ }
386
+ assertRoleRuntimeMutationAllowed(store, {
387
+ scope: "global",
388
+ roleName: name
389
+ }, "desired launch configuration update");
390
+ return updateGlobalRole(existing, {
391
+ activeAgentId: agent.id,
392
+ workspace,
393
+ agentBindings: { ...existing.agentBindings, [agent.id]: binding },
394
+ ...(profile === undefined ? {} : profile)
395
+ }, now);
396
+ }
397
+ assertRoleRuntimeMutationAllowed(store, {
398
+ scope: "global",
399
+ roleName: name
400
+ }, "creation");
401
+ const definition = configuredAgentToDefinition(agent);
402
+ return createGlobalRole(name, [createRoleAgentBinding(definition, config)], definition.id, workspace, now, profile);
165
403
  }
166
404
  function parseAgentSetSelection(answer, candidates) {
167
405
  const value = answer.trim().toLowerCase();
@@ -197,10 +435,22 @@ function parseSingleAgentSelection(answer, agents, fallback) {
197
435
  }
198
436
  return selected.id;
199
437
  }
200
- function resolveWorkspace(value) {
438
+ function resolveWorkspace(value, home) {
201
439
  if (!isAbsolute(value))
202
- throw usageError("Operator workspace must be an absolute path.");
203
- return resolve(value);
440
+ throw usageError("Project workspace must be an absolute path.");
441
+ const requested = resolve(value);
442
+ assertWorkspaceOutsideHome(requested, resolve(home));
443
+ mkdirSync(requested, { recursive: true, mode: 0o700 });
444
+ const workspace = realpathSync(requested);
445
+ const homeRoot = realpathSync(resolve(home));
446
+ assertWorkspaceOutsideHome(workspace, homeRoot);
447
+ return workspace;
448
+ }
449
+ function assertWorkspaceOutsideHome(workspace, homeRoot) {
450
+ const fromHome = relative(homeRoot, workspace);
451
+ if (fromHome === "" || (!fromHome.startsWith("..") && !isAbsolute(fromHome))) {
452
+ throw usageError("Project workspace must be outside YUI_HOME.");
453
+ }
204
454
  }
205
455
  function commandOnPath(command, env) {
206
456
  if (command.includes("/") || command.includes("\\"))
@@ -0,0 +1,102 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { StorageCompatibilityError, loadCompatibleSnapshot } from "./migration/index.js";
4
+ import { createProductionStorageRegistry } from "./migration/productionRegistry.js";
5
+ import { FileTaskStore, validateCurrentStorageStateSnapshot } from "./taskStore.js";
6
+ import { ensureStorageSchema, inspectStorageSchema, STORAGE_SCHEMA_FILE } from "./storageSchema.js";
7
+ import { classifyHome } from "./upgrade/homeClassification.js";
8
+ import { inspectSnapshotVersionState } from "./upgrade/homeMigrationTarget.js";
9
+ import { latestStorageVersionState } from "./upgrade/recordVersions.js";
10
+ export { createProductionStorageRegistry } from "./migration/productionRegistry.js";
11
+ /**
12
+ * Initialize a brand-new Home, or open an existing Home through the same
13
+ * compatibility classification as every ordinary command. Setup is the one
14
+ * ordinary flow that is also responsible for creating the initial manifest.
15
+ */
16
+ export function initializeCompatibleFileTaskStore(home, options = {}) {
17
+ if (inspectStorageSchema(home).status === "uninitialized") {
18
+ ensureStorageSchema(home);
19
+ }
20
+ return openCompatibleFileTaskStore(home, options);
21
+ }
22
+ /**
23
+ * Open current or explicitly compatible-old storage. Compatible records are
24
+ * normalized in memory through strict old-shape validators; FileTaskStore then
25
+ * runs its one current parser and its existing writer emits only current bytes.
26
+ */
27
+ export function openCompatibleFileTaskStore(home, options = {}) {
28
+ const registry = options.registry ?? createProductionStorageRegistry();
29
+ const latest = options.latest ?? latestStorageVersionState();
30
+ const classification = classifyHome({ home, registry, latest });
31
+ switch (classification.classification.status) {
32
+ case "current":
33
+ return new FileTaskStore(home);
34
+ case "compatible-old":
35
+ return new FileTaskStore(home, {
36
+ normalizeState: (raw) => normalizeState(home, raw, registry, latest)
37
+ });
38
+ case "migration-required":
39
+ throw new StorageCompatibilityError("Storage requires an offline migration. Re-run `yui update` when active Sessions are clear.");
40
+ case "unsupported":
41
+ throw new StorageCompatibilityError(describeUnsupported(classification.classification));
42
+ }
43
+ }
44
+ /**
45
+ * Eagerly prove that a compatible-old Home reaches the strict current parser.
46
+ * Staged update/upgrade preflight uses this before any Controller or binary
47
+ * action; ordinary commands may keep the store's normal lazy-read behavior.
48
+ */
49
+ export function validateCompatibleFileTaskStore(home, options = {}) {
50
+ openCompatibleFileTaskStore(home, options).getConfig();
51
+ }
52
+ function normalizeState(home, raw, registry, latest) {
53
+ let state;
54
+ try {
55
+ state = JSON.parse(raw);
56
+ }
57
+ catch (error) {
58
+ throw new StorageCompatibilityError(`Compatible state is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
59
+ }
60
+ if (!isRecord(state)) {
61
+ throw new StorageCompatibilityError("Compatible state must be a JSON object.");
62
+ }
63
+ const schemaManifest = JSON.parse(readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8"));
64
+ if (!isRecord(schemaManifest)) {
65
+ throw new StorageCompatibilityError("Compatible schema manifest must be a JSON object.");
66
+ }
67
+ const snapshot = { schemaManifest, state };
68
+ const source = versionsOf(snapshot, latest);
69
+ const normalized = loadCompatibleSnapshot({
70
+ registry,
71
+ source,
72
+ latest,
73
+ snapshot,
74
+ inspectVersions: (candidate) => versionsOf(candidate, latest),
75
+ validateCurrent: (candidate) => {
76
+ if (candidate.state === null) {
77
+ throw new StorageCompatibilityError("Compatible state unexpectedly disappeared.");
78
+ }
79
+ validateCurrentStorageStateSnapshot(candidate.state);
80
+ }
81
+ });
82
+ return `${JSON.stringify(normalized.state)}\n`;
83
+ }
84
+ function versionsOf(snapshot, latest) {
85
+ const inspection = inspectSnapshotVersionState(snapshot, latest);
86
+ if ("corruption" in inspection) {
87
+ throw new StorageCompatibilityError(inspection.corruption.detail);
88
+ }
89
+ return inspection.source;
90
+ }
91
+ function describeUnsupported(classification) {
92
+ if (classification.verdict === "CORRUPTED") {
93
+ return `Invalid state.json: ${classification.detail}`;
94
+ }
95
+ if (classification.verdict === "NEEDS_NEW_VERSION") {
96
+ return classification.blocker.message;
97
+ }
98
+ return "Storage is unsupported by this Yui release.";
99
+ }
100
+ function isRecord(value) {
101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
102
+ }
@@ -0,0 +1,78 @@
1
+ import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
2
+ import { currentRecordVersions } from "../upgrade/recordVersions.js";
3
+ export const BASELINE_STORAGE_LAYOUT_VERSION = 6;
4
+ export const BASELINE_AGGREGATE_SCHEMA_VERSION = 16;
5
+ const BASELINE_RECORD_DESCRIPTORS = Object.freeze({
6
+ config: descriptor(1, "state.json#/config"),
7
+ configuredAgent: descriptor(2, "state.json#/configuredAgents"),
8
+ project: descriptor(2, "state.json#/projects"),
9
+ agentProfile: descriptor(2, "state.json#/agentProfiles"),
10
+ globalRole: descriptor(3, "state.json#/globalRoles"),
11
+ globalRoleSessionSet: descriptor(3, "state.json#/globalRoleSessionSets"),
12
+ storedTask: descriptor(14, "state.json#/tasks/*"),
13
+ task: descriptor(3, "state.json#/tasks/*/task"),
14
+ taskBrief: descriptor(2, "state.json#/tasks/*/brief"),
15
+ taskRole: descriptor(3, "state.json#/tasks/*/roles"),
16
+ managedWorkspace: descriptor(1, "state.json#/tasks/*/managedWorkspaces"),
17
+ taskRoleSessionSet: descriptor(4, "state.json#/tasks/*/roleSessionSets"),
18
+ workItem: descriptor(6, "state.json#/tasks/*/workItems"),
19
+ agentRun: descriptor(5, "state.json#/tasks/*/agentRuns"),
20
+ reviewRound: descriptor(2, "state.json#/tasks/*/reviewRounds"),
21
+ changeSet: descriptor(2, "state.json#/tasks/*/changeSets"),
22
+ integrationAttempt: descriptor(2, "state.json#/tasks/*/integrationAttempts"),
23
+ activeRunPointer: descriptor(1, "state.json#/tasks/*/activeRuns"),
24
+ message: descriptor(2, "state.json#/tasks/*/messages"),
25
+ inputRequest: descriptor(2, "state.json#/tasks/*/inputRequests"),
26
+ decision: descriptor(1, "state.json#/tasks/*/decisions"),
27
+ milestone: descriptor(1, "state.json#/tasks/*/milestones"),
28
+ event: descriptor(2, "state.json#/tasks/*/events"),
29
+ leaderFailure: descriptor(1, "state.json#/tasks/*/leaderFailure"),
30
+ operatorNotification: descriptor(1, "state.json#/tasks/*/operatorNotification"),
31
+ workMailbox: descriptor(1, "state.json#/mailboxes")
32
+ });
33
+ /** Frozen baseline record-family version map retained as a public delivery contract. */
34
+ export const BASELINE_RECORD_VERSIONS = Object.freeze(Object.fromEntries(Object.entries(BASELINE_RECORD_DESCRIPTORS).map(([kind, entry]) => [kind, entry.version])));
35
+ export function baselineStorageVersionState() {
36
+ return Object.freeze({
37
+ layout: BASELINE_STORAGE_LAYOUT_VERSION,
38
+ aggregate: BASELINE_AGGREGATE_SCHEMA_VERSION,
39
+ record: Object.freeze({ ...BASELINE_RECORD_DESCRIPTORS })
40
+ });
41
+ }
42
+ /** Constant spelling used by delivery-gate callers and historical tests. */
43
+ export const BASELINE_STORAGE_VERSION_STATE = baselineStorageVersionState();
44
+ /**
45
+ * Fail the delivery gate if the immutable baseline is ahead of the live schema,
46
+ * loses a record family, or changes a locator without an explicit scalar
47
+ * storage boundary. The production registry gate separately proves the full
48
+ * executable baseline-to-current transition path.
49
+ */
50
+ export function assertBaselineConsistency(current = {
51
+ layout: CURRENT_STORAGE_LAYOUT_VERSION,
52
+ aggregate: CURRENT_AGGREGATE_SCHEMA_VERSION,
53
+ record: currentRecordVersions()
54
+ }) {
55
+ if (BASELINE_STORAGE_LAYOUT_VERSION > current.layout) {
56
+ throw new Error(`Baseline layout ${BASELINE_STORAGE_LAYOUT_VERSION} exceeds current layout ${current.layout}.`);
57
+ }
58
+ if (BASELINE_AGGREGATE_SCHEMA_VERSION > current.aggregate) {
59
+ throw new Error(`Baseline aggregate ${BASELINE_AGGREGATE_SCHEMA_VERSION} exceeds current aggregate ${current.aggregate}.`);
60
+ }
61
+ const scalarVersionAdvanced = current.layout > BASELINE_STORAGE_LAYOUT_VERSION
62
+ || current.aggregate > BASELINE_AGGREGATE_SCHEMA_VERSION;
63
+ for (const [kind, baselineEntry] of Object.entries(BASELINE_RECORD_DESCRIPTORS)) {
64
+ const currentEntry = current.record[kind];
65
+ if (!Number.isSafeInteger(baselineEntry.version) || baselineEntry.version < 1) {
66
+ throw new Error(`Baseline record family '${kind}' has invalid version ${String(baselineEntry.version)}.`);
67
+ }
68
+ if (currentEntry === undefined || baselineEntry.version > currentEntry.version) {
69
+ throw new Error(`Baseline record family '${kind}' is ahead of or missing from current.`);
70
+ }
71
+ if (baselineEntry.path !== currentEntry.path && !scalarVersionAdvanced) {
72
+ throw new Error(`Baseline record family '${kind}' path drift requires a layout or aggregate version change.`);
73
+ }
74
+ }
75
+ }
76
+ function descriptor(version, path) {
77
+ return Object.freeze({ version, path });
78
+ }