@zq-silk/yui 2.3.0 → 2.5.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 (197) hide show
  1. package/README.md +15 -1
  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 +18 -9
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +65 -14
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +18 -3
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
  23. package/dist/controller/globalInputDelivery.js +2 -4
  24. package/dist/controller/jobControl.js +2 -0
  25. package/dist/controller/nativeHostInput.js +37 -0
  26. package/dist/controller/providerRetryDelivery.js +4 -2
  27. package/dist/controller/runtime.js +93 -2
  28. package/dist/controller/runtimeEventInbox.js +2 -2
  29. package/dist/controller/runtimeEventProcessor.js +3 -3
  30. package/dist/doctor/doctor.js +10 -0
  31. package/dist/execution/reviewMainRun.js +1 -0
  32. package/dist/execution/workItemMainRun.js +1 -0
  33. package/dist/executor/agentAdapter.js +3 -5
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +15 -7
  37. package/dist/integration/gitIntegrationService.js +7 -12
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/interaction/notificationPresentation.js +85 -0
  40. package/dist/interaction/operatorPresentation.js +1 -2
  41. package/dist/job/httpPreview.js +61 -0
  42. package/dist/job/jobRunner.js +12 -2
  43. package/dist/kernel/builtinCapabilities.js +3 -1
  44. package/dist/message/message.js +15 -1
  45. package/dist/output/rolePresentation.js +4 -1
  46. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  47. package/dist/repository/gitCompatibility.js +62 -0
  48. package/dist/repository/gitWorkspace.js +48 -71
  49. package/dist/repository/gitWorktreeInventory.js +121 -0
  50. package/dist/repository/project.js +74 -25
  51. package/dist/resources/resourceDiscovery.js +2 -2
  52. package/dist/resources/resourceGc.js +7 -4
  53. package/dist/resources/resourceRegistrar.js +2 -1
  54. package/dist/runtime/agentEndpoint.js +8 -0
  55. package/dist/runtime/agentHost.js +170 -3
  56. package/dist/runtime/codexAppServerRuntime.js +1 -1
  57. package/dist/runtime/codexNativeAccess.js +175 -0
  58. package/dist/runtime/controlledCodexTui.js +116 -0
  59. package/dist/runtime/publicReply.js +23 -0
  60. package/dist/runtime/runtimeObservation.js +48 -0
  61. package/dist/runtime/structuredProviderHost.js +16 -7
  62. package/dist/runtime/taskCostMetrics.js +133 -0
  63. package/dist/runtime/taskUsageMetrics.js +11 -2
  64. package/dist/runtime/taskUsageQuery.js +38 -0
  65. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  66. package/dist/scheduler/taskExecutionProjection.js +2 -0
  67. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  68. package/dist/storage/homeLayout.js +13 -0
  69. package/dist/storage/sqliteSchema.js +44 -0
  70. package/dist/storage/sqliteStore.js +53 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/storage/storeRpc.js +2 -0
  73. package/dist/storage/taskSearch.js +35 -0
  74. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  75. package/dist/task/ordinaryArchive.js +42 -0
  76. package/dist/tmux/tmuxManager.js +8 -8
  77. package/dist/web/assets/assetManifest.js +11 -51
  78. package/dist/web/assets/client/app/catalog.js +215 -0
  79. package/dist/web/assets/client/app/main.js +148 -0
  80. package/dist/web/assets/client/app/selection.js +139 -0
  81. package/dist/web/assets/client/app/settings.js +326 -0
  82. package/dist/web/assets/client/app/taskView.js +280 -0
  83. package/dist/web/assets/client/domain/context.js +96 -0
  84. package/dist/web/assets/client/domain/records.js +126 -0
  85. package/dist/web/assets/client/domain/runs.js +119 -0
  86. package/dist/web/assets/client/domain/taskForms.js +197 -0
  87. package/dist/web/assets/client/domain/vocab.js +69 -0
  88. package/dist/web/assets/client/domain/work.js +116 -0
  89. package/dist/web/assets/client/i18n/en.js +900 -0
  90. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  91. package/dist/web/assets/client/index.js +114 -0
  92. package/dist/web/assets/client/layout/resizer.js +37 -0
  93. package/dist/web/assets/client/layout/router.js +35 -0
  94. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  95. package/dist/web/assets/client/layout/sizing.js +95 -0
  96. package/dist/web/assets/client/layout/workspace.js +247 -0
  97. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  98. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  99. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  100. package/dist/web/assets/client/lib/i18n.js +54 -0
  101. package/dist/web/assets/client/lib/markdown.js +167 -0
  102. package/dist/web/assets/client/lib/prefs.js +28 -0
  103. package/dist/web/assets/client/lib/theme.js +68 -0
  104. package/dist/web/assets/client/ui/containers.js +101 -0
  105. package/dist/web/assets/client/ui/controls.js +51 -0
  106. package/dist/web/assets/client/ui/forms.js +48 -0
  107. package/dist/web/assets/client/ui/metrics.js +137 -0
  108. package/dist/web/assets/client/ui/primitives.js +92 -0
  109. package/dist/web/assets/client/ui/text.js +33 -0
  110. package/dist/web/assets/client/ui/toast.js +12 -0
  111. package/dist/web/assets/client/views/bodySearch.js +172 -0
  112. package/dist/web/assets/client/views/createTask.js +85 -0
  113. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  114. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  115. package/dist/web/assets/client/views/dock/preview.js +129 -0
  116. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  117. package/dist/web/assets/client/views/globalInput.js +67 -0
  118. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  119. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  120. package/dist/web/assets/client/views/skills.js +152 -0
  121. package/dist/web/assets/client/views/task/actions.js +36 -0
  122. package/dist/web/assets/client/views/task/delivery.js +185 -0
  123. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  124. package/dist/web/assets/client/views/task/files.js +272 -0
  125. package/dist/web/assets/client/views/task/observation.js +153 -0
  126. package/dist/web/assets/client/views/task/overview.js +161 -0
  127. package/dist/web/assets/client/views/task/page.js +131 -0
  128. package/dist/web/assets/client/views/task/records.js +163 -0
  129. package/dist/web/assets/client/views/task/runtime.js +130 -0
  130. package/dist/web/assets/client/views/task/timeline.js +190 -0
  131. package/dist/web/assets/shared/geometry.js +19 -0
  132. package/dist/web/assets/shared/taskTabs.js +8 -0
  133. package/dist/web/assets/shared/themes.js +93 -0
  134. package/dist/web/assets/shell/dialogs.js +63 -0
  135. package/dist/web/assets/shell/dock.js +40 -0
  136. package/dist/web/assets/shell/index.js +40 -0
  137. package/dist/web/assets/shell/settings.js +34 -0
  138. package/dist/web/assets/shell/sidebar.js +45 -0
  139. package/dist/web/assets/styles/index.js +41 -0
  140. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  141. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  142. package/dist/web/assets/styles/markdown.js +22 -3
  143. package/dist/web/assets/styles/tokens.js +10 -63
  144. package/dist/web/assets/styles/ui/controls.js +76 -0
  145. package/dist/web/assets/styles/ui/forms.js +46 -0
  146. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  147. package/dist/web/assets/styles/views/delivery.js +29 -0
  148. package/dist/web/assets/styles/views/dock.js +71 -0
  149. package/dist/web/assets/styles/views/page.js +32 -0
  150. package/dist/web/assets/styles/views/records.js +70 -0
  151. package/dist/web/assets/styles/views/settings.js +26 -0
  152. package/dist/web/assets/styles/views/sidebar.js +74 -0
  153. package/dist/web/assets/styles/views/task.js +80 -0
  154. package/dist/web/assets/styles/views/terminal.js +38 -0
  155. package/dist/web/taskPreviews.js +205 -0
  156. package/dist/web/tmuxWebTerminal.js +16 -9
  157. package/dist/web/webConversation.js +373 -0
  158. package/dist/web/webMaterials.js +155 -0
  159. package/dist/web/webServer.js +295 -18
  160. package/dist/web/webSettings.js +331 -0
  161. package/dist/web/webSkills.js +86 -0
  162. package/dist/web/webSnapshot.js +40 -8
  163. package/dist/web/webTaskSurface.js +106 -8
  164. package/dist/workspace/gitChangeSetCapture.js +3 -1
  165. package/docs/cli-information-contract.md +1 -1
  166. package/docs/cli-information-contract.zh-CN.md +1 -1
  167. package/docs/managed-turn-and-session-runtime.md +31 -0
  168. package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
  169. package/docs/release-workflow.md +54 -0
  170. package/docs/release-workflow.zh-CN.md +39 -0
  171. package/docs/storage-baseline.md +38 -1
  172. package/docs/storage-baseline.zh-CN.md +22 -0
  173. package/docs/task-discovery.md +28 -0
  174. package/docs/testing/verification-levels.md +2 -2
  175. package/i18n/README.zh-CN.md +1 -1
  176. package/package.json +1 -1
  177. package/skills/yui-leader/SKILL.md +5 -4
  178. package/skills/yui-leader/references/execution.md +2 -1
  179. package/skills/yui-operator/SKILL.md +3 -0
  180. package/skills/yui-reviewer/SKILL.md +4 -2
  181. package/skills/yui-runtime/SKILL.md +43 -2
  182. package/skills/yui-worker/SKILL.md +3 -2
  183. package/dist/web/assets/client/app.js +0 -808
  184. package/dist/web/assets/client/components.js +0 -477
  185. package/dist/web/assets/client/detail.js +0 -379
  186. package/dist/web/assets/client/dock.js +0 -309
  187. package/dist/web/assets/client/evidence.js +0 -364
  188. package/dist/web/assets/client/forms.js +0 -230
  189. package/dist/web/assets/client/i18n.js +0 -1277
  190. package/dist/web/assets/client/markdown.js +0 -114
  191. package/dist/web/assets/client/records.js +0 -198
  192. package/dist/web/assets/client/sections.js +0 -349
  193. package/dist/web/assets/client/theme.js +0 -71
  194. package/dist/web/assets/shell.js +0 -161
  195. package/dist/web/assets/styles/components.js +0 -216
  196. package/dist/web/assets/styles/views.js +0 -309
  197. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -9,7 +9,7 @@ import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironme
9
9
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
10
10
  import { NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment } from "../agent/launchEnvironment.js";
11
11
  import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
12
- import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
12
+ import { materializeSessionBootstrap, sessionContextInstructions } from "../context/sessionBootstrapManifest.js";
13
13
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
14
14
  import { activeRoleAgentBinding } from "../role/role.js";
15
15
  import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
@@ -70,11 +70,13 @@ export class FileRoleLaunchPlanner {
70
70
  }
71
71
  /** Recovery addresses the recorded Agent/Session, independent of delivery
72
72
  * admission, a missing worktree, or the old Endpoint code generation. */
73
- planNativeControl(owner) {
73
+ planNativeControl(owner, nativeSessionId) {
74
74
  const set = owner.scope === "task"
75
75
  ? this.store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
76
76
  : this.store.getGlobalRoleSessionSet(owner.roleName);
77
- const session = roleSessionControlTarget(set);
77
+ const session = nativeSessionId === undefined ? roleSessionControlTarget(set)
78
+ : [...Object.values(set?.sessions ?? {}), ...Object.values(set?.history ?? {})]
79
+ .find(entry => entry.nativeSessionId === nativeSessionId);
78
80
  if (session?.adapterId !== "codex")
79
81
  throw new Error("Native metadata control requires a recorded Codex Session.");
80
82
  const configured = this.store.getConfiguredAgent(session.agentId);
@@ -229,7 +231,7 @@ export class FileRoleLaunchPlanner {
229
231
  throw new Error("Global Role has an unmanaged live Session. Stop that exact Session before opening controlled input; it will not be silently replaced.");
230
232
  }
231
233
  const resolvedEffective = activeLiveRoleAgentSession(sessionSet)?.effective
232
- ?? resolveEffectiveLaunch({ role, purpose: "execution" });
234
+ ?? resolveEffectiveLaunch({ store: this.store, role, purpose: "execution" });
233
235
  if (input.effective !== undefined
234
236
  && !isDeepStrictEqual(resolvedEffective, input.effective)) {
235
237
  throw new Error(`Global Role launch effective snapshot changed: ${role.name}.`);
@@ -335,7 +337,12 @@ export class FileRoleLaunchPlanner {
335
337
  Object.assign(launchEnvironment, runtimeIsolation === undefined
336
338
  ? {}
337
339
  : taskRuntimeIsolationEnvironment(runtimeIsolation));
338
- const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, sessionPolicy);
340
+ const skillPackages = input.mode === "resume"
341
+ ? existingSession?.effective.skillPackages : effective.skillPackages;
342
+ if (skillPackages === undefined) {
343
+ throw new Error("This launch has no frozen Skill package evidence (pre-1.5 or externally recorded Session). Create a new Session from current Role configuration; historical Skill versions cannot be reconstructed.");
344
+ }
345
+ const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, { ...sessionPolicy, skillPackages });
339
346
  const bootstrap = materializeSessionBootstrap({
340
347
  yuiHome: this.home,
341
348
  role: launchRole,
@@ -351,7 +358,7 @@ export class FileRoleLaunchPlanner {
351
358
  }
352
359
  const sessionContext = {
353
360
  ...baseSessionContext,
354
- developerInstructions: `Read and follow the exact Yui Session Manifest at ${bootstrap.manifestPath}.`,
361
+ developerInstructions: sessionContextInstructions(bootstrap.manifestPath, bootstrap.manifest.digest),
355
362
  managedContextFile: bootstrap.manifestPath,
356
363
  sessionManifestPath: bootstrap.manifestPath,
357
364
  sessionManifestDigest: bootstrap.manifest.digest
@@ -686,7 +693,7 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
686
693
  if (purpose === "planning") {
687
694
  // Planning has no WorkItem assignment and no managed workspace, so it
688
695
  // resolves the Role's own configured workspace with no writable Project.
689
- return resolveEffectiveLaunch({ role, purpose: "planning" });
696
+ return resolveEffectiveLaunch({ store, role, purpose: "planning" });
690
697
  }
691
698
  const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
692
699
  && !["accepted", "retired"].includes(candidate.status))) ?? null;
@@ -696,6 +703,7 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
696
703
  ?? store.getTaskWorkspace(role.taskId)
697
704
  ?? undefined;
698
705
  return resolveEffectiveLaunch({
706
+ store,
699
707
  role,
700
708
  purpose: "execution",
701
709
  ...(workspace === undefined ? {} : { workspace }),
@@ -5,6 +5,8 @@ import { promisify } from "node:util";
5
5
  import { selectEnvironment } from "../agent/launchEnvironment.js";
6
6
  import { durableJobIdempotencyKey } from "../job/durableJob.js";
7
7
  import { readRuntimeIdentity } from "../release/runtimeRelease.js";
8
+ import { compatibleGitArguments, supportedGit } from "../repository/gitCompatibility.js";
9
+ import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
8
10
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
9
11
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
10
12
  import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
@@ -1047,8 +1049,10 @@ function integrationCheckDirectory(home, taskId, integrationId) {
1047
1049
  return join(home, "artifacts", "integration-checks", taskId, integrationId);
1048
1050
  }
1049
1051
  async function git(args) {
1052
+ const program = supportedGit();
1053
+ const invocation = compatibleGitArguments(args);
1050
1054
  try {
1051
- const result = await executeFile("git", [...args], {
1055
+ const result = await executeFile(program.path, invocation, {
1052
1056
  encoding: "utf8",
1053
1057
  maxBuffer: 16 * 1024 * 1024,
1054
1058
  timeout: 120_000
@@ -1182,15 +1186,6 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
1182
1186
  }
1183
1187
  }
1184
1188
  async function checkedOutWorktreePaths(repositoryPath, targetRef) {
1185
- const porcelain = (await git([
1186
- "-C", repositoryPath, "worktree", "list", "--porcelain"
1187
- ])).trim();
1188
- if (porcelain.length === 0)
1189
- return [];
1190
- return porcelain.split(/\n\n+/u).flatMap((record) => {
1191
- const lines = record.split("\n");
1192
- const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
1193
- const branch = lines.find((line) => line.startsWith("branch "))?.slice("branch ".length);
1194
- return path !== undefined && branch === targetRef ? [path] : [];
1195
- });
1189
+ return (await readGitWorktrees(repositoryPath))
1190
+ .filter(record => record.branch === targetRef).map(record => record.path);
1196
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,85 @@
1
+ import { parseWakeReason } from "../scheduler/wakeReason.js";
2
+ /** Optional producer-authored presentation, stored in the original text, not a
3
+ * parsed business outcome or a second fact store. Reject oversized summaries
4
+ * whole: clipping could remove a failure/authority qualifier. */
5
+ export function authoredNotificationSummary(text) {
6
+ const line = text?.trimStart().split(/\r?\n/u, 1)[0];
7
+ if (!line?.startsWith("Yui-Summary: "))
8
+ return undefined;
9
+ const summary = line.slice("Yui-Summary: ".length).trim();
10
+ return summary.length > 0 && Buffer.byteLength(summary) <= 512
11
+ && !/[\u0000-\u001f\u007f]/u.test(summary) ? summary : undefined;
12
+ }
13
+ export function taskMessagePresentation(message, getRun) {
14
+ const run = message.resultRef?.type === "agent-run-result"
15
+ ? getRun(message.taskId, message.resultRef.runId) : null;
16
+ const summary = authoredNotificationSummary(run?.result?.output ?? message.body);
17
+ const fact = run !== null
18
+ ? `${run.purpose === "review" ? "Review" : "Execution"} result returned (${run.status}); disposition pending`
19
+ : message.kind === "role-result" ? "Role result returned; disposition pending"
20
+ : message.kind === "user" || message.kind === "operator" ? "Additional input received"
21
+ : "System message received";
22
+ return {
23
+ fact,
24
+ ...(summary === undefined ? {} : {
25
+ summary, summarySource: run?.result?.output === undefined ? "message-body" : "agent-run-result"
26
+ })
27
+ };
28
+ }
29
+ export function wakeChangeLabels(reasons) {
30
+ return [...new Set(reasons.map(tag => {
31
+ const { kind } = parseWakeReason(tag);
32
+ switch (kind) {
33
+ case "operator-input":
34
+ case "user-message":
35
+ case "interrupt-then": return "Additional input received";
36
+ case "execution-started": return "Task execution enabled";
37
+ case "task-created": return "Task created";
38
+ case "task-reopened": return "Task reopened; check for scoped follow-up";
39
+ case "role-result": return "Role result returned; disposition pending";
40
+ case "review-failed": return "Review execution failed; inspect original failure";
41
+ case "agent-error":
42
+ case "leader-turn-failed":
43
+ case "role-turn-failed": return "Execution failure needs attention";
44
+ case "job-finished": return "Job ended; inspect outcome";
45
+ case "input-timeout": return "Input deadline reached; inspect recorded decision";
46
+ case "notification-retry": return "Notification retry after recorded correction";
47
+ default: return Buffer.byteLength(kind) <= 120 && !/[\u0000-\u001f\u007f]/u.test(kind)
48
+ ? `Recorded update: ${kind}` : "Other recorded update; inspect wake";
49
+ }
50
+ }))];
51
+ }
52
+ export function wakeMessages(wake, messages) {
53
+ const ids = new Set(wake.refs?.filter(ref => ref.type === "message" && ref.taskId === wake.taskId).map(ref => ref.id));
54
+ const from = Date.parse(wake.fromCursor), to = Date.parse(wake.toCursor);
55
+ return messages.filter(message => ids.has(message.id)
56
+ || Date.parse(message.createdAt) > from && Date.parse(message.createdAt) <= to);
57
+ }
58
+ export function taskNotificationPresentation(wake, store) {
59
+ const messages = wakeMessages(wake, store.listMessages(wake.taskId));
60
+ const labels = wakeChangeLabels(wake.reasons);
61
+ return [
62
+ `Yui Task notification: task=${wake.taskId} wake=${wake.id}.`,
63
+ `Changes: ${labels.slice(0, 4).join("; ")}${labels.length > 4 ? "; more in wake" : ""}.`,
64
+ ...messages.slice(0, 4).map(message => {
65
+ const item = taskMessagePresentation(message, (task, run) => store.getRun(task, run));
66
+ return `- ${message.id} [${message.author.type}${message.author.type === "role" ? `:${message.author.roleName}` : ""}]: ${item.fact}.`
67
+ + (item.summary === undefined ? "" : ` Author summary (${item.summarySource}): ${JSON.stringify(item.summary)}`);
68
+ }),
69
+ ...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
70
+ `Refresh: yui task context ${wake.taskId} --json`,
71
+ `Read inputs/results: yui task wake show ${wake.taskId} ${wake.id} --json`,
72
+ "Apply yui-leader (from the current Manifest); read relevant originals in full and advance authorized work."
73
+ ].join("\n");
74
+ }
75
+ export function globalMessagePresentation(roleName, messageId, message) {
76
+ const summary = authoredNotificationSummary(message?.body);
77
+ return [
78
+ `Yui Global Message: role=${roleName} message=${messageId}.`,
79
+ `Changes: ${message?.author.type ?? "Recorded"} input received.`,
80
+ ...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
81
+ "Refresh Session Context using the current Manifest's loadCommand.",
82
+ `Read original: yui role message show ${roleName} ${messageId} --json`,
83
+ `Apply ${roleName === "operator" ? "yui-operator" : "yui-runtime"} from the current Manifest within existing authority.`
84
+ ].join("\n");
85
+ }
@@ -10,8 +10,7 @@ export function createOperatorBatchPresentation(batchId, items) {
10
10
  receiptId: `operator-batch:${batchId}`,
11
11
  text: [
12
12
  "[Yui updates]",
13
- "Yui recorded the following durable updates. Inspect the referenced records and present only information that changes the user's understanding, authorization, or next action.",
14
- "This notification is not a new execution assignment or authorization to replay prior operations.",
13
+ "Apply yui-operator from the current Manifest: inspect these originals and present material changes or decisions.",
15
14
  ...items.flatMap(renderItem)
16
15
  ].join("\n")
17
16
  };
@@ -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
+ }
@@ -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
+ }