@zq-silk/yui 0.15.8 → 0.15.11

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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -238,6 +238,7 @@ const messages = {
238
238
  "stats.running": "running",
239
239
  "status.active": "Active",
240
240
  "status.all": "All",
241
+ "filter.unarchived": "Unarchived",
241
242
  "status.archived": "Archived",
242
243
  "status.completed": "Completed",
243
244
  "status.draft": "Draft",
@@ -305,6 +306,7 @@ const messages = {
305
306
  "workExec.candidateStatus.none": "None",
306
307
  "workExec.candidateStatus.observed": "Observed",
307
308
  "workExec.candidateStatus.unknown": "Unknown",
309
+ "workExec.action.execute-directly": "Leader executes directly",
308
310
  "workExec.action.dispatch-work": "Dispatch work",
309
311
  "workExec.action.wait-for-lanes": "Wait for Lanes",
310
312
  "workExec.action.retry-or-settle-lanes": "Retry or settle exact Lanes",
@@ -344,6 +346,7 @@ const messages = {
344
346
  "exec.action.inspect-attention": "Inspect attention",
345
347
  "exec.action.recover-leader": "Recover leader",
346
348
  "exec.action.resolve-blocker": "Resolve blocker",
349
+ "exec.action.resolve-integration-conflicts": "Resolve Integration conflicts",
347
350
  "exec.action.complete-task": "Complete task",
348
351
  "exec.action.none": "—",
349
352
  "exec.attention.leader-recovery": "Leader recovery",
@@ -624,6 +627,7 @@ const messages = {
624
627
  "stats.running": "运行中",
625
628
  "status.active": "进行中",
626
629
  "status.all": "全部",
630
+ "filter.unarchived": "未归档",
627
631
  "status.archived": "已归档",
628
632
  "status.completed": "已完成",
629
633
  "status.draft": "草稿",
@@ -691,6 +695,7 @@ const messages = {
691
695
  "workExec.candidateStatus.none": "无",
692
696
  "workExec.candidateStatus.observed": "已观测",
693
697
  "workExec.candidateStatus.unknown": "未知",
698
+ "workExec.action.execute-directly": "Leader 直接执行",
694
699
  "workExec.action.dispatch-work": "派发工作",
695
700
  "workExec.action.wait-for-lanes": "等待 Lane",
696
701
  "workExec.action.retry-or-settle-lanes": "重试或结算指定 Lane",
@@ -730,6 +735,7 @@ const messages = {
730
735
  "exec.action.inspect-attention": "检查注意项",
731
736
  "exec.action.recover-leader": "恢复负责人",
732
737
  "exec.action.resolve-blocker": "解决阻塞",
738
+ "exec.action.resolve-integration-conflicts": "解决集成冲突",
733
739
  "exec.action.complete-task": "完成任务",
734
740
  "exec.action.none": "—",
735
741
  "exec.attention.leader-recovery": "负责人恢复",
@@ -33,27 +33,47 @@ export function renderTaskSurface(container, data, t, locale, actions) {
33
33
  message.required = true;
34
34
  message.maxLength = 8000;
35
35
  chatLabel.append(message);
36
+ // Submission intent is an explicit user choice, never inferred from the body
37
+ // (task-32 §2.5); discuss is the default so the control matches the service.
38
+ const intentLabel = node("label", "", say("Intent", "提交意图"));
39
+ const intentSelect = node("select", "");
40
+ const intentOptions = [
41
+ ["discuss", say("Discuss — route to planning", "讨论 — 进入规划")],
42
+ ["record", say("Record — save only", "记录 — 仅保存")],
43
+ ["develop", say("Develop — request activation", "开发 — 请求激活")]
44
+ ];
45
+ for (const [value, text] of intentOptions) {
46
+ const option = node("option", "", text);
47
+ option.value = value;
48
+ intentSelect.append(option);
49
+ }
50
+ intentSelect.value = "discuss";
51
+ intentLabel.append(intentSelect);
36
52
  chat.addEventListener("input", () => { chat.dataset.unsent = message.value ? "true" : "false"; });
37
53
  const send = node("button", "record-open", say("Send", "发送"));
38
54
  send.type = "submit";
39
55
  send.disabled = !["active", "draft"].includes(task.status);
40
56
  const sent = node("p", "muted", say("Not submitted", "未提交"));
41
57
  sent.setAttribute("role", "status");
42
- chat.append(chatLabel, send, sent);
58
+ // §2.5 facets render here, one per line; cleared and repopulated on every submit.
59
+ const facets = node("div", "section-body");
60
+ facets.dataset.submissionFacets = "";
61
+ chat.append(chatLabel, intentLabel, send, sent, facets);
43
62
  chat.addEventListener("submit", async (event) => {
44
63
  event.preventDefault();
45
64
  if (send.disabled) return;
46
65
  send.disabled = true;
47
66
  chat.dataset.unsent = "true";
48
67
  const requestId = crypto.randomUUID();
68
+ const intent = intentSelect.value;
69
+ clear(facets);
49
70
  sent.textContent = say("Waiting for receipt · ", "等待回执 · ") + requestId;
50
71
  try {
51
- const receipt = await actions.sendMessage(task.id, message.value, requestId);
52
- sent.textContent = (receipt.disposition === "queued"
53
- ? say("Queued for Leader; not proof of execution · ", "已为 Leader 排队,不代表已执行 · ")
54
- : say("Saved to Task context; this Task's Leader is not waking · ",
55
- "已保存到 Task 上下文;该 Task 的 Leader 不会被唤醒 · "))
56
- + receipt.record.id;
72
+ // requestId is the submission key: the same key retried returns the original
73
+ // Message and routing rather than a second submission (task-32 §2.3).
74
+ const receipt = await actions.sendMessage(task.id, message.value, requestId, intent);
75
+ sent.textContent = say("Saved · ", "已保存 · ") + receipt.record.id;
76
+ renderSubmissionFacets(facets, receipt.submission, say);
57
77
  message.value = "";
58
78
  chat.dataset.unsent = "false";
59
79
  send.disabled = false;
@@ -63,11 +83,120 @@ export function renderTaskSurface(container, data, t, locale, actions) {
63
83
  send.disabled = false;
64
84
  return;
65
85
  }
86
+ // A lost response must never auto-replay a write; the same requestId is safe
87
+ // to resend by hand because the server dedups on it (§2.3), but the user
88
+ // reloads and inspects first rather than blindly resending.
66
89
  sent.textContent = say("Unknown submission outcome. Reload and inspect saved messages before sending again · ",
67
90
  "提交结果未知。请重新加载并检查已保存消息,不要盲目重发 · ") + requestId;
68
91
  }
69
92
  });
70
93
  summary.append(chat);
94
+ // decision-3 three-action input control (queue / steer / interrupt). This is
95
+ // the same application-layer path the CLI drives; the Web surface adds no
96
+ // fourth action and no auto-fallback. Steer/interrupt name an exact current
97
+ // Turn (expected-target); a mismatch or missing Turn is a visible failure
98
+ // receipt, never a silent downgrade to a queue.
99
+ const control = node("details", "record-card");
100
+ control.append(node("summary", "", say("Redirect a running Role (queue / steer / interrupt)",
101
+ "改向运行中的 Role(排队/即时插话/打断)")));
102
+ const controlForm = node("form", "record-block");
103
+ const actionLabel = node("label", "", say("Action", "动作"));
104
+ const action = node("select", "");
105
+ for (const [val, en, cn] of [["queue", "Queue (deliver at next opportunity)", "排队(下次机会送达)"],
106
+ ["steer", "Steer (inject into the exact current Turn)", "即时插话(注入当前 Turn)"],
107
+ ["interrupt", "Interrupt (native cancel of the current Turn)", "打断(原生取消当前 Turn)"]]) {
108
+ const opt = node("option", "", say(en, cn));
109
+ opt.value = val;
110
+ action.append(opt);
111
+ }
112
+ actionLabel.append(action);
113
+ const roleLabel = node("label", "", say("Target Role", "目标 Role"));
114
+ const role = node("input", "");
115
+ role.placeholder = say("e.g. worker (blank = Leader for queue)", "如 worker(排队留空 = Leader)");
116
+ roleLabel.append(role);
117
+ const bodyLabel = node("label", "", say("Message", "消息内容"));
118
+ const body = node("textarea", "");
119
+ body.maxLength = 8000;
120
+ bodyLabel.append(body);
121
+ const targetLabel = node("label", "", say("Expected current Turn", "预期当前 Turn"));
122
+ const expectedTarget = node("input", "");
123
+ expectedTarget.placeholder = say("exact current Turn id (attempt/native)", "当前 Turn 的精确标识");
124
+ targetLabel.append(expectedTarget);
125
+ const workItemLabel = node("label", "", say("Work Item (optional)", "Work Item(可选)"));
126
+ const workItem = node("input", "");
127
+ workItemLabel.append(workItem);
128
+ const thenLabel = node("label", "", say("Then-message ref (optional)", "打断后续消息引用(可选)"));
129
+ const thenMessage = node("input", "");
130
+ thenMessage.placeholder = say("task/message id already saved", "已保存的 task/message 引用");
131
+ thenLabel.append(thenMessage);
132
+ const controlSend = node("button", "record-open", say("Submit control", "提交控制"));
133
+ controlSend.type = "submit";
134
+ controlSend.disabled = !["active", "draft"].includes(task.status);
135
+ const controlSent = node("p", "muted", say("Not submitted", "未提交"));
136
+ controlSent.setAttribute("role", "status");
137
+ // Field visibility follows the selected action so the form can only submit a
138
+ // legal shape: interrupt carries no body; queue needs no expected-target;
139
+ // then-message is an interrupt-only continuation.
140
+ const applyAction = () => {
141
+ const kind = action.value;
142
+ bodyLabel.hidden = kind === "interrupt";
143
+ targetLabel.hidden = kind === "queue";
144
+ thenLabel.hidden = kind !== "interrupt";
145
+ roleLabel.hidden = false;
146
+ role.required = kind !== "queue";
147
+ body.required = kind !== "interrupt";
148
+ expectedTarget.required = kind !== "queue";
149
+ };
150
+ action.addEventListener("change", applyAction);
151
+ applyAction();
152
+ controlForm.addEventListener("input", () => { controlForm.dataset.unsent = "true"; });
153
+ controlForm.append(actionLabel, roleLabel, bodyLabel, targetLabel, workItemLabel, thenLabel, controlSend, controlSent);
154
+ controlForm.addEventListener("submit", async (event) => {
155
+ event.preventDefault();
156
+ if (controlSend.disabled) return;
157
+ controlSend.disabled = true;
158
+ controlForm.dataset.unsent = "true";
159
+ const requestId = crypto.randomUUID();
160
+ const kind = action.value;
161
+ const payload = { action: kind, requestId };
162
+ if (kind === "interrupt") {
163
+ payload.role = role.value.trim();
164
+ payload.expectedTarget = expectedTarget.value.trim();
165
+ if (thenMessage.value.trim()) payload.thenMessage = thenMessage.value.trim();
166
+ } else {
167
+ payload.body = body.value;
168
+ if (kind === "steer") { payload.to = role.value.trim(); payload.expectedTarget = expectedTarget.value.trim(); }
169
+ else if (role.value.trim()) payload.to = role.value.trim();
170
+ if (workItem.value.trim()) payload.workItem = workItem.value.trim();
171
+ }
172
+ controlSent.textContent = say("Waiting for receipt · ", "等待回执 · ") + requestId;
173
+ try {
174
+ const receipt = await actions.controlInput(task.id, payload, requestId);
175
+ // The receipt states exactly what settled: a live steer/interrupt names its
176
+ // Host outcome; a queue or a not-delivered/idempotent result names its own
177
+ // state. None of these implies the Provider executed the input.
178
+ const settled = receipt.steer || receipt.interrupt || receipt.delivery
179
+ || { state: receipt.disposition };
180
+ const outcome = settled.outcome ? " · outcome " + settled.outcome : "";
181
+ const detail = settled.detail ? " · " + settled.detail : "";
182
+ controlSent.textContent = say("Receipt: ", "回执:") + receipt.action + " · " + settled.state
183
+ + outcome + detail + (receipt.record ? " · " + receipt.record.id : "");
184
+ controlForm.dataset.unsent = "false";
185
+ controlSend.disabled = false;
186
+ } catch (error) {
187
+ if (error.disposition === "not-submitted") {
188
+ controlSent.textContent = say("Not submitted: ", "未提交:") + error.message;
189
+ controlSend.disabled = false;
190
+ return;
191
+ }
192
+ // A committed input whose live edge is delivery-unknown must not be
193
+ // blindly re-sent under a new requestId or a different action.
194
+ controlSent.textContent = say("Unknown outcome. Re-read the Session before acting; do not resend · ",
195
+ "结果未知。请先重新读取 Session,不要盲目重发 · ") + requestId;
196
+ }
197
+ });
198
+ control.append(controlForm);
199
+ summary.append(control);
71
200
  // A Draft's Leader conversation IS its planning Turn, so this entry point does
72
201
  // reach a Leader. Report the real planning facts the snapshot carries — Turn,
73
202
  // Session, environment and the time they were observed — instead of asserting
@@ -350,4 +479,70 @@ export function renderTaskSurface(container, data, t, locale, actions) {
350
479
  scaffold.append(panels);
351
480
  container.append(scaffold);
352
481
  }
482
+
483
+ // Render each task-32 §2.5 submission facet on its own line, never collapsed into
484
+ // a single "started". This repo has one current server contract: every submission
485
+ // returns its faceted receipt, so there is no old-server flat-disposition path to
486
+ // fall back to. A missing facet block is a contract violation, reported as a
487
+ // limited diagnostic rather than reinterpreted as a plausible flat result.
488
+ function renderSubmissionFacets(container, submission, say) {
489
+ clear(container);
490
+ if (!submission) {
491
+ container.append(node("p", "muted",
492
+ say("Submission receipt is missing its §2.5 facets.", "提交回执缺少 §2.5 分面字段。")));
493
+ return;
494
+ }
495
+ const phaseText = {
496
+ "active": say("Active — delivery context", "进行中 — 交付上下文"),
497
+ "draft-planning": say("Draft, in planning", "草稿,规划中"),
498
+ "draft-unplanned": say("Draft, not yet planned", "草稿,尚未规划")
499
+ };
500
+ const planningText = {
501
+ "entered": say("Entered planning", "已进入规划"),
502
+ "continued": say("Continued planning", "继续规划"),
503
+ "none": say("No planning change", "规划无变化")
504
+ };
505
+ const activationText = {
506
+ "requested": say("Activation requested", "已请求激活"),
507
+ "pending": say("Activation pending", "激活待处理"),
508
+ "failed": say("Activation failed", "激活失败"),
509
+ "manual-required": say("Manual activation required", "需手动激活"),
510
+ "execution-stopped": say("Execution stopped; not activated", "执行已停止;未激活"),
511
+ "none": say("No activation", "无激活")
512
+ };
513
+ const deliveryText = {
514
+ "queued": say("Leader queued to act now", "已为 Leader 排队处理"),
515
+ "none": say("Leader not woken", "未唤醒 Leader")
516
+ };
517
+ const facts = [
518
+ [say("Phase", "阶段"), phaseText[submission.phase] ?? submission.phase],
519
+ [say("Planning", "规划"), planningText[submission.planning] ?? submission.planning],
520
+ [say("Activation", "激活"), activationText[submission.activation] ?? submission.activation],
521
+ [say("Delivery", "投递"), deliveryText[submission.delivery] ?? submission.delivery]
522
+ ];
523
+ for (const [label, value] of facts) {
524
+ container.append(node("p", "muted", label + ": " + value));
525
+ }
526
+ const step = submission.nextStep;
527
+ if (step) {
528
+ const nextText = step.kind === "activate-manually"
529
+ ? say("Next: activate this Task explicitly (yui task activate " + step.taskId + ").",
530
+ "下一步:显式激活该 Task(yui task activate " + step.taskId + ")。")
531
+ : step.kind === "start-execution"
532
+ ? say("Next: start execution first, then activate (" + step.taskId + ").",
533
+ "下一步:先启动执行,再激活(" + step.taskId + ")。")
534
+ : step.kind === "await-pending-activation"
535
+ ? say("Next: waiting on the pending activation " + step.activationRef + ".",
536
+ "下一步:等待待处理的激活 " + step.activationRef + "。")
537
+ : step.kind === "resolve-failed-activation"
538
+ ? say("Next: the prior activation failed (" + step.failure + "); retry with a new request or cancel " + step.activationRef + ".",
539
+ "下一步:之前的激活失败(" + step.failure + ");请用新请求重试或取消 " + step.activationRef + "。")
540
+ : "";
541
+ if (nextText) {
542
+ const next = node("p", "", nextText);
543
+ next.dataset.nextStep = step.kind;
544
+ container.append(next);
545
+ }
546
+ }
547
+ }
353
548
  `;
@@ -53,7 +53,7 @@ export function renderFilters(container, state, t, onFilter) {
53
53
  dot.classList.add(status);
54
54
  btn.append(dot);
55
55
  }
56
- btn.append(node("span", "filter-label", translatedStatus(t, "status", status)));
56
+ btn.append(node("span", "filter-label", status === "all" ? t("filter.unarchived") : translatedStatus(t, "status", status)));
57
57
  const badge = node("span", "filter-count");
58
58
  btn.append(badge);
59
59
  btn.addEventListener("click", function () { onFilter(status); });
@@ -66,8 +66,10 @@ export function renderFilters(container, state, t, onFilter) {
66
66
  if (!btn) return;
67
67
  btn.classList.toggle("is-active", state.filter === status);
68
68
  const label = btn.querySelector(".filter-label");
69
- if (label) label.textContent = translatedStatus(t, "status", status);
70
- const count = status === "all" ? counts.total : counts[status];
69
+ if (label) label.textContent = status === "all" ? t("filter.unarchived") : translatedStatus(t, "status", status);
70
+ const count = status === "all"
71
+ ? (counts.total === undefined ? undefined : counts.total - (counts.archived || 0))
72
+ : counts[status];
71
73
  const badge = btn.querySelector(".filter-count");
72
74
  if (badge) {
73
75
  if (count !== undefined && count !== null) {
@@ -105,6 +107,7 @@ export function renderTasks(container, state, t, locale, onSelect) {
105
107
  const attentionIds = new Set((state.attention || []).map(function (item) { return item.taskId; }));
106
108
  const groups = { attention: [], active: [], draft: [], finished: [], cancelled: [], archived: [] };
107
109
  (state.tasks || []).forEach(function (task) {
110
+ if (state.filter === "all" && task.status === "archived") return;
108
111
  if (state.filter !== "all" && task.status !== state.filter) return;
109
112
  if (!taskMatchesQuery(task, query, locale)) return;
110
113
  groups[taskGroupOf(task, attentionIds)].push(task);
@@ -215,7 +218,7 @@ export function renderOverview(detail, state, t, locale, onSelect) {
215
218
  if (activeTasks.length) {
216
219
  duo.append(overviewBlock(t("overview.activeNow"), activeTasks, t, locale, onSelect));
217
220
  }
218
- const recentTasks = (state.tasks || []).slice().sort(byNewest).slice(0, 8);
221
+ const recentTasks = (state.tasks || []).filter(function (task) { return task.status !== "archived"; }).sort(byNewest).slice(0, 8);
219
222
  if (recentTasks.length) {
220
223
  duo.append(overviewBlock(t("overview.recent"), recentTasks, t, locale, onSelect));
221
224
  }
@@ -30,6 +30,7 @@ export const DASHBOARD_HTML = `<!doctype html>
30
30
  </div>
31
31
  <span class="live" title="Local loopback"><i aria-hidden="true"></i><span class="sr-only" data-i18n="brand.connection">Local loopback</span></span>
32
32
  </div>
33
+ <button id="global-input-open" class="operator-open" type="button">Global inputs · 全局消息</button>
33
34
  <label class="search">
34
35
  <span class="sr-only" data-i18n="search.label">Search tasks</span>
35
36
  <input id="search" type="search" placeholder="Filter by title, ID, tag, or project…" data-i18n-placeholder="search.placeholder">
@@ -91,6 +92,28 @@ export const DASHBOARD_HTML = `<!doctype html>
91
92
  </nav>
92
93
  <main id="detail" class="detail" aria-labelledby="page-title" tabindex="-1"></main>
93
94
  </div>
95
+ <dialog id="global-input-dialog" class="global-input-dialog" aria-labelledby="global-input-title">
96
+ <h2 id="global-input-title">Global Role input · 全局 Role 消息</h2>
97
+ <p class="muted">Queue is the default. A cancel request does not prove the Turn stopped.<br>默认排队;取消请求不代表执行已停止。</p>
98
+ <form id="global-input-form" class="record-block">
99
+ <label>Role<input id="global-input-role" value="operator" required pattern="[A-Za-z0-9_-]+"></label>
100
+ <label>Action · 动作<select id="global-input-action">
101
+ <option value="queue">Queue · 排队</option>
102
+ <option value="steer">Steer · 当前轮插话</option>
103
+ <option value="interrupt">Interrupt · 请求取消</option>
104
+ </select></label>
105
+ <label id="global-input-body-label">Message · 消息<textarea id="global-input-body" maxlength="8000" required></textarea></label>
106
+ <label id="global-input-target-label" hidden>Expected Turn · 精确目标<input id="global-input-target"></label>
107
+ <label id="global-input-then-label" hidden>Then-message ID · 后续消息引用<input id="global-input-then"></label>
108
+ <div class="record-actions">
109
+ <button id="global-input-inspect" type="button" class="record-open">Read state · 读取状态</button>
110
+ <button id="global-input-submit" type="submit" class="record-open">Submit · 提交</button>
111
+ <button id="global-input-close" type="button" class="record-open">Close · 关闭</button>
112
+ </div>
113
+ <p id="global-input-receipt" role="status" class="muted">Not submitted · 未提交</p>
114
+ <pre id="global-input-state" class="global-input-state"></pre>
115
+ </form>
116
+ </dialog>
94
117
  <aside id="terminal-panel" class="terminal-panel" aria-labelledby="terminal-title" aria-hidden="true" hidden>
95
118
  <header class="terminal-head">
96
119
  <div>
@@ -9,7 +9,7 @@ body{margin:0;min-width:320px;height:100vh;overflow:hidden;font-size:14px}
9
9
  .app-shell{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr) 0;grid-template-rows:100vh;height:100vh;transition:grid-template-columns var(--motion-slow) var(--ease)}
10
10
  body.terminal-active .app-shell{grid-template-columns:var(--sidebar-w) minmax(0,1fr) var(--terminal-w)}
11
11
  /* Sidebar = persistent work index */
12
- .sidebar{display:flex;flex-direction:column;min-height:0;height:100vh;padding:16px 14px 14px}
12
+ .sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;height:100vh;padding:16px 14px 14px}
13
13
  .sidebar-brand{display:flex;gap:10px;align-items:center;flex:none;margin-bottom:14px}
14
14
  .brand-text{display:grid;gap:3px;min-width:0;flex:1}
15
15
  .sidebar .search{flex:none;margin-bottom:10px}
@@ -15,6 +15,18 @@ body{background:var(--ambient),var(--bg);background-attachment:fixed;color:var(-
15
15
  .skip-link:focus{top:14px}
16
16
  button{font:inherit;cursor:pointer}
17
17
  input:focus-visible,select:focus-visible,button:focus-visible,.task:focus-visible,.tab:focus-visible,.overview-row:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
18
+ .global-input-dialog{width:min(34rem,calc(100vw - 2rem));max-height:85vh;overflow:auto;color:var(--text);background:var(--bg-1);border:1px solid var(--border-strong);border-radius:var(--radius-lg);padding:var(--page-space)}
19
+ .global-input-dialog::backdrop{background:color-mix(in srgb,var(--bg) 75%,transparent)}
20
+ .global-input-dialog form,.global-input-dialog label{display:grid;gap:8px}
21
+ .global-input-dialog form{gap:14px}
22
+ .global-input-dialog p{overflow-wrap:anywhere}
23
+ .global-input-dialog form,.global-input-dialog label{min-width:0}
24
+ .global-input-dialog input,.global-input-dialog select,.global-input-dialog textarea{width:100%;box-sizing:border-box;padding:10px;color:var(--text);background:var(--bg);border:1px solid var(--border-strong);border-radius:var(--radius)}
25
+ .global-input-dialog textarea{min-height:100px}
26
+ .global-input-dialog .record-actions{display:flex;flex-wrap:wrap;gap:8px}
27
+ .global-input-dialog button{min-height:44px}
28
+ .global-input-state{white-space:pre-wrap;overflow-wrap:anywhere;font:12px var(--font-mono)}
29
+ .global-input-dialog [hidden]{display:none}
18
30
  /* Sidebar */
19
31
  .sidebar{border-right:1px solid var(--border);background:color-mix(in srgb,var(--bg-1) 84%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
20
32
  .brand-mark{width:40px;height:40px;flex:none;display:grid;place-items:center;font-family:var(--font-display);font-size:21px;font-weight:600;color:var(--on-accent);border-radius:11px;background:linear-gradient(140deg,var(--accent),var(--accent-2));box-shadow:var(--glow)}
@@ -3,6 +3,7 @@ import { createServer } from "node:http";
3
3
  import WebSocket, { WebSocketServer } from "ws";
4
4
  import { usageError } from "../errors/cliError.js";
5
5
  import { WebRequestRejected } from "./webMutation.js";
6
+ import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
6
7
  import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
7
8
  import { buildWebDashboardSnapshot, buildWebTaskDetail } from "./webSnapshot.js";
8
9
  const MAX_JSON_BODY_BYTES = 16 * 1024;
@@ -98,6 +99,38 @@ async function handleHttpRequest(request, response, store, dependencies, token,
98
99
  sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
99
100
  return;
100
101
  }
102
+ const globalControl = /^\/api\/roles\/([^/]+)\/control$/.exec(pathname);
103
+ if (globalControl && dependencies.surface) {
104
+ try {
105
+ const roleName = decodeURIComponent(globalControl[1]);
106
+ if (!/^[A-Za-z0-9_-]+$/.test(roleName))
107
+ throw new WebRequestRejected("Invalid Global Role.");
108
+ if (method === "GET") {
109
+ sendJson(response, 200, dependencies.surface.globalState(roleName), false);
110
+ }
111
+ else if (method === "POST") {
112
+ const body = await readMutationBody(request);
113
+ if (typeof body !== "object" || body === null || Array.isArray(body)
114
+ || Object.keys(body).some(key => !["action", "requestId", "body", "expectedTarget", "thenMessage"].includes(key))) {
115
+ throw new WebRequestRejected("Global input accepts only action, input and exact target fields.");
116
+ }
117
+ const input = body;
118
+ const parsed = parseWebControlInput({ ...input,
119
+ ...(input.action === "interrupt" ? { role: roleName }
120
+ : input.action === "steer" ? { to: roleName } : {}) });
121
+ sendJson(response, 200, {
122
+ ...await dependencies.surface.globalControl(roleName, parsed), requestId: parsed.requestId
123
+ }, false);
124
+ }
125
+ else
126
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
127
+ }
128
+ catch (error) {
129
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Global input unavailable.",
130
+ disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
131
+ }
132
+ return;
133
+ }
101
134
  const panelTarget = /^\/api\/tasks\/([^/]+)\/panels$/.exec(pathname);
102
135
  if (panelTarget && dependencies.panels) {
103
136
  try {
@@ -129,7 +162,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
129
162
  }
130
163
  return;
131
164
  }
132
- const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|metadata|messages)$/.exec(pathname);
165
+ const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|metadata|messages|control)$/.exec(pathname);
133
166
  if (surfaceTarget && dependencies.surface) {
134
167
  try {
135
168
  let taskId;
@@ -160,11 +193,17 @@ async function handleHttpRequest(request, response, store, dependencies, token,
160
193
  else if (method === "POST" && action === "messages") {
161
194
  const body = await readMutationBody(request);
162
195
  if (typeof body !== "object" || body === null || Array.isArray(body)
163
- || Object.keys(body).some((key) => !["body", "requestId"].includes(key))
196
+ || Object.keys(body).some((key) => !["body", "requestId", "intent"].includes(key))
164
197
  || !("requestId" in body) || typeof body.requestId !== "string" || !body.requestId.trim()
165
198
  || !("body" in body) || typeof body.body !== "string")
166
- throw new WebRequestRejected("Expected body and requestId only.");
167
- value = { ...dependencies.surface.message(taskId, body.body), requestId: body.requestId };
199
+ throw new WebRequestRejected("Expected body, requestId and optional intent.");
200
+ const intent = webSubmissionIntent(body);
201
+ // requestId is threaded as the submission key (§2.3) and echoed back on the receipt.
202
+ value = { ...dependencies.surface.message(taskId, body.body, intent, body.requestId), requestId: body.requestId };
203
+ }
204
+ else if (method === "POST" && action === "control") {
205
+ const body = await readMutationBody(request);
206
+ value = { ...await dependencies.surface.control(taskId, parseWebControlInput(body)), requestId: parseWebControlRequestId(body) };
168
207
  }
169
208
  else if (method === "POST" && action === "metadata") {
170
209
  const body = await readMutationBody(request);
@@ -426,6 +465,86 @@ function parseAnswerPath(pathname) {
426
465
  return null;
427
466
  }
428
467
  }
468
+ /** The three-action control body is validated to the exact CLI-equivalent
469
+ * shape before it reaches the surface, so an unknown or malformed field is a
470
+ * visible not-submitted rejection rather than a silent default. `requestId` is
471
+ * the caller-supplied idempotency key common to all three actions. */
472
+ function parseWebControlRequestId(value) {
473
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
474
+ throw new WebRequestRejected("Control input must be an object.");
475
+ }
476
+ const requestId = value.requestId;
477
+ if (typeof requestId !== "string" || !requestId.trim()) {
478
+ throw new WebRequestRejected("A non-empty requestId is required.");
479
+ }
480
+ return requestId;
481
+ }
482
+ function requiredControlString(input, key) {
483
+ const value = input[key];
484
+ if (typeof value !== "string" || !value.trim()) {
485
+ throw new WebRequestRejected(`${key} is required.`);
486
+ }
487
+ return value;
488
+ }
489
+ function optionalControlString(input, key) {
490
+ if (!(key in input) || input[key] === undefined)
491
+ return undefined;
492
+ const value = input[key];
493
+ if (typeof value !== "string" || !value.trim()) {
494
+ throw new WebRequestRejected(`${key} must be a non-empty string when provided.`);
495
+ }
496
+ return value;
497
+ }
498
+ function parseWebControlInput(value) {
499
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
500
+ throw new WebRequestRejected("Control input must be an object.");
501
+ }
502
+ const input = value;
503
+ const action = input.action;
504
+ if (action === "queue") {
505
+ for (const key of Object.keys(input)) {
506
+ if (!["action", "body", "requestId", "to", "workItem", "reviewRound"].includes(key)) {
507
+ throw new WebRequestRejected(`Unexpected field for queue: ${key}.`);
508
+ }
509
+ }
510
+ if (input.workItem !== undefined && input.reviewRound !== undefined) {
511
+ throw new WebRequestRejected("A queue takes at most one of workItem or reviewRound.");
512
+ }
513
+ return { action: "queue", body: requiredControlString(input, "body"),
514
+ requestId: requiredControlString(input, "requestId"),
515
+ ...(optionalControlString(input, "to") === undefined ? {} : { to: optionalControlString(input, "to") }),
516
+ ...(optionalControlString(input, "workItem") === undefined ? {} : { workItem: optionalControlString(input, "workItem") }),
517
+ ...(optionalControlString(input, "reviewRound") === undefined ? {} : { reviewRound: optionalControlString(input, "reviewRound") }) };
518
+ }
519
+ if (action === "steer") {
520
+ for (const key of Object.keys(input)) {
521
+ if (!["action", "body", "requestId", "expectedTarget", "to", "workItem", "reviewRound"].includes(key)) {
522
+ throw new WebRequestRejected(`Unexpected field for steer: ${key}.`);
523
+ }
524
+ }
525
+ if (input.workItem !== undefined && input.reviewRound !== undefined) {
526
+ throw new WebRequestRejected("A steer takes at most one of workItem or reviewRound.");
527
+ }
528
+ return { action: "steer", body: requiredControlString(input, "body"),
529
+ requestId: requiredControlString(input, "requestId"),
530
+ expectedTarget: requiredControlString(input, "expectedTarget"),
531
+ to: requiredControlString(input, "to"),
532
+ ...(optionalControlString(input, "workItem") === undefined ? {} : { workItem: optionalControlString(input, "workItem") }),
533
+ ...(optionalControlString(input, "reviewRound") === undefined ? {} : { reviewRound: optionalControlString(input, "reviewRound") }) };
534
+ }
535
+ if (action === "interrupt") {
536
+ for (const key of Object.keys(input)) {
537
+ if (!["action", "requestId", "expectedTarget", "role", "thenMessage"].includes(key)) {
538
+ throw new WebRequestRejected(`Unexpected field for interrupt: ${key}.`);
539
+ }
540
+ }
541
+ return { action: "interrupt", requestId: requiredControlString(input, "requestId"),
542
+ expectedTarget: requiredControlString(input, "expectedTarget"),
543
+ role: requiredControlString(input, "role"),
544
+ ...(optionalControlString(input, "thenMessage") === undefined ? {} : { thenMessage: optionalControlString(input, "thenMessage") }) };
545
+ }
546
+ throw new WebRequestRejected("action must be one of queue, steer, or interrupt.");
547
+ }
429
548
  function parseWebInputAnswer(value) {
430
549
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
431
550
  throw new Error("Input answer must be an object.");
@@ -450,6 +569,18 @@ async function readMutationBody(request) {
450
569
  throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid request body.");
451
570
  }
452
571
  }
572
+ /** Validate an optional submission intent from a Web message body. Absent leaves
573
+ * it undefined so the shared service applies the discuss default (task-32 §2.5).
574
+ * A present-but-invalid value is rejected rather than silently downgraded. */
575
+ function webSubmissionIntent(body) {
576
+ if (!("intent" in body) || body.intent === undefined)
577
+ return undefined;
578
+ if (typeof body.intent === "string"
579
+ && TASK_SUBMISSION_INTENTS.includes(body.intent)) {
580
+ return body.intent;
581
+ }
582
+ throw new WebRequestRejected(`intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}.`);
583
+ }
453
584
  async function readJsonBody(request) {
454
585
  const chunks = [];
455
586
  let size = 0;
@@ -25,14 +25,15 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
25
25
  statusCounts[task.status] += 1;
26
26
  const taskOpenInputs = reader.listInputRequests(task.id)
27
27
  .filter((request) => request.status === "open").length;
28
- openInputs += taskOpenInputs;
28
+ if (task.status !== "archived")
29
+ openInputs += taskOpenInputs;
29
30
  const taskOpen = reader.listInputRequests(task.id)
30
31
  .filter((request) => request.status === "open");
31
- for (const request of taskOpen) {
32
+ for (const request of task.status === "archived" ? [] : taskOpen) {
32
33
  attention.push({ taskId: task.id, taskTitle: task.title, request });
33
34
  }
34
35
  const events = reader.listEvents?.(task.id) ?? [];
35
- const needsAttentionCount = reader.listRuns(task.id)
36
+ const needsAttentionCount = task.status === "archived" ? 0 : reader.listRuns(task.id)
36
37
  .filter((run) => run.status === "active" && isRoleRunStalled(events, run.id))
37
38
  .length;
38
39
  const execution = buildTaskExecutionProjection(reader, task.id, task, now);