@tea-agent/loop-agent 0.11.0 → 0.13.0-alpha.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 (123) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +33 -4
  3. package/dist/application/dag/generate-task-dag.js +45 -0
  4. package/dist/application/dag/run-dag.js +10 -0
  5. package/dist/application/dag/validate-dag.js +11 -0
  6. package/dist/cli/command-definitions.js +10 -3
  7. package/dist/commands/init.js +74 -7
  8. package/dist/commands/knowledge.js +129 -31
  9. package/dist/governance/manifest-types.js +3 -0
  10. package/dist/shared/package-metadata.js +135 -0
  11. package/dist/task/config-types.js +6 -1
  12. package/dist/worker/cli.js +99 -2
  13. package/dist/worker/delivery/package.js +3 -3
  14. package/dist/worker/feature/decision-loader.js +37 -6
  15. package/dist/worker/feature/next-action.js +10 -2
  16. package/dist/worker/feature/ready-plan-projection.js +81 -0
  17. package/dist/worker/feature/reducer.js +2 -1
  18. package/dist/worker/feature/review.js +19 -2
  19. package/dist/worker/feature/run.js +27 -2
  20. package/dist/worker/follow-up/approve.js +5 -2
  21. package/dist/worker/follow-up/factory.js +1 -1
  22. package/dist/worker/observability/event-history.js +216 -0
  23. package/dist/worker/observability/read-model.js +552 -118
  24. package/dist/worker/observe/paths.js +17 -0
  25. package/dist/worker/observe/routes.js +310 -23
  26. package/dist/worker/observe/server.js +59 -1
  27. package/dist/worker/observe/spec-evidence.js +281 -0
  28. package/dist/worker/observe/static/api.js +46 -0
  29. package/dist/worker/observe/static/app.js +120 -2598
  30. package/dist/worker/observe/static/constants.js +148 -0
  31. package/dist/worker/observe/static/copy.js +67 -0
  32. package/dist/worker/observe/static/dag-helpers.js +172 -0
  33. package/dist/worker/observe/static/dag-model.js +72 -0
  34. package/dist/worker/observe/static/dom.js +61 -0
  35. package/dist/worker/observe/static/format-pool.js +67 -0
  36. package/dist/worker/observe/static/format.js +292 -0
  37. package/dist/worker/observe/static/index.html +300 -82
  38. package/dist/worker/observe/static/kpi.js +94 -0
  39. package/dist/worker/observe/static/relations.js +133 -0
  40. package/dist/worker/observe/static/router.js +93 -0
  41. package/dist/worker/observe/static/run-processing.js +148 -0
  42. package/dist/worker/observe/static/shell-chrome.js +68 -0
  43. package/dist/worker/observe/static/state.js +253 -0
  44. package/dist/worker/observe/static/styles.css +1731 -495
  45. package/dist/worker/observe/static/views/batch.js +227 -0
  46. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  47. package/dist/worker/observe/static/views/dag-inspector.js +596 -0
  48. package/dist/worker/observe/static/views/dag.js +362 -0
  49. package/dist/worker/observe/static/views/dashboard.js +445 -0
  50. package/dist/worker/observe/static/views/failures.js +143 -0
  51. package/dist/worker/observe/static/views/feature.js +492 -0
  52. package/dist/worker/observe/static/views/pool.js +350 -0
  53. package/dist/worker/observe/static/views/run.js +453 -0
  54. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  55. package/dist/worker/observe/static/views/shell.js +7 -0
  56. package/dist/worker/observe/static/views/task.js +314 -0
  57. package/dist/worker/observe/static/views/timeline.js +163 -0
  58. package/dist/worker/pool/doctor.js +165 -0
  59. package/dist/worker/pool/migrate-state.js +303 -0
  60. package/dist/worker/pool/run-store.js +205 -17
  61. package/dist/worker/pool/types.js +17 -1
  62. package/dist/worker/pool/validation.js +100 -15
  63. package/dist/worker/report/morning-report.js +12 -2
  64. package/dist/worker/runner/run-ready.js +41 -26
  65. package/dist/worker/task-graph/ready-planner.js +136 -0
  66. package/dist/workflows/dag/controller-identity.js +104 -0
  67. package/dist/workflows/dag/convergence/controller.js +16 -8
  68. package/dist/workflows/dag/failure-routing.js +12 -1
  69. package/dist/workflows/dag/init-hybrid.js +1233 -11
  70. package/dist/workflows/dag/node-execution.js +123 -29
  71. package/dist/workflows/dag/repair-artifact.js +91 -0
  72. package/dist/workflows/dag/report.js +50 -0
  73. package/dist/workflows/dag/retry-policy.js +138 -0
  74. package/dist/workflows/dag/runner.js +32 -0
  75. package/dist/workflows/dag/runtime-contract.js +87 -0
  76. package/dist/workflows/dag/skill-snapshot.js +2 -0
  77. package/dist/workflows/dag/types.js +45 -1
  78. package/dist/workflows/dag/validate.js +68 -4
  79. package/docs/README.md +1 -1
  80. package/docs/agent-dag-recovery-playbook.md +9 -0
  81. package/docs/agent-dag-runner.md +26 -1
  82. package/docs/architecture/dag-execution.md +6 -0
  83. package/docs/architecture/evolution.md +7 -5
  84. package/docs/architecture/facts-and-state.md +15 -2
  85. package/docs/architecture/worker-and-feature.md +6 -2
  86. package/docs/decisions/README.md +3 -0
  87. package/docs/design/README.md +12 -3
  88. package/docs/exec-plans/active/README.md +2 -2
  89. package/docs/exec-plans/completed/README.md +12 -0
  90. package/docs/feature-workflow.md +108 -2
  91. package/docs/loop-agent-harness.md +15 -4
  92. package/docs/progress/README.md +22 -0
  93. package/docs/reports/README.md +14 -2
  94. package/docs/templates/agent-dag-report.schema.json +17 -0
  95. package/docs/templates/agent-dag.schema.json +69 -1
  96. package/docs/templates/agent-dag.supervised-implementation.json +8 -2
  97. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  98. package/docs/templates/backend-test-dag.json +288 -0
  99. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  100. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  101. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  102. package/docs/templates/knowledge-sync-dag.json +177 -0
  103. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  104. package/docs/verification-matrix.md +2 -1
  105. package/package.json +8 -2
  106. package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
  107. package/scripts/kb-graph-incremental-prepare.mjs +372 -0
  108. package/scripts/kb-graph-incremental-prepare.sh +5 -0
  109. package/scripts/kb-graph-materialize.mjs +105 -0
  110. package/scripts/kb-graph-materialize.sh +4 -0
  111. package/scripts/kb-graph-promote.mjs +153 -0
  112. package/scripts/kb-graph-promote.sh +4 -0
  113. package/scripts/kb-query.mjs +554 -0
  114. package/scripts/kb-query.sh +5 -0
  115. package/skills/agent-worker/SKILL.md +3 -1
  116. package/skills/agent-worker/references/agent-worker-operator.md +18 -1
  117. package/skills/frontend-design-review/SKILL.md +26 -24
  118. package/skills/frontend-implementation/SKILL.md +29 -26
  119. package/skills/frontend-implementation/references/node-contracts.md +50 -19
  120. package/skills/frontend-review/SKILL.md +1 -1
  121. package/skills/loop-agent/references/command-reference.md +2 -0
  122. package/skills/loop-agent/references/hybrid-dag.md +22 -3
  123. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
@@ -0,0 +1,314 @@
1
+ /** Observe UI view module (R5). */
2
+ import {
3
+ UI_TEXT,
4
+ LIVENESS_LABELS,
5
+ ARTIFACT_LABELS,
6
+ ARTIFACT_KEYS,
7
+ STEP_LABELS,
8
+ STEP_ORDER,
9
+ DAG_INSPECTOR_DEFAULT_WIDTH,
10
+ DAG_INSPECTOR_MIN_WIDTH,
11
+ DAG_INSPECTOR_MAX_WIDTH,
12
+ DAG_INSPECTOR_RIGHT_OFFSET,
13
+ DAG_INSPECTOR_VIEWPORT_MARGIN,
14
+ DAG_INSPECTOR_KEYBOARD_STEP,
15
+ TERMINAL_RUN_STATUSES,
16
+ DAG_EFFECTIVE_STATUS_LABELS,
17
+ STATUS_LABELS,
18
+ } from "../constants.js";
19
+ import { clearNode, el, metaGrid, buildTable } from "../dom.js";
20
+ import {
21
+ badge,
22
+ badgeClass,
23
+ statusLabel,
24
+ formatTs,
25
+ formatMs,
26
+ formatDuration,
27
+ formatAgo,
28
+ dagStatusBadge,
29
+ livenessBadge,
30
+ displayDurationMs,
31
+ derivedDurationMs,
32
+ summaryText,
33
+ truncateText,
34
+ repoBaseName,
35
+ parseMarkdownBlocks,
36
+ markdownLinkHref,
37
+ } from "../format.js";
38
+ import { parseRoute, navigate, buildHashPath } from "../router.js";
39
+ import {
40
+ fetchJson,
41
+ fetchJsonResult,
42
+ artifactUrl,
43
+ parseArtifactPreviewResponse,
44
+ } from "../api.js";
45
+ import {
46
+ objectRouteLink,
47
+ findFeatureInSnapshot,
48
+ featureLookupState,
49
+ buildObjectRelations,
50
+ renderObjectRelationBar,
51
+ } from "../relations.js";
52
+ import { copyText, renderRecommendedCommand } from "../copy.js";
53
+ import {
54
+ isPageVisible,
55
+ detailPollDelay,
56
+ dashboardPollDelay,
57
+ captureDagGraphViewportState,
58
+ updateDagGraphViewportState,
59
+ updateDagTimelineViewportState,
60
+ renderViewState,
61
+ mountViewState,
62
+ } from "../state.js";
63
+ import { uiState } from "../state.js";
64
+ import { sortPoolTasks } from "../format-pool.js";
65
+ import {
66
+ dagMetaVisibility,
67
+ isDagRunActive,
68
+ dashboardVisibleFeatures,
69
+ findDagForRun,
70
+ } from "../dag-model.js";
71
+ import { layoutDag } from "../dag-layout.js";
72
+ import { showView, setBreadcrumb, updateHeaderRefresh, mountRelationBar } from "../shell-chrome.js";
73
+ import { artifactLink } from "../kpi.js";
74
+ import { findBatchRunIdForTask, taskRunHistoryRow } from "../run-processing.js";
75
+
76
+ export async function renderTaskDetail(featureId, taskId) {
77
+ if (taskId === undefined) {
78
+ taskId = featureId;
79
+ featureId = undefined;
80
+ }
81
+ const taskApi = featureId
82
+ ? `/api/features/${encodeURIComponent(featureId)}/tasks/${encodeURIComponent(taskId)}`
83
+ : `/api/tasks/${encodeURIComponent(taskId)}`;
84
+ const displayLabel = featureId ? `${featureId}::${taskId}` : taskId;
85
+ showView("task");
86
+ setBreadcrumb([
87
+ { label: UI_TEXT.dashboard, href: "#/" },
88
+ { label: "资源池", href: "#/pool" },
89
+ { label: displayLabel },
90
+ ]);
91
+
92
+ const titleEl = document.getElementById("task-title");
93
+ const metaEl = document.getElementById("task-meta");
94
+ const linksEl = document.getElementById("task-links");
95
+ const evidenceEl = document.getElementById("task-evidence");
96
+ const runsEl = document.getElementById("task-runs");
97
+ if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
98
+ if (titleEl) titleEl.textContent = displayLabel;
99
+ mountViewState(metaEl, "loading", "加载 Task…");
100
+
101
+ const [taskResult, historyResult, snapshot] = await Promise.all([
102
+ fetchJsonResult(taskApi),
103
+ fetchJsonResult(`${taskApi}/runs?limit=20`),
104
+ fetchJson("/api/snapshot"),
105
+ ]);
106
+ const task = taskResult.ok ? taskResult.body : null;
107
+ const history = historyResult.ok ? historyResult.body : null;
108
+ if (snapshot) {
109
+ uiState.lastSnapshot = snapshot;
110
+ updateHeaderRefresh(snapshot.generatedAt);
111
+ }
112
+
113
+ clearNode(metaEl);
114
+ clearNode(linksEl);
115
+ clearNode(evidenceEl);
116
+ clearNode(runsEl);
117
+
118
+ if (!task) {
119
+ if (taskResult.status === 409) {
120
+ const candidates = Array.isArray(taskResult.body?.candidates)
121
+ ? taskResult.body.candidates
122
+ : [];
123
+ mountViewState(
124
+ metaEl,
125
+ "error",
126
+ `Task 身份歧义:${taskId} 同时属于多个 Feature,请选择 Feature 作用域入口。`,
127
+ );
128
+ if (candidates.length > 0) {
129
+ const list = el("div", "artifact-links");
130
+ for (const c of candidates) {
131
+ if (!c?.featureId || !c?.taskId) continue;
132
+ const path = `/feature/${encodeURIComponent(c.featureId)}/task/${encodeURIComponent(c.taskId)}`;
133
+ const a = el("a", "object-link", `${c.featureId}::${c.taskId}`);
134
+ a.href = `#${path}`;
135
+ a.addEventListener("click", (e) => {
136
+ e.preventDefault();
137
+ navigate(path);
138
+ });
139
+ list.appendChild(a);
140
+ list.appendChild(document.createTextNode(" "));
141
+ }
142
+ metaEl.appendChild(list);
143
+ }
144
+ } else {
145
+ mountViewState(metaEl, "error", `未找到 Task:${taskId}`);
146
+ }
147
+ mountRelationBar("task-relations", {}, snapshot);
148
+ return false;
149
+ }
150
+
151
+ metaEl.appendChild(
152
+ metaGrid([
153
+ ["Task ID", task.taskId],
154
+ ["状态", badge(task.status)],
155
+ [
156
+ "Feature",
157
+ task.featureId
158
+ ? objectRouteLink("feature", task.featureId, snapshot)
159
+ : el("span", "muted", "—"),
160
+ ],
161
+ ["更新时间", task.updatedAt ?? task.finishedAt ?? task.startedAt ?? "—"],
162
+ ["最新 worker run", task.workerRunId ?? "—"],
163
+ ["失败分类", task.failureCategory ?? "—"],
164
+ ["建议跟进", task.recommendedFollowUp ?? "—"],
165
+ [
166
+ "活性",
167
+ task.liveness ? (LIVENESS_LABELS[task.liveness] ?? task.liveness) : "—",
168
+ ],
169
+ ]),
170
+ );
171
+
172
+ const batchRunId = findBatchRunIdForTask(snapshot, task);
173
+ const dag = findDagForRun(snapshot, task.workerRunId, task);
174
+ mountRelationBar(
175
+ "task-relations",
176
+ {
177
+ featureId: task.featureId,
178
+ taskId: task.taskId,
179
+ batchRunId: batchRunId ?? undefined,
180
+ workerRunId: task.workerRunId,
181
+ dagRunId: dag?.dagRunId ?? task.dagRunId,
182
+ },
183
+ snapshot,
184
+ );
185
+ linksEl.appendChild(el("h3", "section-heading", "真实关联"));
186
+ const linkList = el("div", "artifact-links");
187
+ linkList.appendChild(objectRouteLink("run", task.workerRunId, snapshot));
188
+ linkList.appendChild(document.createTextNode(" · "));
189
+ linkList.appendChild(
190
+ batchRunId
191
+ ? objectRouteLink("batch", batchRunId, snapshot)
192
+ : el("span", "muted", "batch 缺失"),
193
+ );
194
+ linkList.appendChild(document.createTextNode(" · "));
195
+ linkList.appendChild(
196
+ dag
197
+ ? objectRouteLink("dag", dag.dagRunId, snapshot)
198
+ : task.dagRunId
199
+ ? el("span", "muted", task.dagRunId)
200
+ : el("span", "muted", "DAG 缺失"),
201
+ );
202
+ if (task.featureId) {
203
+ linkList.appendChild(document.createTextNode(" · "));
204
+ linkList.appendChild(objectRouteLink("feature", task.featureId, snapshot));
205
+ }
206
+ linksEl.appendChild(linkList);
207
+
208
+ evidenceEl.appendChild(el("h3", "section-heading", "失败证据"));
209
+ const r = task.artifactRefs ?? {};
210
+ const evidenceLinks = el("div", "artifact-links");
211
+ evidenceLinks.appendChild(artifactLink("报告", r.reportMarkdown));
212
+ evidenceLinks.appendChild(document.createTextNode(" "));
213
+ evidenceLinks.appendChild(artifactLink("诊断", r.doctorMarkdown));
214
+ evidenceLinks.appendChild(document.createTextNode(" "));
215
+ evidenceLinks.appendChild(artifactLink("收尾方案", r.closeoutDraft));
216
+ evidenceLinks.appendChild(document.createTextNode(" "));
217
+ evidenceLinks.appendChild(artifactLink("run 记录", r.runRecordPath));
218
+ if (
219
+ !r.reportMarkdown &&
220
+ !r.doctorMarkdown &&
221
+ !r.closeoutDraft &&
222
+ !r.runRecordPath
223
+ ) {
224
+ evidenceEl.appendChild(el("p", "empty", "暂无 allowlist 内的证据路径。"));
225
+ } else {
226
+ evidenceEl.appendChild(evidenceLinks);
227
+ }
228
+
229
+ runsEl.appendChild(el("h3", "section-heading", "运行历史"));
230
+ const runs = history?.runs ?? [];
231
+ if (runs.length === 0) {
232
+ mountViewState(runsEl, "empty", "暂无 ledger 运行记录。");
233
+ } else {
234
+ const rows = runs.map(taskRunHistoryRow);
235
+ runsEl.appendChild(
236
+ buildTable(
237
+ ["worker run", "状态", "时间", "batch", "retryOf", "失败分类"],
238
+ rows,
239
+ ),
240
+ );
241
+ if (history?.nextBefore) {
242
+ const more = el("button", "link-button", "加载更早的运行");
243
+ more.type = "button";
244
+ more.addEventListener("click", () => {
245
+ void loadOlderTaskRuns(featureId, taskId, history.nextBefore, runsEl);
246
+ });
247
+ runsEl.appendChild(more);
248
+ }
249
+ }
250
+
251
+ const active = ["running", "pending", "blocked", "stale"].includes(
252
+ (task.status ?? "").toLowerCase(),
253
+ );
254
+ return active;
255
+ }
256
+
257
+ export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
258
+ if (runsEl === undefined) {
259
+ runsEl = before;
260
+ before = taskId;
261
+ taskId = featureId;
262
+ featureId = undefined;
263
+ }
264
+ const taskApi = featureId
265
+ ? `/api/features/${encodeURIComponent(featureId)}/tasks/${encodeURIComponent(taskId)}`
266
+ : `/api/tasks/${encodeURIComponent(taskId)}`;
267
+ const page = await fetchJson(
268
+ `${taskApi}/runs?limit=20&before=${encodeURIComponent(before)}`,
269
+ );
270
+ if (!page?.runs?.length) return;
271
+ const table = runsEl.querySelector("table");
272
+ if (!table) return;
273
+ const tbody = table.querySelector("tbody") || table;
274
+ for (const run of page.runs) {
275
+ const tr = document.createElement("tr");
276
+ for (const cell of taskRunHistoryRow(run).cells) {
277
+ const td = document.createElement("td");
278
+ if (cell instanceof Node) td.appendChild(cell);
279
+ else td.textContent = cell;
280
+ tr.appendChild(td);
281
+ }
282
+ tbody.appendChild(tr);
283
+ }
284
+ const btn = runsEl.querySelector("button.link-button");
285
+ if (btn) {
286
+ if (page.nextBefore) {
287
+ btn.onclick = () => {
288
+ void loadOlderTaskRuns(featureId, taskId, page.nextBefore, runsEl);
289
+ };
290
+ } else {
291
+ btn.remove();
292
+ }
293
+ }
294
+ }
295
+
296
+ export function startTaskPolling(featureId, taskId) {
297
+ if (taskId === undefined) {
298
+ taskId = featureId;
299
+ featureId = undefined;
300
+ }
301
+ if (uiState.taskPollTimer) clearTimeout(uiState.taskPollTimer);
302
+ const generation = ++uiState.pollingGeneration;
303
+ const poll = async () => {
304
+ const active = await renderTaskDetail(featureId, taskId);
305
+ if (generation !== uiState.pollingGeneration || !active) return;
306
+ const delay = detailPollDelay("running", isPageVisible());
307
+ if (delay == null) return;
308
+ uiState.taskPollTimer = setTimeout(() => {
309
+ uiState.taskPollTimer = null;
310
+ void poll();
311
+ }, delay);
312
+ };
313
+ void poll();
314
+ }
@@ -0,0 +1,163 @@
1
+ /** Observe UI view module (R5). */
2
+ import {
3
+ UI_TEXT,
4
+ LIVENESS_LABELS,
5
+ ARTIFACT_LABELS,
6
+ ARTIFACT_KEYS,
7
+ STEP_LABELS,
8
+ STEP_ORDER,
9
+ DAG_INSPECTOR_DEFAULT_WIDTH,
10
+ DAG_INSPECTOR_MIN_WIDTH,
11
+ DAG_INSPECTOR_MAX_WIDTH,
12
+ DAG_INSPECTOR_RIGHT_OFFSET,
13
+ DAG_INSPECTOR_VIEWPORT_MARGIN,
14
+ DAG_INSPECTOR_KEYBOARD_STEP,
15
+ TERMINAL_RUN_STATUSES,
16
+ DAG_EFFECTIVE_STATUS_LABELS,
17
+ STATUS_LABELS,
18
+ } from "../constants.js";
19
+ import { clearNode, el, metaGrid, buildTable } from "../dom.js";
20
+ import {
21
+ badge,
22
+ badgeClass,
23
+ statusLabel,
24
+ formatTs,
25
+ formatMs,
26
+ formatDuration,
27
+ formatAgo,
28
+ dagStatusBadge,
29
+ livenessBadge,
30
+ displayDurationMs,
31
+ derivedDurationMs,
32
+ summaryText,
33
+ truncateText,
34
+ repoBaseName,
35
+ parseMarkdownBlocks,
36
+ markdownLinkHref,
37
+ } from "../format.js";
38
+ import { parseRoute, navigate, buildHashPath } from "../router.js";
39
+ import {
40
+ fetchJson,
41
+ artifactUrl,
42
+ parseArtifactPreviewResponse,
43
+ } from "../api.js";
44
+ import {
45
+ objectRouteLink,
46
+ findFeatureInSnapshot,
47
+ featureLookupState,
48
+ buildObjectRelations,
49
+ renderObjectRelationBar,
50
+ } from "../relations.js";
51
+ import { copyText, renderRecommendedCommand } from "../copy.js";
52
+ import {
53
+ isPageVisible,
54
+ detailPollDelay,
55
+ dashboardPollDelay,
56
+ captureDagGraphViewportState,
57
+ updateDagGraphViewportState,
58
+ updateDagTimelineViewportState,
59
+ renderViewState,
60
+ mountViewState,
61
+ } from "../state.js";
62
+ import { uiState } from "../state.js";
63
+ import { sortPoolTasks } from "../format-pool.js";
64
+ import {
65
+ dagMetaVisibility,
66
+ isDagRunActive,
67
+ dashboardVisibleFeatures,
68
+ findDagForRun,
69
+ } from "../dag-model.js";
70
+ import { layoutDag } from "../dag-layout.js";
71
+
72
+ export async function mountBoundedEventTimeline(container, apiBase, title) {
73
+ clearNode(container);
74
+ container.appendChild(el("h3", "section-heading", title));
75
+ const note = el(
76
+ "p",
77
+ "process-timeline-note",
78
+ "有界历史(默认 50 / 最大 200);分页使用 cursor,与 SSE 行号 offset 分离。",
79
+ );
80
+ container.appendChild(note);
81
+ const scroll = el("div", "process-timeline-scroll event-history-scroll");
82
+ const listHost = el("div", "event-history-list");
83
+ scroll.appendChild(listHost);
84
+ container.appendChild(scroll);
85
+ const footer = el("div", "event-history-footer");
86
+ container.appendChild(footer);
87
+
88
+ let nextCursor = null;
89
+ let loading = false;
90
+ const accumulated = [];
91
+
92
+ const renderList = () => {
93
+ clearNode(listHost);
94
+ if (accumulated.length === 0) {
95
+ mountViewState(listHost, "empty", "暂无事件");
96
+ return;
97
+ }
98
+ const ul = el("ul", "process-timeline");
99
+ for (let index = 0; index < accumulated.length; index += 1) {
100
+ const event = accumulated[index];
101
+ const li = el(
102
+ "li",
103
+ `process-timeline-item process-timeline-event has-time${index === 0 ? " is-first" : ""}${index === accumulated.length - 1 ? " is-last" : ""}`,
104
+ );
105
+ const rail = el("span", "process-timeline-rail");
106
+ rail.appendChild(el("span", "process-timeline-dot"));
107
+ li.appendChild(rail);
108
+ li.appendChild(
109
+ el("span", "process-timeline-time", formatTs(event.at) || "—"),
110
+ );
111
+ const label = `${event.type ?? "event"}${event.taskId ? ` · ${event.taskId}` : ""}${event.workerRunId ? ` · ${event.workerRunId}` : ""}`;
112
+ li.appendChild(el("span", "process-timeline-label", label));
113
+ ul.appendChild(li);
114
+ }
115
+ listHost.appendChild(ul);
116
+ };
117
+
118
+ const loadPage = async (cursor) => {
119
+ if (loading) return;
120
+ loading = true;
121
+ if (accumulated.length === 0) {
122
+ mountViewState(listHost, "loading", "加载事件…");
123
+ }
124
+ const qs = new URLSearchParams();
125
+ qs.set("limit", "50");
126
+ if (cursor) qs.set("cursor", cursor);
127
+ const page = await fetchJson(`${apiBase}?${qs.toString()}`);
128
+ loading = false;
129
+ if (!page) {
130
+ mountViewState(listHost, "error", "无法加载事件历史。");
131
+ return;
132
+ }
133
+ const events = page.events ?? [];
134
+ for (const event of events) accumulated.push(event);
135
+ nextCursor = page.nextCursor ?? null;
136
+ renderList();
137
+ clearNode(footer);
138
+ if (page.corruptLineCount > 0) {
139
+ footer.appendChild(
140
+ renderViewState(
141
+ "degraded",
142
+ `跳过损坏行 ${page.corruptLineCount} 条(failure-safe)。`,
143
+ ),
144
+ );
145
+ }
146
+ if (nextCursor) {
147
+ const btn = el(
148
+ "button",
149
+ "btn-secondary event-history-more",
150
+ "加载更早事件",
151
+ );
152
+ btn.type = "button";
153
+ btn.addEventListener("click", () => {
154
+ void loadPage(nextCursor);
155
+ });
156
+ footer.appendChild(btn);
157
+ } else if (accumulated.length > 0) {
158
+ footer.appendChild(el("p", "muted", "已到历史边界"));
159
+ }
160
+ };
161
+
162
+ await loadPage(null);
163
+ }
@@ -0,0 +1,165 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { findRunByWorkerRunId, getEventsJsonlPath, listLegacyStateFiles, listTaskPoolStates, pathExists, readJsonlFile, sha256File, } from "./run-store.js";
4
+ import { TASK_POOL_STATE_ERROR_CODES, } from "./types.js";
5
+ /**
6
+ * Read-only inventory of legacy flat states and feature-scoped v2 states.
7
+ */
8
+ export async function diagnoseTaskPoolStates(repoRoot, options = {}) {
9
+ const findings = [];
10
+ const legacyFiles = await listLegacyStateFiles(repoRoot);
11
+ const legacy = [];
12
+ for (const file of legacyFiles) {
13
+ const sha256 = await sha256File(file.path);
14
+ let state;
15
+ try {
16
+ state = JSON.parse(await readFile(file.path, "utf-8"));
17
+ }
18
+ catch {
19
+ legacy.push({
20
+ taskId: file.taskId,
21
+ legacyPath: file.path,
22
+ sha256,
23
+ resolution: "unresolved",
24
+ code: TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED,
25
+ detail: "legacy state file is corrupt",
26
+ });
27
+ findings.push({
28
+ code: TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED,
29
+ message: `corrupt legacy state: ${file.taskId}`,
30
+ });
31
+ continue;
32
+ }
33
+ const resolved = await resolveLegacyIdentity(repoRoot, state, options.mapping);
34
+ legacy.push({
35
+ taskId: file.taskId,
36
+ legacyPath: file.path,
37
+ sha256,
38
+ status: state.status,
39
+ ...resolved,
40
+ });
41
+ if (resolved.resolution === "unresolved" || resolved.resolution === "ambiguous") {
42
+ findings.push({
43
+ code: resolved.code ?? TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED,
44
+ message: resolved.detail ??
45
+ `legacy state requires migration: ${file.taskId}`,
46
+ });
47
+ }
48
+ else {
49
+ findings.push({
50
+ code: TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED,
51
+ message: `legacy state ${file.taskId} maps to ${resolved.featureId}/${file.taskId} via ${resolved.resolution}`,
52
+ });
53
+ }
54
+ }
55
+ const v2States = await listTaskPoolStates(repoRoot);
56
+ const v2 = v2States.map((state) => ({
57
+ featureId: state.featureId,
58
+ taskId: state.taskId,
59
+ status: state.status,
60
+ }));
61
+ return {
62
+ schemaVersion: 1,
63
+ repoRoot,
64
+ legacyCount: legacy.length,
65
+ v2Count: v2.length,
66
+ legacy,
67
+ v2,
68
+ findings,
69
+ };
70
+ }
71
+ export async function resolveLegacyIdentity(repoRoot, state, mapping) {
72
+ const candidates = [];
73
+ if (state.workerRunId) {
74
+ const run = await findRunByWorkerRunId(repoRoot, state.workerRunId);
75
+ if (run && run.taskId === state.taskId) {
76
+ return {
77
+ resolution: "workerRunId",
78
+ featureId: run.featureId,
79
+ };
80
+ }
81
+ }
82
+ if (state.retryOfWorkerRunId) {
83
+ const run = await findRunByWorkerRunId(repoRoot, state.retryOfWorkerRunId);
84
+ if (run && run.taskId === state.taskId) {
85
+ return {
86
+ resolution: "retryOfWorkerRunId",
87
+ featureId: run.featureId,
88
+ };
89
+ }
90
+ }
91
+ const events = await readJsonlFile(getEventsJsonlPath(repoRoot));
92
+ const followUpHits = events.filter((event) => event.type === "follow-up-approved" &&
93
+ event.proposedTaskId === state.taskId);
94
+ const uniqueFeatureIds = [...new Set(followUpHits.map((event) => event.featureId))];
95
+ if (uniqueFeatureIds.length === 1) {
96
+ return {
97
+ resolution: "follow-up-approved",
98
+ featureId: uniqueFeatureIds[0],
99
+ };
100
+ }
101
+ if (uniqueFeatureIds.length > 1) {
102
+ for (const featureId of uniqueFeatureIds) {
103
+ candidates.push({ featureId, taskId: state.taskId });
104
+ }
105
+ return {
106
+ resolution: "ambiguous",
107
+ candidates,
108
+ code: TASK_POOL_STATE_ERROR_CODES.IDENTITY_AMBIGUOUS,
109
+ detail: `multiple follow-up-approved features for ${state.taskId}`,
110
+ };
111
+ }
112
+ const mapped = mapping?.[state.taskId];
113
+ if (mapped) {
114
+ return {
115
+ resolution: "operator-mapping",
116
+ featureId: mapped,
117
+ };
118
+ }
119
+ return {
120
+ resolution: "unresolved",
121
+ code: TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED,
122
+ detail: `no unique identity evidence for legacy state ${state.taskId}`,
123
+ };
124
+ }
125
+ export async function loadOperatorMapping(mappingPath) {
126
+ if (!mappingPath)
127
+ return undefined;
128
+ if (!(await pathExists(mappingPath))) {
129
+ throw new Error(`mapping file not found: ${mappingPath}`);
130
+ }
131
+ const raw = JSON.parse(await readFile(mappingPath, "utf-8"));
132
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
133
+ throw new Error("mapping file must be a JSON object of taskId -> featureId");
134
+ }
135
+ const mapping = {};
136
+ for (const [taskId, featureId] of Object.entries(raw)) {
137
+ if (typeof featureId !== "string" || featureId.length === 0) {
138
+ throw new Error(`mapping for ${taskId} must be a non-empty featureId string`);
139
+ }
140
+ mapping[taskId] = featureId;
141
+ }
142
+ return mapping;
143
+ }
144
+ export function formatDoctorHuman(report) {
145
+ const lines = [
146
+ `Task Pool doctor`,
147
+ `repo: ${report.repoRoot}`,
148
+ `legacy states: ${report.legacyCount}`,
149
+ `v2 states: ${report.v2Count}`,
150
+ ];
151
+ for (const item of report.legacy) {
152
+ const target = item.featureId != null ? `${item.featureId}/${item.taskId}` : "(unresolved)";
153
+ lines.push(`- legacy ${item.taskId}: ${item.resolution} -> ${target}${item.detail ? ` (${item.detail})` : ""}`);
154
+ }
155
+ for (const finding of report.findings) {
156
+ lines.push(`[${finding.code}] ${finding.message}`);
157
+ }
158
+ if (report.findings.length === 0) {
159
+ lines.push("No migration-required findings.");
160
+ }
161
+ return `${lines.join("\n")}\n`;
162
+ }
163
+ export function relativeLegacyName(legacyPath) {
164
+ return path.basename(legacyPath);
165
+ }