@tea-agent/loop-agent 0.26.0 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +1056 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/application/dag/generate-task-dag.js +33 -0
  5. package/dist/cli/command-definitions.js +25 -10
  6. package/dist/cli/help.js +4 -3
  7. package/dist/cli/program.js +43 -17
  8. package/dist/commands/cursor-prompt.js +6 -6
  9. package/dist/commands/import-prd.js +7 -2
  10. package/dist/commands/init.js +7 -5
  11. package/dist/commands/loop-benchmark.js +11 -11
  12. package/dist/commands/pi-reuse-benchmark.js +16 -16
  13. package/dist/commands/task-source-prepare.js +474 -0
  14. package/dist/executors/dag-pi-executor.js +40 -5
  15. package/dist/executors/shell-executor.js +111 -0
  16. package/dist/executors/shell-presets.js +12 -4
  17. package/dist/executors/shell-write-guard.js +161 -25
  18. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  19. package/dist/task/config-types.js +6 -0
  20. package/dist/task/contract/constants.js +1 -0
  21. package/dist/task/contract/project.js +8 -0
  22. package/dist/task/contract/schema.js +1 -0
  23. package/dist/task/frontend-preflight.js +131 -0
  24. package/dist/task/runtime.js +2 -4
  25. package/dist/task/source-prepare/build-draft.js +224 -0
  26. package/dist/task/source-prepare/completeness.js +195 -0
  27. package/dist/task/source-prepare/index.js +7 -0
  28. package/dist/task/source-prepare/parse-intent.js +373 -0
  29. package/dist/task/source-prepare/path-policy.js +197 -0
  30. package/dist/task/source-prepare/prepare.js +506 -0
  31. package/dist/task/source-prepare/reference-integrity.js +274 -0
  32. package/dist/task/source-prepare/types.js +7 -0
  33. package/dist/worker/observability/read-model.js +134 -0
  34. package/dist/worker/observe/static/copy.js +67 -67
  35. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  36. package/dist/worker/observe/static/dag-layout.js +83 -83
  37. package/dist/worker/observe/static/dom.js +220 -220
  38. package/dist/worker/observe/static/relations.js +133 -133
  39. package/dist/worker/observe/static/router.js +93 -93
  40. package/dist/worker/observe/static/run-processing.js +148 -148
  41. package/dist/worker/observe/static/state.js +61 -0
  42. package/dist/worker/observe/static/styles.css +8 -0
  43. package/dist/worker/observe/static/views/batch.js +227 -227
  44. package/dist/worker/observe/static/views/dag-graph.js +248 -172
  45. package/dist/worker/observe/static/views/dag-inspector.js +374 -157
  46. package/dist/worker/observe/static/views/dag.js +4 -11
  47. package/dist/worker/observe/static/views/failures.js +143 -143
  48. package/dist/worker/observe/static/views/feature.js +492 -492
  49. package/dist/worker/observe/static/views/run.js +453 -453
  50. package/dist/worker/observe/static/views/shell.js +7 -7
  51. package/dist/worker/observe/static/views/timeline.js +163 -163
  52. package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
  53. package/dist/workflows/dag/canvas-observer.js +275 -275
  54. package/dist/workflows/dag/convergence/controller.js +110 -21
  55. package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
  56. package/dist/workflows/dag/frontend-review-context.js +7 -1
  57. package/dist/workflows/dag/frontend-verification-trace.js +14 -3
  58. package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
  59. package/dist/workflows/dag/init-hybrid.js +96 -34
  60. package/dist/workflows/dag/output-protocol.js +180 -7
  61. package/dist/workflows/dag/runner.js +141 -52
  62. package/dist/workflows/dag/types.js +4 -0
  63. package/dist/workflows/dag/validate.js +3 -2
  64. package/docs/skills/README.md +7 -7
  65. package/docs/templates/adr.md +60 -60
  66. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  67. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  68. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  69. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  70. package/docs/templates/agent-dag-report.schema.json +473 -473
  71. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  72. package/docs/templates/backend-test-dag.json +100 -8
  73. package/docs/templates/backend-test-result.schema.json +99 -99
  74. package/docs/templates/feature-spec.md +53 -53
  75. package/docs/templates/frontend-design-contract.md +42 -42
  76. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  77. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  78. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  79. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  80. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  81. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  82. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  83. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  84. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  85. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  86. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  87. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  88. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  89. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  90. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  91. package/docs/templates/frontend-eval/metrics.md +138 -138
  92. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  93. package/docs/templates/frontend-task-constraints.md +35 -35
  94. package/docs/templates/frontend-task-requirement.md +70 -70
  95. package/docs/templates/init-evolution-review.md +35 -35
  96. package/docs/templates/init-managed-agents.md +10 -5
  97. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  98. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  99. package/docs/templates/knowledge-sync-dag.json +178 -178
  100. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  101. package/docs/templates/product-line/closeout.yaml +9 -9
  102. package/docs/templates/product-line/design.md +13 -13
  103. package/docs/templates/product-line/links.md +10 -10
  104. package/docs/templates/product-line/requirement.md +17 -17
  105. package/docs/templates/product-line/test-plan.md +7 -7
  106. package/docs/templates/production-readiness-checklist.md +57 -57
  107. package/docs/templates/project-start-checklist.md +9 -9
  108. package/docs/templates/qa-report.md +48 -48
  109. package/docs/templates/sprint-contract.md +29 -29
  110. package/docs/templates/worker-dogfood-evidence.md +80 -80
  111. package/docs/templates/worker-dogfood-setup.md +68 -68
  112. package/package.json +1 -1
  113. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  114. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  115. package/scripts/kb-graph-materialize.mjs +105 -105
  116. package/scripts/kb-graph-promote.mjs +164 -164
  117. package/scripts/kb-query.mjs +554 -554
  118. package/skills/ai-engineering-context/SKILL.md +48 -48
  119. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  120. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  121. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  122. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  123. package/skills/analyze-product-dependencies/references/example.md +76 -76
  124. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  125. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  126. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  127. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  128. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  129. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  130. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  131. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  132. package/skills/analyze-product-requirements/SKILL.md +90 -90
  133. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  134. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  135. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  136. package/skills/analyze-product-requirements/references/example.md +86 -86
  137. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  138. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  139. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  140. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  141. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  142. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  143. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  144. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  145. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  146. package/skills/browser-tools/browser-content.js +103 -103
  147. package/skills/browser-tools/browser-cookies.js +35 -35
  148. package/skills/browser-tools/browser-eval.js +53 -53
  149. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  150. package/skills/browser-tools/browser-nav.js +44 -44
  151. package/skills/browser-tools/browser-pick.js +162 -162
  152. package/skills/browser-tools/browser-screenshot.js +34 -34
  153. package/skills/browser-tools/browser-start.js +86 -86
  154. package/skills/browser-tools/package-lock.json +2556 -2556
  155. package/skills/browser-tools/package.json +19 -19
  156. package/skills/code-review-core/SKILL.md +20 -20
  157. package/skills/codebase-scout/SKILL.md +19 -19
  158. package/skills/grill-me/SKILL.md +10 -10
  159. package/skills/loop-agent/SKILL.md +5 -2
  160. package/skills/loop-agent/references/README.md +67 -67
  161. package/skills/loop-agent/references/command-reference.md +17 -15
  162. package/skills/loop-agent/references/docs-converge.md +126 -126
  163. package/skills/loop-agent/references/harness-policy.md +3 -4
  164. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  165. package/skills/loop-agent/references/learned/README.md +21 -21
  166. package/skills/loop-agent/references/long-running-loop.md +57 -57
  167. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  168. package/skills/loop-agent/references/pi-prompt.md +23 -23
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  170. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  171. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  172. package/skills/loop-agent/references/task-workflow.md +7 -5
  173. package/skills/playwright-cli/SKILL.md +420 -420
  174. package/skills/playwright-cli/references/element-attributes.md +23 -23
  175. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  176. package/skills/playwright-cli/references/request-mocking.md +87 -87
  177. package/skills/playwright-cli/references/running-code.md +241 -241
  178. package/skills/playwright-cli/references/session-management.md +225 -225
  179. package/skills/playwright-cli/references/storage-state.md +275 -275
  180. package/skills/playwright-cli/references/test-generation.md +433 -433
  181. package/skills/playwright-cli/references/tracing.md +139 -139
  182. package/skills/playwright-cli/references/video-recording.md +143 -143
  183. package/skills/requesting-code-review/SKILL.md +101 -101
  184. package/skills/requesting-code-review/code-reviewer.md +168 -168
  185. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  186. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  187. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  188. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  189. package/skills/systematic-debugging/find-polluter.sh +63 -63
  190. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  191. package/skills/systematic-debugging/test-academic.md +14 -14
  192. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  193. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  194. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  195. package/skills/using-git-worktrees/SKILL.md +215 -215
  196. package/skills/verification-before-completion/SKILL.md +154 -154
  197. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,7 +1,7 @@
1
- /**
2
- * Shell service locator removed in R5 boundary final (BND-02).
3
- * Views import focused modules directly (shell-chrome, kpi, dag-helpers, run-processing).
4
- * This file is retained only so historical bundle paths do not 404; do not reintroduce getShell.
5
- */
6
-
7
- export {};
1
+ /**
2
+ * Shell service locator removed in R5 boundary final (BND-02).
3
+ * Views import focused modules directly (shell-chrome, kpi, dag-helpers, run-processing).
4
+ * This file is retained only so historical bundle paths do not 404; do not reintroduce getShell.
5
+ */
6
+
7
+ export {};
@@ -1,163 +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
- }
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,277 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ const SHA256 = /^[a-f0-9]{64}$/;
6
+ const MAX_DIAGNOSTIC_CHARS = 12_000;
7
+ export const backendPytestCollectionFindingSchema = z.object({
8
+ kind: z.string().min(1),
9
+ classification: z.literal("test-asset-defect"),
10
+ repairability: z.enum(["repairable", "blocked"]),
11
+ detail: z.string().min(1),
12
+ }).strict();
13
+ export const backendPytestCollectionFactsSchema = z.object({
14
+ schemaId: z.literal("backend-test-pytest-collection-v1"),
15
+ phase: z.enum(["initial", "final", "effective"]),
16
+ status: z.enum(["PASS", "REPAIRABLE", "BLOCKED"]),
17
+ repairEligible: z.boolean(),
18
+ repairAttempt: z.number().int().min(0).max(1),
19
+ mappedScripts: z.array(z.string()).min(1),
20
+ assetFiles: z.array(z.string()).min(1),
21
+ inputHashes: z.record(z.string(), z.string().regex(SHA256)),
22
+ pytestExitCode: z.number().int(),
23
+ collectedItemCount: z.number().int().min(0),
24
+ collectedItemIds: z.array(z.string()),
25
+ findings: z.array(backendPytestCollectionFindingSchema),
26
+ stdoutExcerpt: z.string(),
27
+ stderrExcerpt: z.string(),
28
+ collectionSource: z.enum(["initial", "final"]).optional(),
29
+ }).strict();
30
+ function repoRef(workspaceRoot, absolutePath) {
31
+ return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, "/");
32
+ }
33
+ function sha256(bytes) {
34
+ return createHash("sha256").update(bytes).digest("hex");
35
+ }
36
+ async function walkPythonFiles(root) {
37
+ const results = [];
38
+ const visit = async (current) => {
39
+ let entries;
40
+ try {
41
+ entries = await readdir(current, { withFileTypes: true });
42
+ }
43
+ catch {
44
+ return;
45
+ }
46
+ for (const entry of entries) {
47
+ if (entry.name === "__pycache__" || entry.name === ".pytest_cache")
48
+ continue;
49
+ const absolute = path.join(current, entry.name);
50
+ if (entry.isDirectory())
51
+ await visit(absolute);
52
+ else if (entry.isFile() && entry.name.endsWith(".py"))
53
+ results.push(absolute);
54
+ }
55
+ };
56
+ await visit(root);
57
+ return results;
58
+ }
59
+ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScripts) {
60
+ const normalizedScripts = [...new Set(mappedScripts.map((value) => value.replaceAll("\\", "/")))].sort();
61
+ if (normalizedScripts.length === 0)
62
+ throw new Error("backend pytest collection requires mapped scripts");
63
+ for (const script of normalizedScripts) {
64
+ if (!/^testcase(?:\/[A-Za-z0-9_.-]+)*\/test_[A-Za-z0-9_.-]+\.py$/.test(script) && !/^testcase\/test_[A-Za-z0-9_.-]+\.py$/.test(script)) {
65
+ throw new Error(`unsafe backend pytest mapped script: ${script}`);
66
+ }
67
+ const absolute = path.resolve(workspaceRoot, script);
68
+ const testcaseRoot = path.resolve(workspaceRoot, "testcase");
69
+ if (absolute !== testcaseRoot && !absolute.startsWith(`${testcaseRoot}${path.sep}`)) {
70
+ throw new Error(`backend pytest mapped script escapes testcase: ${script}`);
71
+ }
72
+ await readFile(absolute);
73
+ }
74
+ const pythonFiles = await walkPythonFiles(path.resolve(workspaceRoot, "testcase"));
75
+ const assetFiles = [...new Set(pythonFiles.map((file) => repoRef(workspaceRoot, file)))].sort();
76
+ const inputHashes = {};
77
+ for (const file of assetFiles)
78
+ inputHashes[file] = sha256(await readFile(path.resolve(workspaceRoot, file)));
79
+ return { mappedScripts: normalizedScripts, assetFiles, inputHashes };
80
+ }
81
+ function bounded(value) {
82
+ const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
83
+ return normalized.length <= MAX_DIAGNOSTIC_CHARS
84
+ ? normalized
85
+ : `${normalized.slice(0, MAX_DIAGNOSTIC_CHARS)}\n...[truncated]`;
86
+ }
87
+ function collectionItems(stdout) {
88
+ return [...new Set(stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => {
89
+ if (!line.includes("::"))
90
+ return false;
91
+ if (/^(ERROR|FAILED|E\s+)/.test(line))
92
+ return false;
93
+ return /^testcase\//.test(line.replaceAll("\\", "/"));
94
+ }).map((line) => line.replaceAll("\\", "/")))];
95
+ }
96
+ function classifyCollectionFailure(output) {
97
+ const normalized = output.replaceAll("\\", "/");
98
+ const blockedPatterns = [
99
+ [/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
100
+ [/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
101
+ [/(?:apps|src|server|api)\/[A-Za-z0-9_./-]+\.py/i, "production-module-import"],
102
+ [/permission denied|access is denied|timed? out|not recognized as an internal|no such file or directory.*python/i, "environment-or-spawn"],
103
+ ];
104
+ for (const [pattern, kind] of blockedPatterns) {
105
+ if (pattern.test(normalized))
106
+ return { status: "BLOCKED", kind, detail: "collection failure is outside generated testcase asset repair scope" };
107
+ }
108
+ if (/ImportError:\s*cannot import name/i.test(normalized) && /(?:from ['\"]testcase\.|\/testcase\/)/i.test(normalized)) {
109
+ return { status: "REPAIRABLE", kind: "missing-local-import-symbol", detail: "generated testcase asset imports a missing local symbol" };
110
+ }
111
+ if (/ModuleNotFoundError:\s*No module named ['\"]testcase(?:\.|['\"])/i.test(normalized)) {
112
+ return { status: "REPAIRABLE", kind: "missing-local-testcase-module", detail: "generated testcase asset imports a missing local testcase module" };
113
+ }
114
+ if (/partially initialized module ['\"]testcase\.|circular import/i.test(normalized)) {
115
+ return { status: "REPAIRABLE", kind: "local-circular-import", detail: "generated testcase assets contain a local circular import" };
116
+ }
117
+ if (/SyntaxError|IndentationError|TabError/i.test(normalized) && /testcase\/[A-Za-z0-9_./-]+\.py/i.test(normalized)) {
118
+ return { status: "REPAIRABLE", kind: "generated-python-syntax", detail: "generated testcase Python source is not syntactically collectable" };
119
+ }
120
+ return { status: "BLOCKED", kind: "unclassified-collection-error", detail: "collection failure cannot be safely attributed to generated testcase assets" };
121
+ }
122
+ export function assessBackendPytestCollection(input) {
123
+ const items = collectionItems(input.stdout);
124
+ if (input.exitCode === 0) {
125
+ return backendPytestCollectionFactsSchema.parse({
126
+ schemaId: "backend-test-pytest-collection-v1",
127
+ phase: input.phase,
128
+ status: "PASS",
129
+ repairEligible: false,
130
+ repairAttempt: input.phase === "final" ? 1 : 0,
131
+ mappedScripts: input.inventory.mappedScripts,
132
+ assetFiles: input.inventory.assetFiles,
133
+ inputHashes: input.inventory.inputHashes,
134
+ pytestExitCode: input.exitCode,
135
+ collectedItemCount: items.length,
136
+ collectedItemIds: items,
137
+ findings: [],
138
+ stdoutExcerpt: bounded(input.stdout),
139
+ stderrExcerpt: bounded(input.stderr),
140
+ });
141
+ }
142
+ const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`);
143
+ return backendPytestCollectionFactsSchema.parse({
144
+ schemaId: "backend-test-pytest-collection-v1",
145
+ phase: input.phase,
146
+ status: classification.status,
147
+ repairEligible: input.phase === "initial" && classification.status === "REPAIRABLE",
148
+ repairAttempt: input.phase === "final" ? 1 : 0,
149
+ mappedScripts: input.inventory.mappedScripts,
150
+ assetFiles: input.inventory.assetFiles,
151
+ inputHashes: input.inventory.inputHashes,
152
+ pytestExitCode: input.exitCode,
153
+ collectedItemCount: items.length,
154
+ collectedItemIds: items,
155
+ findings: [{
156
+ kind: classification.kind,
157
+ classification: "test-asset-defect",
158
+ repairability: classification.status === "REPAIRABLE" ? "repairable" : "blocked",
159
+ detail: classification.detail,
160
+ }],
161
+ stdoutExcerpt: bounded(input.stdout),
162
+ stderrExcerpt: bounded(input.stderr),
163
+ });
164
+ }
165
+ export function renderBackendPytestCollectionReport(facts) {
166
+ return [
167
+ `# Backend pytest Collection ${facts.phase}`,
168
+ "",
169
+ "## Status",
170
+ "",
171
+ facts.status,
172
+ "",
173
+ `- Repair eligible: ${facts.repairEligible}`,
174
+ `- Repair attempt: ${facts.repairAttempt}`,
175
+ `- Pytest exit code: ${facts.pytestExitCode}`,
176
+ `- Mapped scripts: ${facts.mappedScripts.length}`,
177
+ `- Bound Python assets: ${facts.assetFiles.length}`,
178
+ `- Collected items: ${facts.collectedItemCount}`,
179
+ "",
180
+ "## Findings",
181
+ "",
182
+ ...(facts.findings.length ? facts.findings.map((finding) => `- ${finding.kind}: ${finding.detail}`) : ["- None"]),
183
+ "",
184
+ "## Collected Items",
185
+ "",
186
+ ...(facts.collectedItemIds.length ? facts.collectedItemIds.map((item) => `- ${item}`) : ["- None"]),
187
+ "",
188
+ "## Bounded stdout",
189
+ "",
190
+ "```text",
191
+ facts.stdoutExcerpt,
192
+ "```",
193
+ "",
194
+ "## Bounded stderr",
195
+ "",
196
+ "```text",
197
+ facts.stderrExcerpt,
198
+ "```",
199
+ "",
200
+ ].join("\n");
201
+ }
202
+ export async function writeBackendPytestCollectionArtifacts(input) {
203
+ const contractsDir = path.join(input.runDir, "contracts");
204
+ const reportsDir = path.join(input.runDir, "reports");
205
+ await mkdir(contractsDir, { recursive: true });
206
+ await mkdir(reportsDir, { recursive: true });
207
+ const factsPath = path.join(contractsDir, `backend-test-pytest-collection-${input.stem}.json`);
208
+ const reportPath = path.join(reportsDir, `backend-test-pytest-collection-${input.stem}.md`);
209
+ await writeFile(factsPath, `${JSON.stringify(input.facts, null, 2)}\n`, "utf8");
210
+ await writeFile(reportPath, renderBackendPytestCollectionReport(input.facts), "utf8");
211
+ return { factsPath, reportPath };
212
+ }
213
+ export async function readBackendPytestCollectionFacts(filePath) {
214
+ return backendPytestCollectionFactsSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
215
+ }
216
+ async function assertBackendPytestRepairSafety(workspaceRoot, initial, final) {
217
+ if (JSON.stringify(initial.mappedScripts) !== JSON.stringify(final.mappedScripts)) {
218
+ throw new Error("backend pytest repair changed mapped script scope");
219
+ }
220
+ const forbidden = [
221
+ [/pytest\.mark\.(?:skip|skipif|xfail)\b|pytest\.(?:skip|xfail)\s*\(/, "skip/xfail"],
222
+ [/sys\.path\s*\.|PYTHONPATH/, "sys.path/PYTHONPATH mutation"],
223
+ [/except\s+ImportError\b/, "ImportError fallback"],
224
+ [/except\s+(?:Exception|BaseException)\b[\s\S]{0,160}?\bpass\b/, "broad exception swallowing"],
225
+ ];
226
+ for (const file of final.assetFiles) {
227
+ const source = await readFile(path.resolve(workspaceRoot, file), "utf8");
228
+ for (const [pattern, label] of forbidden) {
229
+ if (pattern.test(source))
230
+ throw new Error(`backend pytest repair safety blocked ${label}: ${file}`);
231
+ }
232
+ }
233
+ }
234
+ function assertSameInventory(expected, actual) {
235
+ const expectedFiles = Object.keys(expected.inputHashes).sort();
236
+ const actualFiles = Object.keys(actual.inputHashes).sort();
237
+ if (JSON.stringify(expectedFiles) !== JSON.stringify(actualFiles))
238
+ throw new Error("backend pytest collection asset inventory drift");
239
+ for (const file of expectedFiles) {
240
+ if (expected.inputHashes[file] !== actual.inputHashes[file])
241
+ throw new Error(`backend pytest collection hash drift: ${file}`);
242
+ }
243
+ }
244
+ export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
245
+ if (effective.phase !== "effective" || effective.status !== "PASS") {
246
+ throw new Error("backend pytest execution requires effective collection PASS facts");
247
+ }
248
+ const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
249
+ assertSameInventory(effective, current);
250
+ }
251
+ export async function materializeEffectiveBackendPytestCollection(input) {
252
+ if (input.initial.status === "PASS") {
253
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
254
+ assertSameInventory(input.initial, current);
255
+ return backendPytestCollectionFactsSchema.parse({
256
+ ...input.initial,
257
+ phase: "effective",
258
+ collectionSource: "initial",
259
+ });
260
+ }
261
+ if (input.initial.status === "BLOCKED")
262
+ throw new Error("backend pytest collection is blocked and not repairable");
263
+ if (!input.final)
264
+ throw new Error("backend pytest collection repair requires final collection facts");
265
+ if (input.final.status !== "PASS")
266
+ throw new Error("backend pytest collection still fails after bounded repair");
267
+ await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
268
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
269
+ assertSameInventory(input.final, current);
270
+ return backendPytestCollectionFactsSchema.parse({
271
+ ...input.final,
272
+ phase: "effective",
273
+ repairEligible: false,
274
+ repairAttempt: 1,
275
+ collectionSource: "final",
276
+ });
277
+ }