@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
package/README.md CHANGED
@@ -46,7 +46,7 @@ For the self-built coding Agent (without a Controller or Yui Home), see
46
46
  [`yui agent`](docs/native-agent.md). Its explicit configuration and product state
47
47
  are separate from the orchestration setup below.
48
48
 
49
- You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git, tmux, and
49
+ You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git 2.29.0 or newer, tmux, and
50
50
  Node.js `^20.17.0`, `^22.9.0`, `^24.0.0` or `^26.0.0`. On macOS install tmux with
51
51
  `brew install tmux`. For the simplest setup, have Codex CLI or Claude Code CLI
52
52
  installed and ready to use with your own account. Yui coordinates those
@@ -162,6 +162,20 @@ The task list remains at the left, task details occupy the primary reading area,
162
162
  and recorded Task messages live in a secondary discussion pane. You can resize,
163
163
  swap or hide that pane; on narrow screens it opens over the detail. Native Sessions
164
164
  remain separate from recorded Task messages.
165
+ For a current controlled Codex Operator or Task Leader, **Conversation** and
166
+ **Terminal** are two views of the same Thread. Terminal opens the native Codex
167
+ TUI through the existing Host; it does not start another Agent. Only one browser
168
+ terminal holds the writer at a time. Returning to Conversation or closing the
169
+ page detaches that browser, without stopping the Agent. Conversation drafts and
170
+ the browser's default-view preference survive switching. Native questions and
171
+ approvals answer the original Codex request; Leader Task questions remain on
172
+ the separate InputRequest channel. The default-view preference is shared with
173
+ Settings; saving it does not switch the current view or Session.
174
+ MCP forms and URL confirmations can be completed in Terminal or declined in
175
+ Conversation. Native additional input is saved as a user Message with its exact
176
+ steer receipt before submission.
177
+ Attachments and native configuration changes
178
+ are not supported by this managed text-input surface.
165
179
  The detail has four tabs: Overview (user input, progress and key Decisions),
166
180
  Delivery (WorkItems, Reviews, Integration and remote delivery, read on demand),
167
181
  Runtime (Roles, native Session activity, open execution and usage) and Records
@@ -2,6 +2,7 @@ import { openTaskArtifactRepository } from "./taskArtifactRepository.js";
2
2
  import { saveArtifactFile } from "./gitArtifactRef.js";
3
3
  import { requireCommitId } from "./managedGit.js";
4
4
  import { safeRelativeArtifactPath } from "./artifactPaths.js";
5
+ import { isRasterArtifact, MAX_IMAGE_BYTES, rasterMetadata } from "./imageMetadata.js";
5
6
  /**
6
7
  * Capability-shaped adapter over the per-Task local Git artifact repository.
7
8
  *
@@ -13,11 +14,8 @@ import { safeRelativeArtifactPath } from "./artifactPaths.js";
13
14
  * self-certifying commit-pinned reference (`taskId + commit + relativePath`),
14
15
  * which is the identity used for frozen Candidate/Review/completion evidence.
15
16
  *
16
- * Content is handled as UTF-8 text with the same 8 MiB cap as the former
17
- * immutable text artifact. Binary artifacts, high-frequency runtime data and
18
- * very large files are out of scope for this first version by contract (§3.5)
19
- * and are rejected or referenced through their existing resource boundary, not
20
- * carried here.
17
+ * Text is UTF-8 (8 MiB). Static raster images use explicit base64 (4 MiB
18
+ * decoded), with the same immutable repository and image limits as Web.
21
19
  */
22
20
  /** Text cap for a single artifact file, mirroring the former immutable-artifact limit. */
23
21
  const MAX_ARTIFACT_TEXT_BYTES = 8 * 1024 * 1024;
@@ -35,7 +33,23 @@ function requireTextContent(content) {
35
33
  * commit exactly that path, and return the resulting commit-pinned reference.
36
34
  */
37
35
  export async function saveArtifactCapability(home, taskId, input) {
38
- const bytes = requireTextContent(input.content);
36
+ if (input.encoding !== undefined && input.encoding !== "utf8" && input.encoding !== "base64") {
37
+ throw new Error("Artifact encoding must be utf8 or base64.");
38
+ }
39
+ let bytes;
40
+ if (input.encoding === "base64") {
41
+ if (!isRasterArtifact(input.relativePath) || typeof input.content !== "string"
42
+ || input.content.length > Math.ceil(MAX_IMAGE_BYTES / 3) * 4) {
43
+ throw new Error("Base64 artifacts require a static PNG, JPEG or WebP path and at most 4 MiB.");
44
+ }
45
+ bytes = Buffer.from(input.content, "base64");
46
+ if (bytes.toString("base64") !== input.content)
47
+ throw new Error("Artifact image must use canonical base64.");
48
+ rasterMetadata(bytes);
49
+ }
50
+ else {
51
+ bytes = requireTextContent(input.content);
52
+ }
39
53
  return saveArtifactFile(home, taskId, {
40
54
  relativePath: input.relativePath,
41
55
  bytes,
@@ -45,8 +59,8 @@ export async function saveArtifactCapability(home, taskId, input) {
45
59
  }
46
60
  /**
47
61
  * `artifact.read`: read `relativePath` at HEAD, or at a pinned `commit` for
48
- * frozen evidence. Returns the bytes as UTF-8 text plus the resolved commit and
49
- * digest, so a caller can record or verify the exact reference.
62
+ * frozen evidence. Images return explicit base64 and bounded raster metadata;
63
+ * text returns UTF-8. Both carry the resolved commit and exact digest.
50
64
  */
51
65
  export async function readArtifactCapability(home, taskId, input) {
52
66
  const repo = openTaskArtifactRepository(home, taskId);
@@ -54,12 +68,27 @@ export async function readArtifactCapability(home, taskId, input) {
54
68
  throw new Error(`Artifact repository is unavailable for ${taskId}.`);
55
69
  const safeRelative = safeRelativeArtifactPath(input.relativePath);
56
70
  const content = await repo.read(safeRelative, input.commit === undefined ? undefined : requireCommitId(input.commit));
71
+ const image = isRasterArtifact(safeRelative) ? rasterMetadata(content.bytes) : null;
72
+ let text;
73
+ if (image)
74
+ text = content.bytes.toString("base64");
75
+ else {
76
+ try {
77
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(content.bytes);
78
+ }
79
+ catch {
80
+ throw new Error("Artifact is not readable UTF-8 text or a supported raster image.");
81
+ }
82
+ if (text.includes("\0"))
83
+ throw new Error("Binary artifact is not supported as text.");
84
+ }
57
85
  return {
58
86
  taskId,
59
87
  relativePath: content.relativePath,
60
88
  commit: content.commit,
61
- content: content.bytes.toString("utf8"),
62
- digest: content.digest
89
+ content: text,
90
+ digest: content.digest,
91
+ ...(image ? { encoding: "base64", ...image } : {})
63
92
  };
64
93
  }
65
94
  /**
@@ -0,0 +1,120 @@
1
+ import { extname } from "node:path";
2
+ export const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
3
+ export function isRasterArtifact(path) {
4
+ return [".png", ".jpg", ".jpeg", ".webp"].includes(extname(path).toLowerCase());
5
+ }
6
+ /** Bounded raster metadata inspection, not a decoder. The viewer also reports
7
+ * browser decode errors. Never admit SVG/HTML or trust a caller's MIME type. */
8
+ export function rasterMetadata(bytes) {
9
+ if (bytes.length > MAX_IMAGE_BYTES)
10
+ throw new Error("Image exceeds 4 MiB.");
11
+ let mime, width = 0, height = 0;
12
+ if (bytes.length >= 33 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
13
+ && bytes.toString("ascii", 12, 16) === "IHDR" && bytes.readUInt32BE(8) === 13) {
14
+ mime = "image/png";
15
+ width = bytes.readUInt32BE(16);
16
+ height = bytes.readUInt32BE(20);
17
+ let end = false;
18
+ for (let offset = 8; offset + 12 <= bytes.length;) {
19
+ const length = bytes.readUInt32BE(offset);
20
+ const type = bytes.toString("ascii", offset + 4, offset + 8);
21
+ if (offset + length + 12 > bytes.length)
22
+ throw new Error("Truncated PNG image.");
23
+ if (type === "acTL")
24
+ throw new Error("Animated images are unsupported; save a static frame.");
25
+ offset += length + 12;
26
+ if (type === "IEND") {
27
+ end = length === 0 && offset === bytes.length;
28
+ break;
29
+ }
30
+ }
31
+ if (!end)
32
+ throw new Error("Incomplete PNG image.");
33
+ }
34
+ else if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
35
+ mime = "image/jpeg";
36
+ let offset = 2;
37
+ while (offset + 4 <= bytes.length) {
38
+ if (bytes[offset++] !== 255)
39
+ throw new Error("Invalid JPEG image marker.");
40
+ while (bytes[offset] === 255)
41
+ offset++;
42
+ const marker = bytes[offset++];
43
+ if (marker === 0xda || marker === 0xd9)
44
+ break;
45
+ if (offset + 2 > bytes.length)
46
+ throw new Error("Truncated JPEG image.");
47
+ const length = bytes.readUInt16BE(offset);
48
+ if (length < 2 || offset + length > bytes.length)
49
+ throw new Error("Truncated JPEG image.");
50
+ if ([0xc0, 0xc1, 0xc2].includes(marker)) {
51
+ if (length < 8)
52
+ throw new Error("Invalid JPEG image frame.");
53
+ if (width || height)
54
+ throw new Error("Multiple JPEG image frames are unsupported.");
55
+ height = bytes.readUInt16BE(offset + 3);
56
+ width = bytes.readUInt16BE(offset + 5);
57
+ }
58
+ offset += length;
59
+ }
60
+ if (bytes[bytes.length - 2] !== 255 || bytes[bytes.length - 1] !== 217)
61
+ throw new Error("Incomplete JPEG image.");
62
+ }
63
+ else if (bytes.length >= 30 && bytes.toString("ascii", 0, 4) === "RIFF"
64
+ && bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length) {
65
+ mime = "image/webp";
66
+ const kind = bytes.toString("ascii", 12, 16);
67
+ if (kind === "VP8X") {
68
+ if (bytes[20] & 2)
69
+ throw new Error("Animated images are unsupported; save a static frame.");
70
+ width = 1 + bytes.readUIntLE(24, 3);
71
+ height = 1 + bytes.readUIntLE(27, 3);
72
+ }
73
+ else if (kind === "VP8 " && bytes.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
74
+ width = bytes.readUInt16LE(26) & 0x3fff;
75
+ height = bytes.readUInt16LE(28) & 0x3fff;
76
+ }
77
+ else if (kind === "VP8L" && bytes[20] === 47) {
78
+ width = 1 + (bytes.readUInt32LE(21) & 0x3fff);
79
+ height = 1 + ((bytes.readUInt32LE(21) >>> 14) & 0x3fff);
80
+ }
81
+ let frames = 0;
82
+ for (let offset = 12; offset < bytes.length;) {
83
+ if (offset + 8 > bytes.length)
84
+ throw new Error("Truncated WebP image.");
85
+ const length = bytes.readUInt32LE(offset + 4);
86
+ const type = bytes.toString("ascii", offset, offset + 4);
87
+ if (type === "ANIM" || type === "ANMF")
88
+ throw new Error("Animated images are unsupported; save a static frame.");
89
+ if (offset + 8 + length > bytes.length)
90
+ throw new Error("Truncated WebP image.");
91
+ if (type === "VP8 " || type === "VP8L") {
92
+ let frameWidth = 0, frameHeight = 0;
93
+ if (type === "VP8 " && length >= 10
94
+ && bytes.subarray(offset + 11, offset + 14).equals(Buffer.from([157, 1, 42]))) {
95
+ frameWidth = bytes.readUInt16LE(offset + 14) & 0x3fff;
96
+ frameHeight = bytes.readUInt16LE(offset + 16) & 0x3fff;
97
+ }
98
+ else if (type === "VP8L" && length >= 5 && bytes[offset + 8] === 47) {
99
+ frameWidth = 1 + (bytes.readUInt32LE(offset + 9) & 0x3fff);
100
+ frameHeight = 1 + ((bytes.readUInt32LE(offset + 9) >>> 14) & 0x3fff);
101
+ }
102
+ if (frameWidth !== width || frameHeight !== height || ++frames > 1) {
103
+ throw new Error("Invalid WebP image frame dimensions.");
104
+ }
105
+ }
106
+ offset += 8 + length + (length % 2);
107
+ if (offset > bytes.length)
108
+ throw new Error("Truncated WebP image.");
109
+ }
110
+ if (frames !== 1)
111
+ throw new Error("WebP image has no readable frame.");
112
+ }
113
+ else {
114
+ throw new Error("Unsupported or damaged image. Use static PNG, JPEG or WebP (not SVG/GIF).");
115
+ }
116
+ if (!width || !height || width > 8192 || height > 8192 || width * height > 16_000_000) {
117
+ throw new Error("Unsupported image dimensions: maximum 8192 per side and 16 million pixels.");
118
+ }
119
+ return { mime, width, height };
120
+ }
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { supportedGit } from "../repository/gitCompatibility.js";
3
4
  const executeFile = promisify(execFile);
4
5
  /**
5
6
  * Hardened, isolated Git runner for per-Task artifact repositories.
@@ -44,7 +45,7 @@ const HARDENING_FLAGS = Object.freeze([
44
45
  "-c", "protocol.file.allow=never",
45
46
  // Hooks / filters / external programs — no committed script may execute.
46
47
  "-c", `core.hooksPath=${NULL_DEVICE}`,
47
- "-c", "core.fsmonitor=false",
48
+ "-c", "core.fsmonitor=",
48
49
  "-c", "core.pager=cat",
49
50
  "-c", "core.editor=false",
50
51
  // Signing — never shell out to a signing program.
@@ -77,8 +78,9 @@ function managedGitEnvironment() {
77
78
  PATH: process.env.PATH ?? "/usr/bin:/bin",
78
79
  LANG: "C.UTF-8",
79
80
  LC_ALL: "C.UTF-8",
80
- // A neutral HOME keeps any accidental lookup inside a controlled area; config
81
- // lookups are already redirected to the null device below.
81
+ // Older supported Git does not recognize GIT_CONFIG_GLOBAL/SYSTEM.
82
+ // A neutral HOME and absent XDG_CONFIG_HOME also block its global lookup;
83
+ // GIT_CONFIG_NOSYSTEM is understood by all supported versions.
82
84
  HOME: process.platform === "win32" ? (process.env.USERPROFILE ?? "") : "/nonexistent",
83
85
  GIT_CONFIG_NOSYSTEM: "1",
84
86
  GIT_CONFIG_GLOBAL: NULL_DEVICE,
@@ -122,8 +124,9 @@ function assertSafeArguments(args) {
122
124
  */
123
125
  async function spawnManagedGit(repoPath, args, options) {
124
126
  assertSafeArguments(args);
127
+ const program = supportedGit();
125
128
  try {
126
- const result = await executeFile("git", [...HARDENING_FLAGS, "-C", repoPath, ...args], {
129
+ const result = await executeFile(program.path, [...HARDENING_FLAGS, "-C", repoPath, ...args], {
127
130
  encoding: "buffer",
128
131
  env: managedGitEnvironment(),
129
132
  maxBuffer: options?.maxBuffer ?? 16 * 1024 * 1024,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
+ import { supportedGit } from "../repository/gitCompatibility.js";
5
6
  import { acquireArtifactCommitLock } from "./artifactCommitLock.js";
6
7
  import { resolveContainedArtifactPath, safeRelativeArtifactPath, taskArtifactRepoPath } from "./artifactPaths.js";
7
8
  import { ManagedGitError, externalProgramConfigViolations, managedGit, managedGitBuffer, managedGitSucceeds, requireCommitId } from "./managedGit.js";
@@ -96,6 +97,7 @@ export function openTaskArtifactRepository(home, taskId) {
96
97
  return currentHead(repoPath);
97
98
  };
98
99
  const ensure = async () => {
100
+ supportedGit();
99
101
  if (exists()) {
100
102
  await assertNoRemote(taskId, repoPath);
101
103
  return;
@@ -189,6 +191,10 @@ export function openTaskArtifactRepository(home, taskId) {
189
191
  if (objectType !== "blob") {
190
192
  throw new Error(`Artifact ${safeRelative} at ${pinned} is not a file (${objectType}).`);
191
193
  }
194
+ const size = Number((await managedGit(repoPath, ["cat-file", "-s", objectSpec])).trim());
195
+ if (!Number.isSafeInteger(size) || size > MAX_ARTIFACT_BYTES) {
196
+ throw new Error(`Artifact ${safeRelative} exceeds the ${MAX_ARTIFACT_BYTES}-byte limit.`);
197
+ }
192
198
  const bytes = await managedGitBuffer(repoPath, ["cat-file", "blob", objectSpec], {
193
199
  maxBuffer: MAX_ARTIFACT_BYTES + 4096
194
200
  }).catch((error) => {
@@ -387,7 +387,14 @@ const taskChildren = [
387
387
  usage: "yui task list [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
388
388
  options: ["--all", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
389
389
  },
390
+ {
391
+ name: "search", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
392
+ summary: "Search authorized Task bodies and return exact, reference-only source citations.",
393
+ usage: "yui task search <query> [--task <id>] [--kind <brief|message|decision|result|completion>] [--limit <1..100>] [--cursor <cursor>]",
394
+ options: ["--task", "--kind", "--limit", "--cursor"]
395
+ },
390
396
  { name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] }, summary: "Show Task metadata and counts; long details use contentPage.", usage: "yui task show <id> [--cursor <cursor>]", options: ["--cursor"] },
397
+ { name: "usage", discovery: { surface: "public", audiences: ["public", "operator", "leader"] }, summary: "Read bounded Task usage and sourced costs; request detail pages explicitly.", usage: "yui task usage <task> [--limit <0..50>] [--offset <n>]", options: ["--limit", "--offset"] },
391
398
  {
392
399
  name: "artifact", discovery: { surface: "managed", audiences: ["operator", "leader"] },
393
400
  summary: "Save Task files in local Git and read current or commit-pinned content.",
@@ -395,7 +402,7 @@ const taskChildren = [
395
402
  children: [
396
403
  { name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] }, summary: "List saved Task artifacts.", usage: "yui task artifact list <task>" },
397
404
  { name: "read", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] }, summary: "Read a file at HEAD or an exact commit.", usage: "yui task artifact read <task> <relative-path> [<commit>]" },
398
- { name: "save", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Save and locally commit one file.", usage: "yui task artifact save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]", options: ["--message", "--expected-head"] }
405
+ { name: "save", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Save UTF-8 text or a base64 static PNG/JPEG/WebP image and locally commit one file.", usage: "yui task artifact save <task> <relative-path> <content> [--encoding utf8|base64] [--message <text>] [--expected-head <commit>]", options: ["--encoding", "--message", "--expected-head"], optionValues: { "--encoding": ["utf8", "base64"] } }
399
406
  ]
400
407
  },
401
408
  {
@@ -1089,7 +1096,7 @@ const taskChildren = [
1089
1096
  sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "resolve"] }],
1090
1097
  children: [
1091
1098
  { name: "list", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "List recorded Leader wakes.", usage: "yui task wake list <task> [--limit <1..100>] [--cursor <cursor>]", options: ["--limit", "--cursor"] },
1092
- { name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show a fixed wake window and exact detail reads.", usage: "yui task wake show <task> <wake> [--cursor <cursor>]", options: ["--cursor"] },
1099
+ { name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show fixed wake inputs/results; expand diagnostics with --history.", usage: "yui task wake show <task> <wake> [--history] [--cursor <cursor>]", options: ["--history", "--cursor"] },
1093
1100
  { name: "retry", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Retry a rejected notification after correcting its cause; preserve original input.",
1094
1101
  usage: "yui task wake retry <task> <wake> --reason <correction>", options: ["--reason"] },
1095
1102
  { name: "resolve", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Release an unknown notification claim after explicit quiescence evidence, without replay.",
@@ -1514,13 +1521,14 @@ export const ROOT_COMMAND = buildNode({
1514
1521
  {
1515
1522
  name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1516
1523
  summary: "List Project knowledge.",
1517
- usage: "yui project knowledge list <project> [--all]",
1518
- options: ["--all"]
1524
+ usage: "yui project knowledge list <project> [--all] [--limit <1..100>] [--cursor <cursor>]",
1525
+ options: ["--all", "--limit", "--cursor"]
1519
1526
  },
1520
1527
  {
1521
1528
  name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1522
1529
  summary: "Read one Project knowledge entry.",
1523
- usage: "yui project knowledge show <project> <knowledge>"
1530
+ usage: "yui project knowledge show <project> <knowledge> [--cursor <cursor>]",
1531
+ options: ["--cursor"]
1524
1532
  },
1525
1533
  {
1526
1534
  name: "propose", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
@@ -1538,13 +1546,14 @@ export const ROOT_COMMAND = buildNode({
1538
1546
  {
1539
1547
  name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1540
1548
  summary: "List Knowledge promotion proposals (pending by default).",
1541
- usage: "yui project knowledge proposals list <project> [--status pending|accepted|rejected] [--all]",
1542
- options: ["--status", "--all"]
1549
+ usage: "yui project knowledge proposals list <project> [--status pending|accepted|rejected] [--all] [--limit <1..100>] [--cursor <cursor>]",
1550
+ options: ["--status", "--all", "--limit", "--cursor"]
1543
1551
  },
1544
1552
  {
1545
1553
  name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1546
1554
  summary: "Show one Knowledge promotion proposal.",
1547
- usage: "yui project knowledge proposals show <project> <proposal>"
1555
+ usage: "yui project knowledge proposals show <project> <proposal> [--cursor <cursor>]",
1556
+ options: ["--cursor"]
1548
1557
  }
1549
1558
  ]
1550
1559
  },
@@ -1586,7 +1595,7 @@ export const ROOT_COMMAND = buildNode({
1586
1595
  sections: [
1587
1596
  { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "activation", "execution", "complete", "cancel", "reopen", "retire", "list", "show", "context", "next-action", "remote-delivery", "archive-preflight", "archive", "archive-cleanup", "replace", "reconcile", "upstream", "artifact"] },
1588
1597
  { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
1589
- { id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
1598
+ { id: "knowledge", title: "Task Knowledge", entries: ["search", "brief", "decision", "milestone", "event", "continuation", "wake", "usage"] }
1590
1599
  ],
1591
1600
  children: taskChildren
1592
1601
  },
@@ -35,7 +35,7 @@ export function assertTaskInvocationScope(args, env) {
35
35
  if (args[0] === "jobs" || (args[0] === "task" && ["create", "overlap"].includes(args[1] ?? ""))) {
36
36
  throw usageError("This global operation is outside the caller's Task; use its scoped Context.");
37
37
  }
38
- if (args[0] !== "task" || args[1] === "list")
38
+ if (args[0] !== "task" || ["list", "search"].includes(args[1] ?? ""))
39
39
  return;
40
40
  const diagnostic = taskDiagnosticTarget(args);
41
41
  if (diagnostic !== undefined) {
@@ -251,6 +251,11 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
251
251
  lines.push("", "Observed usage — Task lifetime, observed sources only (audit time window not applied):");
252
252
  for (const usage of report.usage.data) {
253
253
  lines.push(` ${usage.taskId}: tokens=${formatUsageMetric(usage.tokens)}; tools=${formatUsageMetric(usage.toolCalls)}; elapsed=${formatUsageMetric(usage.elapsedSeconds, "s")}; native execution sum=${formatUsageMetric(usage.executionSeconds, "s")}`);
254
+ for (const kind of ["actual", "estimated"]) {
255
+ const costs = usage.costs[kind];
256
+ lines.push(` ${kind} cost: ${costs.amounts.length === 0 ? `unknown (${costs.reasons.join(", ")})`
257
+ : costs.amounts.map(amount => `${formatUsageMetric(amount)} ${amount.currency}`).join("; ")} (observed sources only, not a bill)`);
258
+ }
254
259
  }
255
260
  }
256
261
  else {
@@ -16,8 +16,38 @@ import { isSystemRoleName, SYSTEM_ROLE_NAMES, systemRoleDescription } from "../r
16
16
  import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
17
17
  import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
18
18
  import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
19
+ /** Preview the complete binding, not just the fields supplied by the caller. */
20
+ export function previewGlobalRoleAgentConfigurationMutation(args, store, options = {}) {
21
+ const [command, rawName, ...tail] = args;
22
+ if (command !== "add" && command !== "update")
23
+ return undefined;
24
+ const name = roleName(rawName);
25
+ const parsed = parseRoleOptions(tail, roleOptionSpecs({
26
+ update: command === "update", includeAgent: true, includeWorkspace: true
27
+ }));
28
+ if (command === "update" && !hasAgentConfigOptions(parsed))
29
+ return undefined;
30
+ const role = command === "update" ? requireRole(name, store) : undefined;
31
+ const agentId = command === "add"
32
+ ? required(parsed.one("--agent"), "--agent")
33
+ : parsed.one("--agent")?.trim() || role.activeAgentId;
34
+ const agent = requireAgent(agentId, store);
35
+ const binding = patchRoleAgentBinding(role?.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent)), parsed);
36
+ return {
37
+ agentId, config: binding.config,
38
+ cwd: trimmed(parsed.one("--workspace")) ?? role?.workspace
39
+ ?? (options.yuiHome !== undefined ? managedGlobalRoleWorkspace(options.yuiHome)
40
+ : store.getConfig().defaultWorkspace ?? process.cwd())
41
+ };
42
+ }
19
43
  export function applyGlobalInputControl(name, input, store, options = {}) {
20
44
  assertGlobalInputAuthority(store, options, name);
45
+ if (input.expectedSessionId !== undefined) {
46
+ const set = store.getGlobalRoleSessionSet(name);
47
+ if (set?.sessions[set.activeAgentId]?.nativeSessionId !== input.expectedSessionId) {
48
+ throw usageError("Selected native Session changed; refresh before sending.");
49
+ }
50
+ }
21
51
  return input.action === "interrupt"
22
52
  ? interruptGlobalInput(name, input, store)
23
53
  : messageGlobalInput(name, input, store, options);
@@ -153,6 +183,7 @@ function roleContext(args, store, options) {
153
183
  // Historical interrupt receipts are available through Session inspection;
154
184
  // only this Turn's exact identity is required at entry.
155
185
  sessionManifestPath: environment.YUI_SESSION_MANIFEST,
186
+ skillPackages: session?.effective.skillPackages ?? null,
156
187
  cliCommand: "yui"
157
188
  });
158
189
  const data = boundedDocument(context, `global:${name}/context`, parsed.cursor);
@@ -206,6 +237,7 @@ function addRole(args, store, options) {
206
237
  ? managedGlobalRoleWorkspace(options.yuiHome)
207
238
  : tx.getConfig().defaultWorkspace ?? process.cwd());
208
239
  const binding = patchRoleAgentBinding(createRoleAgentBinding(definition(agent)), parsed);
240
+ options.validateAgentConfiguration?.({ agentId, config: binding.config, cwd: workspace });
209
241
  const profile = roleProfileFrom(parsed);
210
242
  validateConfiguredRoleSkills(options.yuiHome, profile.skills ?? []);
211
243
  const role = createGlobalRole(name, [binding], agent.id, workspace, now, profile);
@@ -295,7 +327,11 @@ function updateRole(args, store, options) {
295
327
  const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
296
328
  const agent = requireAgent(agentId, tx);
297
329
  const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
298
- bindings = { ...role.agentBindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
330
+ const patched = patchRoleAgentBinding(binding, parsed);
331
+ options.validateAgentConfiguration?.({
332
+ agentId, config: patched.config, cwd: workspace ?? role.workspace
333
+ });
334
+ bindings = { ...role.agentBindings, [agentId]: patched };
299
335
  }
300
336
  const updated = updateGlobalRole(role, {
301
337
  ...(workspace === undefined ? {} : { workspace }),
@@ -537,7 +573,8 @@ export function sendGlobalRoleMessageCommand(store, roleName, body, author, inpu
537
573
  const original = existing.inputControl ?? existing.interruptThen?.reusedInput;
538
574
  if (original?.action !== inputControl.action
539
575
  || existing.body !== body
540
- || original.expectedTarget !== inputControl.expectedTarget) {
576
+ || original.expectedTarget !== inputControl.expectedTarget
577
+ || original.expectedSessionId !== inputControl.expectedSessionId) {
541
578
  throw usageError(`Input requestId ${inputControl.requestId} was already used with different content or target; use a new requestId for a new input.`);
542
579
  }
543
580
  return { message: existing, idempotentReplay: true };
@@ -630,7 +667,8 @@ function messageGlobalInput(name, input, store, options) {
630
667
  ? environment.YUI_ROLE === "operator" ? "operator" : "agent" : "user" };
631
668
  const persisted = store.transaction((tx) => {
632
669
  requireRole(name, tx);
633
- return sendGlobalRoleMessageCommand(tx, name, body, author, { action, requestId, ...(expectedTarget === undefined ? {} : { expectedTarget }) }, now);
670
+ return sendGlobalRoleMessageCommand(tx, name, body, author, { action, requestId, ...(expectedTarget === undefined ? {} : { expectedTarget }),
671
+ ...(input.expectedSessionId === undefined ? {} : { expectedSessionId: input.expectedSessionId }) }, now);
634
672
  });
635
673
  if (action === "queue") {
636
674
  const state = persisted.idempotentReplay ? "idempotent-replay" : "queued";