@zq-silk/yui 0.16.0 → 0.99.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 (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +128 -20
  95. package/docs/release-workflow.zh-CN.md +107 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -3,6 +3,7 @@ import { Terminal } from "/assets/vendor/xterm.mjs";
3
3
  import { FitAddon } from "/assets/vendor/addon-fit.mjs";
4
4
  import { createI18n } from "/assets/js/i18n.js";
5
5
  import { createThemeController } from "/assets/js/theme.js";
6
+ import { updateTaskObservations } from "/assets/js/task-summary.js";
6
7
  import {
7
8
  renderError,
8
9
  renderFilters,
@@ -46,6 +47,10 @@ const state = {
46
47
  attention: [],
47
48
  catalogAttention: null,
48
49
  catalogScope: null,
50
+ catalogQuery: "",
51
+ sessionOverview: null,
52
+ sessionLoading: false,
53
+ sessionError: null,
49
54
  catalogAll: false,
50
55
  catalogTotal: 0,
51
56
  catalogCursor: null,
@@ -144,6 +149,10 @@ function detailActions() {
144
149
  answerInput: answerInput,
145
150
  openTerminal: openTerminal,
146
151
  inspect: inspectRecord,
152
+ artifacts: taskId => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/artifacts"),
153
+ evidence: taskId => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/evidence"),
154
+ readArtifact: (taskId, path, commit) => requestJson("/api/tasks/" + encodeURIComponent(taskId)
155
+ + "/artifacts?" + new URLSearchParams({ path, commit })),
147
156
  sendMessage: (taskId, body, requestId, intent) => submitMutation(taskId + "/messages",
148
157
  "/api/tasks/" + encodeURIComponent(taskId) + "/messages",
149
158
  { body, requestId, ...(intent === undefined ? {} : { intent }) }),
@@ -209,7 +218,11 @@ function detailKeyOf(detail) {
209
218
  // every poll and would re-render the detail continuously.
210
219
  function runtimeSignatureOf(detail) {
211
220
  const roles = (detail.runtime && detail.runtime.roles) || [];
212
- return detail.runtimeStatus + "|" + roles.map(function (role) {
221
+ return detail.runtimeStatus + "|" + JSON.stringify([
222
+ detail.runtime?.sessions?.sessions, detail.runtime?.remoteDelivery,
223
+ detail.runtime?.execution?.attention, detail.runtime?.execution?.blockers,
224
+ detail.runtime?.execution?.next
225
+ ]) + "|" + roles.map(function (role) {
213
226
  const session = role.runtimeSession;
214
227
  return role.name + ":" + (session
215
228
  ? session.nativeSessionId + "/" + session.status
@@ -223,10 +236,13 @@ function renderCurrentDetail(force) {
223
236
  // after the user has moved focus. This is view state, never Task state.
224
237
  if (elements.detail.dataset.taskId === state.detail.task.id
225
238
  && (elements.detail.querySelector('[data-unsent="true"]')
239
+ || elements.detail.querySelector('[data-reading="true"]')
226
240
  || elements.detail.contains(document.activeElement))) return;
227
241
  const key = i18n.getLocale() + "|" + state.detailKey
228
242
  + "|" + runtimeSignatureOf(state.detail);
229
243
  if (!force && key === renderedDetailKey) return;
244
+ const openSections = Array.from(elements.detail.querySelectorAll("details[data-view-key][open]"))
245
+ .map(element => element.dataset.viewKey);
230
246
  renderTaskDetail(
231
247
  elements.detail,
232
248
  state.detail,
@@ -234,6 +250,9 @@ function renderCurrentDetail(force) {
234
250
  i18n.getLocale(),
235
251
  detailActions()
236
252
  );
253
+ elements.detail.querySelectorAll("details[data-view-key]").forEach(element => {
254
+ element.open = openSections.includes(element.dataset.viewKey);
255
+ });
237
256
  renderedDetailKey = key;
238
257
  } else if (!state.selected) {
239
258
  renderedDetailKey = null;
@@ -319,6 +338,7 @@ function showToast(message) {
319
338
  }
320
339
 
321
340
  function clearSelection() {
341
+ if (!canLeaveDetail()) return;
322
342
  state.selected = null;
323
343
  state.detail = null;
324
344
  setDetailActive(false);
@@ -382,6 +402,7 @@ async function loadTaskDetail(taskId, showLoading) {
382
402
  const previous = state.detail && state.detail.task.id === taskId ? state.detail : null;
383
403
  const detail = {
384
404
  task, core,
405
+ viewState: previous ? previous.viewState : {},
385
406
  runtime: previous && previous.runtime,
386
407
  runtimeStatus: previous ? previous.runtimeStatus : "waiting",
387
408
  runtimeObservedAt: previous ? previous.runtimeObservedAt : new Date().toISOString()
@@ -419,6 +440,7 @@ async function loadTaskDetail(taskId, showLoading) {
419
440
  }
420
441
 
421
442
  function updateRuntimePanel(detail) {
443
+ updateTaskObservations(elements.detail, detail, i18n.t, i18n.getLocale());
422
444
  const status = elements.detail.querySelector("[data-runtime-status]");
423
445
  const value = elements.detail.querySelector("[data-runtime-value]");
424
446
  if (status) status.textContent = detail.runtimeStatus + " · " + detail.runtimeObservedAt;
@@ -433,11 +455,19 @@ function updateRuntimePanel(detail) {
433
455
  }
434
456
 
435
457
  async function inspectRecord(taskId, ref) {
436
- const query = new URLSearchParams({ store: ref.store, ref: ref.refId, digest: ref.digest });
458
+ const query = new URLSearchParams({ store: ref.store, ref: ref.refId });
459
+ if (ref.digest) query.set("digest", ref.digest);
437
460
  return requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/inspect?" + query);
438
461
  }
439
462
 
440
463
  async function selectTask(taskId) {
464
+ // Re-selecting the already visible Task is not navigation. Keep the same
465
+ // draft and fixed artifact selection, just as an ordinary refresh does.
466
+ if (state.selected === taskId && state.detail) return;
467
+ if (state.selected !== taskId && !canLeaveDetail()) {
468
+ syncUrlFromState({ replace: true });
469
+ return;
470
+ }
441
471
  // Switching tasks drops the previous section; staying on the same task
442
472
  // (URL-driven loads, popstate) keeps it.
443
473
  if (urlTaskId() !== taskId) {
@@ -465,6 +495,17 @@ async function selectTask(taskId) {
465
495
  }
466
496
  }
467
497
 
498
+ function canLeaveDetail() {
499
+ return !elements.detail.querySelector('[data-unsent="true"]') || window.confirm(
500
+ i18n.getLocale().startsWith("zh") ? "此任务有未提交或结果尚不确定的输入。仍要离开?"
501
+ : "This Task has unsent input or an unresolved submission. Leave anyway?");
502
+ }
503
+ window.addEventListener("beforeunload", event => {
504
+ if (!elements.detail.querySelector('[data-unsent="true"]')) return;
505
+ event.preventDefault();
506
+ event.returnValue = "";
507
+ });
508
+
468
509
  async function answerInput(input, answer) {
469
510
  if (!state.detail) return;
470
511
  const taskId = state.detail.task.id;
@@ -513,6 +554,11 @@ async function refreshDashboard(options) {
513
554
  const dashboard = await requestJson("/api/dashboard?" + query.toString());
514
555
  if (request !== catalogRequest) return;
515
556
  state.tasks = dashboard.tasks;
557
+ if (state.catalogQuery !== query.toString() || (state.sessionOverview && JSON.stringify(
558
+ state.sessionOverview.tasks.map(task => task.taskId)) !== JSON.stringify(dashboard.tasks.map(task => task.id)))) {
559
+ state.sessionOverview = null;
560
+ }
561
+ state.catalogQuery = query.toString();
516
562
  state.counts = { ...dashboard.counts, openInputs: dashboard.attention.openInputs.count };
517
563
  state.catalogAttention = dashboard.attention;
518
564
  state.catalogScope = dashboard.scope;
@@ -689,6 +735,8 @@ if (elements.detailTabs) {
689
735
  if (!targetId || !elements.detail) return;
690
736
  const section = elements.detail.querySelector("#" + targetId);
691
737
  if (!section) return;
738
+ const folded = section.querySelector(":scope > details");
739
+ if (folded) folded.open = true;
692
740
  section.scrollIntoView({ behavior: "smooth", block: "start" });
693
741
  setSectionParam(targetId);
694
742
  elements.detailTabs.querySelectorAll(".tab").forEach(function (other) {
@@ -718,7 +766,28 @@ elements.catalogAttentionReset.addEventListener("click", function () {
718
766
  syncUrlFromState({ replace: false });
719
767
  resetCatalog();
720
768
  });
721
- elements.detail.addEventListener("click", function (event) {
769
+ elements.detail.addEventListener("click", async function (event) {
770
+ const observe = event.target.closest("[data-observe-sessions]");
771
+ if (observe) {
772
+ if (state.sessionLoading) return;
773
+ const query = state.catalogQuery;
774
+ state.sessionLoading = true;
775
+ observe.disabled = true;
776
+ try {
777
+ const result = await requestJson("/api/dashboard/sessions?" + query, { signal: AbortSignal.timeout(5000) });
778
+ if (query !== state.catalogQuery) return;
779
+ if (JSON.stringify(result.tasks.map(task => task.taskId)) !== JSON.stringify(state.tasks.map(task => task.id))) {
780
+ throw new Error("Catalog page changed; refresh it before reading Sessions.");
781
+ }
782
+ state.sessionOverview = result;
783
+ state.sessionError = null;
784
+ } catch (error) { state.sessionError = error.message; }
785
+ finally {
786
+ state.sessionLoading = false;
787
+ if (!state.selected) showOverview();
788
+ }
789
+ return;
790
+ }
722
791
  const button = event.target.closest("[data-catalog-attention]");
723
792
  if (!button) return;
724
793
  state.attentionFilter = button.dataset.catalogAttention;
@@ -92,7 +92,8 @@ export function sectionHead(label, options) {
92
92
  export function anchorSection(id, head, body) {
93
93
  const section = node("section", "detail-section anchor");
94
94
  section.id = id;
95
- section.append(head, body);
95
+ if (head) section.append(head);
96
+ section.append(body);
96
97
  return section;
97
98
  }
98
99
 
@@ -281,6 +281,7 @@ const messages = {
281
281
  "sync.label": "Last sync",
282
282
  "tabs.exec": "AgentRuns",
283
283
  "tabs.focus": "Focus",
284
+ "tabs.results": "Results",
284
285
  "tabs.history": "History",
285
286
  "tabs.label": "Task sections",
286
287
  "tabs.messages": "Messages",
@@ -704,6 +705,7 @@ const messages = {
704
705
  "sync.label": "最近同步",
705
706
  "tabs.exec": "执行记录",
706
707
  "tabs.focus": "焦点",
708
+ "tabs.results": "成果",
707
709
  "tabs.history": "历史",
708
710
  "tabs.label": "任务分区",
709
711
  "tabs.messages": "消息",
@@ -0,0 +1,345 @@
1
+ export const TASK_SUMMARY_SCRIPT = `
2
+ import { node, clear } from "/assets/js/dom.js";
3
+ import { richText, inputCard, observabilityMetricCard } from "/assets/js/components.js";
4
+ import { formatDateTime } from "/assets/js/format.js";
5
+
6
+ const sessionLabels = {
7
+ active: ["Recent activity", "近期有活动"], waiting: ["Waiting", "正在等待"],
8
+ quiet: ["Quiet · not a proven deadlock", "安静 · 尚不能判定卡死"],
9
+ diagnostic: ["Needs diagnosis", "需要诊断"], unknown: ["Observation unknown", "观测未知"],
10
+ stopped: ["Stopped / failed", "已停止/失败"], idle: ["Turn ended", "该轮已结束"],
11
+ background: ["Background unsettled", "后台未结清"]
12
+ };
13
+ const sayFor = locale => (en, cn) => locale.startsWith("zh") ? cn : en;
14
+ function block(title, key) {
15
+ const section = node("section", "record-card summary-block");
16
+ section.dataset.summary = key;
17
+ section.append(node("h3", "", title));
18
+ return section;
19
+ }
20
+ function disclosure(title, key) {
21
+ const details = node("details", "record-card");
22
+ details.dataset.viewKey = key;
23
+ details.append(node("summary", "", title));
24
+ return details;
25
+ }
26
+
27
+ export function renderSessionSummary(container, observation, locale) {
28
+ clear(container);
29
+ const say = sayFor(locale);
30
+ container.append(node("h3", "", say("Session activity", "会话活动")));
31
+ if (!observation) {
32
+ container.append(node("p", "muted", say(
33
+ "Observation unavailable or not yet read; this does not mean zero running Sessions.",
34
+ "观测尚未读取或不可用;不代表运行会话为零。")));
35
+ return;
36
+ }
37
+ const metrics = node("div", "session-counts");
38
+ for (const [key, count] of Object.entries(observation.counts)) {
39
+ if (!count) continue;
40
+ const label = sessionLabels[key];
41
+ metrics.append(node("span", "session-count", count + " · " + say(...label)));
42
+ }
43
+ if (!observation.sessions.length) metrics.append(node("p", "muted", say(
44
+ "No selected native Session is recorded. Historical resources were not inspected.",
45
+ "未记录当前选用的原生会话;未盘点历史资源。")));
46
+ container.append(metrics, node("p", "muted", say(
47
+ "Scope: this Task's selected native Sessions. Activity is not delivery progress; no live process probe.",
48
+ "范围:本 Task 当前选用的原生会话。活动不等于交付进展;未探测实时进程。")));
49
+ container.append(node("small", "muted", say("Independent observation read: ", "独立观察读取于:")
50
+ + formatDateTime(observation.readAt, locale)));
51
+ const details = disclosure(say("Session facts and timestamps", "会话依据与时间"), "sessions");
52
+ for (const session of observation.sessions) {
53
+ const row = node("article", "session-row");
54
+ row.append(node("strong", "", session.roleName + " · " + say(...sessionLabels[session.group])));
55
+ row.append(node("p", "record-copy", session.reason));
56
+ if (session.waitingReason) row.append(node("p", "", say("Waiting for: ", "等待:") + session.waitingReason));
57
+ row.append(node("p", "muted", say("Last activity: ", "最近活动:")
58
+ + (session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : say("unobserved", "未观测"))));
59
+ row.append(node("p", "muted", say("Native input updated: ", "原生输入记录更新:") + formatDateTime(session.sourceUpdatedAt, locale)));
60
+ if (session.operations.length) row.append(node("p", "muted", say(
61
+ "Operations without a terminal (not proof of recent activity): ",
62
+ "尚无终态的操作(不证明近期仍有活动):") + session.operations.join(", ")));
63
+ if (session.background.length) row.append(node("p", "", say("Background: ", "后台:")
64
+ + session.background.map(item => item.id + " · " + item.execution).join(", ")));
65
+ row.append(node("small", "mono", [session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / ")));
66
+ details.append(row);
67
+ }
68
+ container.append(details);
69
+ }
70
+
71
+ export function renderTaskSummary(container, data, t, locale, actions, recordCard) {
72
+ const say = sayFor(locale);
73
+ const entries = store => data.core.records.filter(entry => entry.ref.store === store);
74
+ const available = store => entries(store).filter(entry => !entry.omitted).map(entry => entry.value);
75
+ const scope = node("p", "muted", say("Scope: ", "范围:") + data.task.id + " · "
76
+ + say("Reading does not accept results or acknowledge Messages.", "读取不验收成果,也不确认消息。"));
77
+ container.append(scope);
78
+ const grid = node("div", "task-summary-grid");
79
+ container.append(grid);
80
+ const attention = block(say("Needs your input", "需要你处理"), "attention");
81
+ attention.id = "detail-attention";
82
+ const inputCount = data.core.attention.openInputs.count;
83
+ attention.append(node("p", "", inputCount ? inputCount + say(" open questions", " 项待答问题")
84
+ : say("No open InputRequest requires your answer.", "当前没有待你回答的 InputRequest。")));
85
+ const inputs = entries("input-request").filter(entry => !entry.omitted && entry.value.status === "open");
86
+ inputs.forEach(entry => attention.append(inputCard(entry.value, { single: true }, t, locale, actions)));
87
+ const missing = data.core.attention.openInputs.refs.filter(ref => !inputs.some(entry => entry.ref.refId === ref.refId));
88
+ missing.forEach(ref => attention.append(recordCard({ ref, omitted: true })));
89
+ if (inputCount > inputs.length + missing.length) attention.append(node("p", "muted", say(
90
+ "Some open questions are outside this bounded read. Read all original questions with: ",
91
+ "部分待答问题未包含在本次有界读取中,请读取全部原问题:") + "yui task input list " + data.task.id));
92
+ const technical = node("div", "");
93
+ technical.dataset.technicalSummary = "";
94
+ attention.append(technical);
95
+ grid.append(attention);
96
+ const sessions = block("", "sessions");
97
+ sessions.dataset.sessionSummary = "";
98
+ grid.append(sessions);
99
+ const progress = block(say("Task progress", "任务进展"), "progress");
100
+ const briefEntry = entries("task-brief")[0];
101
+ const brief = available("task-brief")[0];
102
+ if (data.task.completionSummary) progress.append(richText(say("Recorded completion", "已记录完成"), data.task.completionSummary, t));
103
+ if (brief) {
104
+ progress.append(richText(say("Current focus", "当前工作"), brief.currentFocus, t));
105
+ progress.append(richText(say("Leader's progress report", "Leader 进展摘要"), brief.leaderSummary, t));
106
+ progress.append(node("small", "muted", say("Brief updated: ", "Brief 更新:") + formatDateTime(brief.updatedAt, locale)));
107
+ } else if (briefEntry) progress.append(recordCard(briefEntry));
108
+ else progress.append(node("p", "muted", say("No progress summary in this read; no percentage or ETA is inferred.",
109
+ "本次读取未包含进展摘要;不推算完成百分比或预计时间。")));
110
+ const next = node("p", "muted");
111
+ next.dataset.nextSummary = "";
112
+ progress.append(next);
113
+ grid.append(progress);
114
+ const conclusions = block(say("Key conclusions", "关键结论"), "conclusions");
115
+ const decisions = entries("task-decision").filter(entry => !entry.omitted && entry.value.status === "active");
116
+ decisions.slice(0, 3).forEach(entry => {
117
+ const card = recordCard(entry);
118
+ card.prepend(node("small", "muted", say("Recorded decision", "已记录决定")));
119
+ conclusions.append(card);
120
+ });
121
+ if (!decisions.length) conclusions.append(node("p", "muted", say(
122
+ "No current Decision is included. Reports and recommendations are not user approval.",
123
+ "本次读取未包含当前 Decision;报告和建议不代表用户批准。")));
124
+ if (decisions.length > 3 || data.core.omitted.records || entries("task-decision").some(entry => entry.omitted)) {
125
+ conclusions.append(node("p", "muted", say("More conclusions or omitted sources are available in history / Context.",
126
+ "更多结论或省略来源请查看历史/Context。")));
127
+ }
128
+ grid.append(conclusions);
129
+ updateTaskObservations(container, data, t, locale);
130
+ }
131
+
132
+ export function updateTaskObservations(container, data, t, locale) {
133
+ const say = sayFor(locale);
134
+ const sessions = container.querySelector("[data-session-summary]");
135
+ if (sessions && !sessions.contains(document.activeElement)) {
136
+ const wasOpen = sessions.querySelector("details")?.open;
137
+ renderSessionSummary(sessions, data.runtimeStatus === "available" ? data.runtime?.sessions : null, locale);
138
+ if (wasOpen && sessions.querySelector("details")) sessions.querySelector("details").open = true;
139
+ }
140
+ const technical = container.querySelector("[data-technical-summary]");
141
+ const execution = data.runtimeStatus === "available" ? data.runtime?.execution : null;
142
+ if (technical) {
143
+ clear(technical);
144
+ if (!execution) technical.append(node("p", "muted", say(
145
+ "Technical attention and native waits have not been observed; no open question does not prove health.",
146
+ "技术关注和原生等待尚未观测;没有待答问题不代表系统完全健康。")));
147
+ else {
148
+ const facts = execution.attention.concat(execution.blockers.filter(item => item.kind !== "input"));
149
+ if (facts.length) technical.append(node("h4", "", say("Technical attention · owner assigned, not necessarily started",
150
+ "技术关注 · 有负责人不代表已开始处理")));
151
+ facts.forEach(item => technical.append(node("p", "record-copy", item.owner + " · " + item.summary + " · " + item.id)));
152
+ if (!facts.length) technical.append(node("p", "muted", say("No technical attention in the current Task projection.",
153
+ "当前任务投影未列出技术关注。")));
154
+ }
155
+ const waits = data.runtimeStatus === "available" ? data.runtime?.sessions?.sessions.filter(session =>
156
+ session.group === "waiting" && ["user", "permission"].includes(session.waitingReason)) ?? [] : [];
157
+ waits.forEach(session => technical.append(node("p", "", session.roleName + say(
158
+ " is waiting for user/permission input. Inspect its original Session; this is not a new approval request.",
159
+ " 明确等待用户/权限输入。请查看原 Session;这里没有新建审批请求。"))));
160
+ }
161
+ const next = container.querySelector("[data-next-summary]");
162
+ if (next) next.textContent = execution ? say("Next owner: ", "下一步负责人:") + execution.next.owner
163
+ + " · " + t("exec.action." + execution.next.action) + " · " + execution.reason
164
+ : say("Next owner/action observation unavailable.", "下一步负责人/动作观测不可用。");
165
+ }
166
+
167
+ export function renderEvidence(container, data, t, locale, actions) {
168
+ const say = sayFor(locale);
169
+ const state = data.viewState;
170
+ const artifacts = disclosure(say("Saved files · read a fixed version", "已保存文件 · 读取固定版本"), "artifacts");
171
+ const body = node("div", "section-body");
172
+ artifacts.append(body);
173
+ const selection = node("div", "section-body");
174
+ let readSequence = 0;
175
+ const read = async (path, commit) => {
176
+ const sequence = ++readSequence;
177
+ selection.dataset.reading = "true";
178
+ selection.textContent = say("Reading selected revision…", "正在读取所选版本…");
179
+ try {
180
+ const result = await actions.readArtifact(data.task.id, path, commit);
181
+ if (sequence !== readSequence) return;
182
+ state.artifact = result;
183
+ drawSelection();
184
+ } catch (error) {
185
+ if (sequence === readSequence) selection.textContent = say("Selected revision unavailable: ", "所选版本不可用:") + error.message;
186
+ } finally { if (sequence === readSequence) selection.dataset.reading = "false"; }
187
+ };
188
+ function drawSelection() {
189
+ clear(selection);
190
+ const value = state.artifact;
191
+ if (!value) return;
192
+ const ref = "git:" + value.commit + ":" + value.relativePath;
193
+ selection.append(node("strong", "", say("Fixed result · ", "固定成果 · ") + value.relativePath));
194
+ selection.append(node("p", "mono", ref));
195
+ selection.append(node("p", "muted", say(
196
+ "This revision stays selected across refreshes. Reading or discussing it does not accept, publish or execute it.",
197
+ "刷新后仍保留此版本。读取或讨论不会验收、发布或执行该成果。")));
198
+ const copy = node("button", "record-open", say("Copy source for discussion", "复制来源用于讨论"));
199
+ copy.type = "button";
200
+ copy.addEventListener("click", async () => {
201
+ try { await navigator.clipboard.writeText(data.task.id + " " + ref); copy.textContent = say("Copied", "已复制"); }
202
+ catch { copy.textContent = say("Copy the source shown above", "请复制上方来源"); }
203
+ });
204
+ selection.append(copy);
205
+ const content = node("pre", "surface-json artifact-text", value.content);
206
+ content.tabIndex = 0;
207
+ content.setAttribute("aria-label", say("Fixed artifact text", "固定成果正文"));
208
+ selection.append(content);
209
+ }
210
+ const load = node("button", "record-open", say("Read current file list", "读取当前文件列表"));
211
+ load.type = "button";
212
+ const list = node("div", "section-body");
213
+ load.addEventListener("click", async () => {
214
+ load.disabled = true;
215
+ list.dataset.reading = "true";
216
+ try {
217
+ const result = await actions.artifacts(data.task.id);
218
+ state.artifactList = result;
219
+ drawList();
220
+ } catch (error) { list.textContent = say("File list unavailable: ", "文件列表不可用:") + error.message; }
221
+ finally { load.disabled = false; list.dataset.reading = "false"; }
222
+ });
223
+ function drawList() {
224
+ clear(list);
225
+ const current = state.artifactList;
226
+ if (!current) return;
227
+ if (state.artifact && current.commit !== state.artifact.commit) list.append(node("p", "", say(
228
+ "The file list is at a different revision. Your selected result below has not changed.",
229
+ "文件列表来自不同版本,下方所选成果未被替换。")));
230
+ if (!current.entries.length) list.append(node("p", "muted", say("No saved files at this read.", "本次读取没有已保存文件。")));
231
+ current.entries.forEach(entry => {
232
+ const button = node("button", "record-open artifact-choice", entry.relativePath + " · " + entry.size + " B");
233
+ button.type = "button";
234
+ button.addEventListener("click", () => read(entry.relativePath, current.commit));
235
+ list.append(button);
236
+ });
237
+ }
238
+ body.append(node("p", "muted", say("Files are loaded only on request; missing Context rows do not mean no files.",
239
+ "文件仅在请求时读取;Context 没有文件行不代表没有成果。")), load, list);
240
+ // Explicit frozen references stay attached to their original revision.
241
+ const sources = [data.task.completionArtifactRefs ?? [],
242
+ data.core.records.filter(entry => !entry.omitted && ["task-brief", "task-decision"].includes(entry.ref.store))
243
+ .map(entry => JSON.stringify(entry.value))].flat().join(" ");
244
+ const refs = [...sources.matchAll(/git:([0-9a-f]{40}):([^\\s"\\\\]+?)(?=[\\s"\\\\]|$)/g)];
245
+ const seen = new Set();
246
+ for (const match of refs) {
247
+ if (seen.has(match[0])) continue;
248
+ seen.add(match[0]);
249
+ const button = node("button", "record-open artifact-choice", say("Referenced revision · ", "已引用版本 · ") + match[2]);
250
+ button.type = "button";
251
+ button.addEventListener("click", () => read(match[2], match[1]));
252
+ body.append(button);
253
+ }
254
+ body.append(selection);
255
+ drawList();
256
+ drawSelection();
257
+ container.append(artifacts);
258
+ const delivery = disclosure(say("Delivery, review and retained resources", "交付、审阅与保留资源"), "delivery");
259
+ const output = node("div", "section-body");
260
+ delivery.append(output);
261
+ delivery.addEventListener("toggle", async () => {
262
+ if (!delivery.open) return;
263
+ clear(output);
264
+ const runtime = data.runtimeStatus === "available" ? data.runtime : null;
265
+ output.append(node("p", "", say("Local completion, review, remote delivery and resource cleanup are separate facts.",
266
+ "本地完成、审阅、远程交付和资源清理是不同事实。")));
267
+ if (!runtime) {
268
+ output.append(node("p", "muted", say("Evidence observation unavailable. Core requirements remain readable.",
269
+ "证据观测不可用;核心要求仍可读取。")));
270
+ return;
271
+ }
272
+ const remote = runtime.remoteDelivery;
273
+ if (remote) {
274
+ output.append(node("h4", "", say("Remote delivery", "远程交付") + " · " + remote.status));
275
+ remote.projects.forEach(project => {
276
+ const card = node("article", "record-card");
277
+ card.append(node("strong", "", project.directory + " · " + project.coverage));
278
+ card.append(node("p", "", project.reason));
279
+ card.append(node("p", "mono", say("Expected local commit: ", "期望本地提交:") + (project.expectedLocalCommit || say("unknown", "未知"))));
280
+ card.append(node("p", "", "PR/MR · " + (project.state || "none") + " · "
281
+ + say("Evidence: ", "证据:") + (project.verification || "unverified")));
282
+ if (project.adoption) card.append(node("p", "mono", say("Adoption: ", "采用记录:") + project.adoption.id));
283
+ output.append(card);
284
+ });
285
+ }
286
+ output.append(node("p", "muted", say(
287
+ "Review reports are evidence, not acceptance. Read the original report and its fixed candidate in Reviews below. Missing checks are not passes.",
288
+ "审阅报告是证据,不是验收。请在下方审阅中读取原报告与固定候选;缺失检查不算通过。")));
289
+ const original = node("div", "section-body");
290
+ output.append(original);
291
+ original.textContent = say("Reading original checks and review bindings…", "读取原始检查与审阅绑定…");
292
+ void actions.evidence(data.task.id).then(evidence => {
293
+ clear(original);
294
+ original.append(node("h4", "", say("Integration checks · exact recorded candidate", "集成检查 · 精确记录候选")));
295
+ if (!evidence.integrations.length) original.append(node("p", "muted", say(
296
+ "No Integration check records. Direct Leader verification may be recorded in the completion report.",
297
+ "没有 Integration 检查记录;Leader 直接执行的验证可能记录于完成报告。")));
298
+ for (const attempt of evidence.integrations) {
299
+ const card = node("article", "record-card");
300
+ card.append(node("strong", "", attempt.id + " · " + attempt.status),
301
+ node("p", "mono", (attempt.candidateCommit || say("No check candidate", "无检查候选"))),
302
+ node("p", "muted", say("Coverage of the physical current head has not been checked here.",
303
+ "此处未核对对物理当前 head 的覆盖。")));
304
+ if (!attempt.checks?.length) card.append(node("p", "muted", say("No check outcomes recorded.", "未记录检查结果。")));
305
+ (attempt.checks || []).forEach(check => card.append(node("p", "", check.name + " · " + check.outcome),
306
+ ...(check.details ? [node("p", "muted", check.details)] : [])));
307
+ original.append(card);
308
+ }
309
+ original.append(node("h4", "", say("Review bindings · not semantic approval", "审阅绑定 · 不等于语义通过")));
310
+ if (!evidence.reviews.length) original.append(node("p", "muted", say("No Review records.", "没有 Review 记录。")));
311
+ for (const round of evidence.reviews) {
312
+ const card = node("article", "record-card");
313
+ card.append(node("strong", "", round.id + " · " + round.status));
314
+ card.append(node("p", "mono", round.taskCandidate
315
+ ? round.taskCandidate.projects.map(p => p.projectId + " · " + p.commit).join("\\n")
316
+ : [round.workItemId, round.candidateId, round.reviewBaseCommit].filter(Boolean).join(" · ")));
317
+ if (round.reviewerRunId) {
318
+ const report = node("button", "record-open", say("Read original review report", "读取原始审阅报告"));
319
+ report.type = "button";
320
+ report.addEventListener("click", async () => {
321
+ report.disabled = true;
322
+ try {
323
+ const source = await actions.inspect(data.task.id, { store: "run", refId: round.reviewerRunId });
324
+ card.append(richText(round.reviewerRunId, source.value.result?.output
325
+ || say("No report recorded.", "未记录报告。"), t));
326
+ } catch (error) { card.append(node("p", "muted", error.message)); report.disabled = false; }
327
+ });
328
+ card.append(report);
329
+ }
330
+ original.append(card);
331
+ }
332
+ original.append(node("h4", "", say("Recorded workspace owners", "已记录工作区所有者")));
333
+ evidence.workspaces.forEach(workspace => original.append(node("p", "mono", workspace.owner.type + " · " + workspace.root)));
334
+ }).catch(error => { original.textContent = say("Original evidence unavailable: ", "原始证据不可用:") + error.message; });
335
+ const metrics = observabilityMetricCard(runtime.observability, t);
336
+ if (metrics) output.append(metrics);
337
+ output.append(node("h4", "", say("Retained workspaces · recorded ownership only", "保留工作区 · 仅已记录所有权")));
338
+ output.append(node("p", "muted", say(
339
+ "No physical cleanup inspection was run. Use the existing read-only preflight when needed; it does not authorize archive or cleanup:",
340
+ "未执行物理清理检查。需要时使用既有只读预检;预检不授权归档或清理:")));
341
+ output.append(node("code", "", "yui task archive-preflight " + data.task.id + " --integrated"));
342
+ });
343
+ container.append(delivery);
344
+ }
345
+ `;