@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,256 @@
1
+ import { rmdir } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { updateRole } from "../role/role.js";
4
+ import { createRoleWorkspace } from "../worktree/roleWorkspace.js";
5
+ import { NodeGitWorkspace } from "./gitWorkspace.js";
6
+ /**
7
+ * Prepares one deterministic worktree per Task Role. Git and the deterministic
8
+ * path remain the physical truth; the compact RoleWorkspace records only the
9
+ * identity needed to validate launches and safely reconcile archive cleanup.
10
+ */
11
+ export class FileTaskWorkspacePreparer {
12
+ store;
13
+ git;
14
+ now;
15
+ worktreeRoot;
16
+ constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date()) {
17
+ this.store = store;
18
+ this.git = git;
19
+ this.now = now;
20
+ this.worktreeRoot = join(resolve(home), "worktrees");
21
+ }
22
+ async prepareTaskWorkspace(taskId) {
23
+ const task = requireTask(this.store, taskId);
24
+ if (task.repositoryId === undefined) {
25
+ return {
26
+ taskId,
27
+ status: task.status === "draft" ? "draft" : "ready",
28
+ ...(task.cwd === undefined ? {} : { path: task.cwd })
29
+ };
30
+ }
31
+ const repository = this.store.getRepository(task.repositoryId);
32
+ if (repository === null)
33
+ throw new Error(`Repository not found: ${task.repositoryId}.`);
34
+ if (task.status === "draft")
35
+ return { taskId, status: "draft" };
36
+ if (task.status === "archived")
37
+ return this.#cleanupArchived(task, repository.path);
38
+ const taskRoot = this.#taskRoot(task.id);
39
+ if (task.cwd !== undefined && resolve(task.cwd) !== taskRoot) {
40
+ throw new Error(`Task workspace root does not match its deterministic path: ${task.id}.`);
41
+ }
42
+ const baseRef = task.baseRef ?? repository.defaultBranch;
43
+ // A Task base branch may be deleted after its first Role worktree is
44
+ // created. The persisted starting commit remains a stable base for Roles
45
+ // added later without introducing a separate ref ledger.
46
+ const physicalBase = this.store.listRoleWorkspaces(task.id)[0]?.baseCommit ?? baseRef;
47
+ const prepared = [];
48
+ for (const role of this.store.listRoles(task.id)) {
49
+ const workspace = await this.git.ensureWorktree({
50
+ repositoryPath: repository.path,
51
+ container: this.worktreeRoot,
52
+ taskId: task.id,
53
+ roleName: role.name,
54
+ baseRef: physicalBase
55
+ });
56
+ prepared.push({ roleName: role.name, ...workspace });
57
+ }
58
+ const ready = this.store.transaction((store) => {
59
+ const latest = requireTask(store, task.id);
60
+ if (latest.status !== "active")
61
+ return false;
62
+ if (latest.repositoryId !== repository.id) {
63
+ throw new Error(`Task repository changed while preparing its workspaces: ${task.id}.`);
64
+ }
65
+ const now = this.now();
66
+ for (const physical of prepared) {
67
+ const role = store.getRole(task.id, physical.roleName);
68
+ if (role === null)
69
+ continue;
70
+ const existing = store.getRoleWorkspace(task.id, role.name);
71
+ const workspace = existing ?? createRoleWorkspace({
72
+ taskId: task.id,
73
+ roleName: role.name,
74
+ repositoryId: repository.id,
75
+ path: physical.path,
76
+ branch: physical.branch,
77
+ baseRef,
78
+ baseCommit: physical.baseCommit
79
+ }, now);
80
+ assertWorkspaceIdentity(workspace, {
81
+ taskId: task.id,
82
+ roleName: role.name,
83
+ repositoryId: repository.id,
84
+ path: physical.path,
85
+ branch: physical.branch,
86
+ baseRef
87
+ });
88
+ if (role.workspace !== workspace.path) {
89
+ store.saveRole(task.id, updateRole(role, { workspace: workspace.path }, now));
90
+ }
91
+ if (existing === null)
92
+ store.saveRoleWorkspace(task.id, workspace);
93
+ }
94
+ const roles = store.listRoles(task.id);
95
+ const allReady = roles.length > 0 && roles.every((role) => {
96
+ const workspace = store.getRoleWorkspace(task.id, role.name);
97
+ return workspace !== null
98
+ && workspace.repositoryId === repository.id
99
+ && workspace.path === role.workspace;
100
+ });
101
+ if (allReady && latest.cwd !== taskRoot) {
102
+ store.saveTask({ ...latest, cwd: taskRoot, updatedAt: now.toISOString() });
103
+ }
104
+ else if (!allReady && latest.cwd !== undefined) {
105
+ const { cwd: _cwd, ...withoutWorkspace } = latest;
106
+ store.saveTask({ ...withoutWorkspace, updatedAt: now.toISOString() });
107
+ }
108
+ return allReady;
109
+ });
110
+ if (ready)
111
+ return { taskId, status: "ready", path: taskRoot };
112
+ // Archive may win while Git is creating worktrees. Re-read state and use
113
+ // the same dirty-preserving archive policy before any scheduler delivery.
114
+ const latest = requireTask(this.store, task.id);
115
+ if (latest.status === "archived")
116
+ return this.#cleanupArchived(latest, repository.path);
117
+ return { taskId, status: latest.status === "draft" ? "draft" : "pending" };
118
+ }
119
+ async reconcileTaskWorkspaces() {
120
+ return [
121
+ ...await this.prepareActiveTaskWorkspaces(),
122
+ ...await this.cleanupArchivedTaskWorkspaces()
123
+ ];
124
+ }
125
+ async prepareActiveTaskWorkspaces() {
126
+ const results = [];
127
+ for (const task of this.store.listTasks()) {
128
+ if (task.repositoryId === undefined || task.status !== "active")
129
+ continue;
130
+ try {
131
+ results.push(await this.prepareTaskWorkspace(task.id));
132
+ }
133
+ catch (error) {
134
+ results.push(failedPreparation(task.id, error));
135
+ }
136
+ }
137
+ return results;
138
+ }
139
+ async cleanupArchivedTaskWorkspaces() {
140
+ const results = [];
141
+ for (const task of this.store.listTasks()) {
142
+ if (task.repositoryId === undefined || task.status !== "archived")
143
+ continue;
144
+ try {
145
+ results.push(await this.prepareTaskWorkspace(task.id));
146
+ }
147
+ catch (error) {
148
+ results.push(failedPreparation(task.id, error));
149
+ }
150
+ }
151
+ return results;
152
+ }
153
+ async #cleanupArchived(task, repositoryPath) {
154
+ const removals = new Map();
155
+ const failures = [];
156
+ for (const role of this.store.listRoles(task.id)) {
157
+ try {
158
+ const removal = await this.git.removeWorktree({
159
+ repositoryPath,
160
+ container: this.worktreeRoot,
161
+ taskId: task.id,
162
+ roleName: role.name
163
+ });
164
+ removals.set(role.name, removal);
165
+ if (removal !== "dirty") {
166
+ this.#recordArchivedRoleCleanup(task.id, repositoryPath, role.name);
167
+ }
168
+ }
169
+ catch (error) {
170
+ failures.push(`${role.name}: ${error instanceof Error ? error.message : String(error)}`);
171
+ }
172
+ }
173
+ const dirty = [...removals.values()].some((removal) => removal === "dirty");
174
+ if (failures.length > 0) {
175
+ return {
176
+ taskId: task.id,
177
+ status: "failed",
178
+ path: this.#taskRoot(task.id),
179
+ error: `Role worktree cleanup failed (${failures.join("; ")})`
180
+ };
181
+ }
182
+ this.#recordArchivedTaskCleanup(task.id, dirty);
183
+ if (!dirty)
184
+ await removeEmptyDirectory(this.#taskRoot(task.id));
185
+ return dirty
186
+ ? { taskId: task.id, status: "archived-dirty", path: this.#taskRoot(task.id) }
187
+ : { taskId: task.id, status: "archived-clean" };
188
+ }
189
+ #recordArchivedRoleCleanup(taskId, repositoryPath, roleName) {
190
+ this.store.transaction((store) => {
191
+ const latest = requireTask(store, taskId);
192
+ if (latest.status !== "archived")
193
+ return;
194
+ const now = this.now();
195
+ const role = store.getRole(taskId, roleName);
196
+ if (role === null)
197
+ return;
198
+ if (role.workspace === repositoryPath
199
+ && store.getRoleWorkspace(taskId, role.name) === null)
200
+ return;
201
+ if (role.workspace !== repositoryPath) {
202
+ store.saveRole(taskId, updateRole(role, { workspace: repositoryPath }, now));
203
+ }
204
+ store.removeRoleWorkspace(taskId, role.name);
205
+ });
206
+ }
207
+ #recordArchivedTaskCleanup(taskId, dirty) {
208
+ this.store.transaction((store) => {
209
+ const latest = requireTask(store, taskId);
210
+ if (latest.status !== "archived")
211
+ return;
212
+ if (!dirty && latest.cwd !== undefined) {
213
+ const { cwd: _cwd, ...withoutWorkspace } = latest;
214
+ store.saveTask({ ...withoutWorkspace, updatedAt: this.now().toISOString() });
215
+ }
216
+ });
217
+ }
218
+ #taskRoot(taskId) {
219
+ return join(this.worktreeRoot, taskId);
220
+ }
221
+ }
222
+ function failedPreparation(taskId, error) {
223
+ return {
224
+ taskId,
225
+ status: "failed",
226
+ error: error instanceof Error ? error.message : String(error)
227
+ };
228
+ }
229
+ function assertWorkspaceIdentity(workspace, expected) {
230
+ for (const key of [
231
+ "taskId", "roleName", "repositoryId", "path", "branch", "baseRef"
232
+ ]) {
233
+ if (workspace[key] !== expected[key]) {
234
+ throw new Error(`RoleWorkspace ${key} does not match its deterministic identity: ${expected.taskId}/${expected.roleName}.`);
235
+ }
236
+ }
237
+ }
238
+ async function removeEmptyDirectory(path) {
239
+ try {
240
+ await rmdir(path);
241
+ }
242
+ catch (error) {
243
+ if (!isErrno(error, "ENOENT") && !isErrno(error, "ENOTEMPTY"))
244
+ throw error;
245
+ }
246
+ }
247
+ function isErrno(value, code) {
248
+ return typeof value === "object" && value !== null && "code" in value
249
+ && value.code === code;
250
+ }
251
+ function requireTask(store, taskId) {
252
+ const task = store.getTask(taskId);
253
+ if (task === null)
254
+ throw new Error(`Task not found: ${taskId}.`);
255
+ return task;
256
+ }
@@ -0,0 +1,246 @@
1
+ export function createRoleAgentBinding(agent, config) {
2
+ const agentId = requireSafeIdentity(agent.id, "Role Agent id");
3
+ const adapterId = requireSupportedAdapterId(agent.adapterId);
4
+ const effectiveConfig = config ?? { adapterId };
5
+ if (effectiveConfig.adapterId !== adapterId) {
6
+ throw new Error(`Role Agent config adapter does not match Agent: ${agentId}.`);
7
+ }
8
+ return cloneBinding({ agentId, adapterId, config: effectiveConfig });
9
+ }
10
+ export function createRole(taskId, name, bindings, activeAgentId, workspace, now, profile = {}) {
11
+ const owner = createRoleOwner(name, bindings, activeAgentId, workspace, now, profile);
12
+ return validateTaskRole({
13
+ ...owner,
14
+ taskId: requireSafeIdentity(taskId, "Task id"),
15
+ status: "idle"
16
+ });
17
+ }
18
+ export function createGlobalRole(name, bindings, activeAgentId, workspace, now, profile = {}) {
19
+ return createRoleOwner(name, bindings, activeAgentId, workspace, now, profile);
20
+ }
21
+ export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalRole.name) {
22
+ validateGlobalRole(globalRole);
23
+ const timestamp = now.toISOString();
24
+ return validateTaskRole({
25
+ ...cloneProfile(globalRole),
26
+ schemaVersion: 2,
27
+ taskId: requireSafeIdentity(taskId, "Task id"),
28
+ name: requireSafeIdentity(name, "Role name"),
29
+ activeAgentId: globalRole.activeAgentId,
30
+ agentBindings: cloneBindings(globalRole.agentBindings),
31
+ workspace: globalRole.workspace,
32
+ status: "idle",
33
+ createdAt: timestamp,
34
+ updatedAt: timestamp
35
+ });
36
+ }
37
+ export function activeRoleAgentBinding(role) {
38
+ validateRoleOwner(role);
39
+ return role.agentBindings[role.activeAgentId];
40
+ }
41
+ export function updateGlobalRole(role, patch, now) {
42
+ validateGlobalRole(role);
43
+ const updated = {
44
+ ...role,
45
+ ...cloneRolePatch(patch),
46
+ updatedAt: now.toISOString()
47
+ };
48
+ clearProfileFields(updated, patch);
49
+ return validateGlobalRole(updated);
50
+ }
51
+ export function updateRole(role, patch, now) {
52
+ validateTaskRole(role);
53
+ const updated = {
54
+ ...role,
55
+ ...cloneRolePatch(patch),
56
+ updatedAt: now.toISOString()
57
+ };
58
+ clearProfileFields(updated, patch);
59
+ return validateTaskRole(updated);
60
+ }
61
+ export function updateRoleStatus(role, status, now) {
62
+ validateTaskRole(role);
63
+ if (!isRoleStatus(status))
64
+ throw new Error(`Role status is invalid: ${status}.`);
65
+ return { ...role, status, updatedAt: now.toISOString() };
66
+ }
67
+ export function switchActiveRoleAgent(role, sessions, targetAgentId, runtime, now) {
68
+ validateRoleOwner(role);
69
+ if (runtime.activeRun) {
70
+ throw new Error("Cannot switch Role Agent while an active AgentRun exists.");
71
+ }
72
+ if (runtime.nativeProcessRunning) {
73
+ throw new Error("Cannot switch Role Agent while the native Agent process is running.");
74
+ }
75
+ const normalizedTarget = requireSafeIdentity(targetAgentId, "Target Agent id");
76
+ if (!Object.hasOwn(role.agentBindings, normalizedTarget)) {
77
+ throw new Error(`Role Agent is not bound: ${normalizedTarget}.`);
78
+ }
79
+ assertSessionOwnerMatchesRole(role, sessions);
80
+ if (sessions.activeAgentId !== role.activeAgentId) {
81
+ throw new Error(`Role session active Agent does not match Role: ${role.name}.`);
82
+ }
83
+ const fromAgentId = role.activeAgentId;
84
+ const mode = sessions.sessions[normalizedTarget] === undefined ? "new" : "resume";
85
+ const timestamp = now.toISOString();
86
+ const updatedRole = "taskId" in role
87
+ ? updateRole(role, { activeAgentId: normalizedTarget }, now)
88
+ : updateGlobalRole(role, { activeAgentId: normalizedTarget }, now);
89
+ const updatedSessions = {
90
+ ...sessions,
91
+ activeAgentId: normalizedTarget,
92
+ updatedAt: timestamp
93
+ };
94
+ return {
95
+ role: updatedRole,
96
+ sessions: updatedSessions,
97
+ mode,
98
+ event: {
99
+ type: "role.agent_switched",
100
+ payload: { fromAgentId, toAgentId: normalizedTarget, mode }
101
+ }
102
+ };
103
+ }
104
+ export function validateGlobalRole(role) {
105
+ return validateRoleOwner(role);
106
+ }
107
+ export function validateTaskRole(role) {
108
+ requireSafeIdentity(role.taskId, "Task id");
109
+ if (!isRoleStatus(role.status))
110
+ throw new Error(`Role status is invalid: ${role.status}.`);
111
+ return validateRoleOwner(role);
112
+ }
113
+ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile) {
114
+ const mappedBindings = {};
115
+ for (const sourceBinding of bindings) {
116
+ const binding = validateRoleAgentBinding(cloneBinding(sourceBinding));
117
+ if (Object.hasOwn(mappedBindings, binding.agentId)) {
118
+ throw new Error(`Role Agent binding is duplicated: ${binding.agentId}.`);
119
+ }
120
+ mappedBindings[binding.agentId] = binding;
121
+ }
122
+ const timestamp = now.toISOString();
123
+ return validateGlobalRole({
124
+ ...cloneProfile(profile),
125
+ schemaVersion: 2,
126
+ name: requireSafeIdentity(name, "Role name"),
127
+ activeAgentId: requireSafeIdentity(activeAgentId, "Role active Agent id"),
128
+ agentBindings: mappedBindings,
129
+ workspace: requireText(workspace, "Role workspace"),
130
+ createdAt: timestamp,
131
+ updatedAt: timestamp
132
+ });
133
+ }
134
+ function validateRoleOwner(role) {
135
+ if (role.schemaVersion !== 2)
136
+ throw new Error("Role schema version is invalid.");
137
+ requireSafeIdentity(role.name, "Role name");
138
+ requireSafeIdentity(role.activeAgentId, "Role active Agent id");
139
+ requireText(role.workspace, "Role workspace");
140
+ const entries = Object.entries(role.agentBindings);
141
+ if (entries.length === 0)
142
+ throw new Error("Role requires at least one Agent binding.");
143
+ for (const [agentId, binding] of entries) {
144
+ validateRoleAgentBinding(binding);
145
+ if (agentId !== binding.agentId) {
146
+ throw new Error(`Role Agent binding identity is inconsistent: ${agentId}.`);
147
+ }
148
+ }
149
+ if (!Object.hasOwn(role.agentBindings, role.activeAgentId)) {
150
+ throw new Error(`Role active Agent is not bound: ${role.activeAgentId}.`);
151
+ }
152
+ return role;
153
+ }
154
+ function validateRoleAgentBinding(binding) {
155
+ const agentId = requireSafeIdentity(binding.agentId, "Role Agent id");
156
+ const adapterId = requireSupportedAdapterId(binding.adapterId);
157
+ if (binding.config === null || typeof binding.config !== "object" || Array.isArray(binding.config)) {
158
+ throw new Error(`Role Agent config is invalid: ${agentId}.`);
159
+ }
160
+ if (binding.config.adapterId !== adapterId) {
161
+ throw new Error(`Role Agent binding adapter is inconsistent: ${agentId}.`);
162
+ }
163
+ return binding;
164
+ }
165
+ function assertSessionOwnerMatchesRole(role, sessions) {
166
+ const matches = "taskId" in role
167
+ ? sessions.owner.scope === "task"
168
+ && sessions.owner.taskId === role.taskId
169
+ && sessions.owner.roleName === role.name
170
+ : sessions.owner.scope === "global" && sessions.owner.roleName === role.name;
171
+ if (!matches)
172
+ throw new Error(`Role session owner does not match Role: ${role.name}.`);
173
+ }
174
+ function cloneRolePatch(patch) {
175
+ return {
176
+ ...(patch.name === undefined ? {} : { name: requireSafeIdentity(patch.name, "Role name") }),
177
+ ...(patch.activeAgentId === undefined
178
+ ? {}
179
+ : { activeAgentId: requireSafeIdentity(patch.activeAgentId, "Role active Agent id") }),
180
+ ...(patch.workspace === undefined ? {} : { workspace: requireText(patch.workspace, "Role workspace") }),
181
+ ...(patch.agentBindings === undefined ? {} : { agentBindings: cloneBindings(patch.agentBindings) }),
182
+ ...cloneProfile(patch)
183
+ };
184
+ }
185
+ function cloneBindings(bindings) {
186
+ return Object.fromEntries(Object.entries(bindings).map(([agentId, binding]) => [agentId, cloneBinding(binding)]));
187
+ }
188
+ function cloneBinding(binding) {
189
+ return {
190
+ agentId: binding.agentId,
191
+ adapterId: binding.adapterId,
192
+ config: cloneJson(binding.config)
193
+ };
194
+ }
195
+ function cloneProfile(profile) {
196
+ return {
197
+ ...(profile.description === undefined ? {} : { description: profile.description }),
198
+ ...(profile.responsibilities === undefined ? {} : { responsibilities: [...profile.responsibilities] }),
199
+ ...(profile.constraints === undefined ? {} : { constraints: [...profile.constraints] }),
200
+ ...(profile.expectedOutput === undefined ? {} : { expectedOutput: profile.expectedOutput }),
201
+ ...(profile.systemPrompt === undefined ? {} : { systemPrompt: profile.systemPrompt }),
202
+ ...(profile.skills === undefined ? {} : { skills: [...profile.skills] })
203
+ };
204
+ }
205
+ function clearProfileFields(role, patch) {
206
+ for (const key of [
207
+ "description",
208
+ "responsibilities",
209
+ "constraints",
210
+ "expectedOutput",
211
+ "systemPrompt",
212
+ "skills"
213
+ ]) {
214
+ if (Object.hasOwn(patch, key) && patch[key] === undefined)
215
+ delete role[key];
216
+ }
217
+ }
218
+ function cloneJson(value) {
219
+ return JSON.parse(JSON.stringify(value));
220
+ }
221
+ function requireSupportedAdapterId(value) {
222
+ const normalized = requireText(value, "Role Agent adapter id");
223
+ if (normalized !== "codex" && normalized !== "claude") {
224
+ throw new Error(`Role Agent adapter is unsupported: ${normalized}.`);
225
+ }
226
+ return normalized;
227
+ }
228
+ function requireSafeIdentity(value, label) {
229
+ const normalized = requireText(value, label);
230
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
231
+ || /[\/\\\0]/.test(normalized)) {
232
+ throw new Error(`${label} is invalid.`);
233
+ }
234
+ return normalized;
235
+ }
236
+ function requireText(value, label) {
237
+ if (typeof value !== "string" || value.includes("\0"))
238
+ throw new Error(`${label} is invalid.`);
239
+ const normalized = value.trim();
240
+ if (normalized.length === 0)
241
+ throw new Error(`${label} is required.`);
242
+ return normalized;
243
+ }
244
+ function isRoleStatus(value) {
245
+ return ["idle", "running", "detached", "exited", "failed"].includes(value);
246
+ }
@@ -0,0 +1,20 @@
1
+ export const SYSTEM_OPERATOR_ROLE = "operator";
2
+ export const SYSTEM_LEADER_ROLE = "leader";
3
+ export const SYSTEM_WORKER_ROLE = "worker";
4
+ export const SYSTEM_ROLE_NAMES = [
5
+ SYSTEM_OPERATOR_ROLE,
6
+ SYSTEM_LEADER_ROLE,
7
+ SYSTEM_WORKER_ROLE
8
+ ];
9
+ export function isSystemRoleName(name) {
10
+ return SYSTEM_ROLE_NAMES.includes(name);
11
+ }
12
+ export function systemRoleDescription(name) {
13
+ if (name === SYSTEM_OPERATOR_ROLE)
14
+ return "global user-facing CLI operator";
15
+ if (name === SYSTEM_LEADER_ROLE)
16
+ return "task leader and role coordinator";
17
+ if (name === SYSTEM_WORKER_ROLE)
18
+ return "task worker executing delegated work";
19
+ return "";
20
+ }
@@ -0,0 +1,102 @@
1
+ export function createAgentRun(id, taskId, roleName, mode, input, now, context = {}) {
2
+ if (mode !== "new" && mode !== "resume") {
3
+ throw new Error(`Agent run dispatch mode is invalid: ${mode}.`);
4
+ }
5
+ const timestamp = now.toISOString();
6
+ return {
7
+ schemaVersion: 1,
8
+ id: requireSafeIdentity(id, "Agent run id"),
9
+ taskId: requireSafeIdentity(taskId, "Task id"),
10
+ roleName: requireSafeIdentity(roleName, "Role name"),
11
+ mode,
12
+ input: requireText(input, "Agent run input"),
13
+ ...(context.workItemId === undefined
14
+ ? {}
15
+ : { workItemId: requireSafeIdentity(context.workItemId, "Work item id") }),
16
+ status: "active",
17
+ createdAt: timestamp,
18
+ updatedAt: timestamp
19
+ };
20
+ }
21
+ export function isActiveAgentRun(run) {
22
+ return run.status === "active";
23
+ }
24
+ export function markAgentRunDelivered(run, now) {
25
+ if (run.status !== "active") {
26
+ throw new Error(`Cannot deliver a terminal Agent run: ${run.id}.`);
27
+ }
28
+ if (run.deliveredAt !== undefined)
29
+ return run;
30
+ const timestamp = now.toISOString();
31
+ return { ...run, deliveredAt: timestamp, updatedAt: timestamp };
32
+ }
33
+ export function validateAgentRun(run) {
34
+ if (run.schemaVersion !== 1)
35
+ throw new Error("Agent run must use schemaVersion 1.");
36
+ requireSafeIdentity(run.id, "Agent run id");
37
+ requireSafeIdentity(run.taskId, "Task id");
38
+ requireSafeIdentity(run.roleName, "Role name");
39
+ if (run.mode !== "new" && run.mode !== "resume") {
40
+ throw new Error(`Agent run dispatch mode is invalid: ${String(run.mode)}.`);
41
+ }
42
+ requireText(run.input, "Agent run input");
43
+ if (run.workItemId !== undefined)
44
+ requireSafeIdentity(run.workItemId, "Work item id");
45
+ if (!["active", "yielded", "failed"].includes(run.status)) {
46
+ throw new Error(`Agent run status is invalid: ${String(run.status)}.`);
47
+ }
48
+ requireTimestamp(run.createdAt, "Agent run createdAt");
49
+ requireTimestamp(run.updatedAt, "Agent run updatedAt");
50
+ if (run.deliveredAt !== undefined)
51
+ requireTimestamp(run.deliveredAt, "Agent run deliveredAt");
52
+ if (run.status === "active") {
53
+ if (run.summary !== undefined || run.endedAt !== undefined) {
54
+ throw new Error("An active Agent run cannot have terminal metadata.");
55
+ }
56
+ }
57
+ else {
58
+ requireText(run.summary ?? "", "Agent run summary");
59
+ requireTimestamp(run.endedAt ?? "", "Agent run endedAt");
60
+ }
61
+ return run;
62
+ }
63
+ export function yieldAgentRun(run, summary, now) {
64
+ return finishAgentRun(run, "yielded", requireText(summary, "Agent run summary"), now);
65
+ }
66
+ export function failAgentRun(run, summary, now) {
67
+ return finishAgentRun(run, "failed", requireText(summary, "Agent run summary"), now);
68
+ }
69
+ function finishAgentRun(run, status, summary, now) {
70
+ if (run.status !== "active") {
71
+ throw new Error(`Agent run is already terminal: ${run.id}.`);
72
+ }
73
+ const timestamp = now.toISOString();
74
+ return {
75
+ ...run,
76
+ status,
77
+ summary,
78
+ updatedAt: timestamp,
79
+ endedAt: timestamp
80
+ };
81
+ }
82
+ function requireSafeIdentity(value, label) {
83
+ const normalized = requireText(value, label);
84
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
85
+ || /[\/\\\0]/.test(normalized)) {
86
+ throw new Error(`${label} is invalid.`);
87
+ }
88
+ return normalized;
89
+ }
90
+ function requireText(value, label) {
91
+ if (typeof value !== "string" || value.includes("\0"))
92
+ throw new Error(`${label} is invalid.`);
93
+ const normalized = value.trim();
94
+ if (normalized.length === 0)
95
+ throw new Error(`${label} is required.`);
96
+ return normalized;
97
+ }
98
+ function requireTimestamp(value, label) {
99
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
100
+ throw new Error(`${label} must be an ISO timestamp.`);
101
+ }
102
+ }