@tea-agent/loop-agent 0.13.0-beta.0 → 0.14.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 (127) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +56 -305
  3. package/README.md +13 -19
  4. package/dist/commands/init.js +92 -23
  5. package/dist/executors/pi-event-serializer.js +33 -11
  6. package/dist/executors/shell-executor.js +200 -21
  7. package/dist/infrastructure/evaluation/candidate-store.js +5 -1
  8. package/dist/worker/observe/spec-evidence.js +19 -10
  9. package/dist/worker/observe/static/app.js +4 -3
  10. package/dist/worker/observe/static/constants.js +10 -2
  11. package/dist/worker/observe/static/dag-helpers.js +37 -8
  12. package/dist/worker/observe/static/dom.js +159 -0
  13. package/dist/worker/observe/static/format-pool.d.ts +71 -0
  14. package/dist/worker/observe/static/format-pool.js +67 -0
  15. package/dist/worker/observe/static/format.js +27 -2
  16. package/dist/worker/observe/static/index.html +76 -34
  17. package/dist/worker/observe/static/kpi.js +12 -6
  18. package/dist/worker/observe/static/markdown-render.js +124 -0
  19. package/dist/worker/observe/static/shell-chrome.js +8 -2
  20. package/dist/worker/observe/static/state.js +20 -0
  21. package/dist/worker/observe/static/styles.css +662 -60
  22. package/dist/worker/observe/static/views/dag-inspector.js +65 -142
  23. package/dist/worker/observe/static/views/dag.js +9 -0
  24. package/dist/worker/observe/static/views/dashboard.js +512 -269
  25. package/dist/worker/observe/static/views/pool.js +595 -237
  26. package/dist/worker/observe/static/views/session-timeline.js +577 -11
  27. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  28. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  29. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  30. package/dist/workflows/dag/decision-envelope.js +57 -2
  31. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  32. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  33. package/dist/workflows/dag/frontend-repair.js +341 -0
  34. package/dist/workflows/dag/frontend-risk.js +161 -0
  35. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  36. package/dist/workflows/dag/init-hybrid.js +1020 -125
  37. package/dist/workflows/dag/repair-artifact.js +43 -3
  38. package/dist/workflows/dag/skill-instructions.js +4 -2
  39. package/dist/workflows/dag/types.js +29 -8
  40. package/docs/README.md +2 -0
  41. package/docs/agent-dag-recovery-playbook.md +3 -3
  42. package/docs/agent-dag-runner.md +3 -3
  43. package/docs/architecture/README.md +3 -3
  44. package/docs/architecture/dag-execution.md +1 -1
  45. package/docs/architecture/evolution.md +13 -13
  46. package/docs/architecture/facts-and-state.md +1 -1
  47. package/docs/architecture/runtime-boundaries.md +7 -7
  48. package/docs/architecture/system-overview.md +3 -3
  49. package/docs/architecture/worker-and-feature.md +3 -3
  50. package/docs/design/README.md +7 -7
  51. package/docs/development-principles.md +4 -4
  52. package/docs/exec-plans/active/README.md +2 -4
  53. package/docs/exec-plans/completed/README.md +29 -6
  54. package/docs/feature-workflow.md +57 -32
  55. package/docs/init-surface.manifest.json +21 -3
  56. package/docs/loop-agent-harness.md +8 -8
  57. package/docs/production-readiness.md +1 -1
  58. package/docs/progress/README.md +20 -3
  59. package/docs/reports/README.md +53 -7
  60. package/docs/templates/agent-dag.supervised-implementation.json +127 -8
  61. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  62. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  63. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +6 -4
  64. package/docs/templates/backend-test-dag.json +269 -21
  65. package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
  66. package/docs/templates/backend-test-dag.review-cases.prompt.md +6 -4
  67. package/docs/templates/backend-test-execution.schema.json +133 -0
  68. package/docs/templates/backend-test-result.schema.json +99 -0
  69. package/docs/templates/branch-merge-report.md +93 -0
  70. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  71. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  72. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  73. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  74. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  75. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  76. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  77. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  78. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  79. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  80. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  81. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  82. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  83. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  84. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  85. package/docs/templates/frontend-eval/metrics.md +138 -0
  86. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  87. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  88. package/docs/verification-matrix.md +1 -1
  89. package/examples/decision-gate-agent-dag.json +4 -4
  90. package/examples/hybrid-loop-agent-dag.json +1 -1
  91. package/package.json +2 -2
  92. package/skills/ai-engineering-context/SKILL.md +2 -2
  93. package/skills/browser-tools/SKILL.md +196 -0
  94. package/skills/browser-tools/browser-content.js +103 -0
  95. package/skills/browser-tools/browser-cookies.js +35 -0
  96. package/skills/browser-tools/browser-eval.js +53 -0
  97. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  98. package/skills/browser-tools/browser-nav.js +44 -0
  99. package/skills/browser-tools/browser-pick.js +162 -0
  100. package/skills/browser-tools/browser-screenshot.js +34 -0
  101. package/skills/browser-tools/browser-start.js +86 -0
  102. package/skills/browser-tools/package-lock.json +2556 -0
  103. package/skills/browser-tools/package.json +19 -0
  104. package/skills/frontend-implementation/SKILL.md +3 -1
  105. package/skills/frontend-implementation/references/node-contracts.md +17 -66
  106. package/skills/frontend-verification/SKILL.md +1 -1
  107. package/skills/grill-with-docs/SKILL.md +5 -5
  108. package/skills/grill-with-docs/adr-format.md +3 -3
  109. package/skills/init-capability-evolution/SKILL.md +5 -5
  110. package/skills/loop-agent/SKILL.md +5 -5
  111. package/skills/loop-agent/references/README.md +3 -3
  112. package/skills/loop-agent/references/command-reference.md +39 -17
  113. package/skills/loop-agent/references/docs-converge.md +15 -15
  114. package/skills/loop-agent/references/harness-policy.md +2 -2
  115. package/skills/loop-agent/references/hybrid-dag.md +20 -15
  116. package/skills/loop-agent/references/multi-worktree.md +1 -1
  117. package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
  118. package/skills/loop-agent/references/task-workflow.md +1 -1
  119. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
  120. package/skills/requesting-code-review/SKILL.md +1 -1
  121. package/skills/systematic-debugging/CREATION-LOG.md +3 -3
  122. package/skills/systematic-debugging/SKILL.md +1 -1
  123. package/skills/systematic-debugging/test-academic.md +1 -1
  124. package/skills/systematic-debugging/test-pressure-1.md +1 -1
  125. package/skills/systematic-debugging/test-pressure-2.md +1 -1
  126. package/skills/systematic-debugging/test-pressure-3.md +1 -1
  127. package/skills/verification-before-completion/SKILL.md +1 -1
@@ -3,9 +3,15 @@
3
3
  export const ACTIVE_DETAIL_POLL_MS = 2000;
4
4
  export const HIDDEN_POLL_MS = 30000;
5
5
  export const DASHBOARD_POLL_MS = 5000;
6
- export const DAG_INSPECTOR_DEFAULT_WIDTH = 440;
6
+ /** Optional pool auto-refresh interval (default off; manual refresh preferred). */
7
+ export const POOL_POLL_MS = 20000;
8
+ /** Fallback when viewport is unavailable; open default is half viewport. */
9
+ export const DAG_INSPECTOR_DEFAULT_WIDTH = 720;
10
+ /** Open width as a fraction of the viewport (clamped to min/max). */
11
+ export const DAG_INSPECTOR_DEFAULT_WIDTH_RATIO = 0.5;
7
12
  export const DAG_INSPECTOR_MIN_WIDTH = 360;
8
- export const DAG_INSPECTOR_MAX_WIDTH = 680;
13
+ /** Absolute ceiling; effective max also respects viewport margin. */
14
+ export const DAG_INSPECTOR_MAX_WIDTH = 1200;
9
15
  export const DAG_INSPECTOR_RIGHT_OFFSET = 24;
10
16
  export const DAG_INSPECTOR_VIEWPORT_MARGIN = 48;
11
17
  export const DAG_INSPECTOR_KEYBOARD_STEP = 24;
@@ -37,6 +43,8 @@ export const UI_TEXT = {
37
43
  refreshStatus: "更新",
38
44
  failureInbox: "异常 Task",
39
45
  nearRealtime: "轮询刷新",
46
+ manualRefresh: "手动刷新",
47
+ autoRefresh: "自动刷新",
40
48
  };
41
49
 
42
50
  export const STATUS_LABELS = {
@@ -48,16 +48,45 @@ export function getActiveNodes(dag) {
48
48
  return (dag.nodes ?? []).filter((n) => isNodeActive(n.status));
49
49
  }
50
50
 
51
- export function dagSortTime(dag) {
52
- return dag.startedAt ?? dag.finishedAt ?? "";
53
- }
51
+ export function dagSortTime(dag) {
52
+ return dag.startedAt ?? dag.finishedAt ?? "";
53
+ }
54
+
55
+ /** Visual urgency for active DAG cards: attention > running > idle. */
56
+ export function dagCardUrgency(dag) {
57
+ const status = (dag?.effectiveStatus ?? "").toLowerCase();
58
+ const liveness = (dag?.liveness ?? "").toLowerCase();
59
+ const lifecycle = (dag?.lifecycle ?? "").toLowerCase();
60
+ if (
61
+ lifecycle === "paused" ||
62
+ dag?.stateConsistent === false ||
63
+ ["interrupted", "remote-unknown"].includes(status) ||
64
+ ["stale", "orphaned"].includes(liveness)
65
+ ) {
66
+ return "attention";
67
+ }
68
+ if (isNodeActive(dag?.status) || getActiveNodes(dag).length > 0) {
69
+ return "running";
70
+ }
71
+ return "idle";
72
+ }
54
73
 
55
- export function renderActiveDagCard(dag, nowMs) {
56
- const card = el("div", "active-dag-card");
57
- if (isNodeActive(dag.status) || getActiveNodes(dag).length > 0) {
58
- card.classList.add("active-dag-card-running");
74
+ export function compareActiveDagUrgency(a, b) {
75
+ const rank = { attention: 0, running: 1, idle: 2 };
76
+ const diff = (rank[dagCardUrgency(a)] ?? 2) - (rank[dagCardUrgency(b)] ?? 2);
77
+ if (diff !== 0) return diff;
78
+ return dagSortTime(b).localeCompare(dagSortTime(a));
59
79
  }
60
- const title = dag.title || dag.dagRunId;
80
+
81
+ export function renderActiveDagCard(dag, nowMs) {
82
+ const card = el("div", "active-dag-card");
83
+ const urgency = dagCardUrgency(dag);
84
+ if (urgency === "attention") {
85
+ card.classList.add("active-dag-card-attention");
86
+ } else if (urgency === "running") {
87
+ card.classList.add("active-dag-card-running");
88
+ }
89
+ const title = dag.title || dag.dagRunId;
61
90
  const header = el("div", "active-dag-header");
62
91
  header.appendChild(el("span", "active-dag-title", title));
63
92
  header.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
@@ -1,5 +1,164 @@
1
1
  /** DOM helpers (Observe UI R5). */
2
2
 
3
+ /**
4
+ * Stable key → node maps, kept per (parent, scope) so reconcileKeyed never
5
+ * depends on dataset support. A WeakMap keyed by parent holds a map of
6
+ * scope → Map<key, node>. A stale parent (e.g. after view teardown) releases
7
+ * its bookkeeping automatically. The default scope "default" is used when a
8
+ * caller reconciles all keyed children of a parent in a single pass; multiple
9
+ * reconcilers that share one parent must pass distinct scope strings so they
10
+ * do not clobber each other's keys.
11
+ */
12
+ const reconcileMaps = new WeakMap();
13
+
14
+ /**
15
+ * Keyed DOM reconciliation: preserve node identity when a stable key recurs
16
+ * across renders; create a fresh node (with fresh closures) when the key is
17
+ * new; remove nodes whose key dropped out of the item set.
18
+ *
19
+ * Callers drive node construction via `create` and content sync via optional
20
+ * `update`. The parent is not bulk-cleared, so headings/headers placed outside
21
+ * this call survive, and scrolling/focus/pointer state on reused nodes is kept.
22
+ *
23
+ * @param {Node} parent
24
+ * @param {Array} items
25
+ * @param {{ getKey: (item: any, index: number) => string,
26
+ * create: (item: any, index: number) => Node,
27
+ * update?: (node: any, item: any, index: number) => void,
28
+ * scope?: string }} opts
29
+ * @returns {Array<Node>} the (possibly reused) nodes in item order
30
+ */
31
+ export function reconcileKeyed(parent, items, opts) {
32
+ const { getKey, create, update, scope = "default" } = opts;
33
+ let scopes = reconcileMaps.get(parent);
34
+ if (!scopes) {
35
+ scopes = new Map();
36
+ reconcileMaps.set(parent, scopes);
37
+ }
38
+ let map = scopes.get(scope);
39
+ if (!map) {
40
+ map = new Map();
41
+ scopes.set(scope, map);
42
+ }
43
+
44
+ const nextKeys = new Set();
45
+ const result = [];
46
+ for (let i = 0; i < items.length; i++) {
47
+ const item = items[i];
48
+ const key = String(getKey(item, i));
49
+ nextKeys.add(key);
50
+ let node = map.get(key);
51
+ if (node) {
52
+ if (update) update(node, item, i);
53
+ } else {
54
+ node = create(item, i);
55
+ map.set(key, node);
56
+ // Sync freshly created nodes so callers can split structure (create) from
57
+ // content (update) and still populate the first paint.
58
+ if (update) update(node, item, i);
59
+ }
60
+ result.push(node);
61
+ }
62
+
63
+ // Remove nodes whose key is no longer present in THIS scope.
64
+ for (const [key, node] of map) {
65
+ if (!nextKeys.has(key)) {
66
+ parent.removeChild(node);
67
+ map.delete(key);
68
+ }
69
+ }
70
+
71
+ const resultSet = new Set(result);
72
+ const currentOrder = childNodesOf(parent).filter((node) => resultSet.has(node));
73
+ const orderChanged =
74
+ currentOrder.length !== result.length ||
75
+ currentOrder.some((node, index) => node !== result[index]);
76
+ if (orderChanged) {
77
+ for (const node of result) parent.appendChild(node);
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ function childNodesOf(node) {
84
+ return Array.from(node.childNodes ?? node.children ?? []);
85
+ }
86
+
87
+ function nodeKind(node) {
88
+ if (node.nodeType === 3) return "#text";
89
+ return node.nodeName ?? node.tagName ?? "";
90
+ }
91
+
92
+ function syncElementAttributes(current, next) {
93
+ if (current.getAttributeNames && next.getAttributeNames) {
94
+ const nextNames = new Set(next.getAttributeNames());
95
+ for (const name of current.getAttributeNames()) {
96
+ if (!nextNames.has(name)) current.removeAttribute(name);
97
+ }
98
+ for (const name of nextNames) {
99
+ current.setAttribute(name, next.getAttribute(name));
100
+ }
101
+ } else if (current.attrs && next.attrs) {
102
+ for (const name of Object.keys(current.attrs)) {
103
+ if (!(name in next.attrs)) delete current.attrs[name];
104
+ }
105
+ for (const [name, value] of Object.entries(next.attrs)) {
106
+ current.setAttribute(name, value);
107
+ }
108
+ }
109
+
110
+ for (const prop of [
111
+ "className",
112
+ "href",
113
+ "title",
114
+ "target",
115
+ "type",
116
+ "value",
117
+ "hidden",
118
+ "tabIndex",
119
+ ]) {
120
+ if (prop in next) current[prop] = next[prop];
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Recursively sync a structurally compatible node without replacing compatible
126
+ * descendants. Existing event listeners and focusable node identity survive;
127
+ * incompatible tag/node kinds are reported to the caller for replacement.
128
+ */
129
+ export function syncCompatibleNode(current, next) {
130
+ if (!current || !next || nodeKind(current) !== nodeKind(next)) return false;
131
+ if (current.nodeType === 3 || nodeKind(current) === "#TEXT") {
132
+ current.textContent = next.textContent;
133
+ return true;
134
+ }
135
+
136
+ syncElementAttributes(current, next);
137
+ const currentChildren = childNodesOf(current);
138
+ const nextChildren = childNodesOf(next);
139
+ if (nextChildren.length === 0) {
140
+ current.textContent = next.textContent;
141
+ return true;
142
+ }
143
+
144
+ for (let index = 0; index < Math.max(currentChildren.length, nextChildren.length); index++) {
145
+ const currentChild = currentChildren[index];
146
+ const nextChild = nextChildren[index];
147
+ if (!nextChild && currentChild) {
148
+ current.removeChild(currentChild);
149
+ continue;
150
+ }
151
+ if (nextChild && !currentChild) {
152
+ current.appendChild(nextChild);
153
+ continue;
154
+ }
155
+ if (!syncCompatibleNode(currentChild, nextChild)) {
156
+ current.replaceChild(nextChild, currentChild);
157
+ }
158
+ }
159
+ return true;
160
+ }
161
+
3
162
  export function clearNode(node) {
4
163
  while (node.firstChild) node.removeChild(node.firstChild);
5
164
  }
@@ -0,0 +1,71 @@
1
+ export type PoolTaskLike = {
2
+ taskId?: string;
3
+ featureId?: string;
4
+ status?: string;
5
+ liveness?: string;
6
+ updatedAt?: string;
7
+ finishedAt?: string;
8
+ startedAt?: string;
9
+ title?: string;
10
+ priority?: string;
11
+ type?: string;
12
+ reason?: string;
13
+ recommendedFollowUp?: string;
14
+ failureCategory?: string;
15
+ [key: string]: unknown;
16
+ };
17
+
18
+ export type PoolStatusCounts = {
19
+ running: number;
20
+ pending: number;
21
+ blocked: number;
22
+ failed: number;
23
+ succeeded: number;
24
+ other: number;
25
+ };
26
+
27
+ export type PlannerReadyItem = {
28
+ featureId: string;
29
+ taskId: string;
30
+ title?: string;
31
+ priority?: string;
32
+ type?: string;
33
+ reason: string;
34
+ };
35
+
36
+ export type FeaturePlanningLike = {
37
+ featureId?: string;
38
+ planning?: {
39
+ selected?: Array<{
40
+ featureId?: string;
41
+ taskId?: string;
42
+ title?: string;
43
+ priority?: string;
44
+ type?: string;
45
+ reason?: string;
46
+ }>;
47
+ };
48
+ };
49
+
50
+ export function sortPoolTasks(tasks: PoolTaskLike[] | null | undefined): PoolTaskLike[];
51
+ export function matchesPoolStatusFilter(
52
+ taskStatus: string | null | undefined,
53
+ filter: string | null | undefined,
54
+ ): boolean;
55
+ export function poolStatusCounts(
56
+ tasks: Array<{ status?: string }> | null | undefined,
57
+ ): PoolStatusCounts;
58
+ export function isPoolAttentionTask(
59
+ task: PoolTaskLike | null | undefined,
60
+ ): boolean;
61
+ export function attentionRank(task: PoolTaskLike | null | undefined): number;
62
+ export function sortAttentionTasks(
63
+ tasks: PoolTaskLike[] | null | undefined,
64
+ ): PoolTaskLike[];
65
+ export function collectPlannerReadyTasks(
66
+ features: FeaturePlanningLike[] | null | undefined,
67
+ ): PlannerReadyItem[];
68
+ export function taskIdentityKey(
69
+ featureId: string | null | undefined,
70
+ taskId: string | null | undefined,
71
+ ): string;
@@ -65,3 +65,70 @@ export function poolStatusCounts(tasks) {
65
65
  }
66
66
  return counts;
67
67
  }
68
+
69
+ /** Tasks that need operator attention on the pool ops surface. */
70
+ export function isPoolAttentionTask(task) {
71
+ const st = (task?.status ?? "").toLowerCase();
72
+ const live = (task?.liveness ?? "").toLowerCase();
73
+ if (["failed", "error", "blocked", "stale"].includes(st)) return true;
74
+ if (["stale", "orphaned", "timeout-risk"].includes(live)) return true;
75
+ return false;
76
+ }
77
+
78
+ export function attentionRank(task) {
79
+ const st = (task?.status ?? "").toLowerCase();
80
+ const live = (task?.liveness ?? "").toLowerCase();
81
+ if (st === "failed" || st === "error") return 0;
82
+ if (live === "timeout-risk") return 1;
83
+ if (st === "stale" || live === "stale" || live === "orphaned") return 2;
84
+ if (st === "blocked") return 3;
85
+ return 9;
86
+ }
87
+
88
+ export function sortAttentionTasks(tasks) {
89
+ const list = (tasks ?? []).filter(isPoolAttentionTask);
90
+ list.sort((a, b) => {
91
+ const diff = attentionRank(a) - attentionRank(b);
92
+ if (diff !== 0) return diff;
93
+ const ta = a.updatedAt || a.finishedAt || a.startedAt || "";
94
+ const tb = b.updatedAt || b.finishedAt || b.startedAt || "";
95
+ return tb.localeCompare(ta);
96
+ });
97
+ return list;
98
+ }
99
+
100
+ /**
101
+ * Collect Ready Planner `selected` across features.
102
+ * Preserves snapshot feature order and each feature's planner-selected order.
103
+ * Does not re-rank Ready tasks on the client.
104
+ */
105
+ export function collectPlannerReadyTasks(features) {
106
+ const out = [];
107
+ const seen = new Set();
108
+ for (const feature of features ?? []) {
109
+ const featureId = feature?.featureId;
110
+ const selected = feature?.planning?.selected ?? [];
111
+ for (const item of selected) {
112
+ const taskId = item?.taskId;
113
+ if (!taskId) continue;
114
+ const fid = item.featureId ?? featureId;
115
+ if (!fid) continue;
116
+ const key = `${fid}\0${taskId}`;
117
+ if (seen.has(key)) continue;
118
+ seen.add(key);
119
+ out.push({
120
+ featureId: fid,
121
+ taskId,
122
+ title: item.title,
123
+ priority: item.priority,
124
+ type: item.type,
125
+ reason: item.reason ?? "dependencies-satisfied",
126
+ });
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+
132
+ export function taskIdentityKey(featureId, taskId) {
133
+ return `${featureId ?? ""}\0${taskId ?? ""}`;
134
+ }
@@ -16,7 +16,22 @@ export function parseMarkdownBlocks(markdown) {
16
16
  let code = null;
17
17
 
18
18
  const flushParagraph = () => {
19
- const text = paragraph.join(" ").trim();
19
+ if (paragraph.length === 0) return;
20
+ // Preserve Markdown hard breaks (trailing " " or "\") as \n segments.
21
+ // Soft-wrapped lines still join with a space.
22
+ let text = "";
23
+ let prevHardBreak = false;
24
+ for (let i = 0; i < paragraph.length; i += 1) {
25
+ const raw = paragraph[i] ?? "";
26
+ const hardBreak = / {2}$/.test(raw) || /\\$/.test(raw);
27
+ const cleaned = raw.replace(/(?: {2}|\\)$/, "").trim();
28
+ if (!cleaned && !hardBreak) continue;
29
+ if (i === 0 || text === "") text = cleaned;
30
+ else if (prevHardBreak) text += `\n${cleaned}`;
31
+ else text += ` ${cleaned}`;
32
+ prevHardBreak = hardBreak;
33
+ }
34
+ text = text.trim();
20
35
  if (text) blocks.push({ type: "paragraph", text });
21
36
  paragraph = [];
22
37
  };
@@ -114,7 +129,8 @@ export function parseMarkdownBlocks(markdown) {
114
129
  continue;
115
130
  }
116
131
  flushList();
117
- paragraph.push(line.trim());
132
+ // Keep trailing hard-break markers; trim only leading indent.
133
+ paragraph.push(line.replace(/^\s+/, ""));
118
134
  }
119
135
  if (code)
120
136
  blocks.push({
@@ -290,3 +306,12 @@ export function repoBaseName(repoRoot) {
290
306
  return parts[parts.length - 1] || trimmed || "未绑定仓库";
291
307
  }
292
308
 
309
+ /** Compact path for header display; full path stays in title/tooltip. */
310
+ export function formatRepoPathDisplay(repoRoot) {
311
+ if (!repoRoot) return "路径未知";
312
+ return String(repoRoot)
313
+ .replace(/\\/g, "/")
314
+ .replace(/^\/Users\/[^/]+/, "~")
315
+ .replace(/^\/home\/[^/]+/, "~");
316
+ }
317
+
@@ -51,21 +51,17 @@
51
51
  id="view-dashboard"
52
52
  class="view view-dashboard"
53
53
  >
54
- <div class="view-heading">
55
- <p class="view-kicker">OBSERVE</p>
56
- <h2>运行总览</h2>
57
- <p>聚焦正在推进的 DAG、需要处理的风险与近期运行记录。</p>
58
- </div>
59
- <div
60
- id="dashboard-repo"
61
- class="repo-banner"
62
- aria-label="当前仓库"
63
- ></div>
64
- <section
65
- id="dashboard-features"
66
- class="panel panel-features"
67
- aria-label="Feature 决策"
68
- ></section>
54
+ <header class="page-header dashboard-page-header">
55
+ <div class="view-heading page-header-heading">
56
+ <p class="view-kicker">OBSERVE</p>
57
+ <h2>运行总览</h2>
58
+ </div>
59
+ <div
60
+ id="dashboard-repo"
61
+ class="repo-banner repo-banner-inline"
62
+ aria-label="当前仓库"
63
+ ></div>
64
+ </header>
69
65
  <section
70
66
  id="dashboard-kpi"
71
67
  class="panel panel-kpi"
@@ -78,10 +74,15 @@
78
74
  ></section>
79
75
  <aside
80
76
  id="dashboard-risk"
81
- class="panel panel-risk"
77
+ class="panel panel-primary panel-risk"
82
78
  aria-label="运行风险"
83
79
  ></aside>
84
80
  </div>
81
+ <section
82
+ id="dashboard-features"
83
+ class="panel panel-features"
84
+ aria-label="Feature 决策"
85
+ ></section>
85
86
  <div class="dashboard-secondary">
86
87
  <section id="dashboard-dags" class="panel"></section>
87
88
  <section id="dashboard-batches" class="panel"></section>
@@ -189,38 +190,79 @@
189
190
  <section id="failures-list" class="panel panel-table"></section>
190
191
  </section>
191
192
 
192
- <section data-view="pool" id="view-pool" class="view" hidden>
193
- <div class="view-heading">
194
- <p class="view-kicker">TASK POOL</p>
195
- <h2>资源池总览</h2>
196
- <p>
197
- 盘点当前 Worker Task 状态分布与最新运行,无需翻查
198
- .harness/task-pool/。
199
- </p>
200
- </div>
193
+ <section data-view="pool" id="view-pool" class="view view-pool" hidden>
194
+ <header class="page-header pool-page-header">
195
+ <div class="view-heading page-header-heading">
196
+ <p class="view-kicker">TASK POOL</p>
197
+ <h2>资源池总览</h2>
198
+ </div>
199
+ <div id="pool-toolbar" class="pool-toolbar" aria-label="刷新控制">
200
+ <div class="pool-toolbar-actions">
201
+ <button
202
+ type="button"
203
+ id="pool-refresh-btn"
204
+ class="pool-refresh-btn"
205
+ >
206
+ 刷新
207
+ </button>
208
+ <label class="pool-auto-refresh-label">
209
+ <input type="checkbox" id="pool-auto-refresh" />
210
+ 自动刷新
211
+ </label>
212
+ <span id="pool-refresh-status" class="muted"></span>
213
+ </div>
214
+ <span
215
+ id="pool-updated-at"
216
+ class="muted pool-updated-at"
217
+ aria-live="polite"
218
+ ></span>
219
+ </div>
220
+ </header>
201
221
  <section
202
222
  id="pool-fault"
203
- class="panel"
223
+ class="panel panel-meta"
204
224
  aria-label="投影告警"
225
+ hidden
205
226
  ></section>
206
227
  <section
207
228
  id="pool-kpi"
208
229
  class="panel panel-kpi"
209
- aria-label="状态分布"
230
+ aria-label="池健康"
210
231
  ></section>
232
+ <div class="pool-workspace">
233
+ <section
234
+ id="pool-attention"
235
+ class="panel panel-primary"
236
+ aria-label="需处理"
237
+ ></section>
238
+ <section
239
+ id="pool-ready"
240
+ class="panel panel-primary"
241
+ aria-label="Ready"
242
+ ></section>
243
+ </div>
211
244
  <div id="pool-empty" class="panel empty-state" hidden></div>
245
+ <div
246
+ id="pool-filters"
247
+ class="pool-filters"
248
+ aria-label="清单筛选"
249
+ ></div>
212
250
  <section
213
251
  id="pool-tasks"
214
252
  class="panel panel-table"
215
- aria-label="当前 Task 列表"
216
- ></section>
217
- <section
218
- id="pool-events"
219
- class="panel panel-timeline"
220
- aria-label="最近事件"
253
+ aria-label="全部 Task"
221
254
  ></section>
255
+ <details id="pool-events-details" class="pool-events-details">
256
+ <summary>最近事件(可选,默认折叠)</summary>
257
+ <section
258
+ id="pool-events"
259
+ class="panel panel-timeline"
260
+ aria-label="最近事件"
261
+ ></section>
262
+ </details>
222
263
  <p class="panel muted advisory-note" id="pool-advisory">
223
- 数据为本地 derived/advisory 投影,仅供运营参考,非权威写路径。
264
+ 数据为本地 derived/advisory 投影,仅供运营参考,非权威写路径。Ready
265
+ 列表与 Ready Planner 同源,前端不重排。
224
266
  </p>
225
267
  </section>
226
268
 
@@ -1,12 +1,12 @@
1
1
  /** KPI cards, repo banner, projection fault, artifact/inbox links. */
2
2
  import { UI_TEXT } from "./constants.js";
3
3
  import { clearNode, el } from "./dom.js";
4
- import { repoBaseName } from "./format.js";
4
+ import { formatRepoPathDisplay, repoBaseName } from "./format.js";
5
5
  import { navigate } from "./router.js";
6
6
 
7
7
  export function failuresInboxLink(count) {
8
- const wrap = el("span");
9
- wrap.appendChild(document.createTextNode(`${count} `));
8
+ const wrap = el("span", "kpi-value-with-link");
9
+ wrap.appendChild(el("span", "kpi-value-num", String(count)));
10
10
  const a = el("a", "inbox-link", `${UI_TEXT.failureInbox} →`);
11
11
  a.href = "#/failures";
12
12
  a.addEventListener("click", (e) => {
@@ -59,13 +59,19 @@ export function renderRepoBanner(repoEl, snapshot) {
59
59
  if (!repoEl) return;
60
60
  clearNode(repoEl);
61
61
  const repoRoot = snapshot.repoRoot ?? "";
62
+ const iconWrap = el("span", "repo-banner-icon");
62
63
  const icon = el("i", "ri-git-repository-line");
63
64
  icon.setAttribute("aria-hidden", "true");
64
- repoEl.appendChild(icon);
65
+ iconWrap.appendChild(icon);
66
+ repoEl.appendChild(iconWrap);
65
67
  const text = el("div", "repo-banner-text");
66
68
  text.appendChild(el("span", "repo-banner-name", repoBaseName(repoRoot)));
67
- const pathLine = el("span", "repo-banner-path", repoRoot || "路径未知");
68
- pathLine.title = repoRoot || "";
69
+ const pathLine = el(
70
+ "span",
71
+ "repo-banner-path",
72
+ formatRepoPathDisplay(repoRoot),
73
+ );
74
+ pathLine.title = repoRoot || "路径未知";
69
75
  text.appendChild(pathLine);
70
76
  repoEl.appendChild(text);
71
77
  }