@zq-silk/yui 0.0.0

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 (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. package/skills/yui-worker/SKILL.md +31 -0
@@ -0,0 +1,1032 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { validateConfiguredAgent } from "../agent/agent.js";
6
+ import { reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
7
+ import { validateInputRequest } from "../input/inputRequest.js";
8
+ import { validateRoleSessionSet } from "../executor/agentExecutor.js";
9
+ import { validateTaskMessage } from "../message/message.js";
10
+ import { validateAgentRun } from "../run/agentRun.js";
11
+ import { validateRepository } from "../repository/repository.js";
12
+ import { validateGlobalRole, validateTaskRole } from "../role/role.js";
13
+ import { validateTask } from "../task/task.js";
14
+ import { validateRoleWorkspace } from "../worktree/roleWorkspace.js";
15
+ import { writeTextFileAtomically } from "./durableFile.js";
16
+ import { requireStorageSchema } from "./storageSchema.js";
17
+ export const STORAGE_STATE_FILE = "state.json";
18
+ const STORAGE_LOCK_DIRECTORY = ".state.lock";
19
+ const LOCK_TIMEOUT_MS = 5_000;
20
+ const LOCK_RETRY_MS = 10;
21
+ export const COMPLETION_SHELLS = ["bash", "zsh", "fish"];
22
+ export class FileTaskStore {
23
+ rootDir;
24
+ #transaction = null;
25
+ constructor(rootDir) {
26
+ this.rootDir = rootDir;
27
+ requireStorageSchema(rootDir);
28
+ }
29
+ rootDirectory() { return this.rootDir; }
30
+ transaction(execute) {
31
+ if (this.#transaction !== null)
32
+ return synchronousResult(execute(this));
33
+ return this.#withWriteLock(() => {
34
+ const state = this.#readState();
35
+ this.#transaction = { state, baseRevision: state.revision, dirty: false };
36
+ try {
37
+ const result = synchronousResult(execute(this));
38
+ if (this.#transaction.dirty)
39
+ this.#commit(state, this.#transaction.baseRevision);
40
+ return result;
41
+ }
42
+ finally {
43
+ this.#transaction = null;
44
+ }
45
+ });
46
+ }
47
+ getConfig() { return clone(this.#state().config); }
48
+ saveConfig(config) {
49
+ const stored = versioned(config, 1, "Yui config");
50
+ validateYuiConfig(stored);
51
+ this.#mutate((state) => { state.config = stored; });
52
+ }
53
+ saveConfiguredAgent(agent) {
54
+ const stored = identified(agent, 2, "id", agent.id, "Configured Agent");
55
+ validateConfiguredAgent(stored);
56
+ this.#mutate((state) => { state.configuredAgents[stored.id] = stored; });
57
+ }
58
+ createConfiguredAgentIfAbsent(agent) {
59
+ return this.transaction((store) => {
60
+ if (store.getConfiguredAgent(agent.id) !== null)
61
+ return null;
62
+ store.saveConfiguredAgent(agent);
63
+ return clone(agent);
64
+ });
65
+ }
66
+ updateConfiguredAgent(id, patch, now) {
67
+ return this.transaction((store) => {
68
+ const existing = store.getConfiguredAgent(id);
69
+ if (existing === null)
70
+ return null;
71
+ const candidate = versioned({
72
+ ...existing,
73
+ ...clone(patch),
74
+ updatedAt: now.toISOString()
75
+ }, 2, "Configured Agent");
76
+ const unchanged = isDeepStrictEqual({ ...existing, updatedAt: candidate.updatedAt }, candidate);
77
+ if (unchanged)
78
+ return { status: "unchanged", agent: existing };
79
+ store.saveConfiguredAgent(candidate);
80
+ return { status: "updated", agent: candidate };
81
+ });
82
+ }
83
+ listConfiguredAgents() { return values(this.#state().configuredAgents, "id"); }
84
+ getConfiguredAgent(id) { return optional(this.#state().configuredAgents[id]); }
85
+ removeConfiguredAgent(id) {
86
+ return this.#remove((state) => state.configuredAgents, id);
87
+ }
88
+ nextRepositoryId() {
89
+ return this.#nextGlobalId("repository", (state) => Object.keys(state.repositories));
90
+ }
91
+ saveRepository(repository) {
92
+ const stored = identified(repository, 1, "id", repository.id, "Repository");
93
+ validateRepository(stored);
94
+ this.#mutate((state) => { state.repositories[stored.id] = stored; });
95
+ }
96
+ createRepositoryIfAbsent(repository) {
97
+ return this.transaction((store) => {
98
+ if (store.getRepository(repository.id) !== null)
99
+ return null;
100
+ if (store.listRepositories().some((entry) => (entry.name === repository.name || entry.path === repository.path)))
101
+ return null;
102
+ store.saveRepository(repository);
103
+ return clone(repository);
104
+ });
105
+ }
106
+ listRepositories() { return values(this.#state().repositories, "id"); }
107
+ getRepository(id) { return optional(this.#state().repositories[id]); }
108
+ removeRepository(id) {
109
+ return this.transaction((store) => {
110
+ if (store.listTasks().some((task) => task.repositoryId === id)) {
111
+ throw new StorageRecordError(`Repository is still used by a Task: ${id}`);
112
+ }
113
+ return this.#remove((state) => state.repositories, id);
114
+ });
115
+ }
116
+ saveGlobalRole(role) {
117
+ const stored = identified(role, 2, "name", role.name, "Global Role");
118
+ validateGlobalRole(stored);
119
+ const sessions = this.getGlobalRoleSessionSet(stored.name);
120
+ if (sessions !== null)
121
+ assertSessionsMatchRole(sessions, stored);
122
+ this.#mutate((state) => { state.globalRoles[stored.name] = stored; });
123
+ }
124
+ saveGlobalRoleWithSessionSet(role, sessions) {
125
+ const storedRole = identified(role, 2, "name", role.name, "Global Role");
126
+ validateGlobalRole(storedRole);
127
+ const storedSessions = sessions === null ? null : globalSessions(sessions);
128
+ if (storedSessions !== null)
129
+ assertSessionsMatchRole(storedSessions, storedRole);
130
+ this.transaction(() => {
131
+ this.#mutate((state) => {
132
+ state.globalRoles[storedRole.name] = storedRole;
133
+ if (storedSessions === null)
134
+ delete state.globalRoleSessionSets[storedRole.name];
135
+ else
136
+ state.globalRoleSessionSets[storedRole.name] = storedSessions;
137
+ });
138
+ });
139
+ }
140
+ createGlobalRoleIfAbsent(role) {
141
+ return this.transaction((store) => {
142
+ if (store.getGlobalRole(role.name) !== null)
143
+ return null;
144
+ store.saveGlobalRole(role);
145
+ return clone(role);
146
+ });
147
+ }
148
+ listGlobalRoles() { return values(this.#state().globalRoles, "name"); }
149
+ getGlobalRole(name) { return optional(this.#state().globalRoles[name]); }
150
+ removeGlobalRole(name) {
151
+ return this.transaction(() => {
152
+ const removed = this.#remove((state) => state.globalRoles, name);
153
+ this.#mutate((state) => { delete state.globalRoleSessionSets[name]; });
154
+ return removed;
155
+ });
156
+ }
157
+ getGlobalRoleSessionSet(name) {
158
+ return optional(this.#state().globalRoleSessionSets[name]);
159
+ }
160
+ listGlobalRoleSessionSets() {
161
+ return values(this.#state().globalRoleSessionSets, (set) => set.owner.roleName);
162
+ }
163
+ saveGlobalRoleSessionSet(sessions) {
164
+ const stored = globalSessions(sessions);
165
+ const role = this.getGlobalRole(stored.owner.roleName);
166
+ if (role === null)
167
+ throw new StorageRecordError(`Global Role not found: ${stored.owner.roleName}`);
168
+ assertSessionsMatchRole(stored, role);
169
+ this.#mutate((state) => { state.globalRoleSessionSets[stored.owner.roleName] = stored; });
170
+ }
171
+ nextTaskId() { return this.#nextGlobalId("task", (state) => Object.keys(state.tasks)); }
172
+ saveTask(task) {
173
+ const stored = validateTask(identified(task, 1, "id", task.id, "Task"));
174
+ this.#mutate((state) => {
175
+ if (stored.repositoryId !== undefined && state.repositories[stored.repositoryId] === undefined) {
176
+ throw new StorageRecordError(`Task Repository not found: ${stored.repositoryId}`);
177
+ }
178
+ const aggregate = state.tasks[stored.id] ?? emptyStoredTask(stored);
179
+ aggregate.task = stored;
180
+ state.tasks[stored.id] = aggregate;
181
+ });
182
+ }
183
+ listTasks() { return values(this.#state().tasks, (aggregate) => aggregate.task.id).map((entry) => clone(entry.task)); }
184
+ getTask(id) { return optional(this.#state().tasks[id]?.task); }
185
+ getTaskBrief(taskId) {
186
+ return optional(this.#state().tasks[taskId]?.brief ?? undefined);
187
+ }
188
+ saveTaskBrief(taskId, brief) {
189
+ const stored = storedTaskBrief(brief);
190
+ this.#requireTaskForWrite(taskId);
191
+ this.#mutate((state) => { state.tasks[taskId].brief = stored; });
192
+ }
193
+ clearTaskBrief(taskId) {
194
+ this.#requireTaskForWrite(taskId);
195
+ this.#mutate((state) => { state.tasks[taskId].brief = null; });
196
+ }
197
+ saveRole(taskId, role) {
198
+ const aggregate = this.#requireTaskForWrite(taskId);
199
+ const stored = identified(role, 2, "name", role.name, "Task Role");
200
+ if (stored.taskId !== taskId)
201
+ throw new StorageRecordError(`Task Role belongs to another Task: ${stored.taskId}`);
202
+ validateTaskRole(stored);
203
+ const sessions = this.getRoleSessionSet(taskId, stored.name);
204
+ if (sessions !== null)
205
+ assertSessionsMatchRole(sessions, stored);
206
+ this.#mutate((state) => { state.tasks[aggregate.task.id].roles[stored.name] = stored; });
207
+ }
208
+ listRoles(taskId) { return values(this.#requireTask(taskId).roles, "name"); }
209
+ getRole(taskId, name) { return optional(this.#state().tasks[taskId]?.roles[name]); }
210
+ saveTaskRoleWithSessionSet(role, sessions) {
211
+ const storedRole = identified(role, 2, "name", role.name, "Task Role");
212
+ validateTaskRole(storedRole);
213
+ const storedSessions = taskSessions(sessions);
214
+ assertSessionsMatchRole(storedSessions, storedRole);
215
+ this.transaction(() => {
216
+ this.#requireTaskForWrite(storedRole.taskId);
217
+ this.#mutate((state) => {
218
+ state.tasks[storedRole.taskId].roles[storedRole.name] = storedRole;
219
+ state.tasks[storedRole.taskId].roleSessionSets[storedRole.name] = storedSessions;
220
+ });
221
+ });
222
+ }
223
+ removeTaskRole(taskId, name) {
224
+ return this.transaction(() => {
225
+ const aggregate = this.#requireTask(taskId);
226
+ if (aggregate.roleWorkspaces[name] !== undefined) {
227
+ throw new StorageRecordError(`Task Role workspace must be cleaned before removing the Role: ${taskId}/${name}`);
228
+ }
229
+ const removed = this.#remove(() => aggregate.roles, name);
230
+ this.#mutate(() => { delete aggregate.roleSessionSets[name]; delete aggregate.activeRuns[name]; });
231
+ return removed;
232
+ });
233
+ }
234
+ saveRoleWorkspace(taskId, workspace) {
235
+ const aggregate = this.#requireTaskForWrite(taskId);
236
+ const stored = clone(workspace);
237
+ validateRoleWorkspace(stored);
238
+ if (stored.taskId !== taskId) {
239
+ throw new StorageRecordError(`RoleWorkspace belongs to another Task: ${stored.taskId}`);
240
+ }
241
+ if (aggregate.roles[stored.roleName] === undefined) {
242
+ throw new StorageRecordError(`Task Role not found: ${taskId}/${stored.roleName}`);
243
+ }
244
+ if (aggregate.task.repositoryId !== stored.repositoryId) {
245
+ throw new StorageRecordError(`RoleWorkspace Repository does not match Task: ${taskId}/${stored.roleName}`);
246
+ }
247
+ this.#mutate((state) => {
248
+ state.tasks[taskId].roleWorkspaces[stored.roleName] = stored;
249
+ });
250
+ }
251
+ listRoleWorkspaces(taskId) {
252
+ return values(this.#requireTask(taskId).roleWorkspaces, "roleName");
253
+ }
254
+ getRoleWorkspace(taskId, roleName) {
255
+ return optional(this.#state().tasks[taskId]?.roleWorkspaces[roleName]);
256
+ }
257
+ removeRoleWorkspace(taskId, roleName) {
258
+ this.#requireTaskForWrite(taskId);
259
+ return this.#remove((state) => state.tasks[taskId].roleWorkspaces, roleName);
260
+ }
261
+ getRoleSessionSet(taskId, roleName) {
262
+ return optional(this.#state().tasks[taskId]?.roleSessionSets[roleName]);
263
+ }
264
+ getTaskRoleSessionSet(taskId, roleName) {
265
+ return this.getRoleSessionSet(taskId, roleName);
266
+ }
267
+ listRoleSessionSets(taskId) {
268
+ return values(this.#requireTask(taskId).roleSessionSets, (set) => set.owner.roleName);
269
+ }
270
+ saveRoleSessionSet(sessions) {
271
+ const stored = taskSessions(sessions);
272
+ const taskId = stored.owner.taskId;
273
+ this.#requireTaskForWrite(taskId);
274
+ const role = this.getRole(taskId, stored.owner.roleName);
275
+ if (role === null)
276
+ throw new StorageRecordError(`Task Role not found: ${taskId}/${stored.owner.roleName}`);
277
+ assertSessionsMatchRole(stored, role);
278
+ this.#mutate((state) => { state.tasks[taskId].roleSessionSets[stored.owner.roleName] = stored; });
279
+ }
280
+ saveTaskRoleSessionSet(sessions) { this.saveRoleSessionSet(sessions); }
281
+ getRoleSession(taskId, roleName) {
282
+ const set = this.getRoleSessionSet(taskId, roleName);
283
+ return set === null ? null : optional(set.sessions[set.activeAgentId]);
284
+ }
285
+ nextWorkItemId(_taskId) { return this.#nextGlobalId("work-item", (state) => allKeys(state, "workItems")); }
286
+ getWorkItem(taskId, id) { return optional(this.#state().tasks[taskId]?.workItems[id]); }
287
+ findWorkItem(id) { return findUnique(this.#state(), "workItems", id, "Work item"); }
288
+ listWorkItems(taskId) { return values(this.#requireTask(taskId).workItems, "id"); }
289
+ saveWorkItem(taskId, item) {
290
+ const stored = identified(item, 1, "id", item.id, "Work item");
291
+ if (stored.taskId !== taskId)
292
+ throw new StorageRecordError(`Work item belongs to another Task: ${stored.taskId}`);
293
+ this.#requireTaskForWrite(taskId);
294
+ this.#mutate((state) => { state.tasks[taskId].workItems[stored.id] = stored; });
295
+ }
296
+ nextAgentRunId(_taskId) { return this.#nextGlobalId("agent-run", (state) => allKeys(state, "agentRuns")); }
297
+ getAgentRun(taskId, id) { return optional(this.#state().tasks[taskId]?.agentRuns[id]); }
298
+ findAgentRun(id) { return findUnique(this.#state(), "agentRuns", id, "Agent run"); }
299
+ listAgentRuns(taskId) { return values(this.#requireTask(taskId).agentRuns, "id"); }
300
+ saveAgentRun(run) {
301
+ const stored = identified(run, 1, "id", run.id, "Agent run");
302
+ validateAgentRun(stored);
303
+ this.#requireTaskForWrite(stored.taskId);
304
+ this.#mutate((state) => { state.tasks[stored.taskId].agentRuns[stored.id] = stored; });
305
+ }
306
+ getActiveAgentRun(taskId, roleName) {
307
+ const aggregate = this.#state().tasks[taskId];
308
+ const pointer = aggregate?.activeRuns[roleName];
309
+ if (aggregate === undefined || pointer === undefined)
310
+ return null;
311
+ const run = aggregate.agentRuns[pointer.runId];
312
+ if (run === undefined)
313
+ throw new StorageRecordError(`Active Agent run pointer is dangling: ${taskId}/${roleName}`);
314
+ return clone(run);
315
+ }
316
+ saveActiveAgentRun(run) {
317
+ if (run.status !== "active")
318
+ throw new StorageRecordError(`Active Agent run must have active status: ${run.id}`);
319
+ this.transaction((store) => {
320
+ const current = store.getActiveAgentRun(run.taskId, run.roleName);
321
+ if (current !== null && current.id !== run.id) {
322
+ throw new StorageRecordError(`Role already has an active Agent run: ${run.taskId}/${run.roleName}`);
323
+ }
324
+ store.saveAgentRun(run);
325
+ this.#mutate((state) => {
326
+ state.tasks[run.taskId].activeRuns[run.roleName] = { schemaVersion: 1, runId: run.id };
327
+ });
328
+ });
329
+ }
330
+ clearActiveAgentRun(taskId, roleName) {
331
+ this.#mutate((state) => { const task = state.tasks[taskId]; if (task !== undefined)
332
+ delete task.activeRuns[roleName]; });
333
+ }
334
+ nextMessageId(_taskId) { return this.#nextGlobalId("message", (state) => allKeys(state, "messages")); }
335
+ saveMessage(taskId, message) {
336
+ validateTaskMessage(message);
337
+ this.#saveTaskRecord(taskId, "messages", message, "Message");
338
+ }
339
+ listMessages(taskId) { return values(this.#requireTask(taskId).messages, "id"); }
340
+ nextInputRequestId(_taskId) {
341
+ return this.#nextGlobalId("input", (state) => allKeys(state, "inputRequests"));
342
+ }
343
+ saveInputRequest(taskId, request) {
344
+ const stored = validateInputRequest(request);
345
+ if (stored.taskId !== taskId) {
346
+ throw new StorageRecordError(`Input request belongs to another Task: ${stored.taskId}`);
347
+ }
348
+ this.#mutate((state) => {
349
+ const aggregate = requireTaskFromState(state, taskId);
350
+ const owner = taskRecordOwner(state, "inputRequests", stored.id);
351
+ if (owner !== null && owner !== taskId) {
352
+ throw new StorageRecordError(`Input request already exists in another Task: ${stored.id}`);
353
+ }
354
+ const existing = aggregate.inputRequests[stored.id];
355
+ if (existing === undefined && stored.status !== "open") {
356
+ throw new StorageRecordError(`Input request must start open: ${stored.id}`);
357
+ }
358
+ if (existing !== undefined && !isValidInputRequestTransition(existing, stored)) {
359
+ throw new StorageRecordError(`Input request cannot be overwritten: ${stored.id}`);
360
+ }
361
+ aggregate.inputRequests[stored.id] = stored;
362
+ });
363
+ }
364
+ getInputRequest(taskId, requestId) {
365
+ return optional(this.#state().tasks[taskId]?.inputRequests[requestId]);
366
+ }
367
+ findInputRequest(requestId) {
368
+ return findUnique(this.#state(), "inputRequests", requestId, "Input request");
369
+ }
370
+ listInputRequests(taskId) {
371
+ return values(this.#requireTask(taskId).inputRequests, "id");
372
+ }
373
+ listAllInputRequests() {
374
+ return Object.values(this.#state().tasks)
375
+ .flatMap((aggregate) => Object.values(aggregate.inputRequests).map(clone))
376
+ .sort((left, right) => numericCompare(left.id, right.id));
377
+ }
378
+ nextDecisionId(_taskId) {
379
+ return this.#nextGlobalId("decision", (state) => allKeys(state, "decisions"));
380
+ }
381
+ saveDecision(taskId, decision) {
382
+ const stored = storedDecision(decision);
383
+ if (stored.taskId !== taskId) {
384
+ throw new StorageRecordError(`Decision belongs to another Task: ${stored.taskId}`);
385
+ }
386
+ this.#mutate((state) => {
387
+ const aggregate = requireTaskFromState(state, taskId);
388
+ const owner = taskRecordOwner(state, "decisions", stored.id);
389
+ if (owner !== null && owner !== taskId) {
390
+ throw new StorageRecordError(`Decision already exists in another Task: ${stored.id}`);
391
+ }
392
+ const existing = aggregate.decisions[stored.id];
393
+ if (existing === undefined && stored.status !== "active") {
394
+ throw new StorageRecordError(`Decision must start active: ${stored.id}`);
395
+ }
396
+ if (existing !== undefined && !isValidDecisionSupersession(existing, stored)) {
397
+ throw new StorageRecordError(`Decision cannot be overwritten: ${stored.id}`);
398
+ }
399
+ aggregate.decisions[stored.id] = stored;
400
+ });
401
+ }
402
+ listDecisions(taskId) {
403
+ return values(this.#requireTask(taskId).decisions, "id");
404
+ }
405
+ getDecision(taskId, decisionId) {
406
+ return this.#requireTask(taskId).decisions[decisionId] ?? null;
407
+ }
408
+ nextMilestoneId(_taskId) {
409
+ return this.#nextGlobalId("milestone", (state) => allKeys(state, "milestones"));
410
+ }
411
+ saveMilestone(taskId, milestone) {
412
+ const stored = storedMilestone(milestone);
413
+ if (stored.taskId !== taskId) {
414
+ throw new StorageRecordError(`Milestone belongs to another Task: ${stored.taskId}`);
415
+ }
416
+ this.#mutate((state) => {
417
+ const aggregate = requireTaskFromState(state, taskId);
418
+ if (taskRecordOwner(state, "milestones", stored.id) !== null) {
419
+ throw new StorageRecordError(`Milestone already exists: ${stored.id}`);
420
+ }
421
+ aggregate.milestones[stored.id] = stored;
422
+ });
423
+ }
424
+ listMilestones(taskId) {
425
+ return values(this.#requireTask(taskId).milestones, "id");
426
+ }
427
+ getMilestone(taskId, milestoneId) {
428
+ return this.#requireTask(taskId).milestones[milestoneId] ?? null;
429
+ }
430
+ nextEventId(_taskId) { return this.#nextGlobalId("event", (state) => allKeys(state, "events")); }
431
+ saveEvent(taskId, event) {
432
+ const stored = storedTaskEvent(event);
433
+ this.#mutate((state) => {
434
+ const aggregate = requireTaskFromState(state, taskId);
435
+ if (taskRecordOwner(state, "events", stored.id) !== null) {
436
+ throw new StorageRecordError(`Task event already exists: ${stored.id}`);
437
+ }
438
+ aggregate.events[stored.id] = stored;
439
+ });
440
+ }
441
+ listEvents(taskId) { return values(this.#requireTask(taskId).events, "id"); }
442
+ getPendingWakeup(taskId) { return optional(this.#state().tasks[taskId]?.pendingWakeup ?? undefined); }
443
+ listPendingWakeups() {
444
+ return Object.values(this.#state().tasks).flatMap((task) => task.pendingWakeup === null ? [] : [clone(task.pendingWakeup)]).sort((a, b) => numericCompare(a.taskId, b.taskId));
445
+ }
446
+ savePendingWakeup(value) { this.#saveSingleton(value.taskId, "pendingWakeup", value, "Pending wakeup"); }
447
+ clearPendingWakeup(taskId) { this.#clearSingleton(taskId, "pendingWakeup"); }
448
+ getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
449
+ saveLeaderFailure(value) { this.#saveSingleton(value.taskId, "leaderFailure", value, "Leader failure"); }
450
+ clearLeaderFailure(taskId) { this.#clearSingleton(taskId, "leaderFailure"); }
451
+ getOperatorNotification(taskId) { return optional(this.#state().tasks[taskId]?.operatorNotification ?? undefined); }
452
+ saveOperatorNotification(value) { this.#saveSingleton(value.taskId, "operatorNotification", value, "Operator notification"); }
453
+ clearOperatorNotification(taskId) { this.#clearSingleton(taskId, "operatorNotification"); }
454
+ #saveSingleton(taskId, key, value, label) {
455
+ const stored = identified(value, 1, "taskId", taskId, label);
456
+ this.#requireTaskForWrite(taskId);
457
+ this.#mutate((state) => { state.tasks[taskId][key] = stored; });
458
+ }
459
+ #clearSingleton(key, field) {
460
+ this.#mutate((state) => { if (state.tasks[key] !== undefined)
461
+ state.tasks[key][field] = null; });
462
+ }
463
+ #saveTaskRecord(taskId, key, value, label) {
464
+ const record = versioned(value, 1, label);
465
+ this.#requireTaskForWrite(taskId);
466
+ this.#mutate((state) => {
467
+ if (taskRecordOwner(state, key, record.id) !== null) {
468
+ throw new StorageRecordError(`${label} already exists: ${record.id}`);
469
+ }
470
+ state.tasks[taskId][key][record.id] = clone(value);
471
+ });
472
+ }
473
+ #requireTask(taskId) {
474
+ const aggregate = this.#state().tasks[taskId];
475
+ if (aggregate === undefined)
476
+ throw new StorageRecordError(`Task not found: ${taskId}`);
477
+ return aggregate;
478
+ }
479
+ #requireTaskForWrite(taskId) { return this.#requireTask(taskId); }
480
+ #state() { return this.#transaction?.state ?? this.#readState(); }
481
+ #mutate(execute) { this.#mutateResult((state) => { execute(state); }); }
482
+ #mutateResult(execute) {
483
+ if (this.#transaction !== null) {
484
+ const result = execute(this.#transaction.state);
485
+ this.#transaction.dirty = true;
486
+ return result;
487
+ }
488
+ return this.#withWriteLock(() => {
489
+ const state = this.#readState();
490
+ const result = execute(state);
491
+ this.#commit(state, state.revision);
492
+ return result;
493
+ });
494
+ }
495
+ #remove(select, id) {
496
+ return this.#mutateResult((state) => {
497
+ const records = select(state);
498
+ if (records[id] === undefined)
499
+ return false;
500
+ delete records[id];
501
+ return true;
502
+ });
503
+ }
504
+ #nextGlobalId(prefix, collect) {
505
+ const pattern = new RegExp(`^${prefix}-(\\d+)$`);
506
+ const maximum = collect(this.#state()).reduce((max, id) => {
507
+ const match = pattern.exec(id);
508
+ return match === null ? max : Math.max(max, Number.parseInt(match[1], 10));
509
+ }, 0);
510
+ return `${prefix}-${maximum + 1}`;
511
+ }
512
+ #readState() {
513
+ requireStorageSchema(this.rootDir);
514
+ const path = join(this.rootDir, STORAGE_STATE_FILE);
515
+ if (!existsSync(path))
516
+ return emptyState();
517
+ return parseState(readFileSync(path, "utf8"));
518
+ }
519
+ #commit(state, expectedRevision) {
520
+ const current = this.#readState();
521
+ if (current.revision !== expectedRevision) {
522
+ throw new StorageConflictError(`Storage changed concurrently (expected revision ${expectedRevision}, found ${current.revision}).`);
523
+ }
524
+ state.revision = expectedRevision + 1;
525
+ const content = `${JSON.stringify(state, null, 2)}\n`;
526
+ parseState(content);
527
+ writeTextFileAtomically(join(this.rootDir, STORAGE_STATE_FILE), content);
528
+ }
529
+ #withWriteLock(execute) {
530
+ const release = acquireStorageLock(this.rootDir);
531
+ try {
532
+ return execute();
533
+ }
534
+ finally {
535
+ release();
536
+ }
537
+ }
538
+ }
539
+ export class StorageRecordError extends Error {
540
+ constructor(message) { super(message); this.name = "StorageRecordError"; }
541
+ }
542
+ export class StorageConflictError extends Error {
543
+ constructor(message) { super(message); this.name = "StorageConflictError"; }
544
+ }
545
+ export function resolveYuiHome(env) {
546
+ return env.YUI_HOME === undefined || env.YUI_HOME.length === 0
547
+ ? join(homedir(), ".yui")
548
+ : resolve(env.YUI_HOME);
549
+ }
550
+ export function ensureYuiHome(rootDir) { mkdirSync(rootDir, { recursive: true, mode: 0o700 }); }
551
+ function emptyState() {
552
+ return { schemaVersion: 1, revision: 0, config: { schemaVersion: 1 }, configuredAgents: {}, repositories: {}, globalRoles: {}, globalRoleSessionSets: {}, tasks: {} };
553
+ }
554
+ function emptyStoredTask(task) {
555
+ return {
556
+ schemaVersion: 1,
557
+ task,
558
+ brief: null,
559
+ roles: {},
560
+ roleWorkspaces: {},
561
+ roleSessionSets: {},
562
+ workItems: {},
563
+ agentRuns: {},
564
+ activeRuns: {},
565
+ messages: {},
566
+ inputRequests: {},
567
+ decisions: {},
568
+ milestones: {},
569
+ events: {},
570
+ pendingWakeup: null,
571
+ leaderFailure: null,
572
+ operatorNotification: null
573
+ };
574
+ }
575
+ function parseState(raw) {
576
+ let parsed;
577
+ try {
578
+ parsed = JSON.parse(raw);
579
+ }
580
+ catch (error) {
581
+ throw new StorageRecordError(`Invalid ${STORAGE_STATE_FILE}: ${error instanceof Error ? error.message : String(error)}`);
582
+ }
583
+ const state = object(parsed, "Storage state");
584
+ exact(state, ["schemaVersion", "revision", "config", "configuredAgents", "repositories", "globalRoles", "globalRoleSessionSets", "tasks"], "Storage state");
585
+ if (state.schemaVersion !== 1 || !Number.isInteger(state.revision) || state.revision < 0)
586
+ throw new StorageRecordError("Storage state schemaVersion/revision is invalid.");
587
+ const result = clone(state);
588
+ result.config = versioned(result.config, 1, "Yui config");
589
+ validateYuiConfig(result.config);
590
+ parseMap(result.configuredAgents, (value, key) => {
591
+ const agent = identified(value, 2, "id", key, "Configured Agent");
592
+ validateConfiguredAgent(agent);
593
+ return agent;
594
+ }, "configuredAgents");
595
+ parseMap(result.repositories, (value, key) => {
596
+ const repository = identified(value, 1, "id", key, "Repository");
597
+ validateRepository(repository);
598
+ return repository;
599
+ }, "repositories");
600
+ parseMap(result.globalRoles, (value, key) => {
601
+ const role = identified(value, 2, "name", key, "Global Role");
602
+ validateGlobalRole(role);
603
+ return role;
604
+ }, "globalRoles");
605
+ parseMap(result.globalRoleSessionSets, (value, key) => { const set = globalSessions(value); if (set.owner.roleName !== key)
606
+ throw new StorageRecordError(`Global Role session set identity is inconsistent: ${key}`); return set; }, "globalRoleSessionSets");
607
+ parseMap(result.tasks, (value, key) => parseStoredTask(value, key), "tasks");
608
+ for (const [name, role] of Object.entries(result.globalRoles)) {
609
+ const sessions = result.globalRoleSessionSets[name];
610
+ if (sessions !== undefined)
611
+ assertSessionsMatchRole(sessions, role);
612
+ }
613
+ for (const name of Object.keys(result.globalRoleSessionSets)) {
614
+ if (result.globalRoles[name] === undefined) {
615
+ throw new StorageRecordError(`Global Role session set has no Role: ${name}`);
616
+ }
617
+ }
618
+ for (const aggregate of Object.values(result.tasks)) {
619
+ if (aggregate.task.repositoryId !== undefined
620
+ && result.repositories[aggregate.task.repositoryId] === undefined) {
621
+ throw new StorageRecordError(`Task Repository not found: ${aggregate.task.id}/${aggregate.task.repositoryId}`);
622
+ }
623
+ for (const [name, role] of Object.entries(aggregate.roles)) {
624
+ const sessions = aggregate.roleSessionSets[name];
625
+ if (sessions !== undefined)
626
+ assertSessionsMatchRole(sessions, role);
627
+ const workspace = aggregate.roleWorkspaces[name];
628
+ if (workspace !== undefined && workspace.path !== role.workspace) {
629
+ throw new StorageRecordError(`Task Role workspace path is inconsistent: ${aggregate.task.id}/${name}`);
630
+ }
631
+ }
632
+ for (const name of Object.keys(aggregate.roleSessionSets)) {
633
+ if (aggregate.roles[name] === undefined) {
634
+ throw new StorageRecordError(`Task Role session set has no Role: ${aggregate.task.id}/${name}`);
635
+ }
636
+ }
637
+ for (const [name, workspace] of Object.entries(aggregate.roleWorkspaces)) {
638
+ if (aggregate.roles[name] === undefined) {
639
+ throw new StorageRecordError(`RoleWorkspace has no Task Role: ${aggregate.task.id}/${name}`);
640
+ }
641
+ if (aggregate.task.repositoryId !== workspace.repositoryId) {
642
+ throw new StorageRecordError(`RoleWorkspace Repository does not match Task: ${aggregate.task.id}/${name}`);
643
+ }
644
+ }
645
+ }
646
+ assertGloballyUniqueTaskRecordIds(result, "inputRequests", "Input request");
647
+ assertGloballyUniqueTaskRecordIds(result, "decisions", "Decision");
648
+ assertGloballyUniqueTaskRecordIds(result, "milestones", "Milestone");
649
+ assertGloballyUniqueTaskRecordIds(result, "events", "Task event");
650
+ assertGloballyUniqueTaskRecordIds(result, "messages", "Message");
651
+ return result;
652
+ }
653
+ function parseStoredTask(value, taskId) {
654
+ const aggregate = object(value, `Task aggregate ${taskId}`);
655
+ exact(aggregate, ["schemaVersion", "task", "brief", "roles", "roleWorkspaces", "roleSessionSets", "workItems", "agentRuns", "activeRuns", "messages", "inputRequests", "decisions", "milestones", "events", "pendingWakeup", "leaderFailure", "operatorNotification"], `Task aggregate ${taskId}`);
656
+ versioned(aggregate, 1, `Task aggregate ${taskId}`);
657
+ validateTask(identified(aggregate.task, 1, "id", taskId, "Task"));
658
+ if (aggregate.brief !== null)
659
+ storedTaskBrief(aggregate.brief);
660
+ parseMap(aggregate.roles, (record, key) => { const role = identified(record, 2, "name", key, "Task Role"); if (role.taskId !== taskId)
661
+ throw new StorageRecordError(`Task Role belongs to another Task: ${role.taskId}`); validateTaskRole(role); return role; }, "roles");
662
+ parseMap(aggregate.roleWorkspaces, (record, key) => {
663
+ const workspace = identified(record, 1, "roleName", key, "RoleWorkspace");
664
+ if (workspace.taskId !== taskId) {
665
+ throw new StorageRecordError(`RoleWorkspace belongs to another Task: ${workspace.taskId}`);
666
+ }
667
+ validateRoleWorkspace(workspace);
668
+ return workspace;
669
+ }, "roleWorkspaces");
670
+ parseMap(aggregate.roleSessionSets, (record, key) => { const set = taskSessions(record); if (set.owner.taskId !== taskId || set.owner.roleName !== key)
671
+ throw new StorageRecordError(`Task Role session set identity is inconsistent: ${taskId}/${key}`); return set; }, "roleSessionSets");
672
+ parseMap(aggregate.workItems, (record, key) => { const item = identified(record, 1, "id", key, "Work item"); if (item.taskId !== taskId)
673
+ throw new StorageRecordError(`Work item belongs to another Task: ${item.taskId}`); return item; }, "workItems");
674
+ parseMap(aggregate.agentRuns, (record, key) => { const run = identified(record, 1, "id", key, "Agent run"); if (run.taskId !== taskId)
675
+ throw new StorageRecordError(`Agent run belongs to another Task: ${run.taskId}`); validateAgentRun(run); return run; }, "agentRuns");
676
+ parseMap(aggregate.activeRuns, (record, key) => {
677
+ const pointer = versioned(record, 1, `Active run ${key}`);
678
+ const run = typeof pointer.runId === "string" ? aggregate.agentRuns[pointer.runId] : undefined;
679
+ if (run === undefined || run.status !== "active" || run.roleName !== key) {
680
+ throw new StorageRecordError(`Active run pointer is invalid: ${taskId}/${key}`);
681
+ }
682
+ return pointer;
683
+ }, "activeRuns");
684
+ parseMap(aggregate.messages, (record, key) => {
685
+ const message = identified(record, 1, "id", key, "Message");
686
+ validateTaskMessage(message);
687
+ return message;
688
+ }, "messages");
689
+ parseMap(aggregate.inputRequests, (record, key) => {
690
+ const request = validateInputRequest(record);
691
+ if (request.id !== key) {
692
+ throw new StorageRecordError(`Input request identity is inconsistent: ${key}.`);
693
+ }
694
+ if (request.taskId !== taskId) {
695
+ throw new StorageRecordError(`Input request belongs to another Task: ${request.taskId}`);
696
+ }
697
+ return request;
698
+ }, "inputRequests");
699
+ parseMap(aggregate.decisions, (record, key) => {
700
+ const decision = storedDecision(record);
701
+ if (decision.id !== key)
702
+ throw new StorageRecordError(`Decision identity is inconsistent: ${key}.`);
703
+ if (decision.taskId !== taskId) {
704
+ throw new StorageRecordError(`Decision belongs to another Task: ${decision.taskId}`);
705
+ }
706
+ return decision;
707
+ }, "decisions");
708
+ parseMap(aggregate.milestones, (record, key) => {
709
+ const milestone = storedMilestone(record);
710
+ if (milestone.id !== key)
711
+ throw new StorageRecordError(`Milestone identity is inconsistent: ${key}.`);
712
+ if (milestone.taskId !== taskId) {
713
+ throw new StorageRecordError(`Milestone belongs to another Task: ${milestone.taskId}`);
714
+ }
715
+ return milestone;
716
+ }, "milestones");
717
+ parseMap(aggregate.events, (record, key) => {
718
+ const event = storedTaskEvent(record);
719
+ if (event.id !== key)
720
+ throw new StorageRecordError(`Task event identity is inconsistent: ${key}.`);
721
+ return event;
722
+ }, "events");
723
+ for (const [key, label] of [["pendingWakeup", "Pending wakeup"], ["leaderFailure", "Leader failure"], ["operatorNotification", "Operator notification"]]) {
724
+ const record = aggregate[key];
725
+ if (record !== null)
726
+ identified(record, 1, "taskId", taskId, label);
727
+ }
728
+ return aggregate;
729
+ }
730
+ function validateYuiConfig(config) {
731
+ try {
732
+ reconciliationIntervalMilliseconds(config.reconciliationIntervalSeconds);
733
+ }
734
+ catch (error) {
735
+ throw new StorageRecordError(error instanceof Error ? error.message : "Yui reconciliation interval is invalid.");
736
+ }
737
+ }
738
+ function globalSessions(value) {
739
+ const set = versioned(value, 1, "Global Role session set");
740
+ if (set.owner?.scope !== "global" || typeof set.owner.roleName !== "string")
741
+ throw new StorageRecordError("Global Role session owner is invalid.");
742
+ validateSessions(set.sessions);
743
+ validateRoleSessionSet(set);
744
+ return set;
745
+ }
746
+ function taskSessions(value) {
747
+ const set = versioned(value, 1, "Task Role session set");
748
+ if (set.owner?.scope !== "task" || typeof set.owner.taskId !== "string" || typeof set.owner.roleName !== "string")
749
+ throw new StorageRecordError("Task Role session owner is invalid.");
750
+ validateSessions(set.sessions);
751
+ validateRoleSessionSet(set);
752
+ return set;
753
+ }
754
+ function validateSessions(sessions) {
755
+ parseMap(sessions, (record, key) => identified(record, 1, "agentId", key, "Role Agent session"), "sessions");
756
+ }
757
+ function assertSessionsMatchRole(sessions, role) {
758
+ validateRoleSessionSet(sessions);
759
+ if (sessions.owner.roleName !== role.name || sessions.activeAgentId !== role.activeAgentId) {
760
+ throw new StorageRecordError(`Role session set does not match Role: ${role.name}`);
761
+ }
762
+ if ("taskId" in role && (sessions.owner.scope !== "task" || sessions.owner.taskId !== role.taskId)) {
763
+ throw new StorageRecordError(`Task Role session owner is inconsistent: ${role.taskId}/${role.name}`);
764
+ }
765
+ if (!("taskId" in role) && sessions.owner.scope !== "global") {
766
+ throw new StorageRecordError(`Global Role session owner is inconsistent: ${role.name}`);
767
+ }
768
+ for (const [agentId, session] of Object.entries(sessions.sessions)) {
769
+ const binding = role.agentBindings[agentId];
770
+ if (binding === undefined || binding.adapterId !== session.adapterId) {
771
+ throw new StorageRecordError(`Role Agent session has no matching binding: ${role.name}/${agentId}`);
772
+ }
773
+ }
774
+ }
775
+ function storedTaskBrief(value) {
776
+ const brief = versioned(value, 1, "Task Brief");
777
+ exact(brief, ["schemaVersion", "objective", "boundaries", "currentFocus", "leaderSummary", "updatedAt", "updatedBy"], "Task Brief");
778
+ requireNormalizedText(brief.objective, "Task Brief objective");
779
+ requireNormalizedText(brief.currentFocus, "Task Brief current focus");
780
+ requireNormalizedText(brief.leaderSummary, "Task Brief leader summary");
781
+ requireNormalizedText(brief.updatedBy, "Task Brief updatedBy");
782
+ if (!Array.isArray(brief.boundaries)) {
783
+ throw new StorageRecordError("Task Brief boundaries must be an array.");
784
+ }
785
+ const boundaries = brief.boundaries.map((boundary) => (requireNormalizedText(boundary, "Task Brief boundary")));
786
+ if (new Set(boundaries).size !== boundaries.length) {
787
+ throw new StorageRecordError("Task Brief boundaries must be unique.");
788
+ }
789
+ requireTimestamp(brief.updatedAt, "Task Brief updatedAt");
790
+ return brief;
791
+ }
792
+ function storedDecision(value) {
793
+ const decision = versioned(value, 1, "Decision");
794
+ const baseFields = [
795
+ "schemaVersion", "id", "taskId", "title", "rationale", "status", "createdAt", "updatedAt"
796
+ ];
797
+ if (decision.status === "active") {
798
+ exact(decision, baseFields, "Decision");
799
+ }
800
+ else if (decision.status === "superseded") {
801
+ exact(decision, [...baseFields, "supersededReason", "supersededAt"], "Decision");
802
+ requireNormalizedText(decision.supersededReason, "Decision supersededReason");
803
+ requireTimestamp(decision.supersededAt, "Decision supersededAt");
804
+ if (decision.supersededAt !== decision.updatedAt) {
805
+ throw new StorageRecordError("Decision supersededAt must match updatedAt.");
806
+ }
807
+ }
808
+ else {
809
+ throw new StorageRecordError(`Decision status is invalid: ${String(decision.status)}.`);
810
+ }
811
+ requireRecordIdentity(decision.id, "Decision id");
812
+ requireRecordIdentity(decision.taskId, "Decision Task id");
813
+ requireNormalizedText(decision.title, "Decision title");
814
+ requireNormalizedText(decision.rationale, "Decision rationale");
815
+ requireTimestamp(decision.createdAt, "Decision createdAt");
816
+ requireTimestamp(decision.updatedAt, "Decision updatedAt");
817
+ if (Date.parse(decision.updatedAt) < Date.parse(decision.createdAt)) {
818
+ throw new StorageRecordError("Decision updatedAt cannot precede createdAt.");
819
+ }
820
+ return decision;
821
+ }
822
+ function storedMilestone(value) {
823
+ const milestone = versioned(value, 1, "Milestone");
824
+ exact(milestone, ["schemaVersion", "id", "taskId", "title", "summary", "createdBy", "createdAt"], "Milestone");
825
+ requireRecordIdentity(milestone.id, "Milestone id");
826
+ requireRecordIdentity(milestone.taskId, "Milestone Task id");
827
+ requireNormalizedText(milestone.title, "Milestone title");
828
+ requireNormalizedText(milestone.summary, "Milestone summary");
829
+ if (milestone.createdBy !== "leader") {
830
+ throw new StorageRecordError("Milestone createdBy must be leader.");
831
+ }
832
+ requireTimestamp(milestone.createdAt, "Milestone createdAt");
833
+ return milestone;
834
+ }
835
+ function storedTaskEvent(value) {
836
+ const event = versioned(value, 1, "Task event");
837
+ exact(event, ["schemaVersion", "id", "type", "payload", "createdAt"], "Task event");
838
+ requireRecordIdentity(event.id, "Task event id");
839
+ requireNormalizedText(event.type, "Task event type");
840
+ const payload = object(event.payload, "Task event payload");
841
+ for (const [key, payloadValue] of Object.entries(payload)) {
842
+ requireRecordIdentity(key, "Task event payload key");
843
+ if (typeof payloadValue !== "string" || payloadValue.includes("\0")) {
844
+ throw new StorageRecordError(`Task event payload value is invalid: ${key}.`);
845
+ }
846
+ }
847
+ requireTimestamp(event.createdAt, "Task event createdAt");
848
+ return event;
849
+ }
850
+ function isValidDecisionSupersession(existing, candidate) {
851
+ return existing.status === "active"
852
+ && candidate.status === "superseded"
853
+ && candidate.id === existing.id
854
+ && candidate.taskId === existing.taskId
855
+ && candidate.title === existing.title
856
+ && candidate.rationale === existing.rationale
857
+ && candidate.createdAt === existing.createdAt
858
+ && Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt);
859
+ }
860
+ function isValidInputRequestTransition(existing, candidate) {
861
+ if (existing.status !== "open" || candidate.status === "open")
862
+ return false;
863
+ return candidate.id === existing.id
864
+ && candidate.taskId === existing.taskId
865
+ && isDeepStrictEqual(candidate.requester, existing.requester)
866
+ && candidate.question === existing.question
867
+ && isDeepStrictEqual(candidate.choices, existing.choices)
868
+ && isDeepStrictEqual(candidate.blockedRefs, existing.blockedRefs)
869
+ && isDeepStrictEqual(candidate.policy, existing.policy)
870
+ && candidate.createdAt === existing.createdAt
871
+ && Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt);
872
+ }
873
+ function requireTaskFromState(state, taskId) {
874
+ const aggregate = state.tasks[taskId];
875
+ if (aggregate === undefined)
876
+ throw new StorageRecordError(`Task not found: ${taskId}`);
877
+ return aggregate;
878
+ }
879
+ function taskRecordOwner(state, key, id) {
880
+ for (const [taskId, aggregate] of Object.entries(state.tasks)) {
881
+ if (aggregate[key][id] !== undefined)
882
+ return taskId;
883
+ }
884
+ return null;
885
+ }
886
+ function assertGloballyUniqueTaskRecordIds(state, key, label) {
887
+ const owners = new Map();
888
+ for (const [taskId, aggregate] of Object.entries(state.tasks)) {
889
+ for (const id of Object.keys(aggregate[key])) {
890
+ const owner = owners.get(id);
891
+ if (owner !== undefined) {
892
+ throw new StorageRecordError(`${label} id is duplicated across Tasks: ${id} (${owner}, ${taskId}).`);
893
+ }
894
+ owners.set(id, taskId);
895
+ }
896
+ }
897
+ }
898
+ function requireRecordIdentity(value, label) {
899
+ const normalized = requireNormalizedText(value, label);
900
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
901
+ || /[\/\\\0]/.test(normalized)) {
902
+ throw new StorageRecordError(`${label} is invalid.`);
903
+ }
904
+ return normalized;
905
+ }
906
+ function requireNormalizedText(value, label) {
907
+ if (typeof value !== "string" || value.includes("\0")) {
908
+ throw new StorageRecordError(`${label} is invalid.`);
909
+ }
910
+ const normalized = value.trim();
911
+ if (normalized.length === 0)
912
+ throw new StorageRecordError(`${label} is required.`);
913
+ if (normalized !== value)
914
+ throw new StorageRecordError(`${label} must be normalized.`);
915
+ return normalized;
916
+ }
917
+ function requireTimestamp(value, label) {
918
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
919
+ throw new StorageRecordError(`${label} is invalid.`);
920
+ }
921
+ return value;
922
+ }
923
+ function parseMap(value, parse, label) {
924
+ const records = object(value, label);
925
+ for (const [key, entry] of Object.entries(records))
926
+ parse(entry, key);
927
+ }
928
+ function versioned(value, schemaVersion, label) {
929
+ assertJsonValue(value, label);
930
+ const record = object(value, label);
931
+ if (record.schemaVersion !== schemaVersion)
932
+ throw new StorageRecordError(`${label} must use schemaVersion ${schemaVersion}.`);
933
+ return clone(record);
934
+ }
935
+ function identified(value, schemaVersion, key, expected, label) {
936
+ const record = versioned(value, schemaVersion, label);
937
+ if (record[key] !== expected)
938
+ throw new StorageRecordError(`${label} identity is inconsistent: ${expected}.`);
939
+ return record;
940
+ }
941
+ function object(value, label) {
942
+ if (typeof value !== "object" || value === null || Array.isArray(value))
943
+ throw new StorageRecordError(`${label} must be an object.`);
944
+ return value;
945
+ }
946
+ function exact(value, expected, label) {
947
+ const allowed = new Set(expected);
948
+ const unknown = Object.keys(value).find((key) => !allowed.has(key));
949
+ if (unknown !== undefined)
950
+ throw new StorageRecordError(`${label} has unknown field: ${unknown}.`);
951
+ const missing = expected.find((key) => !Object.hasOwn(value, key));
952
+ if (missing !== undefined)
953
+ throw new StorageRecordError(`${label} is missing field: ${missing}.`);
954
+ }
955
+ function assertJsonValue(value, label, seen = new Set()) {
956
+ if (value === null || typeof value === "string" || typeof value === "boolean")
957
+ return;
958
+ if (typeof value === "number" && Number.isFinite(value))
959
+ return;
960
+ if (typeof value !== "object")
961
+ throw new StorageRecordError(`${label} contains a non-JSON value.`);
962
+ if (seen.has(value))
963
+ throw new StorageRecordError(`${label} contains a cycle.`);
964
+ seen.add(value);
965
+ if (Array.isArray(value))
966
+ for (const entry of value)
967
+ assertJsonValue(entry, label, seen);
968
+ else
969
+ for (const [key, entry] of Object.entries(value)) {
970
+ if (entry === undefined)
971
+ throw new StorageRecordError(`${label} contains undefined at ${key}.`);
972
+ assertJsonValue(entry, label, seen);
973
+ }
974
+ seen.delete(value);
975
+ }
976
+ function clone(value) { assertJsonValue(value, "Stored value"); return JSON.parse(JSON.stringify(value)); }
977
+ function optional(value) { return value === undefined ? null : clone(value); }
978
+ function values(records, identity) {
979
+ return Object.values(records).map(clone).sort((left, right) => numericCompare(typeof identity === "function" ? identity(left) : String(left[identity]), typeof identity === "function" ? identity(right) : String(right[identity])));
980
+ }
981
+ function numericCompare(left, right) { return left.localeCompare(right, undefined, { numeric: true }); }
982
+ function allKeys(state, key) { return Object.values(state.tasks).flatMap((task) => Object.keys(task[key])); }
983
+ function findUnique(state, key, id, label) {
984
+ const matches = Object.values(state.tasks).flatMap((task) => task[key][id] === undefined ? [] : [task[key][id]]);
985
+ if (matches.length > 1)
986
+ throw new StorageRecordError(`${label} id is ambiguous: ${id}`);
987
+ return matches[0] === undefined ? null : clone(matches[0]);
988
+ }
989
+ function synchronousResult(value) { if (typeof value === "object" && value !== null && "then" in value)
990
+ throw new StorageRecordError("FileTaskStore transactions must be synchronous."); return value; }
991
+ function acquireStorageLock(rootDir) {
992
+ ensureYuiHome(rootDir);
993
+ const lock = join(rootDir, STORAGE_LOCK_DIRECTORY);
994
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
995
+ while (true) {
996
+ try {
997
+ mkdirSync(lock, { mode: 0o700 });
998
+ writeFileSync(join(lock, "owner"), `${process.pid}\n`, { mode: 0o600 });
999
+ return () => { rmSync(lock, { recursive: true, force: true }); };
1000
+ }
1001
+ catch (error) {
1002
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
1003
+ throw error;
1004
+ reclaimDeadLock(lock);
1005
+ if (Date.now() >= deadline)
1006
+ throw new StorageConflictError(`Timed out waiting for storage lock: ${lock}`);
1007
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
1008
+ }
1009
+ }
1010
+ }
1011
+ function reclaimDeadLock(lock) {
1012
+ try {
1013
+ const age = Date.now() - statSync(lock).mtimeMs;
1014
+ if (age < 1_000)
1015
+ return;
1016
+ const pid = Number.parseInt(readFileSync(join(lock, "owner"), "utf8"), 10);
1017
+ if (Number.isInteger(pid) && processIsAlive(pid))
1018
+ return;
1019
+ rmSync(lock, { recursive: true, force: true });
1020
+ }
1021
+ catch (error) {
1022
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
1023
+ return;
1024
+ }
1025
+ }
1026
+ function processIsAlive(pid) { try {
1027
+ process.kill(pid, 0);
1028
+ return true;
1029
+ }
1030
+ catch (error) {
1031
+ return error instanceof Error && "code" in error && error.code === "EPERM";
1032
+ } }