@tea-agent/loop-agent 0.7.4 → 0.7.5

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.
package/CHANGELOG.md CHANGED
@@ -4,8 +4,11 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.5] - 2026-07-11
8
+
7
9
  ### 新增
8
10
 
11
+ - Observe DAG Detail 新增离线 SVG 依赖图:read-model 从 canonical `run.json` 投影稳定 edges,支持并行/汇合/失败/跳过状态、键盘节点选择、上下游高亮、滚动与重置视图,并保留节点表格和过程时间线降级路径。
9
12
  - 新增 repository-owned Worker nightly wrapper 与 GitHub Actions 手动/定时适配面:按 feature 互斥、保留超时/批次退出码,并收集 controller、morning report、Observe snapshot 等 artifacts。
10
13
  - 新增 `agent-worker task validate-feature` 和可初始化投影的语言无关产品线模板,校验 AC 唯一性、任务依赖、TaskSpec 路径/验证命令及 QA verdict → success closeout 顺序,并接入仓库治理门禁。
11
14
 
@@ -645,7 +645,7 @@ async function loadDagRuns(repoRoot) {
645
645
  const byId = new Map();
646
646
  const statePaths = await findStateJsonFiles(path.join(repoRoot, ".harness", "dag-runs"));
647
647
  for (const statePath of statePaths) {
648
- const summary = parseDagStateFile(statePath);
648
+ const summary = await parseDagStateFile(statePath);
649
649
  if (summary)
650
650
  byId.set(summary.dagRunId, mergeDagRun(byId.get(summary.dagRunId), summary));
651
651
  }
@@ -674,6 +674,7 @@ function mergeDagRun(existing, incoming) {
674
674
  ...(durationMs !== undefined ? { durationMs } : {}),
675
675
  ...(ranks && ranks.length > 0 ? { ranks } : {}),
676
676
  nodes: mergeDagNodes(existing.nodes, incoming.nodes),
677
+ edges: incoming.edges.length > 0 ? incoming.edges : existing.edges,
677
678
  ...(incoming.dagPath ?? existing.dagPath ? { dagPath: incoming.dagPath ?? existing.dagPath } : {}),
678
679
  };
679
680
  }
@@ -819,6 +820,7 @@ async function loadDagEventFiles(repoRoot) {
819
820
  ...(finishedAt ? { finishedAt } : {}),
820
821
  ...(durationMs !== undefined ? { durationMs } : {}),
821
822
  nodes: [...nodeMap.values()].sort((a, b) => a.nodeId.localeCompare(b.nodeId)),
823
+ edges: [],
822
824
  });
823
825
  }
824
826
  return result;
@@ -845,7 +847,7 @@ async function walkForStateJson(dir, results) {
845
847
  // skip
846
848
  }
847
849
  }
848
- function parseDagStateFile(statePath) {
850
+ async function parseDagStateFile(statePath) {
849
851
  try {
850
852
  if (!existsSync(statePath))
851
853
  return undefined;
@@ -863,6 +865,7 @@ function parseDagStateFile(statePath) {
863
865
  const ranks = readStringMatrix(parsed, "ranks");
864
866
  const rankByNode = buildRankIndex(ranks);
865
867
  const nodes = parseDagNodes(parsed, rankByNode);
868
+ const edges = await parseDagEdges(path.join(runDir, "run.json"), nodes);
866
869
  return {
867
870
  dagRunId,
868
871
  status,
@@ -872,6 +875,7 @@ function parseDagStateFile(statePath) {
872
875
  ...(durationMs !== undefined ? { durationMs } : {}),
873
876
  ...(ranks.length > 0 ? { ranks } : {}),
874
877
  nodes,
878
+ edges,
875
879
  dagPath: runDir,
876
880
  };
877
881
  }
@@ -879,6 +883,30 @@ function parseDagStateFile(statePath) {
879
883
  return undefined;
880
884
  }
881
885
  }
886
+ async function parseDagEdges(runPath, nodes) {
887
+ const parsed = await safeReadJson(runPath);
888
+ if (!parsed)
889
+ return [];
890
+ const tasks = readObjectArray(parsed, "tasks");
891
+ if (tasks.length === 0)
892
+ return [];
893
+ const knownNodeIds = new Set(nodes.map((node) => node.nodeId));
894
+ const edges = new Map();
895
+ for (const task of tasks) {
896
+ const taskId = readString(task, "id") ?? readString(task, "nodeId");
897
+ if (!taskId || !knownNodeIds.has(taskId))
898
+ continue;
899
+ const dependencies = readStringArray(task, "depends_on");
900
+ const compatibleDependencies = dependencies.length > 0 ? dependencies : readStringArray(task, "dependsOn");
901
+ for (const dependencyId of compatibleDependencies) {
902
+ if (dependencyId === taskId || !knownNodeIds.has(dependencyId))
903
+ continue;
904
+ const edge = { from: dependencyId, to: taskId };
905
+ edges.set(`${edge.from}\u0000${edge.to}`, edge);
906
+ }
907
+ }
908
+ return [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
909
+ }
882
910
  function buildRankIndex(ranks) {
883
911
  const index = new Map();
884
912
  for (let i = 0; i < ranks.length; i++) {
@@ -995,6 +1023,14 @@ function readString(value, key) {
995
1023
  const child = value[key];
996
1024
  return typeof child === "string" ? child : undefined;
997
1025
  }
1026
+ function readStringArray(value, key) {
1027
+ if (!value || typeof value !== "object")
1028
+ return [];
1029
+ const child = value[key];
1030
+ if (!Array.isArray(child))
1031
+ return [];
1032
+ return child.filter((item) => typeof item === "string");
1033
+ }
998
1034
  function readNumber(value, key) {
999
1035
  if (!value || typeof value !== "object")
1000
1036
  return undefined;
@@ -1,3 +1,5 @@
1
+ import { layoutDag } from "./dag-layout.js";
2
+
1
3
  const POLL_MS = 2000;
2
4
  const DASHBOARD_POLL_MS = 5000;
3
5
 
@@ -194,6 +196,16 @@ function statusLabel(status) {
194
196
  return status;
195
197
  }
196
198
 
199
+ function statusSymbol(status) {
200
+ const key = (status ?? "unknown").toLowerCase();
201
+ if (["finished", "completed", "succeeded", "done"].includes(key)) return "✓";
202
+ if (["failed", "error"].includes(key)) return "✕";
203
+ if (key === "skipped") return "↷";
204
+ if (["running", "started"].includes(key)) return "●";
205
+ if (["pending", "queued", "blocked"].includes(key)) return "○";
206
+ return "◇";
207
+ }
208
+
197
209
  function badgeClass(status) {
198
210
  if (!status) return "unknown";
199
211
  const key = status.toLowerCase().replace(/_/g, "-");
@@ -704,6 +716,55 @@ function renderRankLanes(dag) {
704
716
  return container;
705
717
  }
706
718
 
719
+ function svgEl(tag, attrs = {}) {
720
+ const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
721
+ for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value));
722
+ return node;
723
+ }
724
+
725
+ function renderDagGraph(dag, dagRunId) {
726
+ const nodes = dag.nodes ?? [];
727
+ const edges = dag.edges ?? [];
728
+ const layout = layoutDag(nodes, edges, dag.ranks ?? []);
729
+ const wrap = el("div", "dag-graph-wrap");
730
+ const toolbar = el("div", "dag-graph-toolbar");
731
+ toolbar.appendChild(el("span", "dag-graph-summary", `${nodes.length} 节点 · ${layout.edges.length} 条依赖`));
732
+ const reset = el("button", "dag-graph-reset", "重置视图");
733
+ toolbar.appendChild(reset);
734
+ wrap.appendChild(toolbar);
735
+ if (edges.length === 0) wrap.appendChild(el("p", "dag-graph-fallback", "该运行没有可用依赖数据;图按 Rank 展示,节点表格仍保留完整数据。"));
736
+ const viewport = el("div", "dag-graph-viewport");
737
+ const svg = svgEl("svg", { class: "dag-graph", viewBox: `0 0 ${layout.width} ${layout.height}`, width: layout.width, height: layout.height, role: "img", "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点` });
738
+ const defs = svgEl("defs");
739
+ const marker = svgEl("marker", { id: "dag-arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse" });
740
+ marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
741
+ defs.appendChild(marker); svg.appendChild(defs);
742
+ const related = new Set([selectedDagNodeId]);
743
+ for (const edge of layout.edges) if (edge.from === selectedDagNodeId || edge.to === selectedDagNodeId) { related.add(edge.from); related.add(edge.to); }
744
+ for (const edge of layout.edges) {
745
+ const path = svgEl("path", { d: edge.path, class: `dag-edge${selectedDagNodeId && (!related.has(edge.from) || !related.has(edge.to)) ? " dag-dimmed" : ""}`, "marker-end": "url(#dag-arrow)" });
746
+ path.appendChild(svgEl("title")); path.firstChild.textContent = `${edge.from} → ${edge.to}`; svg.appendChild(path);
747
+ }
748
+ const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
749
+ for (const position of layout.nodes) {
750
+ const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
751
+ const status = (node.status ?? "unknown").toLowerCase();
752
+ const group = svgEl("g", { transform: `translate(${position.x} ${position.y})`, class: `dag-graph-node status-${status}${node.nodeId === selectedDagNodeId ? " selected" : ""}${selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`, tabindex: 0, role: "button", "aria-label": `${node.nodeId},状态 ${status}` });
753
+ group.appendChild(svgEl("rect", { width: position.width, height: position.height, rx: 10 }));
754
+ const title = svgEl("title"); title.textContent = `${node.nodeId}\n${node.executor ?? "未知执行器"}\n${status}`; group.appendChild(title);
755
+ const label = svgEl("text", { x: 14, y: 26, class: "dag-node-label" }); label.textContent = truncateText(node.label ?? node.nodeId, 26); group.appendChild(label);
756
+ const executor = svgEl("text", { x: 14, y: 50, class: "dag-node-meta" }); executor.textContent = `◈ ${node.executor ?? "unknown"}`; group.appendChild(executor);
757
+ const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" }); state.textContent = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`; group.appendChild(state);
758
+ const activate = () => selectDagNode(dagRunId, node.nodeId);
759
+ group.addEventListener("click", activate);
760
+ group.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } });
761
+ svg.appendChild(group);
762
+ }
763
+ viewport.appendChild(svg); wrap.appendChild(viewport);
764
+ reset.addEventListener("click", () => { viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" }); });
765
+ return wrap;
766
+ }
767
+
707
768
  function expandablePreview(title, content, previewClass) {
708
769
  const wrap = el("details", previewClass ?? "output-preview");
709
770
  const summary = el("summary", null, title);
@@ -1307,11 +1368,11 @@ async function renderDagDetail(dagRunId, initial = true) {
1307
1368
  );
1308
1369
 
1309
1370
  clearNode(ranksEl);
1310
- ranksEl.appendChild(el("h3", null, "Rank 泳道"));
1371
+ ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
1311
1372
  if (nodes.length === 0) {
1312
1373
  ranksEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
1313
1374
  } else {
1314
- ranksEl.appendChild(renderRankLanes(dag));
1375
+ ranksEl.appendChild(renderDagGraph(dag, dagRunId));
1315
1376
  }
1316
1377
 
1317
1378
  clearNode(nodesEl);
@@ -0,0 +1,31 @@
1
+ export type DagLayoutNodeInput = { nodeId: string };
2
+ export type DagLayoutEdgeInput = { from: string; to: string };
3
+ export type DagLayoutNode = {
4
+ nodeId: string;
5
+ rank: number;
6
+ index: number;
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ };
12
+ export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
13
+ export type DagLayout = {
14
+ width: number;
15
+ height: number;
16
+ nodes: DagLayoutNode[];
17
+ edges: DagLayoutEdge[];
18
+ };
19
+
20
+ export function layoutDag(
21
+ nodes?: DagLayoutNodeInput[],
22
+ edges?: DagLayoutEdgeInput[],
23
+ ranks?: string[][],
24
+ options?: Partial<{
25
+ nodeWidth: number;
26
+ nodeHeight: number;
27
+ rankGap: number;
28
+ nodeGap: number;
29
+ padding: number;
30
+ }>,
31
+ ): DagLayout;
@@ -0,0 +1,83 @@
1
+ const DEFAULTS = {
2
+ nodeWidth: 216,
3
+ nodeHeight: 92,
4
+ rankGap: 104,
5
+ nodeGap: 28,
6
+ padding: 32,
7
+ };
8
+
9
+ export function layoutDag(nodes = [], edges = [], ranks = [], options = {}) {
10
+ const config = { ...DEFAULTS, ...options };
11
+ const nodeIds = [...new Set(nodes.map((node) => node.nodeId).filter(Boolean))].sort();
12
+ if (nodeIds.length === 0) return { width: 320, height: 160, nodes: [], edges: [] };
13
+ const known = new Set(nodeIds);
14
+ const validEdges = edges
15
+ .filter((edge) => known.has(edge.from) && known.has(edge.to) && edge.from !== edge.to)
16
+ .sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
17
+ const layers = normalizeLayers(nodeIds, validEdges, ranks);
18
+ const positioned = [];
19
+ for (let rank = 0; rank < layers.length; rank++) {
20
+ const ids = layers[rank];
21
+ for (let index = 0; index < ids.length; index++) {
22
+ positioned.push({
23
+ nodeId: ids[index], rank, index,
24
+ x: config.padding + rank * (config.nodeWidth + config.rankGap),
25
+ y: config.padding + index * (config.nodeHeight + config.nodeGap),
26
+ width: config.nodeWidth, height: config.nodeHeight,
27
+ });
28
+ }
29
+ }
30
+ const byId = new Map(positioned.map((node) => [node.nodeId, node]));
31
+ const routed = validEdges.map((edge) => {
32
+ const from = byId.get(edge.from);
33
+ const to = byId.get(edge.to);
34
+ const x1 = from.x + from.width;
35
+ const y1 = from.y + from.height / 2;
36
+ const x2 = to.x;
37
+ const y2 = to.y + to.height / 2;
38
+ const bend = Math.max(36, (x2 - x1) / 2);
39
+ return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
40
+ });
41
+ const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
42
+ return {
43
+ width: config.padding * 2 + layers.length * config.nodeWidth + (layers.length - 1) * config.rankGap,
44
+ height: config.padding * 2 + maxRows * config.nodeHeight + (maxRows - 1) * config.nodeGap,
45
+ nodes: positioned,
46
+ edges: routed,
47
+ };
48
+ }
49
+
50
+ function normalizeLayers(nodeIds, edges, ranks) {
51
+ const known = new Set(nodeIds);
52
+ const seen = new Set();
53
+ const layers = [];
54
+ if (Array.isArray(ranks)) {
55
+ for (const rank of ranks) {
56
+ if (!Array.isArray(rank)) continue;
57
+ const ids = [...new Set(rank.filter((id) => known.has(id) && !seen.has(id)))].sort();
58
+ ids.forEach((id) => seen.add(id));
59
+ if (ids.length > 0) layers.push(ids);
60
+ }
61
+ }
62
+ if (layers.length === 0 && edges.length > 0) {
63
+ const level = new Map(nodeIds.map((id) => [id, 0]));
64
+ for (let pass = 0; pass < nodeIds.length; pass++) {
65
+ let changed = false;
66
+ for (const edge of edges) {
67
+ const next = Math.min(nodeIds.length - 1, level.get(edge.from) + 1);
68
+ if (next > level.get(edge.to)) { level.set(edge.to, next); changed = true; }
69
+ }
70
+ if (!changed) break;
71
+ }
72
+ for (const id of nodeIds) {
73
+ const rank = level.get(id);
74
+ (layers[rank] ??= []).push(id);
75
+ seen.add(id);
76
+ }
77
+ for (const layer of layers) layer?.sort();
78
+ }
79
+ for (const id of nodeIds) {
80
+ if (!seen.has(id)) (layers[0] ??= []).push(id);
81
+ }
82
+ return layers.filter(Boolean);
83
+ }
@@ -1,6 +1,7 @@
1
1
  :root {
2
2
  --bg: #0d1117;
3
3
  --surface: #161b22;
4
+ --surface-raised: #1c2128;
4
5
  --border: #30363d;
5
6
  --text: #c9d1d9;
6
7
  --muted: #8b949e;
@@ -10,6 +11,17 @@
10
11
  --yellow: #d29922;
11
12
  --blue: #58a6ff;
12
13
  --gray-red: #8b3a3a;
14
+ --space-1: 4px;
15
+ --space-2: 8px;
16
+ --space-3: 12px;
17
+ --space-4: 16px;
18
+ --space-6: 24px;
19
+ --space-8: 32px;
20
+ --radius-sm: 4px;
21
+ --radius-md: 8px;
22
+ --radius-lg: 12px;
23
+ --shadow-1: 0 4px 18px rgba(0, 0, 0, 0.2);
24
+ --transition-fast: 140ms ease;
13
25
  }
14
26
 
15
27
  * {
@@ -18,13 +30,25 @@
18
30
 
19
31
  body {
20
32
  margin: 0;
21
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
33
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
22
34
  font-size: 13px;
23
35
  line-height: 1.5;
24
36
  background: var(--bg);
25
37
  color: var(--text);
26
38
  }
27
39
 
40
+ code,
41
+ pre,
42
+ .output-block,
43
+ .rank-node-id,
44
+ .dag-node-label,
45
+ .dag-node-meta,
46
+ .dag-node-status,
47
+ td:first-child,
48
+ .meta-item dd {
49
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
50
+ }
51
+
28
52
  #app {
29
53
  max-width: 1200px;
30
54
  margin: 0 auto;
@@ -103,11 +127,96 @@ h3,
103
127
  .panel {
104
128
  background: var(--surface);
105
129
  border: 1px solid var(--border);
106
- border-radius: 6px;
130
+ border-radius: var(--radius-md);
107
131
  padding: 0.75rem 1rem;
108
132
  margin-bottom: 1rem;
109
133
  }
110
134
 
135
+ .dag-graph-wrap {
136
+ display: grid;
137
+ gap: var(--space-2);
138
+ }
139
+
140
+ .dag-graph-toolbar {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ gap: var(--space-3);
145
+ }
146
+
147
+ .dag-graph-summary,
148
+ .dag-graph-fallback {
149
+ color: var(--muted);
150
+ font-size: 12px;
151
+ }
152
+
153
+ .dag-graph-fallback { margin: 0; }
154
+
155
+ .dag-graph-reset {
156
+ appearance: none;
157
+ border: 1px solid var(--border);
158
+ border-radius: var(--radius-sm);
159
+ background: var(--surface-raised);
160
+ color: var(--text);
161
+ padding: 6px 10px;
162
+ cursor: pointer;
163
+ }
164
+
165
+ .dag-graph-reset:hover,
166
+ .dag-graph-reset:focus-visible { border-color: var(--link); }
167
+
168
+ .dag-graph-viewport {
169
+ min-height: 220px;
170
+ max-height: 620px;
171
+ overflow: auto;
172
+ border: 1px solid var(--border);
173
+ border-radius: var(--radius-md);
174
+ background: radial-gradient(circle at 1px 1px, rgba(139, 148, 158, 0.18) 1px, transparent 0) 0 0 / 20px 20px, var(--bg);
175
+ scroll-behavior: smooth;
176
+ }
177
+
178
+ .dag-graph { display: block; }
179
+ .dag-edge { fill: none; stroke: var(--muted); stroke-width: 2; opacity: 0.75; transition: opacity var(--transition-fast); }
180
+ .dag-edge + .dag-edge { stroke-width: 1.8; }
181
+ #dag-arrow path { fill: var(--muted); }
182
+ .dag-graph-node { cursor: pointer; transition: opacity var(--transition-fast); }
183
+ .dag-graph-node rect { fill: var(--surface-raised); stroke: var(--border); stroke-width: 1.5; }
184
+ .dag-graph-node:hover rect,
185
+ .dag-graph-node:focus-visible rect { stroke: var(--link); stroke-width: 2.5; }
186
+ .dag-graph-node:focus { outline: none; }
187
+ .dag-graph-node.selected rect { stroke: var(--link); stroke-width: 3; filter: drop-shadow(0 0 6px rgba(88, 166, 255, 0.45)); }
188
+ .dag-graph-node.status-running rect,
189
+ .dag-graph-node.status-started rect { stroke: var(--yellow); }
190
+ .dag-graph-node.status-failed rect,
191
+ .dag-graph-node.status-error rect { stroke: var(--red); fill: rgba(248, 81, 73, 0.1); }
192
+ .dag-graph-node.status-skipped rect { stroke-dasharray: 5 4; }
193
+ .dag-node-label { fill: var(--text); font-size: 13px; font-weight: 700; }
194
+ .dag-node-meta { fill: var(--muted); font-size: 11px; }
195
+ .dag-node-status { fill: var(--text); font-size: 11px; }
196
+ .dag-dimmed { opacity: 0.25; }
197
+ .dag-graph-node.status-running { animation: graph-node-pulse 2s ease-in-out infinite; }
198
+
199
+ @keyframes graph-node-pulse {
200
+ 50% { filter: drop-shadow(0 0 7px rgba(210, 153, 34, 0.45)); }
201
+ }
202
+
203
+ @media (max-width: 680px) {
204
+ #app { padding-inline: var(--space-3); }
205
+ .meta-grid { grid-template-columns: 1fr; }
206
+ .dag-graph-viewport { max-height: 480px; }
207
+ table { min-width: 760px; }
208
+ .panel { overflow-x: auto; }
209
+ }
210
+
211
+ @media (prefers-reduced-motion: reduce) {
212
+ *, *::before, *::after {
213
+ animation-duration: 0.01ms !important;
214
+ animation-iteration-count: 1 !important;
215
+ scroll-behavior: auto !important;
216
+ transition-duration: 0.01ms !important;
217
+ }
218
+ }
219
+
111
220
  .kpi-grid {
112
221
  display: grid;
113
222
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
package/docs/README.md CHANGED
@@ -34,6 +34,9 @@
34
34
  - `exec-plans/completed/README.md` — 已完成的执行计划
35
35
  - `progress/README.md` — 进度交接日志
36
36
  - `reports/README.md` — 验证与审计报告
37
+ - `reports/2026-07-11-round3-worker-expansion.md` — Round 3 Worker 扩量、0.7.4 FE green、QA review blocker 与第二轮 owner decision
38
+ - `reports/2026-07-11-observe-dag-visualization.md` — Observe DAG edges、SVG 图形化、交互与真实 Chrome smoke 验证证据
39
+ - `reports/2026-07-11-0.7.5-init-evolution-review.md` — 0.7.5 package/version 变化对目标项目初始化 surface 的影响审查
37
40
  - `decisions/README.md` — 架构决策
38
41
  - `skills/README.md` — repo-local skill registry and vetting notes
39
42
  - `templates/` — 可复用的规划、报告与 DAG 模板
@@ -7,6 +7,5 @@
7
7
  当前 active execution plan:
8
8
 
9
9
  - `2026-07-10-release-0.6.0-nightly-drill.md` — 技术演练已完成;等待 0.6.0 发布 owner 对 Day-1 provisional remediation 作明确 ratify / revise / reject。
10
- - `2026-07-11-round-2-interactive-ui-productization.md` — 补齐 Round-1 owner gate,发布 interactive-ui controller,完成 React A/B/C、Round 2/3 扩量、CI/cron 和产品线模板/docs CI。
11
10
 
12
- - 已归档:`../completed/2026-07-11-interactive-ui-writer-routing.md`、`../completed/2026-07-10-next-stage-worker-evidence.md` 及更早计划。
11
+ - 已归档:`../completed/2026-07-11-observe-dag-visualization.md`、`../completed/2026-07-11-round-2-interactive-ui-productization.md`、`../completed/2026-07-11-interactive-ui-writer-routing.md`、`../completed/2026-07-10-next-stage-worker-evidence.md` 及更早计划。
@@ -9,6 +9,8 @@ npm 包携带本 README 作为目录契约。具体 completed plan 属于目标
9
9
  - [`2026-07-04-runtime-boundary-remediation.md`](2026-07-04-runtime-boundary-remediation.md) — 整合 CLI/skill/runtime 边界,抽出 DAG/Loop runtime seam,集中 harness store/guard 策略
10
10
  - [`2026-07-10-next-stage-worker-evidence.md`](2026-07-10-next-stage-worker-evidence.md) — Worker retry、EnvFailure、边界治理、初始化投影与真实 BE/FE/QA dogfood evidence 闭环
11
11
  - [`2026-07-11-interactive-ui-writer-routing.md`](2026-07-11-interactive-ui-writer-routing.md) — 新增 interactive-ui writer-only HIGH 路由、UI 交付契约、真实 React dogfood fixture 与 Round-2 A/B/C 实验入口
12
+ - [`2026-07-11-round-2-interactive-ui-productization.md`](2026-07-11-round-2-interactive-ui-productization.md) — 完成 owner gate、0.7.0–0.7.4 发布、React A/B/C、20 个 Worker runs、外部 nightly、产品线模板与 docs CI
13
+ - [`2026-07-11-observe-dag-visualization.md`](2026-07-11-observe-dag-visualization.md) — Observe 投影 canonical DAG edges,新增确定性 SVG 依赖图、节点交互、视觉 tokens、可访问性与真实 Chrome smoke
12
14
  - [`2026-07-04-dag-role-skill-alignment.md`](2026-07-04-dag-role-skill-alignment.md) — 对齐 DAG/Dynamic Workflow role 与 repo-local vetted skills,新增 strict skill audit
13
15
  - [`2026-07-06-production-readiness-hardening.md`](2026-07-06-production-readiness-hardening.md) — 冻结 Production Readiness v0.1,打磨 DAG 主路径 next steps、failure routing、doctor/report/failure handoff 与 dogfood 验证
14
16
  - [`2026-07-08-taskspec-schema-validate.md`](2026-07-08-taskspec-schema-validate.md) — 新增 TaskSpec v0.1 schema、三层校验器、risk→complexity 映射和 5 个 dogfood TaskSpec 样例
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",