@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,315 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { scanControllerResourceInventory } from "../../controller/resourceInventoryLinux.js";
4
+ import { STORAGE_STATE_FILE } from "../taskStore.js";
5
+ /** Pure blocker policy shared by synthetic tests and the real read-only scan. */
6
+ export function classifyOfflineUpgradeFacts(facts) {
7
+ const blockers = [];
8
+ for (const run of facts.runs) {
9
+ if (run.status !== "active")
10
+ continue;
11
+ blockers.push(identity(run, "active-run"));
12
+ }
13
+ for (const session of facts.sessions) {
14
+ if (session.processState === "live") {
15
+ blockers.push(identity(session, "native-session-live"));
16
+ }
17
+ else if (session.processState === "unknown") {
18
+ blockers.push(identity(session, "native-session-unknown"));
19
+ }
20
+ }
21
+ for (const inFlight of facts.inFlight) {
22
+ blockers.push(identity(inFlight, "in-flight-run"));
23
+ }
24
+ for (const completion of facts.pendingCompletions) {
25
+ blockers.push(identity(completion, "pending-completion"));
26
+ }
27
+ for (const lifecycle of facts.lifecycle) {
28
+ blockers.push(identity(lifecycle, lifecycle.reason));
29
+ }
30
+ for (const unknown of facts.unknownRuntime ?? []) {
31
+ blockers.push(identity(unknown, "native-session-unknown"));
32
+ }
33
+ return { total: blockers.length, blockers };
34
+ }
35
+ /**
36
+ * Re-read durable Run/RoleSessionSet/lifecycle facts and the native process
37
+ * inventory without mutating, stopping, acknowledging, or quarantining them.
38
+ */
39
+ export async function inspectOfflineUpgradeInventory(home, environment = process.env) {
40
+ const inventory = await scanControllerResourceInventory({
41
+ currentHome: home,
42
+ scope: "current",
43
+ environment
44
+ });
45
+ const raw = readRawFacts(home, inventory);
46
+ return classifyOfflineUpgradeFacts(raw);
47
+ }
48
+ function readRawFacts(home, inventory) {
49
+ const runs = [];
50
+ const sessions = [];
51
+ const inFlight = [];
52
+ const pendingCompletions = [];
53
+ const lifecycle = [];
54
+ const unknownRuntime = [];
55
+ const state = readStateObject(home);
56
+ // Failure to enumerate processes, panes, or sockets means absence cannot be
57
+ // proven. Ownership-load warnings are expected for an old record shape and
58
+ // are handled by the raw durable scan plus unowned live-pane check below.
59
+ if (inventory.warnings.some(isUndeterminableNativeInventoryWarning)) {
60
+ unknownRuntime.push({});
61
+ }
62
+ for (const [taskId, aggregateValue] of Object.entries(record(state.tasks))) {
63
+ const aggregate = record(aggregateValue);
64
+ for (const runValue of Object.values(record(aggregate.agentRuns))) {
65
+ const run = record(runValue);
66
+ const roleName = text(run.roleName);
67
+ const runId = text(run.id);
68
+ if (roleName === undefined || runId === undefined)
69
+ continue;
70
+ runs.push({ taskId, roleName, runId, status: text(run.status) ?? "unknown" });
71
+ }
72
+ for (const [roleName, setValue] of Object.entries(record(aggregate.roleSessionSets))) {
73
+ collectSessionSet({
74
+ taskId,
75
+ roleName,
76
+ set: record(setValue),
77
+ inventory,
78
+ sessions,
79
+ inFlight,
80
+ pendingCompletions
81
+ });
82
+ }
83
+ }
84
+ for (const [roleName, setValue] of Object.entries(record(state.globalRoleSessionSets))) {
85
+ collectSessionSet({
86
+ roleName,
87
+ set: record(setValue),
88
+ inventory,
89
+ sessions,
90
+ inFlight,
91
+ pendingCompletions
92
+ });
93
+ }
94
+ for (const mailboxValue of Object.values(record(state.mailboxes))) {
95
+ const mailbox = record(mailboxValue);
96
+ const target = record(mailbox.target);
97
+ const kind = text(target.kind);
98
+ if (kind !== "role-runtime" && kind !== "global-role-runtime")
99
+ continue;
100
+ if (mailbox.pending === null && mailbox.processing === null)
101
+ continue;
102
+ lifecycle.push({
103
+ ...(text(target.taskId) === undefined ? {} : { taskId: text(target.taskId) }),
104
+ ...(text(target.roleName) === undefined ? {} : { roleName: text(target.roleName) }),
105
+ reason: "pending-mailbox"
106
+ });
107
+ }
108
+ for (let index = 0; index < countPendingInbox(home); index += 1) {
109
+ lifecycle.push({ reason: "pending-inbox" });
110
+ }
111
+ // Any live native pane/process the durable session sets could not identify is
112
+ // still a blocker: its exact owner cannot be proven stopped.
113
+ for (const resource of inventory.resources) {
114
+ if (resource.kind !== "agent-session"
115
+ || (resource.processes.length === 0 && resource.paneDead !== false))
116
+ continue;
117
+ const owner = resource.owner;
118
+ const known = sessions.some((session) => resourceMatchesSession(resource, session));
119
+ if (known)
120
+ continue;
121
+ const pane = parseManagedPaneTarget(resource.target);
122
+ sessions.push({
123
+ ...(owner.kind === "task-role"
124
+ ? { taskId: owner.taskId }
125
+ : pane?.taskId === undefined ? {} : { taskId: pane.taskId }),
126
+ roleName: owner.kind === "task-role" || owner.kind === "global-role"
127
+ ? owner.roleName
128
+ : pane?.roleName ?? "unknown",
129
+ ...(owner.kind === "task-role" || owner.kind === "global-role"
130
+ ? {
131
+ ...(owner.nativeSessionId === undefined
132
+ ? {}
133
+ : { nativeSessionId: owner.nativeSessionId }),
134
+ ...(owner.launchId === undefined ? {} : { launchId: owner.launchId })
135
+ }
136
+ : {}),
137
+ status: "unknown",
138
+ processState: "unknown",
139
+ history: false
140
+ });
141
+ }
142
+ for (let index = 0; index < runs.length; index += 1) {
143
+ const run = runs[index];
144
+ const session = activeCurrentSession(sessions, run.taskId, run.roleName);
145
+ if (session !== undefined) {
146
+ runs[index] = {
147
+ ...run,
148
+ ...(session.nativeSessionId === undefined
149
+ ? {}
150
+ : { nativeSessionId: session.nativeSessionId }),
151
+ ...(session.launchId === undefined ? {} : { launchId: session.launchId })
152
+ };
153
+ }
154
+ }
155
+ return { runs, sessions, inFlight, pendingCompletions, lifecycle, unknownRuntime };
156
+ }
157
+ function parseManagedPaneTarget(target) {
158
+ if (target === undefined)
159
+ return undefined;
160
+ const match = /^yui-[a-f0-9]{12}-(.+):([^:]+)$/u.exec(target);
161
+ if (match === null)
162
+ return undefined;
163
+ return match[1] === "operator"
164
+ ? { roleName: match[2] }
165
+ : { taskId: match[1], roleName: match[2] };
166
+ }
167
+ function isUndeterminableNativeInventoryWarning(warning) {
168
+ return warning.startsWith("Cannot enumerate Linux processes")
169
+ || warning.startsWith("Cannot inspect tmux")
170
+ || warning.startsWith("Cannot inspect Unix sockets");
171
+ }
172
+ function collectSessionSet(options) {
173
+ const { taskId, roleName, set, inventory } = options;
174
+ const currentSessions = Object.entries(record(set.sessions));
175
+ const activeAgentId = text(set.activeAgentId);
176
+ const historyValue = set.history;
177
+ const historySessions = Array.isArray(historyValue)
178
+ ? historyValue
179
+ : Object.values(record(historyValue));
180
+ for (const { history, value, active } of [
181
+ ...currentSessions.map(([agentId, session]) => ({
182
+ history: false,
183
+ value: session,
184
+ active: activeAgentId !== undefined && agentId === activeAgentId
185
+ })),
186
+ ...historySessions.map((session) => ({ history: true, value: session, active: false }))
187
+ ]) {
188
+ const session = record(value);
189
+ const nativeSessionId = text(session.nativeSessionId);
190
+ const launchId = text(session.launchId);
191
+ const base = {
192
+ ...(taskId === undefined ? {} : { taskId }),
193
+ roleName,
194
+ ...(nativeSessionId === undefined ? {} : { nativeSessionId }),
195
+ ...(launchId === undefined ? {} : { launchId }),
196
+ status: text(session.status) ?? "unknown",
197
+ history,
198
+ active
199
+ };
200
+ options.sessions.push({
201
+ ...base,
202
+ processState: processState(inventory, base)
203
+ });
204
+ }
205
+ const flight = record(set.inFlight);
206
+ const runId = text(flight.runId);
207
+ if (taskId !== undefined && runId !== undefined) {
208
+ const session = activeCurrentSession(options.sessions, taskId, roleName);
209
+ options.inFlight.push({
210
+ taskId,
211
+ roleName,
212
+ runId,
213
+ ...(session?.nativeSessionId === undefined
214
+ ? {}
215
+ : { nativeSessionId: session.nativeSessionId }),
216
+ ...(session?.launchId === undefined ? {} : { launchId: session.launchId })
217
+ });
218
+ }
219
+ const completion = record(set.pendingTurnCompletion);
220
+ const completionTask = text(completion.taskId) ?? taskId;
221
+ if (completionTask !== undefined && Object.keys(completion).length > 0) {
222
+ const completionRole = text(completion.roleName) ?? roleName;
223
+ const completionNativeSessionId = text(completion.nativeSessionId);
224
+ const completionSession = options.sessions.find((candidate) => (candidate.taskId === completionTask
225
+ && candidate.roleName === completionRole
226
+ && completionNativeSessionId !== undefined
227
+ && candidate.nativeSessionId === completionNativeSessionId));
228
+ options.pendingCompletions.push({
229
+ taskId: completionTask,
230
+ roleName: completionRole,
231
+ ...(text(completion.runId) === undefined ? {} : { runId: text(completion.runId) }),
232
+ ...(completionNativeSessionId === undefined
233
+ ? {}
234
+ : { nativeSessionId: completionNativeSessionId }),
235
+ ...(completionSession?.launchId === undefined
236
+ ? {}
237
+ : { launchId: completionSession.launchId })
238
+ });
239
+ }
240
+ }
241
+ function activeCurrentSession(sessions, taskId, roleName) {
242
+ const current = sessions.filter((candidate) => (candidate.taskId === taskId
243
+ && candidate.roleName === roleName
244
+ && !candidate.history));
245
+ return current.find(({ active }) => active === true)
246
+ ?? (current.length === 1 ? current[0] : undefined);
247
+ }
248
+ function processState(inventory, session) {
249
+ const matching = inventory.resources.filter((resource) => (resource.kind === "agent-session" && resourceMatchesSession(resource, session)));
250
+ if (matching.some((resource) => resource.processes.length > 0))
251
+ return "live";
252
+ if (matching.some((resource) => resource.paneDead === false))
253
+ return "unknown";
254
+ return "stopped";
255
+ }
256
+ function resourceMatchesSession(resource, session) {
257
+ const owner = resource.owner;
258
+ if (owner.kind !== "task-role" && owner.kind !== "global-role")
259
+ return false;
260
+ if (session.taskId === undefined) {
261
+ if (owner.kind !== "global-role")
262
+ return false;
263
+ }
264
+ else if (owner.kind !== "task-role" || owner.taskId !== session.taskId)
265
+ return false;
266
+ if (owner.roleName !== session.roleName)
267
+ return false;
268
+ if (session.nativeSessionId !== undefined && owner.nativeSessionId !== session.nativeSessionId) {
269
+ return false;
270
+ }
271
+ if (session.launchId !== undefined && owner.launchId !== session.launchId)
272
+ return false;
273
+ return true;
274
+ }
275
+ function readStateObject(home) {
276
+ const path = join(home, STORAGE_STATE_FILE);
277
+ if (!existsSync(path))
278
+ return {};
279
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
280
+ return record(parsed);
281
+ }
282
+ function countPendingInbox(home) {
283
+ return [join(home, "runtime", "inbox"), join(home, "runtime", "inbox-invalid")]
284
+ .reduce((total, directory) => total + countDirectory(directory), 0);
285
+ }
286
+ function countDirectory(directory) {
287
+ try {
288
+ return readdirSync(directory).filter((name) => (name.endsWith(".json") || name.includes(".tmp-") || !name.startsWith("."))).length;
289
+ }
290
+ catch (error) {
291
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
292
+ return 0;
293
+ return 1;
294
+ }
295
+ }
296
+ function identity(value, reason) {
297
+ return {
298
+ ...(value.taskId === undefined ? {} : { taskId: value.taskId }),
299
+ ...(value.roleName === undefined ? {} : { roleName: value.roleName }),
300
+ ...(value.runId === undefined ? {} : { runId: value.runId }),
301
+ ...(value.nativeSessionId === undefined
302
+ ? {}
303
+ : { nativeSessionId: value.nativeSessionId }),
304
+ ...(value.launchId === undefined ? {} : { launchId: value.launchId }),
305
+ reason
306
+ };
307
+ }
308
+ function record(value) {
309
+ return typeof value === "object" && value !== null && !Array.isArray(value)
310
+ ? value
311
+ : {};
312
+ }
313
+ function text(value) {
314
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
315
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Backward-compatible import path for the one production storage registry.
3
+ * Compatible loading and offline migration share the declaration graph owned by
4
+ * `storage/migration/productionRegistry.ts`.
5
+ */
6
+ export { createProductionStorageRegistry as createProductionMigrationRegistry } from "../migration/productionRegistry.js";
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Pure, read-only structural scan of raw `state.json`, never through the strict
3
+ * `parseState`/`FileTaskStore` gate. The durable manifest remains authoritative;
4
+ * this scanner supplies member versions and counts so callers can detect
5
+ * manifest/state contradictions without treating an empty locator as current.
6
+ * Older member versions remain version evidence rather than a strict-loader
7
+ * parse failure, while malformed JSON/containers/schemaVersion fields are real
8
+ * `CORRUPTED` evidence.
9
+ *
10
+ * The `path` for each family (from `recordVersions.ts`) is a small locator into
11
+ * `state.json`: segments are `/`-separated after the `state.json#/` prefix, and a
12
+ * `*` segment fans out over every member of the map at that level (so the locator
13
+ * `state.json#/tasks/{star}/workItems` visits the `workItems` map of every task,
14
+ * where `{star}` is a literal `*`).
15
+ */
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { STORAGE_STATE_FILE } from "../taskStore.js";
19
+ /**
20
+ * Scan per-family member versions and counts from raw `state.json`.
21
+ *
22
+ * Rules, per family, evaluated purely (no strict parser):
23
+ * - family absent or empty on disk -> latest as an internal scan sentinel,
24
+ * with count zero so the manifest combiner never infers source currency;
25
+ * - every member at one older/current/newer version -> report that version;
26
+ * - members carrying different versions -> corruption.
27
+ *
28
+ * A family is one durable schema boundary, so a manifest cannot make a mixed
29
+ * member set trustworthy by matching either its minimum or maximum. Mixed
30
+ * members are partial-write/corruption evidence, not a migration source version.
31
+ *
32
+ * Returns a `corruption` instead when the JSON is unparseable, the root is not an
33
+ * object, a path container is not the shape the locator describes, or a record is
34
+ * missing/has an invalid `schemaVersion`.
35
+ */
36
+ export function scanSourceRecordVersions(home, latestRecord) {
37
+ const statePath = join(home, STORAGE_STATE_FILE);
38
+ if (!existsSync(statePath)) {
39
+ // No state.json means zero persisted members. The latest-valued scan entry
40
+ // is only a sentinel; counts preserve absence for the manifest combiner.
41
+ return {
42
+ record: cloneRecord(latestRecord),
43
+ counts: Object.fromEntries(Object.keys(latestRecord).map((kind) => [kind, 0]))
44
+ };
45
+ }
46
+ let root;
47
+ try {
48
+ root = JSON.parse(readFileSync(statePath, "utf8"));
49
+ }
50
+ catch (error) {
51
+ return corruption(`state.json is not valid JSON: ${messageOf(error)}`);
52
+ }
53
+ if (!isObject(root)) {
54
+ return corruption("state.json is not a JSON object.");
55
+ }
56
+ return scanRecordVersionsFromState(root, latestRecord);
57
+ }
58
+ /** Extract record versions from an already-parsed state snapshot, read-only. */
59
+ export function scanRecordVersionsFromState(root, latestRecord) {
60
+ if (!isObject(root))
61
+ return corruption("state snapshot is not a JSON object.");
62
+ const record = {};
63
+ const counts = {};
64
+ for (const [kind, entry] of Object.entries(latestRecord)) {
65
+ const scan = scanFamily(root, entry.path, entry.version);
66
+ if ("corruption" in scan) {
67
+ return corruption(`record family '${kind}' (${entry.path}): ${scan.corruption}`);
68
+ }
69
+ record[kind] = { version: scan.version, path: entry.path };
70
+ counts[kind] = scan.count;
71
+ }
72
+ return { record, counts };
73
+ }
74
+ /** Resolve one family's source version from `root` following `path`. */
75
+ function scanFamily(root, path, latestVersion) {
76
+ const segments = parseLocator(path);
77
+ if (segments === null)
78
+ return { corruption: `unrecognized path syntax "${path}"` };
79
+ // Resolve the path to the family's endpoint node(s). A `*` fans out over the
80
+ // members of the map at that level; an absent named key contributes nothing
81
+ // (that branch simply has no records), but a present-but-wrong-typed node is
82
+ // structural damage.
83
+ let nodes = [root];
84
+ for (const segment of segments) {
85
+ const next = [];
86
+ for (const node of nodes) {
87
+ if (segment === "*") {
88
+ if (!isObject(node))
89
+ return { corruption: `expected an object to enumerate at "*"` };
90
+ next.push(...Object.values(node));
91
+ }
92
+ else {
93
+ if (!isObject(node))
94
+ return { corruption: `expected an object at "${segment}"` };
95
+ if (!Object.hasOwn(node, segment))
96
+ continue; // absent branch: no records here
97
+ next.push(node[segment]);
98
+ }
99
+ }
100
+ nodes = next;
101
+ }
102
+ // Each endpoint is either a single record (it carries `schemaVersion`, e.g.
103
+ // tasks/* (the StoredTask aggregate), tasks/*/task, tasks/*/brief) or a map of records (its values carry
104
+ // `schemaVersion`, e.g. configuredAgents, tasks/*/workItems).
105
+ let min = Number.POSITIVE_INFINITY;
106
+ let max = Number.NEGATIVE_INFINITY;
107
+ let count = 0;
108
+ for (const endpoint of nodes) {
109
+ if (endpoint === null || endpoint === undefined)
110
+ continue; // e.g. brief: null
111
+ if (!isObject(endpoint))
112
+ return { corruption: "family endpoint is not an object" };
113
+ if (typeof endpoint.schemaVersion === "number") {
114
+ const version = readSchemaVersion(endpoint);
115
+ if (version === null)
116
+ return { corruption: `record has an invalid schemaVersion` };
117
+ min = Math.min(min, version);
118
+ max = Math.max(max, version);
119
+ count += 1;
120
+ continue;
121
+ }
122
+ for (const member of Object.values(endpoint)) {
123
+ if (member === null)
124
+ continue;
125
+ if (!isObject(member))
126
+ return { corruption: "record member is not an object" };
127
+ const version = readSchemaVersion(member);
128
+ if (version === null)
129
+ return { corruption: "record member has a missing/invalid schemaVersion" };
130
+ min = Math.min(min, version);
131
+ max = Math.max(max, version);
132
+ count += 1;
133
+ }
134
+ }
135
+ if (count === 0)
136
+ return { version: latestVersion, count }; // zero-count sentinel only
137
+ if (min !== max) {
138
+ return {
139
+ corruption: `family contains mixed schemaVersion values ${min} and ${max}`
140
+ };
141
+ }
142
+ return { version: min, count };
143
+ }
144
+ /** Parse a `state.json#/`-prefixed locator into path segments, or `null`. */
145
+ function parseLocator(path) {
146
+ const prefix = `${STORAGE_STATE_FILE}#/`;
147
+ if (!path.startsWith(prefix))
148
+ return null;
149
+ const body = path.slice(prefix.length);
150
+ if (body.length === 0)
151
+ return null;
152
+ return body.split("/");
153
+ }
154
+ /** A positive-integer `schemaVersion`, or `null` when missing/invalid. */
155
+ function readSchemaVersion(record) {
156
+ const version = record.schemaVersion;
157
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
158
+ return null;
159
+ return version;
160
+ }
161
+ function cloneRecord(record) {
162
+ const copy = {};
163
+ for (const [kind, entry] of Object.entries(record)) {
164
+ copy[kind] = { version: entry.version, path: entry.path };
165
+ }
166
+ return copy;
167
+ }
168
+ function corruption(detail) {
169
+ return { corruption: { corrupted: true, detail } };
170
+ }
171
+ function isObject(value) {
172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
173
+ }
174
+ function messageOf(error) {
175
+ return error instanceof Error ? error.message : String(error);
176
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The current-baseline record-axis version map.
3
+ *
4
+ * The three storage axes are `layout`, `aggregate`, and `record`. The layout and
5
+ * aggregate axes are scalars sourced from `storageSchema.ts`; the record axis is
6
+ * a `recordKind -> { version, path }` map because every record family versions
7
+ * independently (see the generic migration core in `../migration`).
8
+ *
9
+ * This map is a SNAPSHOT of the record families that exist on the current
10
+ * baseline, enumerated from their real `schemaVersion`s in `taskStore.ts` and the
11
+ * per-record modules. It is intentionally NOT a frozen "first release" set: as
12
+ * the schema evolves (a new record family, a bumped family version), this map is
13
+ * updated in lockstep, exactly like the scalar constants. It exists so
14
+ * doctor/inspect and the migration planner can reason about record families.
15
+ * Post-baseline record changes require an explicit adjacent registration and
16
+ * transform in the production migration graph.
17
+ */
18
+ import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
19
+ import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
20
+ import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
21
+ import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
22
+ function descriptor(version, path) {
23
+ return Object.freeze({ version, path });
24
+ }
25
+ /**
26
+ * Independent persisted-boundary contract for the direct family keys and
27
+ * locators. The upgrade descriptor table is checked against this contract so
28
+ * omitting a newly persisted family or changing a locator cannot make the
29
+ * scanner silently skip that family.
30
+ */
31
+ const EXPECTED_DIRECT_RECORD_LOCATORS = Object.freeze({
32
+ config: "state.json#/config",
33
+ configuredAgent: "state.json#/configuredAgents",
34
+ project: "state.json#/projects",
35
+ agentProfile: "state.json#/agentProfiles",
36
+ globalRole: "state.json#/globalRoles",
37
+ globalRoleSessionSet: "state.json#/globalRoleSessionSets",
38
+ storedTask: "state.json#/tasks/*",
39
+ task: "state.json#/tasks/*/task",
40
+ taskBrief: "state.json#/tasks/*/brief",
41
+ taskRole: "state.json#/tasks/*/roles",
42
+ managedWorkspace: "state.json#/tasks/*/managedWorkspaces",
43
+ taskRoleSessionSet: "state.json#/tasks/*/roleSessionSets",
44
+ workItem: "state.json#/tasks/*/workItems",
45
+ agentRun: "state.json#/tasks/*/agentRuns",
46
+ reviewRound: "state.json#/tasks/*/reviewRounds",
47
+ changeSet: "state.json#/tasks/*/changeSets",
48
+ integrationAttempt: "state.json#/tasks/*/integrationAttempts",
49
+ activeRunPointer: "state.json#/tasks/*/activeRuns",
50
+ message: "state.json#/tasks/*/messages",
51
+ inputRequest: "state.json#/tasks/*/inputRequests",
52
+ decision: "state.json#/tasks/*/decisions",
53
+ milestone: "state.json#/tasks/*/milestones",
54
+ event: "state.json#/tasks/*/events",
55
+ leaderFailure: "state.json#/tasks/*/leaderFailure",
56
+ operatorNotification: "state.json#/tasks/*/operatorNotification",
57
+ workMailbox: "state.json#/mailboxes"
58
+ });
59
+ /**
60
+ * The one authoritative descriptor table for every direct StorageState and
61
+ * StoredTask record family. `path` is a logical, human-readable location of
62
+ * where the family's records live inside the authoritative store; the generic
63
+ * engine treats it as opaque. Update this table whenever a family is added or
64
+ * its `schemaVersion` changes — it tracks the live schema, it does not freeze
65
+ * it.
66
+ *
67
+ * Lazy-initialized to avoid a circular-dependency TDZ: `recordVersions.ts`
68
+ * imports the `CURRENT_*_SCHEMA_VERSION` constants from `taskStore.ts`, which
69
+ * imports `requireStorageSchema` from `storageSchema.ts`, which (via the
70
+ * provider pattern) depends on this module. Building the descriptors lazily
71
+ * means the constants are only read at runtime, after every module has loaded.
72
+ */
73
+ let currentRecordDescriptors = null;
74
+ function getCurrentRecordDescriptors() {
75
+ if (currentRecordDescriptors === null) {
76
+ currentRecordDescriptors = Object.freeze({
77
+ config: descriptor(CURRENT_CONFIG_SCHEMA_VERSION, "state.json#/config"),
78
+ configuredAgent: descriptor(CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, "state.json#/configuredAgents"),
79
+ project: descriptor(CURRENT_PROJECT_SCHEMA_VERSION, "state.json#/projects"),
80
+ agentProfile: descriptor(CURRENT_AGENT_PROFILE_SCHEMA_VERSION, "state.json#/agentProfiles"),
81
+ globalRole: descriptor(CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, "state.json#/globalRoles"),
82
+ globalRoleSessionSet: descriptor(CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, "state.json#/globalRoleSessionSets"),
83
+ // The task aggregate is itself a versioned record family. Keep this family
84
+ // alongside (and distinct from) the nested `task` record family: an older
85
+ // aggregate wrapper must be classified on the record axis before the strict
86
+ // loader is asked to parse its nested members.
87
+ storedTask: descriptor(CURRENT_STORED_TASK_SCHEMA_VERSION, "state.json#/tasks/*"),
88
+ task: descriptor(CURRENT_TASK_SCHEMA_VERSION, "state.json#/tasks/*/task"),
89
+ taskBrief: descriptor(CURRENT_TASK_BRIEF_SCHEMA_VERSION, "state.json#/tasks/*/brief"),
90
+ taskRole: descriptor(CURRENT_TASK_ROLE_SCHEMA_VERSION, "state.json#/tasks/*/roles"),
91
+ managedWorkspace: descriptor(CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, "state.json#/tasks/*/managedWorkspaces"),
92
+ taskRoleSessionSet: descriptor(CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, "state.json#/tasks/*/roleSessionSets"),
93
+ workItem: descriptor(CURRENT_WORK_ITEM_SCHEMA_VERSION, "state.json#/tasks/*/workItems"),
94
+ agentRun: descriptor(CURRENT_AGENT_RUN_SCHEMA_VERSION, "state.json#/tasks/*/agentRuns"),
95
+ reviewRound: descriptor(CURRENT_REVIEW_ROUND_SCHEMA_VERSION, "state.json#/tasks/*/reviewRounds"),
96
+ changeSet: descriptor(CURRENT_CHANGE_SET_SCHEMA_VERSION, "state.json#/tasks/*/changeSets"),
97
+ integrationAttempt: descriptor(CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, "state.json#/tasks/*/integrationAttempts"),
98
+ activeRunPointer: descriptor(CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, "state.json#/tasks/*/activeRuns"),
99
+ message: descriptor(CURRENT_MESSAGE_SCHEMA_VERSION, "state.json#/tasks/*/messages"),
100
+ inputRequest: descriptor(CURRENT_INPUT_REQUEST_SCHEMA_VERSION, "state.json#/tasks/*/inputRequests"),
101
+ decision: descriptor(CURRENT_DECISION_SCHEMA_VERSION, "state.json#/tasks/*/decisions"),
102
+ milestone: descriptor(CURRENT_MILESTONE_SCHEMA_VERSION, "state.json#/tasks/*/milestones"),
103
+ event: descriptor(CURRENT_EVENT_SCHEMA_VERSION, "state.json#/tasks/*/events"),
104
+ leaderFailure: descriptor(CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "state.json#/tasks/*/leaderFailure"),
105
+ operatorNotification: descriptor(CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION, "state.json#/tasks/*/operatorNotification"),
106
+ workMailbox: descriptor(CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "state.json#/mailboxes")
107
+ });
108
+ }
109
+ return currentRecordDescriptors;
110
+ }
111
+ /** Kept as the public-facing name used by upgrade callers and tests. */
112
+ const CURRENT_RECORD_VERSIONS = getCurrentRecordDescriptors;
113
+ /** The record-axis map for the current baseline (a defensive shallow copy). */
114
+ export function currentRecordVersions(candidate = CURRENT_RECORD_VERSIONS()) {
115
+ assertRecordVersionDescriptors(candidate);
116
+ return { ...candidate };
117
+ }
118
+ /**
119
+ * Fail closed if a record descriptor map drifts from the one authoritative
120
+ * table. The candidate parameter is exported so the regression suite can
121
+ * exercise an unchecked-family version/path drift directly; production callers
122
+ * use the default current descriptor table.
123
+ */
124
+ export function assertRecordVersionDescriptors(candidate = CURRENT_RECORD_VERSIONS()) {
125
+ const expectedKinds = Object.keys(EXPECTED_DIRECT_RECORD_LOCATORS);
126
+ const mappedKinds = Object.keys(candidate);
127
+ const missing = expectedKinds.filter((kind) => !Object.hasOwn(candidate, kind));
128
+ const unexpected = mappedKinds.filter((kind) => !Object.hasOwn(EXPECTED_DIRECT_RECORD_LOCATORS, kind));
129
+ if (missing.length > 0 || unexpected.length > 0) {
130
+ throw new Error(`Record version map completeness drift: missing=${missing.join(",") || "none"}; `
131
+ + `unexpected=${unexpected.join(",") || "none"}.`);
132
+ }
133
+ const descriptors = getCurrentRecordDescriptors();
134
+ for (const kind of expectedKinds) {
135
+ const expected = descriptors[kind];
136
+ const expectedPath = EXPECTED_DIRECT_RECORD_LOCATORS[kind];
137
+ const mapped = candidate[kind];
138
+ if (expected === undefined
139
+ || mapped === undefined
140
+ || mapped === null
141
+ || mapped.version !== expected.version
142
+ || mapped.path !== expectedPath) {
143
+ throw new Error(`Record version map drift for ${kind}: expected=${String(expected?.version)}@${expectedPath}; `
144
+ + `actual=${String(mapped?.version)}@${String(mapped?.path)}.`);
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * The full latest-supported {@link StorageVersionState} this release understands:
150
+ * the scalar layout/aggregate versions plus the current record map. The engine
151
+ * and classifier compare a source against this to decide the four-state verdict.
152
+ */
153
+ export function latestStorageVersionState(recordDescriptors = CURRENT_RECORD_VERSIONS()) {
154
+ return {
155
+ layout: CURRENT_STORAGE_LAYOUT_VERSION,
156
+ aggregate: CURRENT_AGGREGATE_SCHEMA_VERSION,
157
+ record: currentRecordVersions(recordDescriptors)
158
+ };
159
+ }