@zq-silk/yui 0.15.8 → 0.15.11

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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -65,7 +65,9 @@ export class FileTaskRuntimeIsolation {
65
65
  this.#resourceRegistrar().registerTaskRuntimeIsolation(descriptor);
66
66
  return;
67
67
  }
68
- ensureDirectoryChain(this.#runtimeRoot, dirname(root));
68
+ ensureDirectoryChain(this.#controlPlane.managedRuntimeRoot === undefined
69
+ ? this.#runtimeRoot
70
+ : this.#controlPlane.yuiHome, dirname(root));
69
71
  mkdirSync(root, { mode: 0o700 });
70
72
  try {
71
73
  const marker = {
@@ -255,10 +257,22 @@ export function assertTaskRuntimeIsolationPreflight(input) {
255
257
  }
256
258
  }
257
259
  const control = normalizeControlBoundary(input.controlPlane);
258
- const protectedPaths = [control.yuiHome, ...control.globalInstallPaths];
259
260
  for (const root of Object.values(descriptor.roots)) {
260
- if (protectedPaths.some((path) => pathsOverlap(root, path))) {
261
- throw new Error("Task runtime roots overlap the control YUI_HOME or a global install path.");
261
+ if (control.globalInstallPaths.some((path) => pathsOverlap(root, path))) {
262
+ throw new Error("Task runtime roots overlap a global install path.");
263
+ }
264
+ // Precise partition isolation: a runtime root may occupy Home only when it
265
+ // is fully within the single designated managed runtime partition. Every
266
+ // other part of Home — the database, managed Git workspaces, projects —
267
+ // stays protected. Both Task and integration runtimes now supply such a
268
+ // partition; a caller that supplies none keeps the whole-Home overlap ban.
269
+ if (pathsOverlap(root, control.yuiHome)) {
270
+ const partition = control.managedRuntimeRoot;
271
+ const withinPartition = partition !== undefined
272
+ && (root === partition || isWithin(partition, root));
273
+ if (!withinPartition) {
274
+ throw new Error("Task runtime roots overlap the control YUI_HOME outside the managed runtime partition.");
275
+ }
262
276
  }
263
277
  if (pathsOverlap(root, descriptor.workspace.root)) {
264
278
  throw new Error("Task runtime roots must not dirty the managed workspace.");
@@ -441,11 +455,21 @@ function taskRuntimeServiceNamespace(taskId, owner, runtimeRoot) {
441
455
  ])).slice(0, 24)}`;
442
456
  }
443
457
  function normalizeControlBoundary(value) {
458
+ const yuiHome = canonicalPath(value.yuiHome, "Control YUI_HOME");
459
+ const managedRuntimeRoot = value.managedRuntimeRoot === undefined
460
+ ? undefined
461
+ : canonicalPath(value.managedRuntimeRoot, "Managed runtime root");
462
+ if (managedRuntimeRoot !== undefined
463
+ && managedRuntimeRoot !== yuiHome
464
+ && !isWithin(yuiHome, managedRuntimeRoot)) {
465
+ throw new Error("Managed runtime root must be within the control YUI_HOME.");
466
+ }
444
467
  return Object.freeze({
445
- yuiHome: canonicalPath(value.yuiHome, "Control YUI_HOME"),
468
+ yuiHome,
446
469
  controllerSocketPath: canonicalPath(value.controllerSocketPath, "Controller socket path"),
447
470
  tmuxNamespace: requireIdentity(value.tmuxNamespace, "Controller tmux namespace"),
448
- globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path")))
471
+ globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path"))),
472
+ ...(managedRuntimeRoot === undefined ? {} : { managedRuntimeRoot })
449
473
  });
450
474
  }
451
475
  function validateObservation(resource) {
@@ -209,7 +209,8 @@ export class TmuxSessionHost {
209
209
  throw toRuntimeLaunchFailure(error, "validation", launchContext);
210
210
  }
211
211
  }
212
- const interactiveCodex = request.owner.scope === "global" && request.adapterId === "codex";
212
+ const interactiveCodex = request.owner.scope === "global" && request.adapterId === "codex"
213
+ && planned.launch.providerControl === undefined;
213
214
  let reuseInteractivePane = false;
214
215
  if (interactiveCodex) {
215
216
  let status;
@@ -310,6 +311,7 @@ export class TmuxSessionHost {
310
311
  }
311
312
  const reservation = broker.reserve(Object.freeze({
312
313
  schemaVersion: 2,
314
+ ...(request.runId === undefined ? {} : { startupRunId: request.runId }),
313
315
  command: planned.launch.command,
314
316
  args: [...planned.launch.args],
315
317
  environment: { ...planned.launch.env },
@@ -577,8 +579,8 @@ export class AgentHostPromptPushAdapter {
577
579
  try {
578
580
  const result = await sendAgentHostSteerControl({
579
581
  home: this.home,
580
- scope: "task",
581
- taskId: request.owner.taskId,
582
+ scope: request.owner.scope,
583
+ ...(request.owner.scope === "task" ? { taskId: request.owner.taskId } : {}),
582
584
  roleName: request.owner.roleName,
583
585
  control: {
584
586
  protocol: AGENT_HOST_CONTROL_PROTOCOL,
@@ -4,6 +4,8 @@ import { queueLeaderWakeup } from "./wakeupQueue.js";
4
4
  export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
5
5
  /** The sole Task-event boundary into the global Operator mailbox. */
6
6
  export function enqueueOperatorEvent(store, event, reason, now) {
7
+ if (store.getTask?.(event.taskId)?.status === "archived")
8
+ return;
7
9
  enqueueWork(store, { kind: "operator" }, reason, now, [
8
10
  { type: "event", taskId: event.taskId, id: event.id }
9
11
  ], {
@@ -13,6 +15,8 @@ export function enqueueOperatorEvent(store, event, reason, now) {
13
15
  }
14
16
  /** Routes one Role fact to its supervisor without exposing Provider details to Operator code. */
15
17
  export function routeRoleEvent(store, event, roleName, reason, now) {
18
+ if (store.getTask?.(event.taskId)?.status === "archived")
19
+ return;
16
20
  if (roleName === "leader") {
17
21
  enqueueOperatorEvent(store, event, reason, now);
18
22
  return;
@@ -210,9 +210,11 @@ export function projectTaskExecution(facts) {
210
210
  && runs.filter((run) => run.workItemId === item.id).at(-1)?.status === "failed");
211
211
  const candidateReady = workItems.some((item) => (item.status === "open" && item.currentCandidateId !== undefined));
212
212
  const blockedIntegration = integrations.some((attempt) => attempt.status === "blocked");
213
+ const conflictedIntegration = integrations.find((attempt) => attempt.status === "conflicted");
213
214
  const unresolvedIntegration = integrations.some((attempt) => (attempt.status === "running"
214
215
  || attempt.status === "validating"
215
- || attempt.status === "blocked"));
216
+ || attempt.status === "blocked"
217
+ || attempt.status === "conflicted"));
216
218
  const hasLeaderMismatch = attention.some((item) => item.kind === "identity-mismatch");
217
219
  const renderAttention = () => {
218
220
  const first = attention[0];
@@ -279,6 +281,15 @@ export function projectTaskExecution(facts) {
279
281
  if (attention.length > 0) {
280
282
  return renderAttention();
281
283
  }
284
+ if (conflictedIntegration !== undefined) {
285
+ return render({
286
+ task, status: "needs-leader-action", owner: "leader",
287
+ action: "resolve-integration-conflicts",
288
+ summary: `Integration ${conflictedIntegration.id} has Git conflicts; resolve its workspace and continue.`,
289
+ reason: "integration-conflicted", monitoring, failClosed: false,
290
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
291
+ });
292
+ }
282
293
  if (blockedIntegration || failedWork || hasLeaderMismatch) {
283
294
  return render({
284
295
  task,
@@ -37,7 +37,13 @@ const IMMEDIATE_KINDS = new Set([
37
37
  "activation-failed",
38
38
  "task-reopened",
39
39
  "leader-turn-failed",
40
- "role-turn-failed"
40
+ "role-turn-failed",
41
+ // A claimed interrupt-then handoff for a no-Run Leader turn: the operator/Leader
42
+ // explicitly redirected, so the release wake bypasses the aggregation debounce.
43
+ // It is still held by the leader-mailbox busy-gate until the interrupted native
44
+ // Turn reaches its proven terminal (decision-3 §4), so "immediate" only removes
45
+ // the extra 60s aggregation delay once that terminal actually opens the gate.
46
+ "interrupt-then"
41
47
  ]);
42
48
  export function wakeReason(kind, ref) {
43
49
  if (ref === undefined)
@@ -1,5 +1,7 @@
1
1
  import { mergePendingWakeup } from "./pendingWakeup.js";
2
2
  export function queueLeaderWakeup(store, taskId, reason, now) {
3
+ if (store.getTask?.(taskId)?.status === "archived")
4
+ return null;
3
5
  if (store.enqueueLeaderWakeup !== undefined) {
4
6
  return store.enqueueLeaderWakeup(taskId, reason, now);
5
7
  }
@@ -1,10 +1,11 @@
1
1
  import { existsSync, mkdirSync, realpathSync } from "node:fs";
2
- import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
2
+ import { delimiter, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { configuredAgentToDefinition, createConfiguredAgent } from "../agent/agent.js";
5
5
  import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
6
6
  import { resolveTmuxBin } from "../config/yuiConfig.js";
7
7
  import { usageError } from "../errors/cliError.js";
8
+ import { managedGlobalRoleWorkspace } from "../storage/homeLayout.js";
8
9
  import { createGlobalRole, createRoleAgentBinding, updateGlobalRole } from "../role/role.js";
9
10
  import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
10
11
  import { initializeCurrentTaskStore } from "../storage/currentTaskStore.js";
@@ -48,20 +49,28 @@ export async function runSetupCommand(args, env, executor, io = {}) {
48
49
  const currentOperator = store.getGlobalRole(SYSTEM_OPERATOR_ROLE);
49
50
  const selected = await selectOperatorAgent(choices, currentOperator, store.getConfig().defaultAgent, question, io);
50
51
  const agent = selected.existing ?? createConfiguredAgent(selected.id, selected.adapterId, selected.command, [], [], new Date());
51
- const workspace = resolveWorkspace(store.getConfig().defaultWorkspace ?? join(dirname(resolve(home)), "workspace"), home);
52
- const roleStatus = saveMinimumConfiguration(store, agent, workspace, new Set(choices.map(({ id }) => id)));
52
+ // The built-in Global Operator/Leader run under a Home-internal working
53
+ // directory. Yui never auto-invents an external scratch root here: a user
54
+ // who wants an external project-input root configures `default-workspace`
55
+ // explicitly (kept outside Home by its own guard). An already-configured one
56
+ // is carried forward and normalized; none is fabricated.
57
+ const configuredWorkspace = store.getConfig().defaultWorkspace;
58
+ const externalWorkspace = configuredWorkspace === undefined
59
+ ? undefined
60
+ : resolveWorkspace(configuredWorkspace, home);
61
+ const roleWorkspace = managedGlobalRoleWorkspace(home);
62
+ mkdirSync(roleWorkspace, { recursive: true, mode: 0o700 });
63
+ const roleStatus = saveMinimumConfiguration(store, agent, roleWorkspace, externalWorkspace, new Set(choices.map(({ id }) => id)));
53
64
  return `${[
54
65
  "Yui setup complete.",
55
66
  `Yui home: ${home}.`,
56
- `Operator Agent: ${agent.id}.`,
57
- `Leader Agent: ${roleStatus.leaderAgentId}.`,
58
- `Default workspace: ${workspace}.`,
59
- `Operator configuration: ${roleStatus.operator}.`,
60
- `Leader configuration: ${roleStatus.leader}.`,
61
- `Tmux: ${tmuxBin}.`,
62
- "Setup did not configure Review, Worker, Profiles, or shell completion.",
63
- "Provider login and native key/initialization confirmations remain with the Provider; setup did not call a model.",
64
- "Run `yui operator enter` to continue, or `yui config show` to inspect configuration."
67
+ `Operator: ${agent.id} (${roleStatus.operator}).`,
68
+ `Leader: ${roleStatus.leaderAgentId} (${roleStatus.leader}).`,
69
+ `Global Role workspace: ${roleWorkspace}.`,
70
+ ...(externalWorkspace === undefined
71
+ ? []
72
+ : [`Default workspace: ${externalWorkspace}.`]),
73
+ "Run `yui config show` to inspect configuration, or `yui operator enter` to start an Operator session for configuration changes or development tasks."
65
74
  ].join("\n")}\n`;
66
75
  }
67
76
  finally {
@@ -121,14 +130,18 @@ async function selectOperatorAgent(choices, currentOperator, defaultAgent, quest
121
130
  }
122
131
  return selected;
123
132
  }
124
- function saveMinimumConfiguration(store, agent, workspace, usableAgentIds) {
133
+ function saveMinimumConfiguration(store, agent, roleWorkspace, externalWorkspace, usableAgentIds) {
125
134
  return store.transaction((tx) => {
126
135
  tx.saveConfiguredAgent(agent);
127
136
  const config = tx.getConfig();
128
137
  tx.saveConfig({
129
138
  ...config,
130
139
  defaultAgent: config.defaultAgent ?? agent.id,
131
- defaultWorkspace: config.defaultWorkspace ?? workspace
140
+ // Only persist a default workspace the user actually configured; setup no
141
+ // longer fabricates an external Home-sibling root.
142
+ ...(externalWorkspace === undefined
143
+ ? {}
144
+ : { defaultWorkspace: config.defaultWorkspace ?? externalWorkspace })
132
145
  });
133
146
  const now = new Date();
134
147
  const leader = tx.getGlobalRole(SYSTEM_LEADER_ROLE);
@@ -140,7 +153,7 @@ function saveMinimumConfiguration(store, agent, workspace, usableAgentIds) {
140
153
  roleName: SYSTEM_LEADER_ROLE
141
154
  }, "creation");
142
155
  const definition = configuredAgentToDefinition(agent);
143
- tx.saveGlobalRole(createGlobalRole(SYSTEM_LEADER_ROLE, [createRoleAgentBinding(definition)], agent.id, workspace, now));
156
+ tx.saveGlobalRole(createGlobalRole(SYSTEM_LEADER_ROLE, [createRoleAgentBinding(definition)], agent.id, roleWorkspace, now));
144
157
  leaderStatus = "created";
145
158
  leaderAgentId = agent.id;
146
159
  }
@@ -166,7 +179,7 @@ function saveMinimumConfiguration(store, agent, workspace, usableAgentIds) {
166
179
  scope: "global",
167
180
  roleName: SYSTEM_OPERATOR_ROLE
168
181
  }, "creation");
169
- tx.saveGlobalRole(createGlobalRole(SYSTEM_OPERATOR_ROLE, [binding], agent.id, workspace, now));
182
+ tx.saveGlobalRole(createGlobalRole(SYSTEM_OPERATOR_ROLE, [binding], agent.id, roleWorkspace, now));
170
183
  return {
171
184
  operator: "created",
172
185
  leader: leaderStatus,
@@ -0,0 +1,130 @@
1
+ import { isAbsolute, join, relative, resolve } from "node:path";
2
+ /**
3
+ * The single authority for every Yui self-managed path derived from a Home.
4
+ *
5
+ * Historically the managed Git worktrees and provider runtimes escaped Home
6
+ * through two different mechanisms: worktree/task roots were derived from the
7
+ * user-facing `defaultWorkspace` (an out-of-Home directory), and provider
8
+ * runtimes/backups were written to string-built siblings of Home
9
+ * (`${home}.task-runtimes`, `${home}-backups`). Both are unified here so that a
10
+ * single canonical YUI_HOME contains all self-managed data, with only the
11
+ * approved short-path IPC sockets (Controller, tmux, Agent Host, integration
12
+ * runtime) remaining outside it for the `sockaddr_un` length budget. Within the
13
+ * managed workspace root, storage v20 further collapses the former
14
+ * physical-worktree/symlink-view split into a single layer of real worktrees
15
+ * under `tasks/` (see {@link managedTaskRoot}).
16
+ *
17
+ * `defaultWorkspace` is intentionally NOT an input to this module: it remains a
18
+ * user-facing cwd for ad-hoc/global Roles and the origin for external Project
19
+ * input, never a second authority for internal managed layout.
20
+ */
21
+ /** Canonicalize a Home string the same way callers already resolve it. */
22
+ function homeRoot(home) {
23
+ const resolved = resolve(home);
24
+ if (!isAbsolute(resolved)) {
25
+ throw new Error("YUI_HOME must be an absolute path.");
26
+ }
27
+ return resolved;
28
+ }
29
+ /**
30
+ * Root that contains every managed Git workspace. Since storage v20 this holds a
31
+ * single layer of real worktrees under `tasks/` (plus the legacy `worktree/`
32
+ * root, preserved by the 19->20 migration as its rollback anchor). A single
33
+ * parent keeps the managed families adjacent and lets the storage migrations
34
+ * relocate them as one subtree.
35
+ */
36
+ export function managedWorkspacesRoot(home) {
37
+ return join(homeRoot(home), "workspaces");
38
+ }
39
+ /**
40
+ * LEGACY physical worktree root: `<home>/workspaces/worktree`. Before storage
41
+ * v20 every managed clone/linked worktree lived here under a per-Project subtree,
42
+ * surfaced to each Task through a symlink view under `tasks/`. The 19->20
43
+ * migration collapses those into real worktrees under `tasks/` and PRESERVES this
44
+ * tree as the rollback anchor, so this path survives only for that migration's
45
+ * inline layout and for post-upgrade cleanup — no current runtime code derives a
46
+ * live worktree path from it.
47
+ */
48
+ export function managedWorktreeRoot(home) {
49
+ return join(managedWorkspacesRoot(home), "worktree");
50
+ }
51
+ /**
52
+ * Managed Git worktrees, one real worktree per Task owner and Project at
53
+ * `<home>/workspaces/tasks/<taskId>/<owner>/<projectDirectory>` (owner being
54
+ * `main`, `work-items/<id>`, `reviews/<name>`, `integrations/<id>`, or
55
+ * `execution-lanes/<g>/<l>`). Before storage v20 these were symlink views over
56
+ * the physical `worktree/` root; the 19->20 migration makes them the worktrees
57
+ * themselves, so the logical entry path and the physical Git worktree coincide.
58
+ */
59
+ export function managedTaskRoot(home) {
60
+ return join(managedWorkspacesRoot(home), "tasks");
61
+ }
62
+ /**
63
+ * Dedicated partition for provider runtime roots (data/cache/tmp) and the
64
+ * planning cwd. This is the ONE Home subtree the Task runtime isolation
65
+ * boundary permits its roots to overlap; every other part of Home (the
66
+ * database, `workspaces/`, `projects/`) stays protected.
67
+ */
68
+ export function managedRuntimeRoot(home) {
69
+ return join(homeRoot(home), "runtime", "task-runtimes");
70
+ }
71
+ /**
72
+ * Dedicated Home partition for the integration check runtime's provider
73
+ * data/cache/tmp. Distinct from `managedRuntimeRoot` so an integration attempt's
74
+ * disposable roots never collide with a Task runtime's, yet still sit inside the
75
+ * one managed runtime subtree the isolation boundary permits to overlap Home.
76
+ * The integration tmux socket endpoint is NOT here — it stays a short `/tmp`
77
+ * path for the `sockaddr_un` budget (see `gitIntegrationService`).
78
+ */
79
+ export function managedIntegrationRuntimeRoot(home) {
80
+ return join(homeRoot(home), "runtime", "integration-runtimes");
81
+ }
82
+ /**
83
+ * Disposable staging prefix for `yui update`'s side-by-side package install
84
+ * (an upgrade artifact): `<home>/runtime/update-staging`. Kept under Home so a
85
+ * staged upgrade never lands in a shared system temp root.
86
+ */
87
+ export function updateStagingRoot(home) {
88
+ return join(homeRoot(home), "runtime", "update-staging");
89
+ }
90
+ /**
91
+ * Disposable scratch parent for the release workflow's Yui-authored artifacts —
92
+ * the fresh-install smoke directory and the verified publish-snapshot tarball
93
+ * (release artifacts): `<home>/runtime/release-workflow`. Kept under Home so a
94
+ * release artifact never lands in a shared system temp root.
95
+ */
96
+ export function releaseWorkflowScratchRoot(home) {
97
+ return join(homeRoot(home), "runtime", "release-workflow");
98
+ }
99
+ /** Disposable planning cwd parent: `<home>/runtime/task-runtimes/planning`. */
100
+ export function planningRuntimeRoot(home) {
101
+ return join(managedRuntimeRoot(home), "planning");
102
+ }
103
+ /** Planning cwd for a single Task, materialized on demand at launch. */
104
+ export function planningRuntimeCwd(home, taskId) {
105
+ return join(planningRuntimeRoot(home), taskId);
106
+ }
107
+ /** Storage-upgrade database backups: `<home>/backups` (was a Home sibling). */
108
+ export function storageBackupRoot(home) {
109
+ return join(homeRoot(home), "backups");
110
+ }
111
+ /**
112
+ * Default working directory for Yui-owned Roles that carry no user-chosen,
113
+ * external cwd: the built-in Global Operator/Leader created by `yui setup` and
114
+ * ad-hoc Global Roles added without an explicit `--workspace`. Historically
115
+ * these fell back to a Home-sibling `workspace/` directory (or `process.cwd()`),
116
+ * which leaked Yui-managed scratch outside the canonical Home. A user who names
117
+ * an external directory keeps external-resource semantics; only the auto-created
118
+ * default lands here, under Home: `<home>/workspaces/global`.
119
+ *
120
+ * This is a plain cwd, NOT a managed Git workspace, so it is deliberately a
121
+ * sibling of `worktree/`+`tasks/` under `workspaces/`, never inside them.
122
+ */
123
+ export function managedGlobalRoleWorkspace(home) {
124
+ return join(managedWorkspacesRoot(home), "global");
125
+ }
126
+ /** True when `child` is strictly inside `parent` (shared with isolation checks). */
127
+ export function isWithinManagedRoot(parent, child) {
128
+ const nested = relative(resolve(parent), resolve(child));
129
+ return nested.length > 0 && !nested.startsWith("..") && !isAbsolute(nested);
130
+ }