@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,643 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
5
+ export const YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR = "YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR";
6
+ export const YUI_TASK_RUNTIME_SERVICE_NAMESPACE = "YUI_TASK_RUNTIME_SERVICE_NAMESPACE";
7
+ const MARKER_FILE = ".yui-task-runtime-owner.json";
8
+ /**
9
+ * One project-neutral implementation owns descriptor construction, preflight,
10
+ * exact generation activation, and exact generation cleanup. It never scans by
11
+ * process name, Role, PID, age, or an ambient Home.
12
+ */
13
+ export class FileTaskRuntimeIsolation {
14
+ #runtimeRoot;
15
+ #controlPlane;
16
+ #pathLayout;
17
+ #inspectResources;
18
+ constructor(options) {
19
+ this.#runtimeRoot = canonicalPath(options.runtimeRoot, "Task runtime root");
20
+ this.#controlPlane = normalizeControlBoundary(options.controlPlane);
21
+ this.#pathLayout = taskRuntimePathLayout(options.pathLayout);
22
+ this.#inspectResources = options.inspectResources;
23
+ }
24
+ preflight(input) {
25
+ const descriptor = createTaskRuntimeIsolationDescriptor({
26
+ ...input,
27
+ runtimeRoot: this.#runtimeRoot,
28
+ pathLayout: this.#pathLayout
29
+ });
30
+ const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
31
+ assertTaskRuntimeIsolationPreflight({
32
+ descriptor,
33
+ workspace: input.workspace,
34
+ runtimeRoot: this.#runtimeRoot,
35
+ pathLayout: this.#pathLayout,
36
+ controlPlane: this.#controlPlane,
37
+ resources: [
38
+ ...inspectGenerationRoot(descriptor, fingerprint),
39
+ ...(this.#inspectResources?.(descriptor) ?? [])
40
+ ],
41
+ allowExactActive: input.allowExactActive === true
42
+ });
43
+ return Object.freeze({
44
+ descriptor,
45
+ fingerprint,
46
+ environment: taskRuntimeIsolationEnvironment(descriptor)
47
+ });
48
+ }
49
+ activate(preparation) {
50
+ const { descriptor, fingerprint } = validatePreparation(preparation);
51
+ const root = descriptor.roots.generation;
52
+ const existing = inspectGenerationRoot(descriptor, fingerprint);
53
+ if (existing.length > 0) {
54
+ const [resource] = existing;
55
+ if (resource?.ownership !== "owned"
56
+ || resource.descriptorFingerprint !== fingerprint) {
57
+ throw new Error(`Task runtime generation is not exactly owned: ${root}.`);
58
+ }
59
+ ensureOwnedDirectories(descriptor);
60
+ return;
61
+ }
62
+ ensureDirectoryChain(this.#runtimeRoot, dirname(root));
63
+ mkdirSync(root, { mode: 0o700 });
64
+ try {
65
+ const marker = {
66
+ schemaVersion: 1,
67
+ kind: "yui-task-runtime-resource-owner",
68
+ fingerprint,
69
+ descriptor
70
+ };
71
+ writeFileSync(join(root, MARKER_FILE), `${JSON.stringify(marker)}\n`, {
72
+ flag: "wx",
73
+ mode: 0o600
74
+ });
75
+ ensureOwnedDirectories(descriptor);
76
+ }
77
+ catch (error) {
78
+ const current = inspectGenerationRoot(descriptor, fingerprint);
79
+ if (current[0]?.ownership === "owned")
80
+ throw error;
81
+ // The directory was created by this exact activation but never acquired
82
+ // its marker. Remove it only while it remains empty; concurrent unmarked
83
+ // content is ambiguous and must be preserved.
84
+ rmdirSync(root);
85
+ throw error;
86
+ }
87
+ }
88
+ cleanup(preparation, reason) {
89
+ requireCleanupReason(reason);
90
+ const { descriptor, fingerprint } = validatePreparation(preparation);
91
+ const resources = [
92
+ ...inspectGenerationRoot(descriptor, fingerprint),
93
+ ...(this.#inspectResources?.(descriptor) ?? [])
94
+ ];
95
+ if (resources.length === 0)
96
+ return;
97
+ planTaskRuntimeCleanup(descriptor, reason, resources);
98
+ // Re-read the sole durable marker immediately before deletion. A missing,
99
+ // replaced, symlinked, or mismatched generation is never cleanup authority.
100
+ const current = inspectGenerationRoot(descriptor, fingerprint);
101
+ if (current.length !== 1
102
+ || current[0]?.ownership !== "owned"
103
+ || current[0].descriptorFingerprint !== fingerprint) {
104
+ throw new Error("Task runtime resources changed since cleanup preflight.");
105
+ }
106
+ const root = descriptor.roots.generation;
107
+ const claimed = `${root}.cleanup-${randomBytes(16).toString("hex")}`;
108
+ renameSync(root, claimed);
109
+ try {
110
+ const marker = parseMarker(readFileSync(join(claimed, MARKER_FILE), "utf8"));
111
+ if (marker.fingerprint !== fingerprint
112
+ || JSON.stringify(marker.descriptor) !== JSON.stringify(descriptor)) {
113
+ throw new Error("Task runtime resources changed during cleanup claim.");
114
+ }
115
+ rmSync(claimed, { recursive: true });
116
+ }
117
+ catch (error) {
118
+ // Preserve a claimed-but-unverified resource. Restore its exact path only
119
+ // when no concurrent generation has appeared there; never delete it.
120
+ try {
121
+ renameSync(claimed, root);
122
+ }
123
+ catch (restoreError) {
124
+ throw new Error("Task runtime cleanup claim could not be verified or restored.", { cause: restoreError });
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ cleanupTaskLaunch(input) {
130
+ const taskId = requireIdentity(input.taskId, "Task id");
131
+ const launchId = requireIdentity(input.launchId, "Launch id");
132
+ requireCleanupReason(input.reason);
133
+ const taskRoot = taskRuntimeInventoryRoot(this.#runtimeRoot, taskId, this.#pathLayout);
134
+ let entries;
135
+ try {
136
+ const metadata = lstatSync(taskRoot);
137
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
138
+ throw new Error("Task runtime Task root is ambiguous.");
139
+ }
140
+ entries = readdirSync(taskRoot, { withFileTypes: true });
141
+ }
142
+ catch (error) {
143
+ if (isNodeCode(error, "ENOENT"))
144
+ return "absent";
145
+ throw error;
146
+ }
147
+ const launchDigest = taskRuntimeLaunchDigest(taskId, launchId, this.#pathLayout);
148
+ const matches = [];
149
+ for (const entry of entries) {
150
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
151
+ throw new Error("Task runtime owner inventory is ambiguous.");
152
+ }
153
+ const generationRoot = join(taskRoot, entry.name, launchDigest);
154
+ let metadata;
155
+ try {
156
+ metadata = lstatSync(generationRoot);
157
+ }
158
+ catch (error) {
159
+ if (isNodeCode(error, "ENOENT"))
160
+ continue;
161
+ throw new Error("Task runtime generation inventory is ambiguous.", { cause: error });
162
+ }
163
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
164
+ throw new Error("Task runtime generation inventory is ambiguous.");
165
+ }
166
+ let marker;
167
+ try {
168
+ marker = parseMarker(readFileSync(join(generationRoot, MARKER_FILE), "utf8"));
169
+ }
170
+ catch (error) {
171
+ throw new Error("Task runtime launch generation is unmarked or invalid.", {
172
+ cause: error
173
+ });
174
+ }
175
+ const descriptor = marker.descriptor;
176
+ const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
177
+ if (marker.fingerprint !== fingerprint
178
+ || descriptor.taskId !== taskId
179
+ || descriptor.generation.launchId !== launchId
180
+ || descriptor.roots.generation !== generationRoot) {
181
+ throw new Error("Task runtime launch generation ownership is mismatched.");
182
+ }
183
+ matches.push({
184
+ descriptor,
185
+ fingerprint,
186
+ environment: taskRuntimeIsolationEnvironment(descriptor)
187
+ });
188
+ }
189
+ if (matches.length === 0)
190
+ return "absent";
191
+ if (matches.length !== 1) {
192
+ throw new Error("Task runtime launch generation ownership is ambiguous.");
193
+ }
194
+ this.cleanup(matches[0], input.reason);
195
+ return "cleaned";
196
+ }
197
+ }
198
+ export function createTaskRuntimeIsolationDescriptor(input) {
199
+ const workspace = validateManagedWorkspace(input.workspace);
200
+ const owner = taskRuntimeWorkspaceOwner(workspace.owner);
201
+ const taskId = requireIdentity(owner.taskId, "Task id");
202
+ const runId = input.runId === undefined
203
+ ? undefined
204
+ : requireIdentity(input.runId, "Run id");
205
+ const launchId = requireIdentity(input.launchId, "Launch id");
206
+ const generationId = requireIdentity(input.generationId, "Generation id");
207
+ const runtimeRoot = canonicalPath(input.runtimeRoot, "Task runtime root");
208
+ const pathLayout = taskRuntimePathLayout(input.pathLayout);
209
+ const generation = taskRuntimeGenerationRoot(runtimeRoot, taskId, owner, launchId, pathLayout);
210
+ const declared = capabilities(input.policy?.declaredExternalCapabilities ?? [], "Declared external capability");
211
+ const requested = capabilities(input.policy?.requestedExternalCapabilities ?? [], "Requested external capability");
212
+ const portPreference = ports(input.policy?.portPreference ?? [], "Port preference");
213
+ const portAllocations = allocations(input.policy?.portAllocations ?? []);
214
+ return Object.freeze({
215
+ schemaVersion: 1,
216
+ kind: "yui-task-runtime-isolation",
217
+ taskId,
218
+ workspace: Object.freeze({ owner, root: canonicalPath(workspace.root, "Workspace root") }),
219
+ roots: Object.freeze({
220
+ generation,
221
+ data: join(generation, "data"),
222
+ cache: join(generation, "cache"),
223
+ temporary: join(generation, "tmp")
224
+ }),
225
+ serviceNamespace: taskRuntimeServiceNamespace(taskId, owner, launchId, generationId),
226
+ portPreference,
227
+ portAllocations,
228
+ externalCapabilities: Object.freeze({ declared, requested }),
229
+ generation: Object.freeze({
230
+ ...(runId === undefined ? {} : { runId }),
231
+ launchId,
232
+ generationId
233
+ })
234
+ });
235
+ }
236
+ export function parseTaskRuntimeIsolationDescriptor(serialized) {
237
+ let value;
238
+ try {
239
+ value = JSON.parse(serialized);
240
+ }
241
+ catch {
242
+ throw new Error("Task runtime isolation descriptor is invalid JSON.");
243
+ }
244
+ if (!isRecord(value) || value.kind !== "yui-task-runtime-isolation") {
245
+ throw new Error(`Expected yui-task-runtime-isolation descriptor; found ${isRecord(value) && typeof value.kind === "string" ? value.kind : "unknown"}.`);
246
+ }
247
+ if (value.schemaVersion !== 1) {
248
+ throw new Error("Task runtime isolation descriptor schema version is invalid.");
249
+ }
250
+ const workspace = requireRecord(value.workspace, "Task runtime workspace");
251
+ const roots = requireRecord(value.roots, "Task runtime roots");
252
+ const generation = requireRecord(value.generation, "Task runtime generation");
253
+ const external = requireRecord(value.externalCapabilities, "Task runtime external capabilities");
254
+ const owner = taskRuntimeWorkspaceOwner(requireRecord(workspace.owner, "Task runtime workspace owner"));
255
+ const descriptor = {
256
+ schemaVersion: 1,
257
+ kind: "yui-task-runtime-isolation",
258
+ taskId: requireIdentity(value.taskId, "Task id"),
259
+ workspace: {
260
+ owner,
261
+ root: canonicalPath(workspace.root, "Workspace root")
262
+ },
263
+ roots: {
264
+ generation: canonicalPath(roots.generation, "Task runtime generation root"),
265
+ data: canonicalPath(roots.data, "Task runtime data root"),
266
+ cache: canonicalPath(roots.cache, "Task runtime cache root"),
267
+ temporary: canonicalPath(roots.temporary, "Task runtime temporary root")
268
+ },
269
+ serviceNamespace: requireIdentity(value.serviceNamespace, "Service namespace"),
270
+ portPreference: ports(value.portPreference, "Port preference"),
271
+ portAllocations: allocations(value.portAllocations),
272
+ externalCapabilities: {
273
+ declared: capabilities(external.declared, "Declared external capability"),
274
+ requested: capabilities(external.requested, "Requested external capability")
275
+ },
276
+ generation: {
277
+ ...(generation.runId === undefined
278
+ ? {}
279
+ : { runId: requireIdentity(generation.runId, "Run id") }),
280
+ launchId: requireIdentity(generation.launchId, "Launch id"),
281
+ generationId: requireIdentity(generation.generationId, "Generation id")
282
+ }
283
+ };
284
+ return Object.freeze(descriptor);
285
+ }
286
+ export function taskRuntimeIsolationFingerprint(descriptor) {
287
+ return digest(JSON.stringify(parseTaskRuntimeIsolationDescriptor(JSON.stringify(descriptor))));
288
+ }
289
+ export function taskRuntimeIsolationEnvironment(descriptor) {
290
+ const validated = parseTaskRuntimeIsolationDescriptor(JSON.stringify(descriptor));
291
+ return Object.freeze({
292
+ TMPDIR: validated.roots.temporary,
293
+ XDG_CACHE_HOME: validated.roots.cache,
294
+ XDG_DATA_HOME: validated.roots.data,
295
+ XDG_STATE_HOME: join(validated.roots.data, "state"),
296
+ XDG_RUNTIME_DIR: join(validated.roots.temporary, "runtime"),
297
+ [YUI_TASK_RUNTIME_SERVICE_NAMESPACE]: validated.serviceNamespace,
298
+ [YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR]: JSON.stringify(validated)
299
+ });
300
+ }
301
+ export function assertTaskRuntimeIsolationPreflight(input) {
302
+ const descriptor = parseTaskRuntimeIsolationDescriptor(JSON.stringify(input.descriptor));
303
+ const workspace = validateManagedWorkspace(input.workspace);
304
+ const expectedOwner = taskRuntimeWorkspaceOwner(workspace.owner);
305
+ if (descriptor.taskId !== expectedOwner.taskId
306
+ || JSON.stringify(descriptor.workspace.owner) !== JSON.stringify(expectedOwner)
307
+ || descriptor.workspace.root !== workspace.root) {
308
+ throw new Error("Task runtime owner or workspace does not match its ManagedWorkspace.");
309
+ }
310
+ const runtimeRoot = canonicalPath(input.runtimeRoot, "Task runtime root");
311
+ const pathLayout = taskRuntimePathLayout(input.pathLayout);
312
+ const expectedGeneration = taskRuntimeGenerationRoot(runtimeRoot, descriptor.taskId, expectedOwner, descriptor.generation.launchId, pathLayout);
313
+ if (descriptor.roots.generation !== expectedGeneration
314
+ || descriptor.roots.data !== join(expectedGeneration, "data")
315
+ || descriptor.roots.cache !== join(expectedGeneration, "cache")
316
+ || descriptor.roots.temporary !== join(expectedGeneration, "tmp")) {
317
+ throw new Error("Task runtime roots do not match the exact Task, owner, and launch identity.");
318
+ }
319
+ if (descriptor.serviceNamespace !== taskRuntimeServiceNamespace(descriptor.taskId, expectedOwner, descriptor.generation.launchId, descriptor.generation.generationId)) {
320
+ throw new Error("Task runtime service namespace does not match its exact Task launch generation.");
321
+ }
322
+ if (!isWithin(runtimeRoot, descriptor.roots.generation)) {
323
+ throw new Error("Task runtime generation is outside its runtime root.");
324
+ }
325
+ for (const [name, root] of Object.entries(descriptor.roots)) {
326
+ if (name !== "generation" && !isWithin(descriptor.roots.generation, root)) {
327
+ throw new Error(`Task runtime ${name} root is outside its exact generation.`);
328
+ }
329
+ }
330
+ const control = normalizeControlBoundary(input.controlPlane);
331
+ const protectedPaths = [control.yuiHome, ...control.globalInstallPaths];
332
+ for (const root of Object.values(descriptor.roots)) {
333
+ if (protectedPaths.some((path) => pathsOverlap(root, path))) {
334
+ throw new Error("Task runtime roots overlap the control YUI_HOME or a global install path.");
335
+ }
336
+ if (pathsOverlap(root, descriptor.workspace.root)) {
337
+ throw new Error("Task runtime roots must not dirty the managed workspace.");
338
+ }
339
+ if (pathsOverlap(root, control.controllerSocketPath)) {
340
+ throw new Error("Task runtime roots overlap the Controller socket.");
341
+ }
342
+ }
343
+ if (descriptor.serviceNamespace === control.tmuxNamespace) {
344
+ throw new Error("Task runtime service namespace overlaps the Controller tmux namespace.");
345
+ }
346
+ const declared = new Set(descriptor.externalCapabilities.declared);
347
+ const undeclared = descriptor.externalCapabilities.requested.find((capability) => !declared.has(capability));
348
+ if (undeclared !== undefined) {
349
+ throw new Error(`Task runtime external capability is undeclared: ${undeclared}.`);
350
+ }
351
+ const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
352
+ const resourceIds = new Set();
353
+ for (const resource of input.resources ?? []) {
354
+ validateObservation(resource);
355
+ if (resourceIds.has(resource.id)) {
356
+ throw new Error(`Task runtime resource inventory is ambiguous: ${resource.id}.`);
357
+ }
358
+ resourceIds.add(resource.id);
359
+ const exact = resource.ownership === "owned"
360
+ && resource.descriptorFingerprint === fingerprint;
361
+ if (!exact) {
362
+ throw new Error(`Task runtime resource is ambiguous or externally owned: ${resource.id}.`);
363
+ }
364
+ if (resource.state === "unknown"
365
+ || (resource.state === "active" && input.allowExactActive !== true)) {
366
+ throw new Error(`Task runtime resource is not safely reusable: ${resource.id}.`);
367
+ }
368
+ }
369
+ return descriptor;
370
+ }
371
+ export function planTaskRuntimeCleanup(descriptor, reason, resources) {
372
+ requireCleanupReason(reason);
373
+ const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
374
+ const ids = new Set();
375
+ for (const resource of resources) {
376
+ validateObservation(resource);
377
+ if (ids.has(resource.id)) {
378
+ throw new Error(`Task runtime resource inventory is ambiguous: ${resource.id}.`);
379
+ }
380
+ ids.add(resource.id);
381
+ if (resource.ownership !== "owned"
382
+ || resource.descriptorFingerprint !== fingerprint) {
383
+ throw new Error(`Task runtime cleanup refused an unowned resource: ${resource.id}.`);
384
+ }
385
+ if (resource.state !== "inactive") {
386
+ throw new Error(`Task runtime cleanup refused an active or unknown resource: ${resource.id}.`);
387
+ }
388
+ }
389
+ return Object.freeze([...ids].sort());
390
+ }
391
+ function inspectGenerationRoot(descriptor, expectedFingerprint) {
392
+ const root = descriptor.roots.generation;
393
+ let metadata;
394
+ try {
395
+ metadata = lstatSync(root);
396
+ }
397
+ catch (error) {
398
+ if (isNodeCode(error, "ENOENT"))
399
+ return [];
400
+ return [{ id: root, kind: "directory", ownership: "ambiguous", state: "unknown" }];
401
+ }
402
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
403
+ return [{ id: root, kind: "directory", ownership: "ambiguous", state: "unknown" }];
404
+ }
405
+ try {
406
+ const markerPath = join(root, MARKER_FILE);
407
+ const markerMetadata = lstatSync(markerPath);
408
+ if (!markerMetadata.isFile() || markerMetadata.isSymbolicLink()) {
409
+ return [{ id: root, kind: "directory", ownership: "unmarked", state: "unknown" }];
410
+ }
411
+ const marker = parseMarker(readFileSync(markerPath, "utf8"));
412
+ const actualFingerprint = taskRuntimeIsolationFingerprint(marker.descriptor);
413
+ const exact = marker.fingerprint === actualFingerprint
414
+ && actualFingerprint === expectedFingerprint
415
+ && JSON.stringify(marker.descriptor) === JSON.stringify(descriptor);
416
+ return [{
417
+ id: root,
418
+ kind: "directory",
419
+ ownership: exact ? "owned" : "mismatched",
420
+ state: exact ? "inactive" : "unknown",
421
+ descriptorFingerprint: actualFingerprint
422
+ }];
423
+ }
424
+ catch {
425
+ return [{ id: root, kind: "directory", ownership: "unmarked", state: "unknown" }];
426
+ }
427
+ }
428
+ function parseMarker(serialized) {
429
+ const value = JSON.parse(serialized);
430
+ if (!isRecord(value)
431
+ || value.schemaVersion !== 1
432
+ || value.kind !== "yui-task-runtime-resource-owner") {
433
+ throw new Error("Task runtime resource marker is invalid.");
434
+ }
435
+ return {
436
+ schemaVersion: 1,
437
+ kind: "yui-task-runtime-resource-owner",
438
+ fingerprint: requireDigest(value.fingerprint),
439
+ descriptor: parseTaskRuntimeIsolationDescriptor(JSON.stringify(value.descriptor))
440
+ };
441
+ }
442
+ function ensureOwnedDirectories(descriptor) {
443
+ for (const path of [
444
+ descriptor.roots.data,
445
+ descriptor.roots.cache,
446
+ descriptor.roots.temporary,
447
+ join(descriptor.roots.data, "state"),
448
+ join(descriptor.roots.temporary, "runtime")
449
+ ]) {
450
+ ensureDirectoryChain(descriptor.roots.generation, path);
451
+ }
452
+ }
453
+ function ensureDirectoryChain(boundary, target) {
454
+ const root = resolve(boundary);
455
+ const destination = resolve(target);
456
+ if (root !== destination && !isWithin(root, destination)) {
457
+ throw new Error("Task runtime directory escaped its exact ownership boundary.");
458
+ }
459
+ const segments = relative(root, destination).split(/[\\/]/u).filter(Boolean);
460
+ let current = root;
461
+ ensureDirectory(current);
462
+ for (const segment of segments) {
463
+ current = join(current, segment);
464
+ ensureDirectory(current);
465
+ }
466
+ }
467
+ function ensureDirectory(path) {
468
+ try {
469
+ const metadata = lstatSync(path);
470
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
471
+ throw new Error(`Task runtime path is not an owned directory: ${path}.`);
472
+ }
473
+ }
474
+ catch (error) {
475
+ if (!isNodeCode(error, "ENOENT"))
476
+ throw error;
477
+ mkdirSync(path, { mode: 0o700 });
478
+ }
479
+ }
480
+ function validatePreparation(preparation) {
481
+ const descriptor = parseTaskRuntimeIsolationDescriptor(JSON.stringify(preparation.descriptor));
482
+ const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
483
+ if (preparation.fingerprint !== fingerprint) {
484
+ throw new Error("Task runtime isolation preparation fingerprint changed.");
485
+ }
486
+ return { descriptor, fingerprint, environment: taskRuntimeIsolationEnvironment(descriptor) };
487
+ }
488
+ function taskRuntimeWorkspaceOwner(owner) {
489
+ return owner;
490
+ }
491
+ function taskRuntimeGenerationRoot(runtimeRoot, taskId, owner, launchId, pathLayout = "hierarchical") {
492
+ return join(taskRuntimeInventoryRoot(runtimeRoot, taskId, pathLayout), taskRuntimeOwnerDigest(taskId, owner, pathLayout), taskRuntimeLaunchDigest(taskId, launchId, pathLayout));
493
+ }
494
+ function taskRuntimeInventoryRoot(runtimeRoot, taskId, pathLayout) {
495
+ return pathLayout === "compact" ? runtimeRoot : join(runtimeRoot, taskId);
496
+ }
497
+ function taskRuntimeOwnerDigest(taskId, owner, pathLayout) {
498
+ return pathLayout === "compact"
499
+ ? digest(JSON.stringify([taskId, owner])).slice(0, 20)
500
+ : digest(JSON.stringify(owner)).slice(0, 24);
501
+ }
502
+ function taskRuntimeLaunchDigest(taskId, launchId, pathLayout) {
503
+ return digest(JSON.stringify([taskId, launchId])).slice(0, pathLayout === "compact" ? 20 : 24);
504
+ }
505
+ function taskRuntimePathLayout(value) {
506
+ const pathLayout = value ?? "hierarchical";
507
+ if (pathLayout !== "hierarchical" && pathLayout !== "compact") {
508
+ throw new Error("Task runtime path layout is invalid.");
509
+ }
510
+ return pathLayout;
511
+ }
512
+ function taskRuntimeServiceNamespace(taskId, owner, launchId, generationId) {
513
+ return `yui-task-${digest(JSON.stringify([
514
+ taskId,
515
+ owner,
516
+ launchId,
517
+ generationId
518
+ ])).slice(0, 24)}`;
519
+ }
520
+ function normalizeControlBoundary(value) {
521
+ return Object.freeze({
522
+ yuiHome: canonicalPath(value.yuiHome, "Control YUI_HOME"),
523
+ controllerSocketPath: canonicalPath(value.controllerSocketPath, "Controller socket path"),
524
+ tmuxNamespace: requireIdentity(value.tmuxNamespace, "Controller tmux namespace"),
525
+ globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path")))
526
+ });
527
+ }
528
+ function validateObservation(resource) {
529
+ requireText(resource.id, "Task runtime resource id");
530
+ if (!["directory", "service", "port", "external"].includes(resource.kind)) {
531
+ throw new Error("Task runtime resource kind is invalid.");
532
+ }
533
+ if (!["owned", "unmarked", "mismatched", "ambiguous", "external"].includes(resource.ownership)) {
534
+ throw new Error("Task runtime resource ownership is invalid.");
535
+ }
536
+ if (!["inactive", "active", "unknown"].includes(resource.state)) {
537
+ throw new Error("Task runtime resource state is invalid.");
538
+ }
539
+ if (resource.descriptorFingerprint !== undefined) {
540
+ requireDigest(resource.descriptorFingerprint);
541
+ }
542
+ }
543
+ function allocations(value) {
544
+ if (!Array.isArray(value))
545
+ throw new Error("Task runtime port allocations are invalid.");
546
+ const names = new Set();
547
+ const selected = new Set();
548
+ const result = value.map((candidate) => {
549
+ if (!isRecord(candidate))
550
+ throw new Error("Task runtime port allocation is invalid.");
551
+ const name = requireIdentity(candidate.name, "Port allocation name");
552
+ const port = requirePort(candidate.port, "Port allocation");
553
+ if (names.has(name) || selected.has(port)) {
554
+ throw new Error("Task runtime port allocations must be unique.");
555
+ }
556
+ names.add(name);
557
+ selected.add(port);
558
+ return Object.freeze({ name, port });
559
+ });
560
+ return Object.freeze(result.sort((left, right) => left.name.localeCompare(right.name)));
561
+ }
562
+ function ports(value, label) {
563
+ if (!Array.isArray(value))
564
+ throw new Error(`${label} is invalid.`);
565
+ const result = value.map((port) => requirePort(port, label));
566
+ if (new Set(result).size !== result.length)
567
+ throw new Error(`${label} must be unique.`);
568
+ return Object.freeze([...result]);
569
+ }
570
+ function capabilities(value, label) {
571
+ if (!Array.isArray(value))
572
+ throw new Error(`${label} list is invalid.`);
573
+ const result = value.map((item) => requireIdentity(item, label)).sort();
574
+ if (new Set(result).size !== result.length)
575
+ throw new Error(`${label} must be unique.`);
576
+ return Object.freeze(result);
577
+ }
578
+ function requirePort(value, label) {
579
+ if (!Number.isInteger(value) || value < 1 || value > 65_535) {
580
+ throw new Error(`${label} is invalid.`);
581
+ }
582
+ return value;
583
+ }
584
+ function requireCleanupReason(value) {
585
+ if (!["failure", "timeout", "interruption", "completion", "reopen"].includes(value)) {
586
+ throw new Error("Task runtime cleanup reason is invalid.");
587
+ }
588
+ return value;
589
+ }
590
+ function canonicalPath(value, label) {
591
+ const path = requireText(value, label);
592
+ if (!isAbsolute(path))
593
+ throw new Error(`${label} must be absolute.`);
594
+ return resolve(path);
595
+ }
596
+ function pathsOverlap(left, right) {
597
+ return left === right || isWithin(left, right) || isWithin(right, left);
598
+ }
599
+ function isWithin(parent, child) {
600
+ const nested = relative(resolve(parent), resolve(child));
601
+ return nested.length > 0 && !nested.startsWith("..") && !isAbsolute(nested);
602
+ }
603
+ function requireIdentity(value, label) {
604
+ const identity = requireText(value, label);
605
+ if ([".", "..", "__proto__", "prototype", "constructor"].includes(identity)
606
+ || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(identity)) {
607
+ throw new Error(`${label} is invalid.`);
608
+ }
609
+ return identity;
610
+ }
611
+ function requireText(value, label) {
612
+ if (typeof value !== "string"
613
+ || value.length === 0
614
+ || value.trim() !== value
615
+ || value.includes("\0")) {
616
+ throw new Error(`${label} is invalid.`);
617
+ }
618
+ return value;
619
+ }
620
+ function requireDigest(value) {
621
+ const digestValue = requireText(value, "Task runtime fingerprint");
622
+ if (!/^[a-f0-9]{64}$/u.test(digestValue)) {
623
+ throw new Error("Task runtime fingerprint is invalid.");
624
+ }
625
+ return digestValue;
626
+ }
627
+ function requireRecord(value, label) {
628
+ if (!isRecord(value))
629
+ throw new Error(`${label} is invalid.`);
630
+ return value;
631
+ }
632
+ function isRecord(value) {
633
+ return typeof value === "object" && value !== null && !Array.isArray(value);
634
+ }
635
+ function digest(value) {
636
+ return createHash("sha256").update(value).digest("hex");
637
+ }
638
+ function isNodeCode(error, code) {
639
+ return typeof error === "object"
640
+ && error !== null
641
+ && "code" in error
642
+ && error.code === code;
643
+ }