@tea-agent/loop-agent 0.26.1 → 0.26.3

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 (37) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/application/dag/generate-task-dag.js +33 -0
  3. package/dist/commands/task-source-prepare.js +6 -0
  4. package/dist/executors/dag-pi-executor.js +156 -9
  5. package/dist/executors/shell-executor.js +111 -0
  6. package/dist/executors/shell-presets.js +12 -4
  7. package/dist/executors/shell-write-guard.js +145 -12
  8. package/dist/task/config-types.js +6 -0
  9. package/dist/task/contract/constants.js +1 -0
  10. package/dist/task/contract/project.js +8 -0
  11. package/dist/task/contract/schema.js +1 -0
  12. package/dist/task/frontend-preflight.js +131 -0
  13. package/dist/task/runtime.js +2 -4
  14. package/dist/task/source-prepare/build-draft.js +9 -0
  15. package/dist/task/source-prepare/completeness.js +1 -1
  16. package/dist/worker/observability/read-model.js +134 -0
  17. package/dist/worker/observe/static/state.js +61 -0
  18. package/dist/worker/observe/static/styles.css +8 -0
  19. package/dist/worker/observe/static/views/dag-graph.js +107 -31
  20. package/dist/worker/observe/static/views/dag-inspector.js +374 -157
  21. package/dist/worker/observe/static/views/dag.js +4 -11
  22. package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
  23. package/dist/workflows/dag/convergence/controller.js +110 -21
  24. package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
  25. package/dist/workflows/dag/frontend-repair.js +29 -29
  26. package/dist/workflows/dag/frontend-review-context.js +7 -1
  27. package/dist/workflows/dag/frontend-verification-trace.js +26 -5
  28. package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
  29. package/dist/workflows/dag/governance-profile.js +1 -1
  30. package/dist/workflows/dag/init-hybrid.js +115 -39
  31. package/dist/workflows/dag/output-protocol.js +180 -7
  32. package/dist/workflows/dag/runner.js +141 -52
  33. package/dist/workflows/dag/types.js +5 -1
  34. package/dist/workflows/dag/validate.js +3 -2
  35. package/docs/templates/backend-test-dag.json +100 -8
  36. package/package.json +1 -1
  37. package/skills/loop-agent/references/hybrid-dag.md +1 -1
@@ -53,6 +53,8 @@ export const uiState = {
53
53
  dagTimelineViewportState: null,
54
54
  dagNodeOutputViewportState: null,
55
55
  dagNodeInputViewportState: null,
56
+ /** Spec-evidence inspector content scroll (single active region snapshot). */
57
+ dagNodeSpecEvidenceViewportState: null,
56
58
  runOutputViewportState: null,
57
59
  /**
58
60
  * Spec-evidence file preview drill-down. null = list view; otherwise
@@ -67,6 +69,65 @@ export const uiState = {
67
69
  /** Per-dagRunId graph viewport restore map (stable Map instance). */
68
70
  export const dagGraphViewportStates = new Map();
69
71
 
72
+ /**
73
+ * Per-context inspector content scroll map.
74
+ * Key format: dag:<dagRunId>:<nodeId>:<tab>
75
+ */
76
+ export const dagInspectorViewportStates = new Map();
77
+
78
+ /** Build a stable inspector scroll region key (AC-005 context isolation). */
79
+ export function inspectorViewportRegionKey(dagRunId, nodeId, tab) {
80
+ return `dag:${dagRunId ?? ""}:${nodeId ?? ""}:${tab ?? "output"}`;
81
+ }
82
+
83
+ export function updateDagInspectorViewportState(
84
+ _previous,
85
+ regionKey,
86
+ scrollLeft,
87
+ scrollTop,
88
+ ) {
89
+ return {
90
+ regionKey,
91
+ scrollLeft: Math.max(0, Number(scrollLeft) || 0),
92
+ scrollTop: Math.max(0, Number(scrollTop) || 0),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Persist inspector scroll for a context. When `preserveIntent` is true and the
98
+ * incoming position is a zero clamp on short (loading) content while a non-zero
99
+ * intent already exists, keep the previous intent instead of overwriting to 0.
100
+ */
101
+ export function saveDagInspectorViewportState(
102
+ regionKey,
103
+ scrollLeft,
104
+ scrollTop,
105
+ options = {},
106
+ ) {
107
+ if (!regionKey) return null;
108
+ const left = Math.max(0, Number(scrollLeft) || 0);
109
+ const top = Math.max(0, Number(scrollTop) || 0);
110
+ if (options.preserveIntent) {
111
+ const existing = dagInspectorViewportStates.get(regionKey);
112
+ if (
113
+ existing &&
114
+ left === 0 &&
115
+ top === 0 &&
116
+ (existing.scrollLeft > 0 || existing.scrollTop > 0)
117
+ ) {
118
+ return existing;
119
+ }
120
+ }
121
+ const next = updateDagInspectorViewportState(null, regionKey, left, top);
122
+ dagInspectorViewportStates.set(regionKey, next);
123
+ return next;
124
+ }
125
+
126
+ export function getDagInspectorViewportState(regionKey) {
127
+ if (!regionKey) return null;
128
+ return dagInspectorViewportStates.get(regionKey) ?? null;
129
+ }
130
+
70
131
  /** Build a fresh session-event identity token and bump the global sequence. */
71
132
  export function makeSessionEventIdentity(dagRunId, nodeId) {
72
133
  uiState.sessionEventIdentitySeq += 1;
@@ -1522,6 +1522,14 @@ body.is-resizing-dag-graph {
1522
1522
  #dag-arrow path {
1523
1523
  fill: color-mix(in srgb, var(--hairline-strong) 80%, var(--muted));
1524
1524
  }
1525
+ .dag-edge-control {
1526
+ stroke: var(--orange);
1527
+ stroke-width: 2;
1528
+ stroke-dasharray: 6 4;
1529
+ }
1530
+ #dag-arrow-control path {
1531
+ fill: var(--orange);
1532
+ }
1525
1533
  .dag-graph-node {
1526
1534
  cursor: pointer;
1527
1535
  transition: opacity var(--transition-fast);
@@ -26,30 +26,52 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
26
26
  const edges = dag.edges ?? [];
27
27
  const layout = layoutDag(nodes, edges, dag.ranks ?? []);
28
28
  const wrap = existingWrap ?? el("div", "dag-graph-wrap");
29
+ // Preserve .dag-graph-viewport DOM identity across poll refreshes so the
30
+ // user's active scroll/pointer target is not destroyed mid-gesture.
31
+ let viewport =
32
+ existingWrap?.querySelector?.(":scope > .dag-graph-viewport") ??
33
+ existingWrap?.querySelector?.(".dag-graph-viewport") ??
34
+ null;
29
35
  if (existingWrap) {
30
- // reuse-dag-graph-wrap: replace children while keeping wrap identity.
31
- while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
36
+ // Remove only non-interactive siblings. Keeping the viewport connected is
37
+ // important: detaching and re-inserting the same node still cancels a native
38
+ // scrollbar drag/pointer target even though object identity is unchanged.
39
+ for (const child of [...wrap.children]) {
40
+ if (child !== viewport) wrap.removeChild(child);
41
+ }
42
+ if (viewport) {
43
+ while (viewport.firstChild) viewport.removeChild(viewport.firstChild);
44
+ }
32
45
  }
33
46
  const toolbar = el("div", "dag-graph-toolbar");
34
- toolbar.appendChild(
35
- el(
36
- "span",
37
- "dag-graph-summary",
38
- `${nodes.length} 节点 · ${layout.edges.length} 条依赖`,
39
- ),
40
- );
47
+ let summaryText = `${nodes.length} 节点 · ${layout.edges.length} 条依赖`;
48
+ const convergence = dag.convergence;
49
+ if (convergence?.enabled) {
50
+ summaryText += ` · pass ${convergence.currentPass}/${convergence.maxPasses}`;
51
+ if (convergence.terminalReason) {
52
+ summaryText += ` · ${convergence.terminalReason}`;
53
+ }
54
+ }
55
+ toolbar.appendChild(el("span", "dag-graph-summary", summaryText));
41
56
  const reset = el("button", "dag-graph-reset", "重置视图");
42
57
  toolbar.appendChild(reset);
43
- wrap.appendChild(toolbar);
44
- if (edges.length === 0)
45
- wrap.appendChild(
58
+ const appendBeforeViewport = (child) => {
59
+ if (viewport?.parentNode === wrap) wrap.insertBefore(child, viewport);
60
+ else wrap.appendChild(child);
61
+ };
62
+ appendBeforeViewport(toolbar);
63
+ if (edges.length === 0) {
64
+ appendBeforeViewport(
46
65
  el(
47
66
  "p",
48
67
  "dag-graph-fallback",
49
68
  "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。",
50
69
  ),
51
70
  );
52
- const viewport = el("div", "dag-graph-viewport");
71
+ }
72
+ if (!viewport) {
73
+ viewport = el("div", "dag-graph-viewport");
74
+ }
53
75
  viewport.dataset.dagRunId = dagRunId;
54
76
  const svg = svgEl("svg", {
55
77
  class: "dag-graph",
@@ -71,6 +93,17 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
71
93
  });
72
94
  marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
73
95
  defs.appendChild(marker);
96
+ const controlMarker = svgEl("marker", {
97
+ id: "dag-arrow-control",
98
+ viewBox: "0 0 10 10",
99
+ refX: 9,
100
+ refY: 5,
101
+ markerWidth: 7,
102
+ markerHeight: 7,
103
+ orient: "auto-start-reverse",
104
+ });
105
+ controlMarker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
106
+ defs.appendChild(controlMarker);
74
107
  svg.appendChild(defs);
75
108
  const related = new Set([uiState.selectedDagNodeId]);
76
109
  for (const edge of layout.edges)
@@ -91,6 +124,28 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
91
124
  path.firstChild.textContent = `${edge.from} → ${edge.to}`;
92
125
  svg.appendChild(path);
93
126
  }
127
+ // 控制回环:仅由 read model 派生(dag.controlEdges),不进 layout、不参与拓扑。
128
+ // 路径从 review-gate-shell 左侧绕回 repair-pi 右侧,避开节点矩形。
129
+ const layoutByNode = new Map(layout.nodes.map((n) => [n.nodeId, n]));
130
+ for (const ctrl of dag.controlEdges ?? []) {
131
+ const from = layoutByNode.get(ctrl.from);
132
+ const to = layoutByNode.get(ctrl.to);
133
+ if (!from || !to) continue;
134
+ const x1 = from.x;
135
+ const y1 = from.y + from.height / 2;
136
+ const x2 = to.x + to.width;
137
+ const y2 = to.y + to.height / 2;
138
+ const lift = Math.max(48, (from.x - to.x) / 2);
139
+ const rise = Math.max(40, Math.abs(from.y - to.y) / 2 + 16);
140
+ const path = svgEl("path", {
141
+ d: `M ${x1} ${y1} C ${x1 - lift} ${y1 - rise}, ${x2 + lift} ${y2 - rise}, ${x2} ${y2}`,
142
+ class: "dag-edge dag-edge-control",
143
+ "marker-end": "url(#dag-arrow-control)",
144
+ });
145
+ path.appendChild(svgEl("title"));
146
+ path.firstChild.textContent = `${ctrl.from} → ${ctrl.to} (control loop)`;
147
+ svg.appendChild(path);
148
+ }
94
149
  const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
95
150
  for (const position of layout.nodes) {
96
151
  const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
@@ -115,7 +170,14 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
115
170
  executor.textContent = `◈ ${node.executor ?? "unknown"}`;
116
171
  group.appendChild(executor);
117
172
  const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" });
118
- state.textContent = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`;
173
+ let stateText = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`;
174
+ if (node.passes?.length) {
175
+ stateText += ` · P${node.passes.join(",")}`;
176
+ }
177
+ if (node.attemptCount > 0) {
178
+ stateText += ` · attempt ${node.attemptCount}`;
179
+ }
180
+ state.textContent = stateText;
119
181
  group.appendChild(state);
120
182
  const activate = () => selectDagNode(dagRunId, node.nodeId);
121
183
  group.addEventListener("click", activate);
@@ -128,36 +190,45 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
128
190
  svg.appendChild(group);
129
191
  }
130
192
  viewport.appendChild(svg);
131
- wrap.appendChild(viewport);
193
+ if (viewport.parentNode !== wrap) wrap.appendChild(viewport);
132
194
  const graphViewportState =
133
195
  dagGraphViewportStates.get(dagRunId) ??
134
196
  (uiState.dagGraphViewportState?.dagRunId === dagRunId
135
197
  ? uiState.dagGraphViewportState
136
198
  : null);
137
- viewport.addEventListener(
138
- "scroll",
139
- () => {
140
- uiState.dagGraphViewportState = updateDagGraphViewportState(
141
- uiState.dagGraphViewportState,
142
- dagRunId,
143
- viewport.scrollLeft,
144
- viewport.scrollTop,
145
- );
146
- dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
147
- },
148
- { passive: true },
149
- );
199
+ // Bind scroll capture once per viewport instance so reuse does not stack listeners.
200
+ if (viewport.dataset.scrollBound !== "1") {
201
+ viewport.dataset.scrollBound = "1";
202
+ viewport.addEventListener(
203
+ "scroll",
204
+ () => {
205
+ const id = viewport.dataset.dagRunId;
206
+ if (!id) return;
207
+ uiState.dagGraphViewportState = updateDagGraphViewportState(
208
+ uiState.dagGraphViewportState,
209
+ id,
210
+ viewport.scrollLeft,
211
+ viewport.scrollTop,
212
+ );
213
+ dagGraphViewportStates.set(id, uiState.dagGraphViewportState);
214
+ },
215
+ { passive: true },
216
+ );
217
+ }
150
218
  if (graphViewportState) {
151
219
  const restoreViewport = () => {
220
+ if (viewport.dataset.dagRunId !== dagRunId) return;
152
221
  viewport.scrollLeft = graphViewportState.scrollLeft;
153
222
  viewport.scrollTop = graphViewportState.scrollTop;
154
223
  };
224
+ // Apply immediately so identity reuse keeps position even before paint,
225
+ // then again on rAF after SVG layout settles.
226
+ restoreViewport();
155
227
  if (typeof requestAnimationFrame === "function") {
156
228
  requestAnimationFrame(restoreViewport);
157
- } else {
158
- restoreViewport();
159
229
  }
160
230
  }
231
+ // Toolbar is rebuilt each paint; bind reset to the current viewport/dagRunId.
161
232
  reset.addEventListener("click", () => {
162
233
  uiState.dagGraphViewportState = updateDagGraphViewportState(
163
234
  uiState.dagGraphViewportState,
@@ -166,7 +237,12 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
166
237
  0,
167
238
  );
168
239
  dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
169
- viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
240
+ if (typeof viewport.scrollTo === "function") {
241
+ viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
242
+ } else {
243
+ viewport.scrollLeft = 0;
244
+ viewport.scrollTop = 0;
245
+ }
170
246
  });
171
247
  return wrap;
172
248
  }