@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
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,8 +162,24 @@ 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
- The detail prioritizes user input, native Session activity, Task progress and key
166
- conclusions; fixed file results and delivery evidence open on demand. See
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.
179
+ The detail has four tabs: Overview (user input, progress and key Decisions),
180
+ Delivery (WorkItems, Reviews, Integration and remote delivery, read on demand),
181
+ Runtime (Roles, native Session activity, open execution and usage) and Records
182
+ (a timeline of Decisions, milestones, executions and questions). See
167
183
  [attention and progress](docs/observability/README.md#web-attention-and-progress).
168
184
  See [Web permissions](docs/architecture/capabilities-and-resources.md#cli-and-web)
169
185
  and [input timing](docs/managed-turn-and-session-runtime.md#input-timing-queue-steer-and-interrupt).
@@ -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
  {
@@ -422,9 +429,14 @@ const taskChildren = [
422
429
  usage: "yui task archive-preflight <id> (--integrated|--abandon) [--force] [--json]",
423
430
  options: ["--integrated", "--abandon", "--force", "--json"]
424
431
  },
432
+ {
433
+ name: "archive-cleanup", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
434
+ summary: "Retry one foreground exact-owner cleanup for an archived Task; retain unsafe resources and all history.",
435
+ usage: "yui task archive-cleanup <id>"
436
+ },
425
437
  {
426
438
  name: "archive", discovery: { surface: "public", audiences: ["public", "operator", "leader"], optionAudiences: { "--force": ["public", "operator"] } },
427
- summary: "Archive a terminal Task; explicit --force commits despite delivery/cleanup warnings, retaining unsafe resources.",
439
+ summary: "Archive settled, delivered, stopped work, then attempt exact-owner cleanup. Retained physical resources do not require --force; force separately bypasses business admission.",
428
440
  usage: "yui task archive <id> (--integrated|--abandon) [--force] [--source-message <id> --purpose <verbatim-authorization> --request-id <id>]",
429
441
  options: ["--integrated", "--abandon", "--force", "--source-message", "--purpose", "--request-id"]
430
442
  },
@@ -992,7 +1004,7 @@ const taskChildren = [
992
1004
  },
993
1005
  { name: "list", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "List Integration Attempts.", usage: "yui task integration list <task>" },
994
1006
  { name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show one Integration Attempt.", usage: "yui task integration show <task>/<integration>" },
995
- { name: "cleanup", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Remove a terminal Integration worktree and branch.", usage: "yui task integration cleanup <task>/<integration>" }
1007
+ { name: "cleanup", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Release clean terminal Integration resources; preserve attempt history, check logs and dirty diagnostics.", usage: "yui task integration cleanup <task>/<integration>" }
996
1008
  ]
997
1009
  },
998
1010
  {
@@ -1509,13 +1521,14 @@ export const ROOT_COMMAND = buildNode({
1509
1521
  {
1510
1522
  name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1511
1523
  summary: "List Project knowledge.",
1512
- usage: "yui project knowledge list <project> [--all]",
1513
- options: ["--all"]
1524
+ usage: "yui project knowledge list <project> [--all] [--limit <1..100>] [--cursor <cursor>]",
1525
+ options: ["--all", "--limit", "--cursor"]
1514
1526
  },
1515
1527
  {
1516
1528
  name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1517
1529
  summary: "Read one Project knowledge entry.",
1518
- usage: "yui project knowledge show <project> <knowledge>"
1530
+ usage: "yui project knowledge show <project> <knowledge> [--cursor <cursor>]",
1531
+ options: ["--cursor"]
1519
1532
  },
1520
1533
  {
1521
1534
  name: "propose", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
@@ -1533,13 +1546,14 @@ export const ROOT_COMMAND = buildNode({
1533
1546
  {
1534
1547
  name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1535
1548
  summary: "List Knowledge promotion proposals (pending by default).",
1536
- usage: "yui project knowledge proposals list <project> [--status pending|accepted|rejected] [--all]",
1537
- 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"]
1538
1551
  },
1539
1552
  {
1540
1553
  name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
1541
1554
  summary: "Show one Knowledge promotion proposal.",
1542
- usage: "yui project knowledge proposals show <project> <proposal>"
1555
+ usage: "yui project knowledge proposals show <project> <proposal> [--cursor <cursor>]",
1556
+ options: ["--cursor"]
1543
1557
  }
1544
1558
  ]
1545
1559
  },
@@ -1579,9 +1593,9 @@ export const ROOT_COMMAND = buildNode({
1579
1593
  name: "task", discovery: { surface: "public", audiences: ["public", "operator"] },
1580
1594
  summary: "Manage Tasks, WorkItems, AgentRuns, and integration.",
1581
1595
  sections: [
1582
- { 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", "replace", "reconcile", "upstream", "artifact"] },
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"] },
1583
1597
  { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
1584
- { 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"] }
1585
1599
  ],
1586
1600
  children: taskChildren
1587
1601
  },
@@ -236,6 +236,10 @@ export const INTERACTION_POLICIES = Object.freeze([
236
236
  ...taskTarget("archive-preflight"),
237
237
  trailingOptions: { "--integrated": "flag", "--abandon": "flag", "--force": "flag", "--json": "flag" }
238
238
  },
239
+ {
240
+ ...taskTarget("archive-cleanup", 2, ["archived"]),
241
+ confirmation: { action: "Clean archived Task resources", targetArgumentIndex: 2 }
242
+ },
239
243
  {
240
244
  ...taskTarget("archive", 2, ["completed", "cancelled"]),
241
245
  trailingOptions: { "--integrated": "flag", "--abandon": "flag", "--force": "flag" },
@@ -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";