@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
@@ -3,7 +3,10 @@ import { createServer } from "node:http";
3
3
  import WebSocket, { WebSocketServer } from "ws";
4
4
  import { CliError, usageError } from "../errors/cliError.js";
5
5
  import { parseTaskCatalogOptions } from "../context/taskCatalog.js";
6
+ import { parseTaskSearchOptions, searchTaskBodies } from "../context/taskSearch.js";
7
+ import { readTaskUsage } from "../runtime/taskUsageQuery.js";
6
8
  import { WebRequestRejected } from "./webMutation.js";
9
+ import { SETTINGS_HTML } from "./assets/shell/settings.js";
7
10
  import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
8
11
  import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
9
12
  import { buildWebTaskCatalog, buildWebPageSessions, buildWebTaskDetail } from "./webSnapshot.js";
@@ -39,13 +42,14 @@ export function parseWebCommandOptions(args) {
39
42
  export function createYuiWebServer(store, dependencies = {}) {
40
43
  const now = dependencies.now ?? (() => new Date());
41
44
  const token = dependencies.token ?? randomBytes(24).toString("base64url");
45
+ const httpDependencies = { ...dependencies, previews: dependencies.previews?.() };
42
46
  const webSocketServer = new WebSocketServer({
43
47
  noServer: true,
44
48
  perMessageDeflate: false,
45
49
  maxPayload: MAX_TERMINAL_MESSAGE_BYTES
46
50
  });
47
51
  const server = createServer((request, response) => {
48
- void handleHttpRequest(request, response, store, dependencies, token, now).catch(() => {
52
+ void handleHttpRequest(request, response, store, httpDependencies, token, now).catch(() => {
49
53
  if (!response.headersSent)
50
54
  setSecurityHeaders(response);
51
55
  if (!response.writableEnded) {
@@ -100,7 +104,229 @@ async function handleHttpRequest(request, response, store, dependencies, token,
100
104
  sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
101
105
  return;
102
106
  }
107
+ if (pathname.startsWith("/preview/") && dependencies.previews) {
108
+ await dependencies.previews.serve(request, response);
109
+ return;
110
+ }
111
+ const previewMatch = /^\/api\/tasks\/([A-Za-z0-9_-]+)\/previews(?:\/([A-Za-z0-9_-]+)\/stop)?$/.exec(pathname);
112
+ if (previewMatch && dependencies.previews) {
113
+ try {
114
+ if (method === "GET" && !previewMatch[2]) {
115
+ sendJson(response, 200, await dependencies.previews.list(previewMatch[1]), false);
116
+ }
117
+ else if (method === "POST" && previewMatch[2]) {
118
+ const body = await readMutationBody(request);
119
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length) {
120
+ throw new WebRequestRejected("Stop accepts an empty object; the URL selects the exact service.");
121
+ }
122
+ sendJson(response, 200, dependencies.previews.stop(previewMatch[1], previewMatch[2]), false);
123
+ }
124
+ else
125
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
126
+ }
127
+ catch (error) {
128
+ sendJson(response, error instanceof WebRequestRejected ? 409 : 500, {
129
+ error: error instanceof Error ? error.message : "Preview request failed.",
130
+ disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown"
131
+ }, false);
132
+ }
133
+ return;
134
+ }
135
+ if ((pathname === "/api/tasks" || pathname === "/api/projects") && dependencies.surface) {
136
+ try {
137
+ if (pathname === "/api/projects" && method === "GET") {
138
+ const query = new URL(request.url, "http://localhost").searchParams;
139
+ if ([...query.keys()].some(key => key !== "after") || query.getAll("after").length > 1) {
140
+ throw new WebRequestRejected("Project listing accepts only one after cursor.");
141
+ }
142
+ sendJson(response, 200, dependencies.surface.projects(query.get("after") ?? undefined), false);
143
+ }
144
+ else if (pathname === "/api/tasks" && method === "POST") {
145
+ const body = await readMutationBody(request);
146
+ if (!body || typeof body !== "object" || Array.isArray(body)
147
+ || Object.keys(body).some(key => !["title", "requirements", "projectIds", "plan", "requestId"].includes(key))
148
+ || !("title" in body) || typeof body.title !== "string" || !body.title.trim() || body.title.length > 200
149
+ || !("requirements" in body) || typeof body.requirements !== "string" || !body.requirements.trim()
150
+ || !("projectIds" in body) || !Array.isArray(body.projectIds) || body.projectIds.length > 32
151
+ || body.projectIds.some(id => typeof id !== "string" || !/^project-[a-zA-Z0-9_-]+$/.test(id))
152
+ || !("plan" in body) || typeof body.plan !== "boolean"
153
+ || !("requestId" in body) || typeof body.requestId !== "string" || !body.requestId.trim()) {
154
+ throw new WebRequestRejected("Expected title, requirements, projectIds, plan and requestId only.");
155
+ }
156
+ sendJson(response, 200, { ...dependencies.surface.create(body), requestId: body.requestId }, false);
157
+ }
158
+ else
159
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
160
+ }
161
+ catch (error) {
162
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Creation unavailable.",
163
+ disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
164
+ }
165
+ return;
166
+ }
167
+ if (pathname.startsWith("/api/skills/")) {
168
+ try {
169
+ const service = dependencies.skills;
170
+ if (!service)
171
+ throw new WebRequestRejected("Skill service unavailable.");
172
+ const q = new URL(request.url, "http://localhost").searchParams;
173
+ if ([...q.keys()].some(k => !["scope", "task", "role", "id", "resource", "frozen", "digest", "run", "q", "cursor"].includes(k)
174
+ || q.getAll(k).length !== 1))
175
+ throw new WebRequestRejected("Unknown or duplicate Skill query.");
176
+ if (q.has("frozen") && !["true", "false"].includes(q.get("frozen"))
177
+ || q.has("run") && q.get("frozen") !== "true")
178
+ throw new WebRequestRejected("A Run read requires an explicit frozen version.");
179
+ const selected = { scope: q.get("scope"), role: q.get("role"), ...(q.has("task") ? { task: q.get("task") } : {}) };
180
+ let value;
181
+ if (method === "GET" && pathname === "/api/skills/catalog")
182
+ value = service.catalog(q.get("q") ?? "", q.get("cursor") ?? "0");
183
+ else if (method === "GET" && pathname === "/api/skills/roles")
184
+ value = service.roles(q.get("scope") ?? "", q.get("task") ?? undefined);
185
+ else if (method === "GET" && pathname === "/api/skills/role")
186
+ value = service.role(selected);
187
+ else if (method === "GET" && pathname === "/api/skills/file")
188
+ value = service.file({
189
+ id: q.get("id") ?? "", resource: q.get("resource") ?? undefined,
190
+ frozen: q.get("frozen") === "true", digest: q.get("digest") ?? undefined, run: q.get("run") ?? undefined, target: selected
191
+ });
192
+ else if (method === "POST" && pathname === "/api/skills/role")
193
+ value = service.save(await readMutationBody(request));
194
+ else {
195
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
196
+ return;
197
+ }
198
+ sendJson(response, 200, value, false);
199
+ }
200
+ catch (error) {
201
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Skill operation unavailable.",
202
+ disposition: method === "GET" || error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
203
+ }
204
+ return;
205
+ }
206
+ if (pathname === "/api/settings" || pathname === "/api/settings/group" || pathname === "/api/settings/capabilities") {
207
+ try {
208
+ if (!dependencies.settings)
209
+ throw new WebRequestRejected("Settings service unavailable.");
210
+ const query = new URL(request.url, "http://localhost").searchParams;
211
+ if ([...query.keys()].some(k => !["id", "refresh", "q", "cursor"].includes(k)))
212
+ throw new WebRequestRejected("Unknown settings query.");
213
+ let value;
214
+ if (method === "GET" && pathname === "/api/settings")
215
+ value = dependencies.settings.index(query.get("q") ?? "", query.get("cursor") ?? "0");
216
+ else if (method === "GET" && pathname === "/api/settings/group")
217
+ value = dependencies.settings.read(query.get("id") ?? "");
218
+ else if (method === "GET" && pathname === "/api/settings/capabilities")
219
+ value = await dependencies.settings.capabilities(query.get("id") ?? "", query.get("refresh") === "true");
220
+ else if (method === "POST" && pathname === "/api/settings/group")
221
+ value = await dependencies.settings.save(await readMutationBody(request));
222
+ else {
223
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
224
+ return;
225
+ }
226
+ sendJson(response, 200, value, false);
227
+ }
228
+ catch (error) {
229
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Settings unavailable.",
230
+ disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
231
+ }
232
+ return;
233
+ }
103
234
  const artifactTarget = /^\/api\/tasks\/([^/]+)\/(artifacts|evidence)$/.exec(pathname);
235
+ if (["/api/conversation", "/api/conversation/material", "/api/conversation/model"].includes(pathname) && dependencies.conversation) {
236
+ try {
237
+ const q = new URL(request.url, "http://localhost").searchParams;
238
+ if ([...q.keys()].some(k => !["scope", "task", "role", "session", "cursor", "offset", "requestId"].includes(k))) {
239
+ throw new WebRequestRejected("Unknown conversation parameter.");
240
+ }
241
+ const roleName = safeIdentity(q.get("role"), "Role");
242
+ const owner = q.get("scope") === "task"
243
+ ? { scope: "task", taskId: safeIdentity(q.get("task"), "Task"), roleName }
244
+ : q.get("scope") === "global" && !q.has("task") ? { scope: "global", roleName }
245
+ : (() => { throw new WebRequestRejected("Invalid conversation scope."); })();
246
+ let result;
247
+ if (pathname.endsWith("/model")) {
248
+ const id = safeIdentity(q.get("session"), "Session");
249
+ if (method === "GET")
250
+ result = await dependencies.conversation.models(owner, id);
251
+ else if (method === "POST") {
252
+ const body = await readMutationBody(request);
253
+ if (!body || typeof body !== "object" || Array.isArray(body)
254
+ || Object.keys(body).some(k => k !== "model")
255
+ || typeof body.model !== "string") {
256
+ throw new WebRequestRejected("Expected one native model selection.");
257
+ }
258
+ result = await dependencies.conversation.setModel(owner, id, body.model);
259
+ }
260
+ else
261
+ throw new WebRequestRejected("Model access requires GET or POST.");
262
+ }
263
+ else if (pathname.endsWith("/material")) {
264
+ if (method !== "POST")
265
+ throw new WebRequestRejected("Material upload requires POST.");
266
+ const body = await readMutationBody(request, 2 * 1024 * 1024);
267
+ if (!body || typeof body !== "object" || Array.isArray(body)
268
+ || Object.keys(body).some(k => !["requestId", "name", "content"].includes(k))) {
269
+ throw new WebRequestRejected("Expected material requestId, name and UTF-8 content only.");
270
+ }
271
+ const value = body;
272
+ if (typeof value.requestId !== "string" || typeof value.name !== "string" || typeof value.content !== "string") {
273
+ throw new WebRequestRejected("Invalid material fields.");
274
+ }
275
+ result = await dependencies.conversation.material(owner, safeIdentity(q.get("session"), "Session"), value);
276
+ }
277
+ else if (method === "GET" && q.has("requestId")) {
278
+ result = dependencies.conversation.receipt(owner, safeIdentity(q.get("requestId"), "Request"));
279
+ }
280
+ else if (method === "GET" && q.has("session")) {
281
+ const cursor = q.get("cursor");
282
+ if (cursor !== null && cursor.length > 16000)
283
+ throw new WebRequestRejected("Cursor too large.");
284
+ result = await dependencies.conversation.history(owner, safeIdentity(q.get("session"), "Session"), cursor ?? undefined);
285
+ }
286
+ else if (method === "GET") {
287
+ const offset = q.get("offset") ?? "0";
288
+ if (!/^\d{1,6}$/.test(offset))
289
+ throw new WebRequestRejected("Invalid session offset.");
290
+ result = dependencies.conversation.state(owner, Number(offset));
291
+ }
292
+ else if (method === "POST") {
293
+ const body = await readMutationBody(request);
294
+ if (!body || typeof body !== "object" || Array.isArray(body)
295
+ || Object.keys(body).some(k => !["action", "requestId", "body", "expectedTarget", "materials", "nativeRequestId", "result"].includes(k))) {
296
+ throw new WebRequestRejected("Invalid conversation input.");
297
+ }
298
+ const value = body;
299
+ if (value.action === "native-respond") {
300
+ if ((typeof value.nativeRequestId !== "string" && typeof value.nativeRequestId !== "number")
301
+ || (typeof value.expectedTarget !== "string" && value.expectedTarget !== null)
302
+ || !value.result || typeof value.result !== "object" || Array.isArray(value.result)) {
303
+ throw new WebRequestRejected("Expected exact native request and Turn response.");
304
+ }
305
+ result = await dependencies.conversation.respond(owner, safeIdentity(q.get("session"), "Session"), value.nativeRequestId, value.expectedTarget, value.result);
306
+ sendJson(response, 200, result, false);
307
+ return;
308
+ }
309
+ if (!["queue", "steer", "interrupt"].includes(String(value.action))
310
+ || typeof value.requestId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value.requestId)
311
+ || value.body !== undefined && typeof value.body !== "string"
312
+ || value.expectedTarget !== undefined && typeof value.expectedTarget !== "string"
313
+ || value.materials !== undefined && (!Array.isArray(value.materials) || value.materials.length > 8)) {
314
+ throw new WebRequestRejected("Expected action, requestId, input and exact Turn.");
315
+ }
316
+ result = await dependencies.conversation.control(owner, safeIdentity(q.get("session"), "Session"), value);
317
+ }
318
+ else {
319
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
320
+ return;
321
+ }
322
+ sendJson(response, 200, result, false);
323
+ }
324
+ catch (error) {
325
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Conversation unavailable.",
326
+ disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
327
+ }
328
+ return;
329
+ }
104
330
  if (artifactTarget && dependencies.surface) {
105
331
  if (method !== "GET") {
106
332
  sendJson(response, 405, { error: "Artifacts are read-only." }, false);
@@ -115,15 +341,21 @@ async function handleHttpRequest(request, response, store, dependencies, token,
115
341
  sendJson(response, 200, dependencies.surface.evidence(taskId), false);
116
342
  return;
117
343
  }
118
- if ([...query.keys()].some(key => !["path", "commit"].includes(key))) {
119
- throw new WebRequestRejected("Artifact read accepts only path and commit.");
344
+ if ([...query.keys()].some(key => !["path", "commit", "before", "offset"].includes(key) || query.getAll(key).length !== 1)) {
345
+ throw new WebRequestRejected("Artifact read accepts path, fixed commits and offset only.");
120
346
  }
121
347
  const path = query.get("path");
122
348
  const commit = query.get("commit");
123
- if ((path === null) !== (commit === null))
124
- throw new WebRequestRejected("Select path and fixed commit together.");
125
- const result = path === null ? await dependencies.surface.artifacts(taskId)
126
- : await dependencies.surface.artifact(taskId, path, commit);
349
+ const before = query.get("before");
350
+ const offset = query.get("offset") ?? "0";
351
+ if (!/^\d{1,9}$/u.test(offset))
352
+ throw new WebRequestRejected("Invalid artifact offset.");
353
+ if (path !== null && commit === null || before !== null && path === null
354
+ || Number(offset) > 0 && commit === null)
355
+ throw new WebRequestRejected("Select a fixed commit for this read.");
356
+ const result = path === null ? await dependencies.surface.artifacts(taskId, commit ?? undefined, Number(offset))
357
+ : before === null ? await dependencies.surface.artifact(taskId, path, commit, Number(offset))
358
+ : await dependencies.surface.artifactDiff(taskId, path, commit, before, Number(offset));
127
359
  sendJson(response, 200, result, false);
128
360
  }
129
361
  catch (error) {
@@ -194,7 +426,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
194
426
  }
195
427
  return;
196
428
  }
197
- const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|list|metadata|messages|control)$/.exec(pathname);
429
+ const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(workbench|context|delta|inspect|search-source|list|metadata|messages|control|activate|archive)$/.exec(pathname);
198
430
  if (surfaceTarget && dependencies.surface) {
199
431
  try {
200
432
  let taskId;
@@ -207,7 +439,21 @@ async function handleHttpRequest(request, response, store, dependencies, token,
207
439
  const action = surfaceTarget[2];
208
440
  const query = new URL(request.url, "http://localhost").searchParams;
209
441
  let value;
210
- if (method === "GET" && action === "context") {
442
+ if (method === "POST" && (action === "activate" || action === "archive")) {
443
+ const body = await readMutationBody(request);
444
+ if (!body || typeof body !== "object" || Array.isArray(body)
445
+ || Object.keys(body).some(key => key !== "requestId")
446
+ || !("requestId" in body) || typeof body.requestId !== "string" || !body.requestId.trim()) {
447
+ throw new WebRequestRejected("Expected requestId only.");
448
+ }
449
+ value = { requestId: body.requestId, result: action === "activate"
450
+ ? dependencies.surface.activate(taskId, body.requestId)
451
+ : await dependencies.surface.archive(taskId) };
452
+ }
453
+ else if (method === "GET" && action === "workbench") {
454
+ value = dependencies.surface.workbench(taskId);
455
+ }
456
+ else if (method === "GET" && action === "context") {
211
457
  value = await dependencies.surface.read(taskId);
212
458
  }
213
459
  else if (method === "GET" && action === "delta") {
@@ -216,6 +462,15 @@ async function handleHttpRequest(request, response, store, dependencies, token,
216
462
  ...(query.has("continuation") ? { continuation: query.get("continuation") } : {})
217
463
  });
218
464
  }
465
+ else if (method === "GET" && action === "search-source") {
466
+ const offset = query.get("offset") ?? "0";
467
+ if (!/^\d{1,12}$/.test(offset))
468
+ throw new WebRequestRejected("Invalid source offset.");
469
+ value = dependencies.surface.searchSource(taskId, {
470
+ store: query.get("store") ?? "", refId: query.get("ref") ?? "",
471
+ digest: query.get("digest") ?? "", field: query.get("field") ?? "", offset: Number(offset)
472
+ });
473
+ }
219
474
  else if (method === "GET" && action === "inspect") {
220
475
  value = dependencies.surface.inspect(taskId, {
221
476
  store: query.get("store") ?? "", refId: query.get("ref") ?? "",
@@ -273,12 +528,20 @@ async function handleHttpRequest(request, response, store, dependencies, token,
273
528
  if (method === "GET" || method === "HEAD") {
274
529
  try {
275
530
  const asset = findWebAsset(pathname);
276
- if (pathname === "/" || pathname === "/index.html") {
277
- sendText(response, 200, "text/html; charset=utf-8", dashboardHtml(token), method === "HEAD");
531
+ if (pathname === "/" || pathname === "/index.html" || pathname === "/settings") {
532
+ sendText(response, 200, "text/html; charset=utf-8", pathname === "/settings" ? SETTINGS_HTML.replace("__YUI_WEB_TOKEN__", token) : dashboardHtml(token), method === "HEAD");
278
533
  }
279
534
  else if (asset !== null) {
280
535
  sendAsset(response, asset, method === "HEAD");
281
536
  }
537
+ else if (pathname === "/api/search") {
538
+ const query = new URL(request.url, "http://localhost").searchParams;
539
+ if (query.getAll("query").length !== 1)
540
+ throw usageError("Search requires one query.");
541
+ const options = parseTaskSearchOptions([query.get("query"),
542
+ ...[...query].filter(([key]) => key !== "query").flatMap(([key, value]) => [`--${key}`, value])]);
543
+ sendJson(response, 200, store.transaction(reader => searchTaskBodies(reader, options)), method === "HEAD");
544
+ }
282
545
  else if (pathname === "/api/dashboard" || pathname === "/api/dashboard/sessions") {
283
546
  const query = new URL(request.url, "http://localhost").searchParams;
284
547
  const options = parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
@@ -289,11 +552,22 @@ async function handleHttpRequest(request, response, store, dependencies, token,
289
552
  : buildWebTaskCatalog(store, options);
290
553
  sendJson(response, 200, snapshot, method === "HEAD");
291
554
  }
555
+ else if (/^\/api\/tasks\/[^/]+\/usage$/u.test(pathname)) {
556
+ const taskId = decodeURIComponent(pathname.split("/")[3]);
557
+ const query = new URL(request.url, "http://localhost").searchParams;
558
+ const offset = Number(query.get("offset") ?? 0), limit = Number(query.get("limit") ?? 0);
559
+ if ([...query.keys()].some(key => key !== "offset" && key !== "limit")
560
+ || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 0 || limit > 50) {
561
+ throw usageError("Usage requires offset >= 0 and limit 0..50.");
562
+ }
563
+ const value = store.readTransaction(reader => readTaskUsage(reader, taskId, { offset, limit, now: now() }));
564
+ sendJson(response, value === null ? 404 : 200, value ?? { error: "Task not found." }, method === "HEAD");
565
+ }
292
566
  else if (pathname.startsWith("/api/tasks/")) {
293
567
  const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
294
568
  const detail = taskId.length === 0 || taskId.includes("/")
295
569
  ? null
296
- : buildWebTaskDetail(store, taskId, now());
570
+ : buildWebTaskDetail(store, taskId, now(), new URL(request.url, "http://localhost").searchParams.get("compact") === "true");
297
571
  sendJson(response, detail === null ? 404 : 200, detail ?? { error: "Task not found." }, method === "HEAD");
298
572
  }
299
573
  else {
@@ -466,10 +740,12 @@ function parseTerminalRequest(parameters) {
466
740
  const roleName = safeIdentity(parameters.get("role"), "Role");
467
741
  const columns = boundedInteger(parameters.get("cols"), 20, 400, "Terminal columns");
468
742
  const rows = boundedInteger(parameters.get("rows"), 5, 200, "Terminal rows");
743
+ const selection = parameters.has("session")
744
+ ? { nativeSessionId: safeIdentity(parameters.get("session"), "Session") } : {};
469
745
  if (scope === "global") {
470
746
  if (parameters.has("task"))
471
747
  throw new Error("Global terminal cannot include a Task.");
472
- return { scope, roleName, columns, rows };
748
+ return { scope, roleName, columns, rows, ...selection };
473
749
  }
474
750
  if (scope === "task") {
475
751
  return {
@@ -477,7 +753,8 @@ function parseTerminalRequest(parameters) {
477
753
  taskId: safeIdentity(parameters.get("task"), "Task"),
478
754
  roleName,
479
755
  columns,
480
- rows
756
+ rows,
757
+ ...selection
481
758
  };
482
759
  }
483
760
  throw new Error("Terminal scope is invalid.");
@@ -616,9 +893,9 @@ function parseWebInputAnswer(value) {
616
893
  }
617
894
  throw new Error("Exactly one non-empty choiceKey or text is required.");
618
895
  }
619
- async function readMutationBody(request) {
896
+ async function readMutationBody(request, limit = MAX_JSON_BODY_BYTES) {
620
897
  try {
621
- return await readJsonBody(request);
898
+ return await readJsonBody(request, limit);
622
899
  }
623
900
  catch (error) {
624
901
  throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid request body.");
@@ -636,13 +913,13 @@ function webSubmissionIntent(body) {
636
913
  }
637
914
  throw new WebRequestRejected(`intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}.`);
638
915
  }
639
- async function readJsonBody(request) {
916
+ async function readJsonBody(request, limit = MAX_JSON_BODY_BYTES) {
640
917
  const chunks = [];
641
918
  let size = 0;
642
919
  for await (const chunk of request) {
643
920
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
644
921
  size += buffer.length;
645
- if (size > MAX_JSON_BODY_BYTES)
922
+ if (size > limit)
646
923
  throw new Error("Request body is too large.");
647
924
  chunks.push(buffer);
648
925
  }