@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,74 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { dataError } from "../errors/cliError.js";
4
+ /** Compatibility entry point used by the restored Task workflow. */
5
+ export function compileDispatchInput(_store, taskId, role, input) {
6
+ const configuredSkillBodies = readConfiguredSkills(role.skills ?? []);
7
+ return buildRoleContext({ taskId, role, input, configuredSkillBodies });
8
+ }
9
+ export function buildRoleContext(context) {
10
+ return context.role.name === "leader"
11
+ ? buildLeaderContext(context)
12
+ : buildWorkerContext(context);
13
+ }
14
+ export function buildLeaderContext(context) {
15
+ return renderDispatchContext("leader", context);
16
+ }
17
+ export function buildWorkerContext(context) {
18
+ return renderDispatchContext("worker", context);
19
+ }
20
+ function renderDispatchContext(kind, context) {
21
+ const profile = context.role;
22
+ const profileLines = [
23
+ `Task: ${context.taskId}`,
24
+ `Role: ${profile.name}`,
25
+ `Active Agent: ${profile.activeAgentId}`,
26
+ profile.description === undefined ? null : `Description: ${profile.description}`,
27
+ ...(profile.responsibilities ?? []).map((item) => `Responsibility: ${item}`),
28
+ ...(profile.constraints ?? []).map((item) => `Constraint: ${item}`),
29
+ profile.expectedOutput === undefined ? null : `Expected output: ${profile.expectedOutput}`,
30
+ profile.systemPrompt === undefined ? null : `Role instruction: ${profile.systemPrompt}`,
31
+ profile.skills === undefined || profile.skills.length === 0
32
+ ? null
33
+ : `Configured role skills: ${profile.skills.join(", ")}`
34
+ ].filter((line) => line !== null);
35
+ return [
36
+ readSystemSkill(kind),
37
+ profileLines.join("\n"),
38
+ ...(context.configuredSkillBodies ?? []).map((body) => body.trim()).filter(Boolean),
39
+ "Yui dispatch:",
40
+ requireText(context.input, "dispatch input")
41
+ ].filter(Boolean).join("\n\n");
42
+ }
43
+ function readConfiguredSkills(skills) {
44
+ const yuiHome = process.env.YUI_HOME;
45
+ if (skills.length === 0)
46
+ return [];
47
+ if (yuiHome === undefined) {
48
+ throw dataError("YUI_HOME is required to load configured Role skills.");
49
+ }
50
+ return skills.map((skill) => {
51
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(skill)) {
52
+ throw dataError(`Invalid configured Skill id: ${skill}`);
53
+ }
54
+ try {
55
+ return readFileSync(join(yuiHome, "skills", skill, "SKILL.md"), "utf8").trim();
56
+ }
57
+ catch (error) {
58
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
59
+ throw dataError(`Configured Skill not found: ${skill}`);
60
+ }
61
+ throw error;
62
+ }
63
+ });
64
+ }
65
+ function readSystemSkill(kind) {
66
+ return readFileSync(new URL(`../../skills/yui-${kind}/SKILL.md`, import.meta.url), "utf8").trim();
67
+ }
68
+ function requireText(value, label) {
69
+ const normalized = value.trim();
70
+ if (normalized.length === 0 || normalized.includes("\0")) {
71
+ throw new Error(`${label} is required.`);
72
+ }
73
+ return normalized;
74
+ }
@@ -0,0 +1,215 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { callController } from "../core/controllerClient.js";
4
+ const STARTUP_TIMEOUT_MS = 5_000;
5
+ const POLL_INTERVAL_MS = 50;
6
+ /** Starts the per-home FileTask Controller on demand and waits until callable. */
7
+ export async function ensureFileTaskController(home, options = {}) {
8
+ const call = options.call ?? callController;
9
+ try {
10
+ return await call(home, "controller.status", {});
11
+ }
12
+ catch (error) {
13
+ if (!isUnavailable(error))
14
+ throw error;
15
+ }
16
+ const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
17
+ const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
18
+ const spawnController = options.spawnController ?? spawnDetachedFileTaskController;
19
+ spawnController(home, { ...process.env, ...options.environment, YUI_HOME: home });
20
+ const deadline = Date.now() + timeoutMs;
21
+ for (;;) {
22
+ try {
23
+ return await call(home, "controller.status", {});
24
+ }
25
+ catch (error) {
26
+ if (!isUnavailable(error))
27
+ throw error;
28
+ if (Date.now() >= deadline) {
29
+ throw new Error(`Controller did not become ready within ${timeoutMs} ms.`, { cause: error });
30
+ }
31
+ await delay(pollMs);
32
+ }
33
+ }
34
+ }
35
+ function spawnDetachedFileTaskController(home, environment) {
36
+ const child = spawn(process.execPath, [fileURLToPath(new URL("./controllerMain.js", import.meta.url))], {
37
+ env: environment,
38
+ detached: true,
39
+ stdio: "ignore"
40
+ });
41
+ child.unref();
42
+ }
43
+ /** Restarts only the per-home Controller process; managed tmux sessions remain untouched. */
44
+ export async function restartFileTaskController(home, options = {}) {
45
+ const call = options.call ?? callController;
46
+ const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
47
+ const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
48
+ const current = await readOptionalControllerStatus(home, call);
49
+ const previousPid = controllerPid(current);
50
+ if (controllerRunning(current)) {
51
+ await callFileTaskController(home, "controller.stop", {}, options);
52
+ const deadline = Date.now() + timeoutMs;
53
+ for (;;) {
54
+ const status = await readOptionalControllerStatus(home, call);
55
+ if (!controllerRunning(status))
56
+ break;
57
+ if (Date.now() >= deadline) {
58
+ throw new Error(`Controller did not stop within ${timeoutMs} ms.`);
59
+ }
60
+ await delay(pollMs);
61
+ }
62
+ }
63
+ const started = await ensureFileTaskController(home, options);
64
+ const pid = controllerPid(started);
65
+ return {
66
+ restarted: true,
67
+ ...(previousPid === undefined ? {} : { previousPid }),
68
+ ...(pid === undefined ? {} : { pid })
69
+ };
70
+ }
71
+ export async function callFileTaskController(home, method, params = {}, options = {}) {
72
+ const call = options.call ?? callController;
73
+ if (method === "controller.status" || method === "controller.stop") {
74
+ try {
75
+ return await call(home, method, params);
76
+ }
77
+ catch (error) {
78
+ if (!isUnavailable(error))
79
+ throw error;
80
+ return method === "controller.status"
81
+ ? { running: false }
82
+ : { stopped: false, alreadyStopped: true };
83
+ }
84
+ }
85
+ await ensureFileTaskController(home, options);
86
+ return call(home, method, params);
87
+ }
88
+ /** Foreground command bridge. It never reads or writes Agent terminal bytes. */
89
+ export class FileTaskWorkflowRuntime {
90
+ home;
91
+ store;
92
+ schedulerStore;
93
+ planner;
94
+ tmux;
95
+ workspacePreparer;
96
+ clientOptions;
97
+ constructor(home, store, schedulerStore, planner, tmux, workspacePreparer, clientOptions = {}) {
98
+ this.home = home;
99
+ this.store = store;
100
+ this.schedulerStore = schedulerStore;
101
+ this.planner = planner;
102
+ this.tmux = tmux;
103
+ this.workspacePreparer = workspacePreparer;
104
+ this.clientOptions = clientOptions;
105
+ }
106
+ notifyStateChanged(taskId) {
107
+ void this.#prepareAndScan(taskId).catch(this.clientOptions.onError ?? (() => { }));
108
+ }
109
+ reconcileTask(taskId) {
110
+ void this.#prepareAndScan(taskId).catch(this.clientOptions.onError ?? (() => { }));
111
+ }
112
+ prepareTaskRoleEnter(input) {
113
+ const role = this.store.getRole(input.taskId, input.roleName);
114
+ if (role === null)
115
+ throw new Error(`Role not found: ${input.taskId}/${input.roleName}.`);
116
+ const session = this.store.getRoleSession(input.taskId, input.roleName);
117
+ const mode = session?.nativeSessionId === undefined ? "new" : "resume";
118
+ const planned = this.planner.plan({
119
+ taskId: input.taskId,
120
+ roleName: input.roleName,
121
+ agentId: role.activeAgentId,
122
+ adapterId: role.agentBindings[role.activeAgentId].adapterId,
123
+ mode,
124
+ ...(mode === "resume" ? { nativeSessionId: session.nativeSessionId } : {})
125
+ });
126
+ this.tmux.ensureRoleWindow(input.taskId, planned.role, planned.launch);
127
+ if (planned.session?.nativeSessionId !== undefined) {
128
+ this.schedulerStore.recordRuntimeNativeSession({
129
+ taskId: input.taskId,
130
+ roleName: input.roleName,
131
+ agentId: planned.session.agentId,
132
+ adapterId: planned.session.adapterId,
133
+ nativeSessionId: planned.session.nativeSessionId
134
+ });
135
+ }
136
+ }
137
+ inspectTaskRolePanes(taskId) {
138
+ return this.tmux.inspectTaskRolePanes(taskId);
139
+ }
140
+ prepareGlobalRoleEnter(roleName, tmuxTaskId = "operator") {
141
+ const role = this.store.getGlobalRole(roleName);
142
+ if (role === null)
143
+ throw new Error(`Global Role not found: ${roleName}.`);
144
+ const sessionSet = this.store.getGlobalRoleSessionSet(roleName);
145
+ const session = sessionSet?.sessions[role.activeAgentId];
146
+ const binding = role.agentBindings[role.activeAgentId];
147
+ const mode = session?.nativeSessionId === undefined ? "new" : "resume";
148
+ const planned = this.planner.planGlobalRole({
149
+ roleName,
150
+ agentId: role.activeAgentId,
151
+ adapterId: binding.adapterId,
152
+ mode,
153
+ ...(mode === "resume" ? { nativeSessionId: session.nativeSessionId } : {})
154
+ });
155
+ this.tmux.ensureRoleWindow(tmuxTaskId, planned.role, planned.launch);
156
+ if (planned.session?.nativeSessionId !== undefined) {
157
+ this.schedulerStore.recordGlobalRuntimeNativeSession({
158
+ roleName,
159
+ agentId: planned.session.agentId,
160
+ adapterId: planned.session.adapterId,
161
+ nativeSessionId: planned.session.nativeSessionId
162
+ });
163
+ }
164
+ }
165
+ async #prepareAndScan(taskId) {
166
+ const task = this.store.getTask(taskId);
167
+ if (task?.repositoryId !== undefined
168
+ && task.status === "active"
169
+ && this.workspacePreparer !== undefined) {
170
+ await this.workspacePreparer.prepareTaskWorkspace(taskId);
171
+ }
172
+ await callFileTaskController(this.home, "scheduler.scan", {}, this.clientOptions);
173
+ }
174
+ }
175
+ function isUnavailable(error) {
176
+ if (typeof error !== "object" || error === null || !("code" in error))
177
+ return false;
178
+ const code = error.code;
179
+ return code === "CONTROLLER_UNAVAILABLE"
180
+ || code === "CONTROLLER_DISCOVERY_INVALID"
181
+ || code === "INVALID_RESPONSE";
182
+ }
183
+ async function readOptionalControllerStatus(home, call) {
184
+ try {
185
+ return await call(home, "controller.status", {});
186
+ }
187
+ catch (error) {
188
+ if (isUnavailable(error))
189
+ return null;
190
+ throw error;
191
+ }
192
+ }
193
+ function controllerRunning(value) {
194
+ return isJsonRecord(value) && value.running === true;
195
+ }
196
+ function controllerPid(value) {
197
+ if (!isJsonRecord(value))
198
+ return undefined;
199
+ return Number.isSafeInteger(value.pid) && value.pid > 0
200
+ ? value.pid
201
+ : undefined;
202
+ }
203
+ function isJsonRecord(value) {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+ function positive(value, fallback, label) {
207
+ const resolved = value ?? fallback;
208
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
209
+ throw new TypeError(`${label} must be a positive integer.`);
210
+ }
211
+ return resolved;
212
+ }
213
+ function delay(milliseconds) {
214
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
215
+ }
@@ -0,0 +1,158 @@
1
+ import { reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
2
+ import { processLeaderWakeups } from "../scheduler/leaderWakeupProcessor.js";
3
+ import { processActiveRoleRunDeliveries } from "../scheduler/activeRoleRunDelivery.js";
4
+ import { stopArchivedTaskRuntimes } from "../scheduler/archivedTaskRuntime.js";
5
+ import { reconcileExitedRoleRuns } from "../scheduler/roleRunLiveness.js";
6
+ import { processOperatorInputNotifications } from "../scheduler/operatorInputNotificationProcessor.js";
7
+ import { startControllerServer } from "../core/controllerServer.js";
8
+ const DEFAULT_RECONCILIATION_INTERVAL_MS = reconciliationIntervalMilliseconds();
9
+ /**
10
+ * Runs one lean scheduler pass. Liveness always precedes wakeup processing so
11
+ * an exited busy Leader is cleared and reconsidered in the same pass.
12
+ */
13
+ export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer) {
14
+ await workspacePreparer?.prepareActiveTaskWorkspaces();
15
+ const stoppedArchivedTaskIds = await stopArchivedTaskRuntimes(store, delivery, now);
16
+ await workspacePreparer?.cleanupArchivedTaskWorkspaces();
17
+ const activeRunDeliveries = await processActiveRoleRunDeliveries(store, delivery, now);
18
+ const failedRunIds = await reconcileExitedRoleRuns(store, delivery, now);
19
+ const autoResolvedInputs = store.resolveExpiredInputRecommendations(now);
20
+ const wakeups = await processLeaderWakeups(store, delivery, now);
21
+ const inputNotifications = await processOperatorInputNotifications(store, delivery);
22
+ return {
23
+ stoppedArchivedTaskIds,
24
+ activeRunDeliveries,
25
+ failedRunIds,
26
+ wakeups,
27
+ inputNotifications,
28
+ autoResolvedInputs
29
+ };
30
+ }
31
+ /**
32
+ * Single-owner periodic runtime for FileTaskStore-backed scheduling. Concurrent
33
+ * pump requests coalesce into one follow-up pass; scheduler effects never
34
+ * overlap. There are deliberately no filesystem watchers or derived indexes.
35
+ */
36
+ export class FileTaskController {
37
+ store;
38
+ delivery;
39
+ #intervalMs;
40
+ #now;
41
+ #onError;
42
+ #workspacePreparer;
43
+ #timer;
44
+ #current;
45
+ #rerunRequested = false;
46
+ #stopped = false;
47
+ constructor(store, delivery, options = {}) {
48
+ this.store = store;
49
+ this.delivery = delivery;
50
+ this.#intervalMs = positiveInteger(options.intervalMs, DEFAULT_RECONCILIATION_INTERVAL_MS, "Controller reconciliation interval");
51
+ this.#now = options.now ?? (() => new Date());
52
+ this.#onError = options.onError ?? (() => { });
53
+ this.#workspacePreparer = options.workspacePreparer;
54
+ }
55
+ get reconciliationIntervalMs() {
56
+ return this.#intervalMs;
57
+ }
58
+ start() {
59
+ if (this.#timer !== undefined)
60
+ return;
61
+ this.#stopped = false;
62
+ void this.pump().catch(this.#onError);
63
+ this.#timer = setInterval(() => {
64
+ void this.pump().catch(this.#onError);
65
+ }, this.#intervalMs);
66
+ this.#timer.unref();
67
+ }
68
+ stop() {
69
+ this.#stopped = true;
70
+ if (this.#timer !== undefined) {
71
+ clearInterval(this.#timer);
72
+ this.#timer = undefined;
73
+ }
74
+ }
75
+ pump() {
76
+ if (this.#stopped) {
77
+ return Promise.reject(new Error("Controller runtime is stopped."));
78
+ }
79
+ if (this.#current !== undefined) {
80
+ this.#rerunRequested = true;
81
+ return this.#current;
82
+ }
83
+ const running = this.#runCoalesced();
84
+ this.#current = running;
85
+ void running.finally(() => {
86
+ if (this.#current === running)
87
+ this.#current = undefined;
88
+ }).catch(() => { });
89
+ return running;
90
+ }
91
+ async #runCoalesced() {
92
+ let result = {
93
+ stoppedArchivedTaskIds: [],
94
+ activeRunDeliveries: [],
95
+ failedRunIds: [],
96
+ wakeups: [],
97
+ inputNotifications: [],
98
+ autoResolvedInputs: []
99
+ };
100
+ do {
101
+ this.#rerunRequested = false;
102
+ result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer);
103
+ } while (this.#rerunRequested && !this.#stopped);
104
+ return result;
105
+ }
106
+ }
107
+ /**
108
+ * Starts the single private Unix-socket Controller for one YUI_HOME. The
109
+ * shared server owns status/stop and rejects a second live instance; this
110
+ * layer adds only scheduler.scan plus an optional command dispatcher.
111
+ */
112
+ export async function startFileTaskController(home, store, delivery, dispatcher, options = {}) {
113
+ const runtime = new FileTaskController(store, delivery, options);
114
+ const server = await startControllerServer(home, async (method, params) => {
115
+ if (method === "scheduler.scan") {
116
+ if (!isEmptyJsonObject(params)) {
117
+ throw controllerApplicationError("INVALID_PARAMS", "scheduler.scan params are invalid.");
118
+ }
119
+ return schedulerResultJson(await runtime.pump());
120
+ }
121
+ if (dispatcher === undefined) {
122
+ throw controllerApplicationError("METHOD_NOT_FOUND", "Controller method was not found.");
123
+ }
124
+ return dispatcher(method, params);
125
+ });
126
+ runtime.start();
127
+ const closed = server.closed.finally(() => runtime.stop());
128
+ return {
129
+ runtime,
130
+ server,
131
+ closed,
132
+ close: async () => {
133
+ runtime.stop();
134
+ await server.close();
135
+ }
136
+ };
137
+ }
138
+ function positiveInteger(value, fallback, label) {
139
+ const resolved = value ?? fallback;
140
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
141
+ throw new TypeError(`${label} must be a positive integer`);
142
+ }
143
+ return resolved;
144
+ }
145
+ function schedulerResultJson(result) {
146
+ return JSON.parse(JSON.stringify(result));
147
+ }
148
+ function isEmptyJsonObject(value) {
149
+ return typeof value === "object"
150
+ && value !== null
151
+ && !Array.isArray(value)
152
+ && Object.keys(value).length === 0;
153
+ }
154
+ function controllerApplicationError(code, message) {
155
+ const error = Object.assign(new Error(message), { code });
156
+ error.name = "CoreApplicationError";
157
+ return error;
158
+ }
@@ -0,0 +1,37 @@
1
+ import { resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { resolveYuiHome } from "../storage/taskStore.js";
4
+ import { startFileTaskControllerRuntime } from "./runtime.js";
5
+ export async function runFileTaskControllerProcess(environment = process.env) {
6
+ const home = resolveYuiHome(environment);
7
+ const controller = await startFileTaskControllerRuntime(home, {
8
+ environment,
9
+ onError: (error) => {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ process.stderr.write(`Controller scheduler error: ${message}\n`);
12
+ }
13
+ });
14
+ const stop = () => {
15
+ void controller.close().catch(() => undefined);
16
+ };
17
+ process.once("SIGINT", stop);
18
+ process.once("SIGTERM", stop);
19
+ try {
20
+ await controller.closed;
21
+ }
22
+ finally {
23
+ process.removeListener("SIGINT", stop);
24
+ process.removeListener("SIGTERM", stop);
25
+ }
26
+ }
27
+ function isEntrypoint() {
28
+ return process.argv[1] !== undefined
29
+ && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
30
+ }
31
+ if (isEntrypoint()) {
32
+ void runFileTaskControllerProcess().catch((error) => {
33
+ const message = error instanceof Error ? error.message : "Controller failed to start.";
34
+ process.stderr.write(`Error: ${message}\n`);
35
+ process.exitCode = 1;
36
+ });
37
+ }