@zq-silk/yui 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +19 -3
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +25 -11
  7. package/dist/cli/interactionPolicy.js +4 -0
  8. package/dist/cli/invocationAuthority.js +1 -1
  9. package/dist/commands/executionAuditCommands.js +5 -0
  10. package/dist/commands/globalRoleCommands.js +41 -3
  11. package/dist/commands/projectCommands.js +89 -94
  12. package/dist/commands/roleSkillValidation.js +4 -8
  13. package/dist/commands/taskCommands.js +30 -9
  14. package/dist/commands/taskIntegrationCommands.js +5 -16
  15. package/dist/commands/taskUsageCommand.js +38 -0
  16. package/dist/context/roleSessionContext.js +18 -2
  17. package/dist/context/runContextPack.js +5 -1
  18. package/dist/context/sessionBootstrapManifest.js +10 -1
  19. package/dist/context/skillCatalog.js +108 -0
  20. package/dist/context/skillPackage.js +161 -0
  21. package/dist/context/taskContext.js +3 -2
  22. package/dist/context/taskSearch.js +127 -0
  23. package/dist/controlPlaneCli.js +51 -39
  24. package/dist/controller/clientRuntime.js +3 -2
  25. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  26. package/dist/controller/jobControl.js +2 -0
  27. package/dist/controller/nativeHostInput.js +37 -0
  28. package/dist/controller/runtime.js +93 -2
  29. package/dist/controller/runtimeEventInbox.js +2 -2
  30. package/dist/controller/runtimeEventProcessor.js +3 -3
  31. package/dist/doctor/doctor.js +10 -0
  32. package/dist/execution/reviewMainRun.js +1 -0
  33. package/dist/execution/workItemMainRun.js +1 -0
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  37. package/dist/integration/gitIntegrationService.js +15 -79
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/job/httpPreview.js +61 -0
  40. package/dist/job/jobCleanupInspection.js +67 -0
  41. package/dist/job/jobRunner.js +12 -2
  42. package/dist/kernel/builtinCapabilities.js +3 -1
  43. package/dist/message/message.js +15 -1
  44. package/dist/output/rolePresentation.js +4 -1
  45. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  46. package/dist/repository/gitCompatibility.js +62 -0
  47. package/dist/repository/gitWorkspace.js +86 -80
  48. package/dist/repository/gitWorktreeInventory.js +121 -0
  49. package/dist/repository/project.js +74 -25
  50. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  51. package/dist/repository/taskWorkspacePreparer.js +39 -43
  52. package/dist/repository/workspaceCleanupInspection.js +88 -7
  53. package/dist/resources/resourceDiscovery.js +2 -2
  54. package/dist/resources/resourceGc.js +7 -4
  55. package/dist/resources/resourceRegistrar.js +18 -11
  56. package/dist/runtime/agentEndpoint.js +8 -0
  57. package/dist/runtime/agentHost.js +170 -3
  58. package/dist/runtime/codexAppServerRuntime.js +1 -1
  59. package/dist/runtime/codexNativeAccess.js +175 -0
  60. package/dist/runtime/controlledCodexTui.js +116 -0
  61. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  62. package/dist/runtime/publicReply.js +23 -0
  63. package/dist/runtime/runtimeObservation.js +48 -0
  64. package/dist/runtime/structuredProviderHost.js +16 -7
  65. package/dist/runtime/taskCostMetrics.js +133 -0
  66. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  67. package/dist/runtime/taskUsageMetrics.js +11 -2
  68. package/dist/runtime/taskUsageQuery.js +38 -0
  69. package/dist/scheduler/taskExecutionProjection.js +2 -0
  70. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  71. package/dist/storage/homeLayout.js +13 -0
  72. package/dist/storage/sqliteSchema.js +44 -0
  73. package/dist/storage/sqliteStore.js +53 -0
  74. package/dist/storage/storageVersions.js +1 -1
  75. package/dist/storage/storeRpc.js +2 -0
  76. package/dist/storage/taskSearch.js +35 -0
  77. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  78. package/dist/task/archiveDiagnostics.js +26 -8
  79. package/dist/task/archivePreflight.js +26 -9
  80. package/dist/task/leaderArchive.js +9 -15
  81. package/dist/task/ordinaryArchive.js +42 -0
  82. package/dist/tmux/tmuxManager.js +8 -8
  83. package/dist/web/assets/assetManifest.js +11 -51
  84. package/dist/web/assets/client/app/catalog.js +215 -0
  85. package/dist/web/assets/client/app/main.js +148 -0
  86. package/dist/web/assets/client/app/selection.js +139 -0
  87. package/dist/web/assets/client/app/settings.js +326 -0
  88. package/dist/web/assets/client/app/taskView.js +280 -0
  89. package/dist/web/assets/client/domain/context.js +96 -0
  90. package/dist/web/assets/client/domain/records.js +126 -0
  91. package/dist/web/assets/client/domain/runs.js +119 -0
  92. package/dist/web/assets/client/domain/taskForms.js +197 -0
  93. package/dist/web/assets/client/domain/vocab.js +69 -0
  94. package/dist/web/assets/client/domain/work.js +116 -0
  95. package/dist/web/assets/client/i18n/en.js +900 -0
  96. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  97. package/dist/web/assets/client/index.js +114 -0
  98. package/dist/web/assets/client/layout/resizer.js +37 -0
  99. package/dist/web/assets/client/layout/router.js +35 -0
  100. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  101. package/dist/web/assets/client/layout/sizing.js +95 -0
  102. package/dist/web/assets/client/layout/workspace.js +247 -0
  103. package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
  104. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  105. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  106. package/dist/web/assets/client/lib/i18n.js +54 -0
  107. package/dist/web/assets/client/lib/markdown.js +167 -0
  108. package/dist/web/assets/client/lib/prefs.js +28 -0
  109. package/dist/web/assets/client/lib/theme.js +68 -0
  110. package/dist/web/assets/client/ui/containers.js +101 -0
  111. package/dist/web/assets/client/ui/controls.js +51 -0
  112. package/dist/web/assets/client/ui/forms.js +48 -0
  113. package/dist/web/assets/client/ui/metrics.js +137 -0
  114. package/dist/web/assets/client/ui/primitives.js +92 -0
  115. package/dist/web/assets/client/ui/text.js +33 -0
  116. package/dist/web/assets/client/ui/toast.js +12 -0
  117. package/dist/web/assets/client/views/bodySearch.js +172 -0
  118. package/dist/web/assets/client/views/createTask.js +85 -0
  119. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  120. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  121. package/dist/web/assets/client/views/dock/preview.js +129 -0
  122. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  123. package/dist/web/assets/client/views/globalInput.js +67 -0
  124. package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
  125. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  126. package/dist/web/assets/client/views/skills.js +152 -0
  127. package/dist/web/assets/client/views/task/actions.js +36 -0
  128. package/dist/web/assets/client/views/task/delivery.js +185 -0
  129. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  130. package/dist/web/assets/client/views/task/files.js +272 -0
  131. package/dist/web/assets/client/views/task/observation.js +153 -0
  132. package/dist/web/assets/client/views/task/overview.js +161 -0
  133. package/dist/web/assets/client/views/task/page.js +131 -0
  134. package/dist/web/assets/client/views/task/records.js +163 -0
  135. package/dist/web/assets/client/views/task/runtime.js +130 -0
  136. package/dist/web/assets/client/views/task/timeline.js +190 -0
  137. package/dist/web/assets/shared/geometry.js +19 -0
  138. package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
  139. package/dist/web/assets/shared/taskTabs.js +8 -0
  140. package/dist/web/assets/shared/themes.js +93 -0
  141. package/dist/web/assets/shell/dialogs.js +63 -0
  142. package/dist/web/assets/shell/dock.js +40 -0
  143. package/dist/web/assets/shell/index.js +40 -0
  144. package/dist/web/assets/shell/settings.js +34 -0
  145. package/dist/web/assets/shell/sidebar.js +45 -0
  146. package/dist/web/assets/styles/index.js +41 -0
  147. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
  148. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  149. package/dist/web/assets/styles/markdown.js +22 -3
  150. package/dist/web/assets/styles/tokens.js +10 -63
  151. package/dist/web/assets/styles/ui/controls.js +76 -0
  152. package/dist/web/assets/styles/ui/forms.js +46 -0
  153. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  154. package/dist/web/assets/styles/views/delivery.js +29 -0
  155. package/dist/web/assets/styles/views/dock.js +71 -0
  156. package/dist/web/assets/styles/views/page.js +32 -0
  157. package/dist/web/assets/styles/views/records.js +70 -0
  158. package/dist/web/assets/styles/views/settings.js +26 -0
  159. package/dist/web/assets/styles/views/sidebar.js +74 -0
  160. package/dist/web/assets/styles/views/task.js +80 -0
  161. package/dist/web/assets/styles/views/terminal.js +38 -0
  162. package/dist/web/taskPreviews.js +205 -0
  163. package/dist/web/tmuxWebTerminal.js +16 -9
  164. package/dist/web/webConversation.js +373 -0
  165. package/dist/web/webMaterials.js +155 -0
  166. package/dist/web/webServer.js +306 -18
  167. package/dist/web/webSettings.js +331 -0
  168. package/dist/web/webSkills.js +86 -0
  169. package/dist/web/webSnapshot.js +40 -8
  170. package/dist/web/webTaskSurface.js +114 -9
  171. package/dist/workspace/gitChangeSetCapture.js +3 -1
  172. package/docs/managed-turn-and-session-runtime.md +9 -0
  173. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  174. package/docs/observability/README.md +13 -3
  175. package/docs/observability/README.zh-CN.md +10 -3
  176. package/docs/release-workflow.md +53 -0
  177. package/docs/release-workflow.zh-CN.md +40 -0
  178. package/docs/storage-baseline.md +38 -1
  179. package/docs/storage-baseline.zh-CN.md +22 -0
  180. package/docs/task-delivery.md +36 -8
  181. package/docs/task-delivery.zh-CN.md +24 -7
  182. package/docs/task-discovery.md +28 -0
  183. package/docs/testing/verification-levels.md +2 -2
  184. package/i18n/README.zh-CN.md +4 -3
  185. package/package.json +1 -1
  186. package/skills/yui-leader/references/execution.md +2 -1
  187. package/skills/yui-leader/references/integration.md +20 -0
  188. package/skills/yui-operator/SKILL.md +4 -0
  189. package/skills/yui-reviewer/SKILL.md +3 -0
  190. package/skills/yui-runtime/SKILL.md +4 -0
  191. package/skills/yui-runtime/references/publication.md +20 -4
  192. package/skills/yui-worker/SKILL.md +3 -0
  193. package/dist/web/assets/client/app.js +0 -804
  194. package/dist/web/assets/client/components.js +0 -418
  195. package/dist/web/assets/client/detail.js +0 -333
  196. package/dist/web/assets/client/dock.js +0 -225
  197. package/dist/web/assets/client/evidence.js +0 -172
  198. package/dist/web/assets/client/forms.js +0 -230
  199. package/dist/web/assets/client/i18n.js +0 -1161
  200. package/dist/web/assets/client/markdown.js +0 -114
  201. package/dist/web/assets/client/records.js +0 -63
  202. package/dist/web/assets/client/sections.js +0 -313
  203. package/dist/web/assets/client/theme.js +0 -71
  204. package/dist/web/assets/shell.js +0 -161
  205. package/dist/web/assets/styles/components.js +0 -169
  206. package/dist/web/assets/styles/views.js +0 -292
@@ -3,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|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") ?? "",
@@ -223,6 +478,17 @@ async function handleHttpRequest(request, response, store, dependencies, token,
223
478
  ...(query.has("cursor") ? { cursor: query.get("cursor") } : {})
224
479
  });
225
480
  }
481
+ else if (method === "GET" && action === "list") {
482
+ const limit = query.get("limit");
483
+ if (limit !== null && !/^\d{1,3}$/u.test(limit))
484
+ throw new WebRequestRejected("Invalid list limit.");
485
+ value = dependencies.surface.list(taskId, {
486
+ store: query.get("store") ?? "",
487
+ ...(query.has("status") ? { status: query.get("status") } : {}),
488
+ ...(query.has("cursor") ? { cursor: query.get("cursor") } : {}),
489
+ ...(limit === null ? {} : { limit: Number(limit) })
490
+ });
491
+ }
226
492
  else if (method === "POST" && action === "messages") {
227
493
  const body = await readMutationBody(request);
228
494
  if (typeof body !== "object" || body === null || Array.isArray(body)
@@ -262,12 +528,20 @@ async function handleHttpRequest(request, response, store, dependencies, token,
262
528
  if (method === "GET" || method === "HEAD") {
263
529
  try {
264
530
  const asset = findWebAsset(pathname);
265
- if (pathname === "/" || pathname === "/index.html") {
266
- 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");
267
533
  }
268
534
  else if (asset !== null) {
269
535
  sendAsset(response, asset, method === "HEAD");
270
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
+ }
271
545
  else if (pathname === "/api/dashboard" || pathname === "/api/dashboard/sessions") {
272
546
  const query = new URL(request.url, "http://localhost").searchParams;
273
547
  const options = parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
@@ -278,11 +552,22 @@ async function handleHttpRequest(request, response, store, dependencies, token,
278
552
  : buildWebTaskCatalog(store, options);
279
553
  sendJson(response, 200, snapshot, method === "HEAD");
280
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
+ }
281
566
  else if (pathname.startsWith("/api/tasks/")) {
282
567
  const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
283
568
  const detail = taskId.length === 0 || taskId.includes("/")
284
569
  ? null
285
- : buildWebTaskDetail(store, taskId, now());
570
+ : buildWebTaskDetail(store, taskId, now(), new URL(request.url, "http://localhost").searchParams.get("compact") === "true");
286
571
  sendJson(response, detail === null ? 404 : 200, detail ?? { error: "Task not found." }, method === "HEAD");
287
572
  }
288
573
  else {
@@ -455,10 +740,12 @@ function parseTerminalRequest(parameters) {
455
740
  const roleName = safeIdentity(parameters.get("role"), "Role");
456
741
  const columns = boundedInteger(parameters.get("cols"), 20, 400, "Terminal columns");
457
742
  const rows = boundedInteger(parameters.get("rows"), 5, 200, "Terminal rows");
743
+ const selection = parameters.has("session")
744
+ ? { nativeSessionId: safeIdentity(parameters.get("session"), "Session") } : {};
458
745
  if (scope === "global") {
459
746
  if (parameters.has("task"))
460
747
  throw new Error("Global terminal cannot include a Task.");
461
- return { scope, roleName, columns, rows };
748
+ return { scope, roleName, columns, rows, ...selection };
462
749
  }
463
750
  if (scope === "task") {
464
751
  return {
@@ -466,7 +753,8 @@ function parseTerminalRequest(parameters) {
466
753
  taskId: safeIdentity(parameters.get("task"), "Task"),
467
754
  roleName,
468
755
  columns,
469
- rows
756
+ rows,
757
+ ...selection
470
758
  };
471
759
  }
472
760
  throw new Error("Terminal scope is invalid.");
@@ -605,9 +893,9 @@ function parseWebInputAnswer(value) {
605
893
  }
606
894
  throw new Error("Exactly one non-empty choiceKey or text is required.");
607
895
  }
608
- async function readMutationBody(request) {
896
+ async function readMutationBody(request, limit = MAX_JSON_BODY_BYTES) {
609
897
  try {
610
- return await readJsonBody(request);
898
+ return await readJsonBody(request, limit);
611
899
  }
612
900
  catch (error) {
613
901
  throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid request body.");
@@ -625,13 +913,13 @@ function webSubmissionIntent(body) {
625
913
  }
626
914
  throw new WebRequestRejected(`intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}.`);
627
915
  }
628
- async function readJsonBody(request) {
916
+ async function readJsonBody(request, limit = MAX_JSON_BODY_BYTES) {
629
917
  const chunks = [];
630
918
  let size = 0;
631
919
  for await (const chunk of request) {
632
920
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
633
921
  size += buffer.length;
634
- if (size > MAX_JSON_BODY_BYTES)
922
+ if (size > limit)
635
923
  throw new Error("Request body is too large.");
636
924
  chunks.push(buffer);
637
925
  }