@zq-silk/yui 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +19 -3
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +25 -11
  7. package/dist/cli/interactionPolicy.js +4 -0
  8. package/dist/cli/invocationAuthority.js +1 -1
  9. package/dist/commands/executionAuditCommands.js +5 -0
  10. package/dist/commands/globalRoleCommands.js +41 -3
  11. package/dist/commands/projectCommands.js +89 -94
  12. package/dist/commands/roleSkillValidation.js +4 -8
  13. package/dist/commands/taskCommands.js +30 -9
  14. package/dist/commands/taskIntegrationCommands.js +5 -16
  15. package/dist/commands/taskUsageCommand.js +38 -0
  16. package/dist/context/roleSessionContext.js +18 -2
  17. package/dist/context/runContextPack.js +5 -1
  18. package/dist/context/sessionBootstrapManifest.js +10 -1
  19. package/dist/context/skillCatalog.js +108 -0
  20. package/dist/context/skillPackage.js +161 -0
  21. package/dist/context/taskContext.js +3 -2
  22. package/dist/context/taskSearch.js +127 -0
  23. package/dist/controlPlaneCli.js +51 -39
  24. package/dist/controller/clientRuntime.js +3 -2
  25. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  26. package/dist/controller/jobControl.js +2 -0
  27. package/dist/controller/nativeHostInput.js +37 -0
  28. package/dist/controller/runtime.js +93 -2
  29. package/dist/controller/runtimeEventInbox.js +2 -2
  30. package/dist/controller/runtimeEventProcessor.js +3 -3
  31. package/dist/doctor/doctor.js +10 -0
  32. package/dist/execution/reviewMainRun.js +1 -0
  33. package/dist/execution/workItemMainRun.js +1 -0
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  37. package/dist/integration/gitIntegrationService.js +15 -79
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/job/httpPreview.js +61 -0
  40. package/dist/job/jobCleanupInspection.js +67 -0
  41. package/dist/job/jobRunner.js +12 -2
  42. package/dist/kernel/builtinCapabilities.js +3 -1
  43. package/dist/message/message.js +15 -1
  44. package/dist/output/rolePresentation.js +4 -1
  45. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  46. package/dist/repository/gitCompatibility.js +62 -0
  47. package/dist/repository/gitWorkspace.js +86 -80
  48. package/dist/repository/gitWorktreeInventory.js +121 -0
  49. package/dist/repository/project.js +74 -25
  50. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  51. package/dist/repository/taskWorkspacePreparer.js +39 -43
  52. package/dist/repository/workspaceCleanupInspection.js +88 -7
  53. package/dist/resources/resourceDiscovery.js +2 -2
  54. package/dist/resources/resourceGc.js +7 -4
  55. package/dist/resources/resourceRegistrar.js +18 -11
  56. package/dist/runtime/agentEndpoint.js +8 -0
  57. package/dist/runtime/agentHost.js +170 -3
  58. package/dist/runtime/codexAppServerRuntime.js +1 -1
  59. package/dist/runtime/codexNativeAccess.js +175 -0
  60. package/dist/runtime/controlledCodexTui.js +116 -0
  61. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  62. package/dist/runtime/publicReply.js +23 -0
  63. package/dist/runtime/runtimeObservation.js +48 -0
  64. package/dist/runtime/structuredProviderHost.js +16 -7
  65. package/dist/runtime/taskCostMetrics.js +133 -0
  66. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  67. package/dist/runtime/taskUsageMetrics.js +11 -2
  68. package/dist/runtime/taskUsageQuery.js +38 -0
  69. package/dist/scheduler/taskExecutionProjection.js +2 -0
  70. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  71. package/dist/storage/homeLayout.js +13 -0
  72. package/dist/storage/sqliteSchema.js +44 -0
  73. package/dist/storage/sqliteStore.js +53 -0
  74. package/dist/storage/storageVersions.js +1 -1
  75. package/dist/storage/storeRpc.js +2 -0
  76. package/dist/storage/taskSearch.js +35 -0
  77. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  78. package/dist/task/archiveDiagnostics.js +26 -8
  79. package/dist/task/archivePreflight.js +26 -9
  80. package/dist/task/leaderArchive.js +9 -15
  81. package/dist/task/ordinaryArchive.js +42 -0
  82. package/dist/tmux/tmuxManager.js +8 -8
  83. package/dist/web/assets/assetManifest.js +11 -51
  84. package/dist/web/assets/client/app/catalog.js +215 -0
  85. package/dist/web/assets/client/app/main.js +148 -0
  86. package/dist/web/assets/client/app/selection.js +139 -0
  87. package/dist/web/assets/client/app/settings.js +326 -0
  88. package/dist/web/assets/client/app/taskView.js +280 -0
  89. package/dist/web/assets/client/domain/context.js +96 -0
  90. package/dist/web/assets/client/domain/records.js +126 -0
  91. package/dist/web/assets/client/domain/runs.js +119 -0
  92. package/dist/web/assets/client/domain/taskForms.js +197 -0
  93. package/dist/web/assets/client/domain/vocab.js +69 -0
  94. package/dist/web/assets/client/domain/work.js +116 -0
  95. package/dist/web/assets/client/i18n/en.js +900 -0
  96. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  97. package/dist/web/assets/client/index.js +114 -0
  98. package/dist/web/assets/client/layout/resizer.js +37 -0
  99. package/dist/web/assets/client/layout/router.js +35 -0
  100. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  101. package/dist/web/assets/client/layout/sizing.js +95 -0
  102. package/dist/web/assets/client/layout/workspace.js +247 -0
  103. package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
  104. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  105. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  106. package/dist/web/assets/client/lib/i18n.js +54 -0
  107. package/dist/web/assets/client/lib/markdown.js +167 -0
  108. package/dist/web/assets/client/lib/prefs.js +28 -0
  109. package/dist/web/assets/client/lib/theme.js +68 -0
  110. package/dist/web/assets/client/ui/containers.js +101 -0
  111. package/dist/web/assets/client/ui/controls.js +51 -0
  112. package/dist/web/assets/client/ui/forms.js +48 -0
  113. package/dist/web/assets/client/ui/metrics.js +137 -0
  114. package/dist/web/assets/client/ui/primitives.js +92 -0
  115. package/dist/web/assets/client/ui/text.js +33 -0
  116. package/dist/web/assets/client/ui/toast.js +12 -0
  117. package/dist/web/assets/client/views/bodySearch.js +172 -0
  118. package/dist/web/assets/client/views/createTask.js +85 -0
  119. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  120. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  121. package/dist/web/assets/client/views/dock/preview.js +129 -0
  122. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  123. package/dist/web/assets/client/views/globalInput.js +67 -0
  124. package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
  125. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  126. package/dist/web/assets/client/views/skills.js +152 -0
  127. package/dist/web/assets/client/views/task/actions.js +36 -0
  128. package/dist/web/assets/client/views/task/delivery.js +185 -0
  129. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  130. package/dist/web/assets/client/views/task/files.js +272 -0
  131. package/dist/web/assets/client/views/task/observation.js +153 -0
  132. package/dist/web/assets/client/views/task/overview.js +161 -0
  133. package/dist/web/assets/client/views/task/page.js +131 -0
  134. package/dist/web/assets/client/views/task/records.js +163 -0
  135. package/dist/web/assets/client/views/task/runtime.js +130 -0
  136. package/dist/web/assets/client/views/task/timeline.js +190 -0
  137. package/dist/web/assets/shared/geometry.js +19 -0
  138. package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
  139. package/dist/web/assets/shared/taskTabs.js +8 -0
  140. package/dist/web/assets/shared/themes.js +93 -0
  141. package/dist/web/assets/shell/dialogs.js +63 -0
  142. package/dist/web/assets/shell/dock.js +40 -0
  143. package/dist/web/assets/shell/index.js +40 -0
  144. package/dist/web/assets/shell/settings.js +34 -0
  145. package/dist/web/assets/shell/sidebar.js +45 -0
  146. package/dist/web/assets/styles/index.js +41 -0
  147. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
  148. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  149. package/dist/web/assets/styles/markdown.js +22 -3
  150. package/dist/web/assets/styles/tokens.js +10 -63
  151. package/dist/web/assets/styles/ui/controls.js +76 -0
  152. package/dist/web/assets/styles/ui/forms.js +46 -0
  153. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  154. package/dist/web/assets/styles/views/delivery.js +29 -0
  155. package/dist/web/assets/styles/views/dock.js +71 -0
  156. package/dist/web/assets/styles/views/page.js +32 -0
  157. package/dist/web/assets/styles/views/records.js +70 -0
  158. package/dist/web/assets/styles/views/settings.js +26 -0
  159. package/dist/web/assets/styles/views/sidebar.js +74 -0
  160. package/dist/web/assets/styles/views/task.js +80 -0
  161. package/dist/web/assets/styles/views/terminal.js +38 -0
  162. package/dist/web/taskPreviews.js +205 -0
  163. package/dist/web/tmuxWebTerminal.js +16 -9
  164. package/dist/web/webConversation.js +373 -0
  165. package/dist/web/webMaterials.js +155 -0
  166. package/dist/web/webServer.js +306 -18
  167. package/dist/web/webSettings.js +331 -0
  168. package/dist/web/webSkills.js +86 -0
  169. package/dist/web/webSnapshot.js +40 -8
  170. package/dist/web/webTaskSurface.js +114 -9
  171. package/dist/workspace/gitChangeSetCapture.js +3 -1
  172. package/docs/managed-turn-and-session-runtime.md +9 -0
  173. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  174. package/docs/observability/README.md +13 -3
  175. package/docs/observability/README.zh-CN.md +10 -3
  176. package/docs/release-workflow.md +53 -0
  177. package/docs/release-workflow.zh-CN.md +40 -0
  178. package/docs/storage-baseline.md +38 -1
  179. package/docs/storage-baseline.zh-CN.md +22 -0
  180. package/docs/task-delivery.md +36 -8
  181. package/docs/task-delivery.zh-CN.md +24 -7
  182. package/docs/task-discovery.md +28 -0
  183. package/docs/testing/verification-levels.md +2 -2
  184. package/i18n/README.zh-CN.md +4 -3
  185. package/package.json +1 -1
  186. package/skills/yui-leader/references/execution.md +2 -1
  187. package/skills/yui-leader/references/integration.md +20 -0
  188. package/skills/yui-operator/SKILL.md +4 -0
  189. package/skills/yui-reviewer/SKILL.md +3 -0
  190. package/skills/yui-runtime/SKILL.md +4 -0
  191. package/skills/yui-runtime/references/publication.md +20 -4
  192. package/skills/yui-worker/SKILL.md +3 -0
  193. package/dist/web/assets/client/app.js +0 -804
  194. package/dist/web/assets/client/components.js +0 -418
  195. package/dist/web/assets/client/detail.js +0 -333
  196. package/dist/web/assets/client/dock.js +0 -225
  197. package/dist/web/assets/client/evidence.js +0 -172
  198. package/dist/web/assets/client/forms.js +0 -230
  199. package/dist/web/assets/client/i18n.js +0 -1161
  200. package/dist/web/assets/client/markdown.js +0 -114
  201. package/dist/web/assets/client/records.js +0 -63
  202. package/dist/web/assets/client/sections.js +0 -313
  203. package/dist/web/assets/client/theme.js +0 -71
  204. package/dist/web/assets/shell.js +0 -161
  205. package/dist/web/assets/styles/components.js +0 -169
  206. package/dist/web/assets/styles/views.js +0 -292
@@ -3,19 +3,19 @@ import { lstat, mkdir, open, realpath, rm, stat } from "node:fs/promises";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { selectEnvironment } from "../agent/launchEnvironment.js";
6
- import { controllerSocketPath } from "../core/controllerEndpoint.js";
7
6
  import { durableJobIdempotencyKey } from "../job/durableJob.js";
8
7
  import { readRuntimeIdentity } from "../release/runtimeRelease.js";
8
+ import { compatibleGitArguments, supportedGit } from "../repository/gitCompatibility.js";
9
+ import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
9
10
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
10
11
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
11
12
  import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
12
- import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
13
+ import { FileTaskWorkspacePreparer, integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
13
14
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
14
- import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
15
- import { integrationTmuxSocketRoot, managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
15
+ import { integrationTmuxSocketRoot } from "../storage/homeLayout.js";
16
+ import { defaultIntegrationRuntimeIsolation } from "../runtime/integrationRuntimeIsolation.js";
16
17
  import { advanceTaskProjectCommit } from "../task/task.js";
17
18
  import { taskIntegrationSettlementCheckout, taskIntegrationTargetCheckout } from "../task/taskAuthority.js";
18
- import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
19
19
  import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
20
20
  import { touchGateArtifact } from "../verification/gateArtifactStore.js";
21
21
  import { beginGateVerification, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
@@ -370,12 +370,9 @@ export class GitIntegrationService {
370
370
  throw new Error("Integration changed before cleanup; read its current record.");
371
371
  }
372
372
  integration = current;
373
- const activeJob = this.store.listDurableJobs(integration.taskId).find(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integration.id
374
- && (job.status === "queued" || job.status === "running"
375
- || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
376
- if (activeJob !== undefined) {
377
- throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
378
- }
373
+ const managedWorkspace = this.store.getIntegrationWorkspace(integration.taskId, integration.id);
374
+ if (managedWorkspace === null)
375
+ return "missing";
379
376
  const task = this.store.getTask(integration.taskId);
380
377
  if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
381
378
  throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
@@ -387,42 +384,8 @@ export class GitIntegrationService {
387
384
  if (taskRepository === undefined) {
388
385
  throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
389
386
  }
390
- const managedWorkspace = this.store.getIntegrationWorkspace(integration.taskId, integration.id);
391
- if (managedWorkspace !== null) {
392
- const runtime = this.#runtimePreparation(integration, managedWorkspace);
393
- this.runtimeIsolation.cleanup(runtime, integration.status === "committed" ? "completion" : "failure");
394
- }
395
- const recorded = managedWorkspace?.entries[0];
396
- const directory = recorded?.directory
397
- ?? task.projectBindings.find(({ projectId }) => projectId === integration.projectId)?.directory;
398
- if (directory === undefined) {
399
- throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
400
- }
401
- const result = await this.git.removeIntegrationWorktree({
402
- repositoryPath: taskRepository,
403
- container: recorded !== undefined
404
- ? dirname(recorded.path)
405
- : integrationWorkspaceRoot(this.home, task.id, integration.id),
406
- directory,
407
- taskSegment: taskWorkspaceRefSegment(task),
408
- integrationId: integration.id,
409
- discardChanges: integration.status === "failed"
410
- });
411
- if (result !== "dirty") {
412
- if (managedWorkspace !== null) {
413
- this.#resourceRegistrar().markWorkspaceDeleted(managedWorkspace);
414
- }
415
- await rm(integrationCheckDirectory(this.home, task.id, integration.id), {
416
- recursive: true,
417
- force: true
418
- });
419
- this.store.removeManagedWorkspace({
420
- type: "integration-attempt",
421
- taskId: integration.taskId,
422
- integrationAttemptId: integration.id
423
- });
424
- }
425
- return result;
387
+ return await new FileTaskWorkspacePreparer(this.home, this.store, this.git, this.now)
388
+ .cleanupIntegrationWorkspace(task.id, integration.id, { runtimeIsolation: this.runtimeIsolation });
426
389
  }
427
390
  finally {
428
391
  release();
@@ -1024,26 +987,6 @@ function isNodeCode(error, code) {
1024
987
  && "code" in error
1025
988
  && error.code === code;
1026
989
  }
1027
- function defaultIntegrationRuntimeIsolation(home, homeId) {
1028
- const controlHome = resolve(home);
1029
- const runtimeRoot = managedIntegrationRuntimeRoot(controlHome);
1030
- return new FileTaskRuntimeIsolation({
1031
- // The integration check's provider data/cache/tmp live in a dedicated Home
1032
- // partition — the same isolation contract every Task runtime obeys — not in
1033
- // a system-wide `/tmp` root. Only the tmux socket ENDPOINT stays a short
1034
- // `/tmp` path (see `integrationCheckEnvironment`) for the `sockaddr_un`
1035
- // budget; ordinary runtime state is now Yui-managed under Home.
1036
- runtimeRoot,
1037
- pathLayout: "compact",
1038
- controlPlane: {
1039
- yuiHome: controlHome,
1040
- managedRuntimeRoot: runtimeRoot,
1041
- controllerSocketPath: controllerSocketPath(homeId),
1042
- tmuxNamespace: yuiTmuxServerName(controlHome),
1043
- globalInstallPaths: [process.execPath]
1044
- }
1045
- });
1046
- }
1047
990
  /**
1048
991
  * Reuse is enabled only for an immutable installed release. Development
1049
992
  * checkouts have no content-addressed release identity, so they return.
@@ -1106,8 +1049,10 @@ function integrationCheckDirectory(home, taskId, integrationId) {
1106
1049
  return join(home, "artifacts", "integration-checks", taskId, integrationId);
1107
1050
  }
1108
1051
  async function git(args) {
1052
+ const program = supportedGit();
1053
+ const invocation = compatibleGitArguments(args);
1109
1054
  try {
1110
- const result = await executeFile("git", [...args], {
1055
+ const result = await executeFile(program.path, invocation, {
1111
1056
  encoding: "utf8",
1112
1057
  maxBuffer: 16 * 1024 * 1024,
1113
1058
  timeout: 120_000
@@ -1241,15 +1186,6 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
1241
1186
  }
1242
1187
  }
1243
1188
  async function checkedOutWorktreePaths(repositoryPath, targetRef) {
1244
- const porcelain = (await git([
1245
- "-C", repositoryPath, "worktree", "list", "--porcelain"
1246
- ])).trim();
1247
- if (porcelain.length === 0)
1248
- return [];
1249
- return porcelain.split(/\n\n+/u).flatMap((record) => {
1250
- const lines = record.split("\n");
1251
- const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
1252
- const branch = lines.find((line) => line.startsWith("branch "))?.slice("branch ".length);
1253
- return path !== undefined && branch === targetRef ? [path] : [];
1254
- });
1189
+ return (await readGitWorktrees(repositoryPath))
1190
+ .filter(record => record.branch === targetRef).map(record => record.path);
1255
1191
  }
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { isAbsolute, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { gitOutputLine, supportedGit } from "../repository/gitCompatibility.js";
6
7
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
7
8
  import { recordIntegrationConflict, requireResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
8
9
  const execute = promisify(execFile);
@@ -281,7 +282,7 @@ async function hasExactRefReceipt(path, branch, before, action) {
281
282
  }
282
283
  const actionId = () => `yui-integration-${randomBytes(16).toString("hex")}`;
283
284
  async function gitPath(path, name) {
284
- const value = (await command(path, ["rev-parse", "--git-path", name])).trim();
285
+ const value = gitOutputLine(await command(path, ["rev-parse", "--git-path", name]));
285
286
  return isAbsolute(value) ? value : resolve(path, value);
286
287
  }
287
288
  async function conflicts(path) {
@@ -300,8 +301,9 @@ async function succeeds(path, args, action) {
300
301
  }
301
302
  }
302
303
  async function command(path, args, action) {
304
+ const program = supportedGit();
303
305
  try {
304
- return (await execute("git", [
306
+ return (await execute(program.path, [
305
307
  "-C", path, "-c", "user.name=Yui", "-c", "user.email=yui@local",
306
308
  "-c", "core.editor=true", "-c", "rebase.backend=merge", ...args
307
309
  ], {
@@ -0,0 +1,61 @@
1
+ import { join } from "node:path";
2
+ import { lstatSync, mkdirSync, rmdirSync, unlinkSync } from "node:fs";
3
+ import { jobPreviewSocketRoot } from "../storage/homeLayout.js";
4
+ /** An opt-in launch contract, not a second service registry. The Job remains
5
+ * the sole owner of the process, cancellation and exit evidence. */
6
+ export function isHttpPreview(env) {
7
+ return env.YUI_HTTP_PREVIEW === "1";
8
+ }
9
+ export function validateHttpPreview(env, steps) {
10
+ if (env.YUI_HTTP_PREVIEW !== undefined && !isHttpPreview(env)) {
11
+ throw new Error("YUI_HTTP_PREVIEW must be 1 when supplied.");
12
+ }
13
+ if (env.YUI_PREVIEW_SOCKET !== undefined || steps.some(step => step.env?.YUI_PREVIEW_SOCKET !== undefined || step.env?.YUI_HTTP_PREVIEW !== undefined)) {
14
+ throw new Error("Preview environment is Job-scoped; YUI_PREVIEW_SOCKET is assigned by the runner.");
15
+ }
16
+ if (isHttpPreview(env) && steps.length !== 1) {
17
+ throw new Error("An HTTP preview Job must have exactly one foreground service step.");
18
+ }
19
+ }
20
+ export function httpPreviewSocket(artifactDir) {
21
+ return join(jobPreviewSocketRoot(artifactDir), "http.sock");
22
+ }
23
+ export function assertHttpPreviewSocket(artifactDir) {
24
+ const dir = jobPreviewSocketRoot(artifactDir);
25
+ return assertSocketAt(dir);
26
+ }
27
+ function assertSocketAt(dir) {
28
+ const parent = lstatSync(dir);
29
+ const path = join(dir, "http.sock");
30
+ const socket = lstatSync(path);
31
+ if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 0o077)
32
+ || !socket.isSocket() || socket.uid !== parent.uid) {
33
+ throw new Error("Preview endpoint is not an owned Unix socket.");
34
+ }
35
+ return path;
36
+ }
37
+ /** Exclusive creation; never adopt or unlink another process's old endpoint.
38
+ * Cleanup checks the exact directory inode and removes no unknown contents. */
39
+ export function prepareHttpPreview(artifactDir) {
40
+ const dir = jobPreviewSocketRoot(artifactDir);
41
+ mkdirSync(dir, { mode: 0o700 });
42
+ const owner = lstatSync(dir);
43
+ return {
44
+ socket: join(dir, "http.sock"),
45
+ cleanup() {
46
+ const current = lstatSync(dir);
47
+ if (!current.isDirectory() || current.dev !== owner.dev || current.ino !== owner.ino) {
48
+ throw new Error(`Preview owner changed; retained endpoint: ${dir}`);
49
+ }
50
+ try {
51
+ const path = assertSocketAt(dir);
52
+ unlinkSync(path);
53
+ }
54
+ catch (error) {
55
+ if (error.code !== "ENOENT")
56
+ throw error;
57
+ }
58
+ rmdirSync(dir);
59
+ }
60
+ };
61
+ }
@@ -0,0 +1,67 @@
1
+ import { isAbsolute, join, resolve } from "node:path";
2
+ import { processGenerationIsLive } from "../core/fileLockOwner.js";
3
+ import { scanProcessPathRefs } from "../resources/liveReferences.js";
4
+ import { inspectTaskRuntimeCleanupClaims, parseTaskRuntimeIsolationDescriptor, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR } from "../runtime/taskRuntimeIsolation.js";
5
+ /** Result uncertainty and physical occupancy are different facts. Reuse the
6
+ * recorded process generation and resource scanner, without rewriting the Job
7
+ * or treating acknowledgement as stop evidence. Every cleanup reads this anew.
8
+ */
9
+ export function jobCleanupBlocker(job, home, phase = "before-removal") {
10
+ if (job.status === "queued" || job.status === "running")
11
+ return `DurableJob is ${job.status}.`;
12
+ if (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined) {
13
+ return "Unknown Job result requires explicit acknowledgement.";
14
+ }
15
+ if (job.status !== "unknown-needs-attention" && job.startedAt === undefined
16
+ && job.process === undefined && job.operation.effect === "none")
17
+ return undefined;
18
+ if (job.process === undefined || home === undefined) {
19
+ return "Job has no exact process/Home evidence for physical inspection.";
20
+ }
21
+ try {
22
+ if (processGenerationIsLive(job.process.pid, job.process.startIdentity)) {
23
+ return "The exact Job runner process is still live.";
24
+ }
25
+ // A reusable idle Role can share this workspace. Its exact-owner stop is
26
+ // allowed only after result/runner checks; path references must then be
27
+ // rechecked before any child deletion or ordinary archive commit.
28
+ if (phase === "before-role-stop")
29
+ return undefined;
30
+ // Steps may outlive a killed runner. Their cwd, logs and isolated runtime
31
+ // paths remain protective references; no process is signalled here.
32
+ const paths = [job.workspace, join(resolve(home), "artifacts", "jobs", job.taskId, job.id),
33
+ ...["TMPDIR", "XDG_DATA_HOME", "XDG_CACHE_HOME"].flatMap(key => {
34
+ const path = job.env[key];
35
+ return path !== undefined && isAbsolute(path) ? [path] : [];
36
+ })];
37
+ const serialized = job.env[YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR];
38
+ if (serialized !== undefined) {
39
+ const descriptor = parseTaskRuntimeIsolationDescriptor(serialized);
40
+ const owner = descriptor.workspace.owner;
41
+ if (descriptor.taskId !== job.taskId || descriptor.workspace.root !== job.workspace
42
+ || owner.type !== job.owner.kind
43
+ || (owner.type === "integration-attempt" && job.owner.kind === "integration-attempt"
44
+ && owner.integrationAttemptId !== job.owner.integrationAttemptId)
45
+ || (owner.type === "work-item" && job.owner.kind === "work-item"
46
+ && owner.workItemId !== job.owner.workItemId)) {
47
+ return "Job runtime descriptor does not match its exact owner/workspace.";
48
+ }
49
+ const claims = inspectTaskRuntimeCleanupClaims(descriptor);
50
+ const uncertain = claims.find(claim => claim.ownership !== "owned" || claim.state !== "inactive");
51
+ if (uncertain !== undefined)
52
+ return `Job runtime cleanup claim is unverified: ${uncertain.id}.`;
53
+ // cwd/fd references follow rename, unlike the recorded environment.
54
+ // Use the same exact claim inventory as deletion, before archive commits.
55
+ paths.push(descriptor.roots.runtime, ...claims.map(claim => claim.id));
56
+ }
57
+ const scan = scanProcessPathRefs(paths);
58
+ if (scan.diagnostics.some(diagnostic => diagnostic.severity === "error")
59
+ || [...scan.refs.values()].some(refs => refs.length > 0)) {
60
+ return "Job workspace/log/runtime references are still live or unverified.";
61
+ }
62
+ }
63
+ catch (error) {
64
+ return `Job physical inspection is unavailable: ${error instanceof Error ? error.message : String(error)}`;
65
+ }
66
+ return undefined;
67
+ }
@@ -12,6 +12,7 @@ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, ut
12
12
  import { join, resolve } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { jobStepDirectory } from "./stepDirectory.js";
15
+ import { prepareHttpPreview, isHttpPreview } from "./httpPreview.js";
15
16
  import { writeTextFileAtomically } from "../storage/durableFile.js";
16
17
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
17
18
  const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
@@ -108,6 +109,7 @@ export async function runDurableJobRunner(specPath) {
108
109
  let exitCode = 0;
109
110
  let signal = null;
110
111
  let failedStep;
112
+ let preview;
111
113
  try {
112
114
  for (let index = 0; index < spec.steps.length; index += 1) {
113
115
  if (cancelled || existsSync(cancelPath)) {
@@ -134,13 +136,16 @@ export async function runDurableJobRunner(specPath) {
134
136
  const stepEnv = step.env === undefined
135
137
  ? spec.env
136
138
  : { ...spec.env, ...step.env };
139
+ const launchEnv = isHttpPreview(spec.env)
140
+ ? { ...stepEnv, YUI_PREVIEW_SOCKET: (preview ??= prepareHttpPreview(artifactDir)).socket }
141
+ : stepEnv;
137
142
  // f4: a new process group (detached) lets the runner kill the whole
138
143
  // step tree on timeout or cancel, for both argv and shell forms.
139
144
  if (step.argv !== undefined) {
140
145
  const [file, ...args] = step.argv;
141
146
  child = spawn(file, args, {
142
147
  cwd: stepCwd,
143
- env: stepEnv,
148
+ env: launchEnv,
144
149
  stdio: ["ignore", logFd, logFd],
145
150
  detached: true
146
151
  });
@@ -148,13 +153,14 @@ export async function runDurableJobRunner(specPath) {
148
153
  else {
149
154
  child = spawn("/bin/sh", ["-lc", step.command], {
150
155
  cwd: stepCwd,
151
- env: stepEnv,
156
+ env: launchEnv,
152
157
  stdio: ["ignore", logFd, logFd],
153
158
  detached: true
154
159
  });
155
160
  }
156
161
  }
157
162
  catch (error) {
163
+ writeFileSync(logFd, `Unable to start Job step: ${error instanceof Error ? error.message : String(error)}\n`);
158
164
  closeSync(logFd);
159
165
  completedSteps.push({
160
166
  name: step.name,
@@ -231,6 +237,10 @@ export async function runDurableJobRunner(specPath) {
231
237
  }
232
238
  finally {
233
239
  clearInterval(heartbeatTimer);
240
+ // The foreground step has settled. Withdraw only its exact endpoint;
241
+ // existing Job/resource checks still own surviving descendant evidence.
242
+ // Failed socket cleanup retains evidence instead of publishing success.
243
+ preview?.cleanup();
234
244
  // f2/rr5: Do NOT remove the signal handlers here. A repeated SIGTERM
235
245
  // between the step loop and the exit.json write must still hit the
236
246
  // idempotent handler (which ignores it) rather than the default handler
@@ -59,7 +59,8 @@ const definitions = [
59
59
  {
60
60
  name: "artifact.save", summary: "Save a file artifact by relativePath into the Task's local Git repository; returns a commit-pinned reference.",
61
61
  effect: "local-mutation", requiredPermissions: ["task:read"], source: "artifacts.saveArtifactCapability",
62
- inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
62
+ inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" },
63
+ encoding: { enum: ["utf8", "base64"] }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
63
64
  outputSchema: { type: "object", required: ["taskId", "commit", "relativePath"] }
64
65
  },
65
66
  {
@@ -319,6 +320,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
319
320
  if (name === "artifact.save")
320
321
  return saveArtifactCapability(store.rootDirectory(), taskId, {
321
322
  relativePath: params.relativePath, content: params.content,
323
+ ...(params.encoding === undefined ? {} : { encoding: params.encoding }),
322
324
  ...(params.message === undefined ? {} : { message: params.message }),
323
325
  ...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
324
326
  });
@@ -42,6 +42,9 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
42
42
  : { inputControl: {
43
43
  action: context.inputControl.action,
44
44
  requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
45
+ ...(context.inputControl.expectedSessionId === undefined ? {} : {
46
+ expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
47
+ }),
45
48
  ...(context.inputControl.expectedTarget === undefined
46
49
  ? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
47
50
  } }),
@@ -236,6 +239,8 @@ export function validateTaskMessage(message) {
236
239
  throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
237
240
  }
238
241
  requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
242
+ if (message.inputControl.expectedSessionId !== undefined)
243
+ requireText(message.inputControl.expectedSessionId, "Message Session");
239
244
  if (message.inputControl.expectedTarget !== undefined) {
240
245
  requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
241
246
  if (message.inputControl.action !== "steer") {
@@ -317,7 +322,11 @@ export function validateTaskMessage(message) {
317
322
  throw new Error("A recipient requires an exact WorkItem or ReviewRound.");
318
323
  }
319
324
  }
320
- if (message.continuation !== undefined && message.recipient?.ownerRunId === undefined) {
325
+ const selectedSessionNondelivery = message.inputControl?.action === "queue"
326
+ && message.inputControl.expectedSessionId !== undefined
327
+ && message.continuation?.runId === undefined
328
+ && message.continuation?.notDeliveredReason !== undefined;
329
+ if (message.continuation !== undefined && message.recipient?.ownerRunId === undefined && !selectedSessionNondelivery) {
321
330
  throw new Error("Message continuation requires an owner Assignment.");
322
331
  }
323
332
  if (message.continuation?.runId !== undefined) {
@@ -373,6 +382,9 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
373
382
  : { inputControl: {
374
383
  action: context.inputControl.action,
375
384
  requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
385
+ ...(context.inputControl.expectedSessionId === undefined ? {} : {
386
+ expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
387
+ }),
376
388
  ...(context.inputControl.expectedTarget === undefined
377
389
  ? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
378
390
  } }),
@@ -404,6 +416,8 @@ export function validateGlobalRoleMessage(message) {
404
416
  throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
405
417
  }
406
418
  requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
419
+ if (message.inputControl.expectedSessionId !== undefined)
420
+ requireText(message.inputControl.expectedSessionId, "Message Session");
407
421
  if (message.inputControl.expectedTarget !== undefined) {
408
422
  requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
409
423
  if (message.inputControl.action !== "steer") {
@@ -22,7 +22,10 @@ export function renderRoleLaunchComparison(role, effective) {
22
22
  permission(binding),
23
23
  current.defaultAccess
24
24
  ];
25
- }), defaultTableWidth());
25
+ }), defaultTableWidth()) + "\n\nSkill package evidence:\n" + (effective.skillPackages === undefined
26
+ ? " Legacy / externally recorded: complete package versions were not recorded."
27
+ : effective.skillPackages.map(skill => ` ${skill.id} @ ${skill.digest}\n ${skill.source.kind}: ${skill.source.path}\n`
28
+ + ` ${skill.fileCount} files; ${skill.byteSize} bytes; inventory: ${skill.manifestPath}`).join("\n"));
26
29
  }
27
30
  export function activeRoleSummary(role) {
28
31
  const binding = role.agentBindings[role.activeAgentId];
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { supportedGit } from "./gitCompatibility.js";
3
4
  /**
4
5
  * Freeze the exact committed heads of a durable managed Lane workspace at the
5
6
  * synchronous runtime-terminalization boundary, before opening the result
@@ -67,7 +68,7 @@ export function snapshotExecutionLaneWorkspaceSync(store, workspace) {
67
68
  }
68
69
  function git(cwd, args) {
69
70
  try {
70
- return execFileSync("git", args, {
71
+ return execFileSync(supportedGit().path, args, {
71
72
  cwd,
72
73
  encoding: "utf8",
73
74
  timeout: 5_000,
@@ -0,0 +1,62 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { resolveExecutable } from "../external/pinnedCommandRunner.js";
4
+ // The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
5
+ // implement it by saving/restoring FETCH_HEAD without racing another writer.
6
+ export const MINIMUM_GIT_VERSION = "2.29.0";
7
+ export function gitVersionSupport(output) {
8
+ const match = /^git version (\d+)\.(\d+)(?:\.(\d+))?(?:[.\s-].*)?$/u.exec(output.trim());
9
+ const version = match === null ? "unknown" : `${match[1]}.${match[2]}.${match[3] ?? "0"}`;
10
+ const supported = match !== null && (Number(match[1]) > 2
11
+ || (Number(match[1]) === 2 && Number(match[2]) >= 29));
12
+ return {
13
+ version, supported,
14
+ detail: `Git ${version}; requires >= ${MINIMUM_GIT_VERSION} (isolated fetch without writing FETCH_HEAD). `
15
+ + "Read-only version check; repository permissions, transport and server capabilities are not tested."
16
+ };
17
+ }
18
+ let cached;
19
+ /** Pin the executable checked, reusing only an unchanged on-disk identity. */
20
+ export function supportedGit() {
21
+ const selected = resolveExecutable("git", process.env.PATH);
22
+ if (selected === undefined)
23
+ throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
24
+ const path = realpathSync(selected);
25
+ const stat = statSync(path);
26
+ const identity = `${path}:${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
27
+ if (cached?.identity === identity)
28
+ return cached;
29
+ const output = execFileSync(path, ["--version"], {
30
+ encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"]
31
+ });
32
+ const support = gitVersionSupport(output);
33
+ if (!support.supported) {
34
+ throw new Error(`${path}: ${support.detail} No Git operation was started by this call. `
35
+ + "Select a supported Git in PATH, run doctor, then retry; existing resources are retained.");
36
+ }
37
+ cached = { identity, path, version: support.version };
38
+ return cached;
39
+ }
40
+ /** Git paths are relative to the actual -C directory, not the Node process. */
41
+ export function gitOutputLine(output) {
42
+ if (!output.endsWith("\n") || output.length === 1 || output.includes("\0")) {
43
+ throw new Error("Git returned an invalid path or line.");
44
+ }
45
+ // Remove exactly Git's record terminator, never spaces/tabs in a path.
46
+ return output.slice(0, -1);
47
+ }
48
+ /** rev-parse --verify accepts exactly one validated operand on old Git.
49
+ * Keep option-looking data out before removing the newer option terminator.
50
+ */
51
+ export function compatibleGitArguments(args) {
52
+ const marker = args.indexOf("--end-of-options");
53
+ if (marker < 0)
54
+ return [...args];
55
+ const operand = args[marker + 1];
56
+ if (!args.includes("rev-parse") || !args.includes("--verify")
57
+ || marker !== args.length - 2 || operand === undefined
58
+ || operand.length === 0 || operand.startsWith("-") || /[\0\r\n]/u.test(operand)) {
59
+ throw new Error("Unsafe Git revision operand; no Git operation was started.");
60
+ }
61
+ return [...args.slice(0, marker), operand];
62
+ }