@zq-silk/yui 0.2.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -0,0 +1,610 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
6
+ import { inspectStorageSchema } from "../storage/storageSchema.js";
7
+ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
8
+ import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
9
+ import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
10
+ import { readControllerDiscovery } from "../core/controllerClient.js";
11
+ import { CONTROLLER_DISCOVERY_PATH } from "../core/protocol.js";
12
+ import { controllerSocketPath } from "../core/controllerEndpoint.js";
13
+ import { buildControllerResourceInventory } from "./resourceInventory.js";
14
+ import { CONTROLLER_DOMAIN_PATH, EPHEMERAL_DOMAIN_GRACE_MS, ephemeralDomainFingerprint, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "./domainIdentity.js";
15
+ export async function scanControllerResourceInventory(options) {
16
+ const currentHome = resolve(options.currentHome);
17
+ const environment = options.environment ?? process.env;
18
+ const observedAt = (options.now ?? (() => new Date()))();
19
+ const warnings = [];
20
+ const activeSockets = readActiveUnixSocketPaths(warnings);
21
+ const processes = listLinuxProcesses(warnings)
22
+ .filter(({ pid }) => pid !== process.pid);
23
+ const homes = new Set([currentHome]);
24
+ if (options.scope === "all") {
25
+ for (const process of processes) {
26
+ if (process.yuiHome !== undefined)
27
+ homes.add(process.yuiHome);
28
+ }
29
+ }
30
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
31
+ const rawTmuxArtifacts = listSocketArtifacts(join(tmpdir(), `tmux-${uid}`), /^yui-[a-f0-9]{24}$/u, "tmux-socket", activeSockets);
32
+ const homeFacts = [];
33
+ const associatedArtifacts = new Set();
34
+ for (const home of [...homes].sort()) {
35
+ const matchingProcesses = processes.filter(({ yuiHome }) => yuiHome === home);
36
+ const state = loadHomeState(home, warnings, options);
37
+ const domain = inspectRuntimeDomain(home, matchingProcesses, state.roles, state.storageStatus, observedAt);
38
+ const tmuxSocketPath = join(tmpdir(), `tmux-${uid}`, yuiTmuxServerName(home));
39
+ const tmuxArtifact = rawTmuxArtifacts.find(({ path }) => path === tmuxSocketPath);
40
+ if (tmuxArtifact !== undefined)
41
+ associatedArtifacts.add(tmuxArtifact.path);
42
+ const panes = options.panes !== undefined
43
+ ? options.panes
44
+ : tmuxArtifact?.active === true
45
+ ? inspectHomePanes(home, environment.YUI_TMUX_BIN ?? "tmux", warnings)
46
+ : [];
47
+ const discovery = await inspectDiscovery(home, matchingProcesses, activeSockets);
48
+ if (discovery.status === "valid"
49
+ && !discovery.socketActive
50
+ && matchingProcesses.some((candidate) => (candidate.kind === "controller"
51
+ && candidate.pid === discovery.pid
52
+ && candidate.startIdentity === discovery.processStartIdentity))) {
53
+ warnings.push(`Controller socket is not reachable for ${home}.`);
54
+ }
55
+ const artifacts = [];
56
+ if (discovery.status === "invalid" && discovery.artifact !== undefined) {
57
+ associatedArtifacts.add(discovery.artifact.path);
58
+ }
59
+ // Keep active sockets visible as report-only resources too. An expired
60
+ // domain must not self-close while its tmux namespace is still active;
61
+ // the next bounded pass can reclassify the exact socket after the pane and
62
+ // server converge.
63
+ if (tmuxArtifact !== undefined)
64
+ artifacts.push(tmuxArtifact);
65
+ const domainPath = join(home, CONTROLLER_DOMAIN_PATH);
66
+ const domainIdentity = readEphemeralDomainIdentity(home);
67
+ if (domainIdentity.status !== "absent" && existsSync(domainPath)) {
68
+ artifacts.push(fileArtifact(domainPath, "domain-identity", domain?.liveness === "active"));
69
+ }
70
+ const validDiscoveryProcess = discovery.status === "valid"
71
+ && (matchingProcesses.some((candidate) => (candidate.pid === discovery.pid
72
+ && candidate.startIdentity === discovery.processStartIdentity))
73
+ // The scanner omits its own process from signalable resources. A
74
+ // Controller nevertheless must recognize its exact discovery fence;
75
+ // otherwise its own expired-domain pass would delete controller.json
76
+ // and strand the still-running server without a callable endpoint.
77
+ || (discovery.pid === process.pid
78
+ && readLinuxProcessStartIdentity(process.pid) === discovery.processStartIdentity));
79
+ const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
80
+ const expectedControllerSocketPath = controllerSocketPath(home);
81
+ if (discovery.status === "valid"
82
+ && !validDiscoveryProcess
83
+ && existsSync(discoveryPath)) {
84
+ artifacts.push(fileArtifact(discoveryPath, "controller-discovery", false));
85
+ }
86
+ if (existsSync(expectedControllerSocketPath)
87
+ && !activeSockets.has(expectedControllerSocketPath)
88
+ && !validDiscoveryProcess) {
89
+ artifacts.push(fileArtifact(expectedControllerSocketPath, "controller-socket", false));
90
+ }
91
+ homeFacts.push({
92
+ yuiHome: home,
93
+ exists: existsSync(home),
94
+ storageStatus: state.storageStatus,
95
+ discovery,
96
+ panes,
97
+ roles: state.roles,
98
+ artifacts,
99
+ ...(domain === undefined ? {} : { domain })
100
+ });
101
+ }
102
+ const globalArtifacts = options.scope === "all"
103
+ ? rawTmuxArtifacts.filter(({ path }) => (!associatedArtifacts.has(path)))
104
+ : [];
105
+ return buildControllerResourceInventory({
106
+ schemaVersion: 1,
107
+ observedAt: observedAt.toISOString(),
108
+ currentHome,
109
+ scope: options.scope,
110
+ processes: options.scope === "all"
111
+ ? processes
112
+ : processes.filter(({ yuiHome }) => yuiHome === currentHome),
113
+ homes: homeFacts,
114
+ globalArtifacts,
115
+ warnings
116
+ });
117
+ }
118
+ export function parseLinuxProcessStat(stat, clockTicks, systemUptimeMs) {
119
+ const closing = stat.lastIndexOf(")");
120
+ if (closing < 0)
121
+ throw new Error("Linux process stat command is invalid.");
122
+ const fields = stat.slice(closing + 1).trim().split(/\s+/u);
123
+ const ppid = Number(fields[1]);
124
+ const userTicks = Number(fields[11]);
125
+ const systemTicks = Number(fields[12]);
126
+ const startIdentity = fields[19];
127
+ if (!Number.isSafeInteger(ppid)
128
+ || ppid < 0
129
+ || !Number.isFinite(userTicks)
130
+ || userTicks < 0
131
+ || !Number.isFinite(systemTicks)
132
+ || systemTicks < 0
133
+ || startIdentity === undefined
134
+ || !/^[0-9]{1,32}$/u.test(startIdentity)
135
+ || !Number.isFinite(clockTicks)
136
+ || clockTicks <= 0) {
137
+ throw new Error("Linux process stat fields are invalid.");
138
+ }
139
+ const startTicks = Number(startIdentity);
140
+ return {
141
+ ppid,
142
+ startIdentity,
143
+ cpuTimeMs: Math.max(0, Math.round((userTicks + systemTicks) * 1000 / clockTicks)),
144
+ ageMs: Math.max(0, Math.round(systemUptimeMs - startTicks * 1000 / clockTicks))
145
+ };
146
+ }
147
+ export function classifyRuntimeProcess(args, command) {
148
+ if (args.some((argument) => /(?:^|\/)controllerMain\.js$/u.test(argument))) {
149
+ return "controller";
150
+ }
151
+ if (args.includes("app-server"))
152
+ return "app-server";
153
+ if (command.startsWith("tmux: server")
154
+ || args.some((argument, index) => (argument === "-L"
155
+ && /^yui-[a-f0-9]{24}$/u.test(args[index + 1] ?? "")))) {
156
+ return "tmux-server";
157
+ }
158
+ const executable = basename(args[0] ?? command);
159
+ if (args.includes("web")
160
+ && args.some((argument) => /(?:^|\/)(?:cli|yui)(?:\.js)?$/u.test(argument))) {
161
+ return "web";
162
+ }
163
+ if (executable === "codex" || executable === "claude")
164
+ return "agent";
165
+ return "other";
166
+ }
167
+ function listLinuxProcesses(warnings) {
168
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
169
+ const clockTicks = readClockTicks();
170
+ const uptimeMs = readSystemUptimeMs();
171
+ let entries;
172
+ try {
173
+ // Request names only. On some Node/filesystem combinations, Dirent
174
+ // construction may lstat unknown /proc entries while readdir is still in
175
+ // progress; a process exiting in that window aborts the whole inventory.
176
+ entries = readdirSync("/proc", { encoding: "utf8" });
177
+ }
178
+ catch (error) {
179
+ warnings.push(`Cannot enumerate Linux processes: ${message(error)}`);
180
+ return [];
181
+ }
182
+ return entries.flatMap((entryName) => {
183
+ try {
184
+ const pid = linuxProcessEntryPid(entryName);
185
+ if (pid === undefined)
186
+ return [];
187
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
188
+ const processUid = parseStatusNumber(status, "Uid");
189
+ if (processUid !== uid)
190
+ return [];
191
+ const parsed = parseLinuxProcessStat(readFileSync(`/proc/${pid}/stat`, "utf8"), clockTicks, uptimeMs);
192
+ const args = splitNullDelimited(readFileSync(`/proc/${pid}/cmdline`));
193
+ const command = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
194
+ const yuiHome = readYuiHome(pid);
195
+ const io = readProcessIo(pid);
196
+ return [{
197
+ pid,
198
+ ppid: parsed.ppid,
199
+ uid: processUid,
200
+ startIdentity: parsed.startIdentity,
201
+ ...(yuiHome === undefined ? {} : { yuiHome }),
202
+ kind: classifyRuntimeProcess(args, command),
203
+ command,
204
+ args,
205
+ rssBytes: parseStatusNumber(status, "VmRSS", 0) * 1024,
206
+ cpuTimeMs: parsed.cpuTimeMs,
207
+ ...(io === undefined ? {} : io),
208
+ ageMs: parsed.ageMs
209
+ }];
210
+ }
211
+ catch {
212
+ // Processes commonly exit during a /proc scan. A point-in-time inventory
213
+ // omits those races instead of turning them into persistent warnings.
214
+ return [];
215
+ }
216
+ });
217
+ }
218
+ /**
219
+ * Resolve one numeric /proc entry name without asking the filesystem for
220
+ * entry metadata. Its process files are then read inside the per-PID race
221
+ * boundary above.
222
+ */
223
+ export function linuxProcessEntryPid(entryName) {
224
+ if (!/^[1-9][0-9]*$/u.test(entryName))
225
+ return undefined;
226
+ const pid = Number(entryName);
227
+ return Number.isSafeInteger(pid) ? pid : undefined;
228
+ }
229
+ function readYuiHome(pid) {
230
+ const environment = splitNullDelimited(readFileSync(`/proc/${pid}/environ`));
231
+ const entry = environment.find((value) => value.startsWith("YUI_HOME="));
232
+ const value = entry?.slice("YUI_HOME=".length);
233
+ return value === undefined || value.length === 0 ? undefined : resolve(value);
234
+ }
235
+ function readProcessIo(pid) {
236
+ try {
237
+ const contents = readFileSync(`/proc/${pid}/io`, "utf8");
238
+ const readBytes = parseOptionalIoCounter(contents, "read_bytes");
239
+ const writeBytes = parseOptionalIoCounter(contents, "write_bytes");
240
+ return readBytes === undefined || writeBytes === undefined
241
+ ? undefined
242
+ : { ioReadBytes: readBytes, ioWriteBytes: writeBytes };
243
+ }
244
+ catch {
245
+ // /proc/<pid>/io can disappear or be restricted during a point-in-time scan.
246
+ // CPU/RSS inventory remains useful, but no IO activity is inferred.
247
+ return undefined;
248
+ }
249
+ }
250
+ function parseOptionalIoCounter(contents, label) {
251
+ const match = new RegExp(`^${label}:\\s+([0-9]+)`, "mu").exec(contents);
252
+ if (match === null)
253
+ return undefined;
254
+ const value = Number(match[1]);
255
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
256
+ }
257
+ function splitNullDelimited(value) {
258
+ return value.toString("utf8").split("\0").filter((entry) => entry.length > 0);
259
+ }
260
+ function parseStatusNumber(status, label, fallback) {
261
+ const match = new RegExp(`^${label}:\\s+([0-9]+)`, "mu").exec(status);
262
+ if (match === null) {
263
+ if (fallback !== undefined)
264
+ return fallback;
265
+ throw new Error(`Linux process ${label} is unavailable.`);
266
+ }
267
+ const value = Number(match[1]);
268
+ if (!Number.isSafeInteger(value) || value < 0) {
269
+ throw new Error(`Linux process ${label} is invalid.`);
270
+ }
271
+ return value;
272
+ }
273
+ function readClockTicks() {
274
+ const result = spawnSync("getconf", ["CLK_TCK"], {
275
+ encoding: "utf8",
276
+ stdio: ["ignore", "pipe", "ignore"]
277
+ });
278
+ const value = Number(result.stdout?.trim());
279
+ return Number.isSafeInteger(value) && value > 0 ? value : 100;
280
+ }
281
+ function readSystemUptimeMs() {
282
+ const seconds = Number(readFileSync("/proc/uptime", "utf8").split(/\s+/u)[0]);
283
+ if (!Number.isFinite(seconds) || seconds < 0) {
284
+ throw new Error("Linux system uptime is invalid.");
285
+ }
286
+ return seconds * 1000;
287
+ }
288
+ function readActiveUnixSocketPaths(warnings) {
289
+ try {
290
+ const lines = readFileSync("/proc/net/unix", "utf8").split("\n").slice(1);
291
+ return new Set(lines.flatMap((line) => {
292
+ const fields = line.trim().split(/\s+/u);
293
+ const path = fields[7];
294
+ return path === undefined || !path.startsWith("/") ? [] : [path];
295
+ }));
296
+ }
297
+ catch (error) {
298
+ warnings.push(`Cannot inspect Unix sockets: ${message(error)}`);
299
+ return new Set();
300
+ }
301
+ }
302
+ function listSocketArtifacts(directory, pattern, artifactKind, activeSockets) {
303
+ try {
304
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
305
+ if (!pattern.test(entry.name))
306
+ return [];
307
+ const path = join(directory, entry.name);
308
+ let metadata;
309
+ try {
310
+ metadata = lstatSync(path);
311
+ }
312
+ catch {
313
+ return [];
314
+ }
315
+ if (!metadata.isSocket())
316
+ return [];
317
+ return [{
318
+ artifactKind,
319
+ path,
320
+ active: activeSockets.has(path),
321
+ fingerprint: statFingerprint(metadata)
322
+ }];
323
+ });
324
+ }
325
+ catch {
326
+ return [];
327
+ }
328
+ }
329
+ async function inspectDiscovery(home, processes, activeSockets) {
330
+ const path = join(home, CONTROLLER_DISCOVERY_PATH);
331
+ try {
332
+ const discovery = await readControllerDiscovery(home);
333
+ return {
334
+ status: "valid",
335
+ pid: discovery.pid,
336
+ processStartIdentity: discovery.processStartIdentity,
337
+ socketPath: discovery.socketPath,
338
+ socketActive: activeSockets.has(discovery.socketPath),
339
+ fingerprint: existsSync(path)
340
+ ? statFingerprint(lstatSync(path))
341
+ : `${discovery.pid}:${discovery.processStartIdentity}`
342
+ };
343
+ }
344
+ catch (error) {
345
+ if (!existsSync(path))
346
+ return { status: "absent" };
347
+ const artifact = fileArtifact(path, "controller-discovery", false);
348
+ const possibleOwner = processes.some(({ kind }) => kind === "controller");
349
+ return possibleOwner
350
+ ? { status: "invalid", artifact: { ...artifact, active: true } }
351
+ : { status: "invalid", artifact };
352
+ }
353
+ }
354
+ function inspectRuntimeDomain(home, processes, roles, storageStatus, now) {
355
+ const stored = readEphemeralDomainIdentity(home);
356
+ if (stored.status === "absent") {
357
+ // A current, ordinary YUI_HOME is a real control domain, not a disposable
358
+ // test domain. Keep it visible even when it is currently idle so a stale
359
+ // artifact cannot acquire ephemeral cleanup authority by omission. An
360
+ // uninitialized temporary directory with no runtime facts remains absent
361
+ // to preserve the legacy empty-home inventory behavior.
362
+ if (storageStatus !== "current"
363
+ && processes.length === 0
364
+ && roles.length === 0)
365
+ return undefined;
366
+ return {
367
+ kind: "unmarked",
368
+ liveness: "unknown",
369
+ disposition: "review",
370
+ reasonCode: "unmarked-domain",
371
+ fingerprint: `unmarked:${home}`,
372
+ tmuxTargets: [],
373
+ ageMs: 0,
374
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
375
+ };
376
+ }
377
+ if (stored.status === "invalid" || stored.identity === undefined) {
378
+ return {
379
+ kind: "invalid",
380
+ liveness: "unknown",
381
+ disposition: "review",
382
+ reasonCode: "invalid-domain-identity",
383
+ fingerprint: `invalid:${stored.fingerprint ?? home}`,
384
+ tmuxTargets: [],
385
+ ageMs: 0,
386
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
387
+ };
388
+ }
389
+ const identity = stored.identity;
390
+ const ageMs = Math.max(0, now.getTime() - Date.parse(identity.createdAt));
391
+ if (identity.tmuxServer !== yuiTmuxServerName(home)) {
392
+ return {
393
+ kind: "invalid",
394
+ liveness: "unknown",
395
+ disposition: "review",
396
+ reasonCode: "invalid-domain-tmux-server",
397
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
398
+ hostPid: identity.hostPid,
399
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
400
+ token: identity.token,
401
+ tmuxServer: identity.tmuxServer,
402
+ tmuxTargets: identity.tmuxTargets,
403
+ createdAt: identity.createdAt,
404
+ ageMs,
405
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
406
+ };
407
+ }
408
+ const hostStartIdentity = readLinuxProcessStartIdentity(identity.hostPid);
409
+ const hostPathExists = existsSync(`/proc/${identity.hostPid}`);
410
+ const hostActive = hostStartIdentity === identity.hostProcessStartIdentity;
411
+ const storageSafe = storageStatus === "current";
412
+ if (hostActive) {
413
+ return {
414
+ kind: "ephemeral-test",
415
+ liveness: "active",
416
+ disposition: "protected",
417
+ reasonCode: "ephemeral-host-active",
418
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
419
+ hostPid: identity.hostPid,
420
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
421
+ token: identity.token,
422
+ tmuxServer: identity.tmuxServer,
423
+ tmuxTargets: identity.tmuxTargets,
424
+ createdAt: identity.createdAt,
425
+ ageMs,
426
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
427
+ };
428
+ }
429
+ const activeRole = roles.some((role) => ((role.ownerKind === "task-role" && role.taskStatus === "active")
430
+ // Global Roles have no Task status. Only a live native Session is a
431
+ // durable liveness fact for that scope; stopped/broken history must not
432
+ // protect an expired disposable domain merely because it has an ID.
433
+ || (role.ownerKind === "global-role" && role.nativeSessionId !== undefined)));
434
+ if (!storageSafe) {
435
+ return {
436
+ kind: "ephemeral-test",
437
+ liveness: hostActive ? "active" : "expired",
438
+ disposition: "review",
439
+ reasonCode: `ephemeral-storage-${storageStatus}`,
440
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
441
+ hostPid: identity.hostPid,
442
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
443
+ token: identity.token,
444
+ tmuxServer: identity.tmuxServer,
445
+ tmuxTargets: identity.tmuxTargets,
446
+ createdAt: identity.createdAt,
447
+ ageMs,
448
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
449
+ };
450
+ }
451
+ if (activeRole) {
452
+ return {
453
+ kind: "ephemeral-test",
454
+ liveness: "expired",
455
+ disposition: "protected",
456
+ reasonCode: "ephemeral-active-task-role",
457
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
458
+ hostPid: identity.hostPid,
459
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
460
+ token: identity.token,
461
+ tmuxServer: identity.tmuxServer,
462
+ tmuxTargets: identity.tmuxTargets,
463
+ createdAt: identity.createdAt,
464
+ ageMs,
465
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
466
+ };
467
+ }
468
+ if (hostStartIdentity === undefined && hostPathExists) {
469
+ return {
470
+ kind: "ephemeral-test",
471
+ liveness: "expired",
472
+ disposition: "review",
473
+ reasonCode: "ephemeral-host-identity-unavailable",
474
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
475
+ hostPid: identity.hostPid,
476
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
477
+ token: identity.token,
478
+ tmuxServer: identity.tmuxServer,
479
+ tmuxTargets: identity.tmuxTargets,
480
+ createdAt: identity.createdAt,
481
+ ageMs,
482
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
483
+ };
484
+ }
485
+ if (ageMs < EPHEMERAL_DOMAIN_GRACE_MS) {
486
+ return {
487
+ kind: "ephemeral-test",
488
+ liveness: "expired",
489
+ disposition: "review",
490
+ reasonCode: hostStartIdentity === undefined
491
+ ? "ephemeral-host-identity-unavailable"
492
+ : "ephemeral-host-grace",
493
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
494
+ hostPid: identity.hostPid,
495
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
496
+ token: identity.token,
497
+ tmuxServer: identity.tmuxServer,
498
+ tmuxTargets: identity.tmuxTargets,
499
+ createdAt: identity.createdAt,
500
+ ageMs,
501
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
502
+ };
503
+ }
504
+ return {
505
+ kind: "ephemeral-test",
506
+ liveness: "expired",
507
+ disposition: "safe",
508
+ reasonCode: hostStartIdentity === undefined
509
+ ? "ephemeral-host-dead"
510
+ : "ephemeral-host-identity-mismatch",
511
+ fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
512
+ hostPid: identity.hostPid,
513
+ hostProcessStartIdentity: identity.hostProcessStartIdentity,
514
+ token: identity.token,
515
+ tmuxServer: identity.tmuxServer,
516
+ tmuxTargets: identity.tmuxTargets,
517
+ createdAt: identity.createdAt,
518
+ ageMs,
519
+ graceMs: EPHEMERAL_DOMAIN_GRACE_MS
520
+ };
521
+ }
522
+ function loadHomeState(home, warnings, options) {
523
+ const schema = (options.inspectStorage ?? inspectStorageSchema)(home);
524
+ const recordOnlyOlder = schema.status === "unsupported"
525
+ && schema.incompatibleComponent === "record"
526
+ && schema.direction === "older"
527
+ && schema.currentLayoutVersion === schema.latestLayoutVersion
528
+ && schema.currentAggregateSchemaVersion === schema.latestAggregateSchemaVersion;
529
+ if (schema.status !== "current" && !recordOnlyOlder) {
530
+ return {
531
+ storageStatus: schema.status,
532
+ roles: []
533
+ };
534
+ }
535
+ try {
536
+ const store = (options.openCompatibleStore ?? openCompatibleFileTaskStore)(home);
537
+ const roles = store.listGlobalRoles().map((role) => {
538
+ const session = activeLiveRoleAgentSession(store.getGlobalRoleSessionSet(role.name));
539
+ const binding = role.agentBindings[role.activeAgentId];
540
+ const agentId = session?.effective.agentId ?? role.activeAgentId;
541
+ const adapterId = session?.effective.adapterId ?? binding?.adapterId;
542
+ return {
543
+ ownerKind: "global-role",
544
+ roleName: role.name,
545
+ agentId,
546
+ ...(adapterId === undefined ? {} : { adapterId }),
547
+ ...(session === null ? {} : { nativeSessionId: session.nativeSessionId }),
548
+ ...(session?.launchId === undefined ? {} : { launchId: session.launchId })
549
+ };
550
+ });
551
+ for (const task of store.listTasks()) {
552
+ for (const role of store.listRoles(task.id)) {
553
+ const session = activeLiveRoleAgentSession(store.getRoleSessionSet(task.id, role.name));
554
+ const binding = role.agentBindings[role.activeAgentId];
555
+ const run = store.getActiveAgentRun(task.id, role.name);
556
+ const agentId = session?.effective.agentId ?? role.activeAgentId;
557
+ const adapterId = session?.effective.adapterId ?? binding?.adapterId;
558
+ roles.push({
559
+ ownerKind: "task-role",
560
+ taskId: task.id,
561
+ taskTitle: task.title,
562
+ taskStatus: task.status,
563
+ roleName: role.name,
564
+ agentId,
565
+ ...(adapterId === undefined ? {} : { adapterId }),
566
+ ...(session === null ? {} : { nativeSessionId: session.nativeSessionId }),
567
+ ...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
568
+ ...(run === null ? {} : { runId: run.id })
569
+ });
570
+ }
571
+ }
572
+ return { storageStatus: schema.status, roles };
573
+ }
574
+ catch (error) {
575
+ warnings.push(`Cannot load runtime ownership for ${home}: ${message(error)}`);
576
+ return { storageStatus: "invalid", roles: [] };
577
+ }
578
+ }
579
+ function inspectHomePanes(home, tmuxBin, warnings) {
580
+ try {
581
+ return new TmuxManager(tmuxBin, new NodeCommandExecutor(), {
582
+ yuiHome: home
583
+ }).inspectRolePaneInventory();
584
+ }
585
+ catch (error) {
586
+ warnings.push(`Cannot inspect tmux for ${home}: ${message(error)}`);
587
+ return [];
588
+ }
589
+ }
590
+ function fileArtifact(path, artifactKind, active) {
591
+ const metadata = lstatSync(path);
592
+ return {
593
+ artifactKind,
594
+ path,
595
+ active,
596
+ fingerprint: statFingerprint(metadata)
597
+ };
598
+ }
599
+ function statFingerprint(metadata) {
600
+ return [
601
+ metadata.dev,
602
+ metadata.ino,
603
+ metadata.mode,
604
+ metadata.size,
605
+ Math.trunc(metadata.mtimeMs)
606
+ ].join(":");
607
+ }
608
+ function message(error) {
609
+ return error instanceof Error ? error.message : String(error);
610
+ }