@tea-agent/loop-agent 0.25.5 → 0.26.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 (173) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +1020 -986
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/commands/cursor-prompt.js +6 -6
  5. package/dist/commands/init.js +68 -129
  6. package/dist/commands/loop-benchmark.js +11 -11
  7. package/dist/commands/pi-reuse-benchmark.js +16 -16
  8. package/dist/executors/dag-pi-executor.js +26 -20
  9. package/dist/executors/model-routing.js +34 -18
  10. package/dist/executors/shell-executor.js +1 -1
  11. package/dist/governance/manifest-types.js +33 -5
  12. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  13. package/dist/task/task-demand-routing.js +3 -1
  14. package/dist/worker/console/chat/model-resolver.js +15 -3
  15. package/dist/worker/observe/static/constants.js +3 -2
  16. package/dist/worker/observe/static/copy.js +67 -67
  17. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  18. package/dist/worker/observe/static/dag-layout.js +83 -83
  19. package/dist/worker/observe/static/dag-model.js +1 -0
  20. package/dist/worker/observe/static/dom.js +220 -220
  21. package/dist/worker/observe/static/relations.js +133 -133
  22. package/dist/worker/observe/static/router.js +93 -93
  23. package/dist/worker/observe/static/run-processing.js +148 -148
  24. package/dist/worker/observe/static/styles.css +182 -42
  25. package/dist/worker/observe/static/views/batch.js +227 -227
  26. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  27. package/dist/worker/observe/static/views/failures.js +143 -143
  28. package/dist/worker/observe/static/views/feature.js +492 -492
  29. package/dist/worker/observe/static/views/run.js +453 -453
  30. package/dist/worker/observe/static/views/shell.js +7 -7
  31. package/dist/worker/observe/static/views/timeline.js +163 -163
  32. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +362 -45
  33. package/dist/workflows/dag/backend-test-case-manifest.js +20 -0
  34. package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
  35. package/dist/workflows/dag/canvas-observer.js +275 -275
  36. package/dist/workflows/dag/init-hybrid.js +18 -15
  37. package/dist/workflows/dag/lifecycle.js +40 -30
  38. package/dist/workflows/dag/node-execution.js +13 -0
  39. package/dist/workflows/dag/types.js +59 -19
  40. package/docs/init-surface.manifest.json +3 -1
  41. package/docs/skills/README.md +7 -7
  42. package/docs/templates/README.md +1 -0
  43. package/docs/templates/adr.md +60 -60
  44. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  45. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  46. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  47. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  48. package/docs/templates/agent-dag-report.schema.json +473 -473
  49. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  50. package/docs/templates/backend-test-dag.json +9 -9
  51. package/docs/templates/backend-test-result.schema.json +99 -99
  52. package/docs/templates/feature-spec.md +53 -53
  53. package/docs/templates/frontend-design-contract.md +42 -42
  54. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  55. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  56. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  57. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  58. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  59. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  60. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  61. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  62. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  63. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  64. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  65. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  66. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  67. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  68. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  69. package/docs/templates/frontend-eval/metrics.md +138 -138
  70. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  71. package/docs/templates/frontend-task-constraints.md +35 -35
  72. package/docs/templates/frontend-task-requirement.md +70 -70
  73. package/docs/templates/harness.schema.json +29 -7
  74. package/docs/templates/init-evolution-review.md +35 -35
  75. package/docs/templates/init-managed-agents.md +137 -0
  76. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  77. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  78. package/docs/templates/knowledge-sync-dag.json +178 -178
  79. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  80. package/docs/templates/product-line/closeout.yaml +9 -9
  81. package/docs/templates/product-line/design.md +13 -13
  82. package/docs/templates/product-line/links.md +10 -10
  83. package/docs/templates/product-line/requirement.md +17 -17
  84. package/docs/templates/product-line/test-plan.md +7 -7
  85. package/docs/templates/production-readiness-checklist.md +57 -57
  86. package/docs/templates/project-start-checklist.md +9 -9
  87. package/docs/templates/qa-report.md +48 -48
  88. package/docs/templates/sprint-contract.md +29 -29
  89. package/docs/templates/worker-dogfood-evidence.md +80 -80
  90. package/docs/templates/worker-dogfood-setup.md +68 -68
  91. package/harness.json +1 -2
  92. package/package.json +1 -1
  93. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  94. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  95. package/scripts/kb-graph-materialize.mjs +105 -105
  96. package/scripts/kb-graph-promote.mjs +164 -164
  97. package/scripts/kb-query.mjs +554 -554
  98. package/skills/ai-engineering-context/SKILL.md +48 -48
  99. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  100. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  101. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  102. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  103. package/skills/analyze-product-dependencies/references/example.md +76 -76
  104. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  105. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  106. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  107. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  108. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  109. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  110. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  111. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  112. package/skills/analyze-product-requirements/SKILL.md +90 -90
  113. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  114. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  115. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  116. package/skills/analyze-product-requirements/references/example.md +86 -86
  117. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  118. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  119. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  120. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  121. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  122. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  123. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  124. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  125. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  126. package/skills/browser-tools/browser-content.js +103 -103
  127. package/skills/browser-tools/browser-cookies.js +35 -35
  128. package/skills/browser-tools/browser-eval.js +53 -53
  129. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  130. package/skills/browser-tools/browser-nav.js +44 -44
  131. package/skills/browser-tools/browser-pick.js +162 -162
  132. package/skills/browser-tools/browser-screenshot.js +34 -34
  133. package/skills/browser-tools/browser-start.js +86 -86
  134. package/skills/browser-tools/package-lock.json +2556 -2556
  135. package/skills/browser-tools/package.json +19 -19
  136. package/skills/code-review-core/SKILL.md +20 -20
  137. package/skills/codebase-scout/SKILL.md +19 -19
  138. package/skills/grill-me/SKILL.md +10 -10
  139. package/skills/loop-agent/references/README.md +67 -67
  140. package/skills/loop-agent/references/command-reference.md +5 -4
  141. package/skills/loop-agent/references/docs-converge.md +126 -126
  142. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  143. package/skills/loop-agent/references/learned/README.md +21 -21
  144. package/skills/loop-agent/references/long-running-loop.md +57 -57
  145. package/skills/loop-agent/references/model-routing.md +2 -0
  146. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  147. package/skills/loop-agent/references/pi-prompt.md +23 -23
  148. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  149. package/skills/playwright-cli/SKILL.md +420 -420
  150. package/skills/playwright-cli/references/element-attributes.md +23 -23
  151. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  152. package/skills/playwright-cli/references/request-mocking.md +87 -87
  153. package/skills/playwright-cli/references/running-code.md +241 -241
  154. package/skills/playwright-cli/references/session-management.md +225 -225
  155. package/skills/playwright-cli/references/storage-state.md +275 -275
  156. package/skills/playwright-cli/references/test-generation.md +433 -433
  157. package/skills/playwright-cli/references/tracing.md +139 -139
  158. package/skills/playwright-cli/references/video-recording.md +143 -143
  159. package/skills/requesting-code-review/SKILL.md +101 -101
  160. package/skills/requesting-code-review/code-reviewer.md +168 -168
  161. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  162. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  163. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  164. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  165. package/skills/systematic-debugging/find-polluter.sh +63 -63
  166. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  167. package/skills/systematic-debugging/test-academic.md +14 -14
  168. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  169. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  170. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  171. package/skills/using-git-worktrees/SKILL.md +215 -215
  172. package/skills/verification-before-completion/SKILL.md +154 -154
  173. package/skills/webapp-testing/SKILL.md +19 -19
@@ -98,6 +98,15 @@ export const backendTestCaseManifestSchema = z
98
98
  .strict(),
99
99
  materializationStatus: z.enum(["available", "partial", "unavailable"]).optional(),
100
100
  sourceFactsIssues: z.array(z.string().min(1)).optional(),
101
+ coverageScope: z.object({
102
+ changeClassification: z.enum(["new-operation", "contract-change", "behavior-change", "bugfix", "implementation-optimization"]),
103
+ coveragePolicy: z.enum(["full-contract", "affected-contract-full", "affected-behavior-full", "reproduction-plus-neighbors", "change-focused-plus-regression-floor"]),
104
+ affectedOperations: z.array(z.string().min(1)),
105
+ affectedRuleKeys: z.array(z.string().min(1)),
106
+ regressionFloor: z.array(z.string().min(1)),
107
+ scopeEvidence: z.array(z.string().min(1)),
108
+ completenessClaim: z.enum(["affected-operations-full", "affected-scope"]),
109
+ }).strict().optional(),
101
110
  ruleCoverageSummary: z.object({
102
111
  explicitAcCount: z.number().int().min(0),
103
112
  coveredAcCount: z.number().int().min(0),
@@ -117,18 +126,29 @@ export const backendTestCaseManifestSchema = z
117
126
  coveredBusinessStateCount: z.number().int().min(0),
118
127
  gapCount: z.number().int().min(0),
119
128
  conflictCount: z.number().int().min(0),
129
+ variantTestPointCount: z.number().int().min(0),
130
+ assertionTestPointCount: z.number().int().min(0),
131
+ crossCuttingTestPointCount: z.number().int().min(0),
132
+ unclassifiedTestPointCount: z.number().int().min(0),
133
+ duplicateBindingTestPointCount: z.number().int().min(0),
120
134
  }).strict().optional(),
121
135
  correspondenceSummary: z.object({
122
136
  markdownModuleCount: z.number().int().min(0),
123
137
  exactModuleCount: z.number().int().min(0),
124
138
  markdownCaseCount: z.number().int().min(0),
125
139
  exactCorrespondenceCount: z.number().int().min(0),
140
+ primarySymbolCount: z.number().int().min(0),
126
141
  missingPytestCount: z.number().int().min(0),
127
142
  multiplePytestCount: z.number().int().min(0),
128
143
  extraPytestCount: z.number().int().min(0),
129
144
  scriptMismatchCount: z.number().int().min(0),
130
145
  testPointCount: z.number().int().min(0),
131
146
  mappedTestPointCount: z.number().int().min(0),
147
+ variantTestPointCount: z.number().int().min(0),
148
+ assertionTestPointCount: z.number().int().min(0),
149
+ crossCuttingTestPointCount: z.number().int().min(0),
150
+ unclassifiedTestPointCount: z.number().int().min(0),
151
+ duplicateBindingTestPointCount: z.number().int().min(0),
132
152
  }).strict().optional(),
133
153
  artifactRefs: z.object({
134
154
  caseCoverageFacts: z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict(),
@@ -1699,6 +1699,17 @@ export function renderBackendTestFacts(input) {
1699
1699
  const caseCoverage = parseAdvisorySummary(input.caseCoverageSummary);
1700
1700
  const traceability = parseAdvisorySummary(input.traceabilitySummary);
1701
1701
  const correspondence = parseAdvisorySummary(input.correspondenceSummary);
1702
+ const correspondenceNumber = (label) => {
1703
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1704
+ const match = new RegExp(`^- ${escaped}: (\\d+)$`, "mi").exec(input.correspondenceSummary ?? "");
1705
+ return match ? Number.parseInt(match[1], 10) : undefined;
1706
+ };
1707
+ const markdownCaseCount = correspondenceNumber("Markdown Cases");
1708
+ const primarySymbolCount = correspondenceNumber("Primary pytest symbols");
1709
+ const variantTestPointCount = correspondenceNumber("Variant Test Points");
1710
+ const assertionTestPointCount = correspondenceNumber("Assertion Test Points");
1711
+ const crossCuttingTestPointCount = correspondenceNumber("Cross-Cutting Test Points");
1712
+ const executionAmplification = markdownCaseCount && markdownCaseCount > 0 ? input.parsed.tests / markdownCaseCount : undefined;
1702
1713
  const catalog = new Map((input.cases ?? []).map((entry) => [entry.id, entry]));
1703
1714
  return ([
1704
1715
  "# 后端自动化测试执行报告",
@@ -1712,6 +1723,12 @@ export function renderBackendTestFacts(input) {
1712
1723
  `| ${input.pytestExitCode === 0 ? "通过" : "未通过"} | ${input.parsed.tests} | ${input.parsed.passed} | ${input.parsed.failed} | ${input.parsed.errors} | ${input.parsed.skipped} | ${(passRate * 100).toFixed(2)}% | ${formatDuration(input.parsed.durationMs)} |`,
1713
1724
  "",
1714
1725
  `- Pytest exit code: ${input.pytestExitCode}`,
1726
+ `- Markdown Cases: ${markdownCaseCount ?? "unavailable"}`,
1727
+ `- Primary pytest symbols: ${primarySymbolCount ?? "unavailable"}`,
1728
+ `- Collected pytest items: ${input.parsed.tests}`,
1729
+ `- Test Point bindings: variant=${variantTestPointCount ?? "unavailable"}, assertion=${assertionTestPointCount ?? "unavailable"}, cross-cutting=${crossCuttingTestPointCount ?? "unavailable"}`,
1730
+ `- Execution amplification (items/case): ${executionAmplification === undefined ? "unavailable" : executionAmplification.toFixed(2)}`,
1731
+ "- Pytest item count is an execution-unit count, not a business-scenario count.",
1715
1732
  "- Pytest-html report: valid",
1716
1733
  "- HTML valid: yes",
1717
1734
  "",
@@ -195,280 +195,280 @@ export function createDagCanvasObserver(opts) {
195
195
  flush: () => writer.flush(),
196
196
  };
197
197
  }
198
- const CANVAS_HEADER = `/* AUTO-GENERATED by . run-dag canvas observer. Derived view only — canonical state lives in .harness/dag-runs/. */
199
- import {
200
- Card,
201
- CardBody,
202
- CardHeader,
203
- Divider,
204
- Grid,
205
- H1,
206
- H2,
207
- Pill,
208
- Row,
209
- Stack,
210
- Stat,
211
- Text,
212
- computeDAGLayout,
213
- useHostTheme,
214
- } from 'cursor/canvas';
215
- import { useMemo } from 'react';
216
-
217
- type TaskStatus = 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'SKIPPED';
218
- type Complexity = 'HIGH' | 'MED' | 'LOW';
219
- type Executor = 'cursor' | 'pi' | 'shell' | 'static';
220
-
221
- interface TaskState {
222
- id: string;
223
- depends_on: string[];
224
- complexity: Complexity;
225
- executor: Executor;
226
- subtask_prompt: string;
227
- status: TaskStatus;
228
- model: string;
229
- startedAt?: number;
230
- finishedAt?: number;
231
- resultText?: string;
232
- errorMessage?: string;
233
- inputTokens?: number;
234
- outputTokens?: number;
235
- durationMs?: number;
236
- skippedReason?: string;
237
- }
238
-
239
- interface RunState {
240
- title: string;
241
- runId: string;
242
- startedAt: number;
243
- finishedAt?: number;
244
- runOutcome?: 'SUCCESS' | 'FAILED' | 'INTERRUPTED' | 'PAUSED';
245
- runMessage?: string;
246
- tasks: TaskState[];
198
+ const CANVAS_HEADER = `/* AUTO-GENERATED by . run-dag canvas observer. Derived view only — canonical state lives in .harness/dag-runs/. */
199
+ import {
200
+ Card,
201
+ CardBody,
202
+ CardHeader,
203
+ Divider,
204
+ Grid,
205
+ H1,
206
+ H2,
207
+ Pill,
208
+ Row,
209
+ Stack,
210
+ Stat,
211
+ Text,
212
+ computeDAGLayout,
213
+ useHostTheme,
214
+ } from 'cursor/canvas';
215
+ import { useMemo } from 'react';
216
+
217
+ type TaskStatus = 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'SKIPPED';
218
+ type Complexity = 'HIGH' | 'MED' | 'LOW';
219
+ type Executor = 'cursor' | 'pi' | 'shell' | 'static';
220
+
221
+ interface TaskState {
222
+ id: string;
223
+ depends_on: string[];
224
+ complexity: Complexity;
225
+ executor: Executor;
226
+ subtask_prompt: string;
227
+ status: TaskStatus;
228
+ model: string;
229
+ startedAt?: number;
230
+ finishedAt?: number;
231
+ resultText?: string;
232
+ errorMessage?: string;
233
+ inputTokens?: number;
234
+ outputTokens?: number;
235
+ durationMs?: number;
236
+ skippedReason?: string;
237
+ }
238
+
239
+ interface RunState {
240
+ title: string;
241
+ runId: string;
242
+ startedAt: number;
243
+ finishedAt?: number;
244
+ runOutcome?: 'SUCCESS' | 'FAILED' | 'INTERRUPTED' | 'PAUSED';
245
+ runMessage?: string;
246
+ tasks: TaskState[];
247
247
  }`;
248
- const CANVAS_BODY = String.raw `const NODE_W = 200;
249
- const NODE_H = 72;
250
-
251
- function pillToneFor(status: TaskStatus): 'neutral' | 'info' | 'success' | 'warning' {
252
- switch (status) {
253
- case 'PENDING':
254
- return 'neutral';
255
- case 'RUNNING':
256
- return 'info';
257
- case 'FINISHED':
258
- return 'success';
259
- case 'ERROR':
260
- return 'warning';
261
- case 'SKIPPED':
262
- return 'neutral';
263
- }
264
- }
265
-
266
- function formatDuration(ms?: number): string {
267
- if (ms === undefined) return '—';
268
- if (ms < 1000) return ms + 'ms';
269
- const s = ms / 1000;
270
- if (s < 60) return s.toFixed(1) + 's';
271
- const m = Math.floor(s / 60);
272
- const rem = Math.round(s - m * 60);
273
- return m + 'm ' + rem + 's';
274
- }
275
-
276
- function elapsed(state: RunState): number {
277
- const end = state.finishedAt ?? Date.now();
278
- return end - state.startedAt;
279
- }
280
-
281
- function DAGGraph({ state }: { state: RunState }): JSX.Element {
282
- const theme = useHostTheme();
283
- const layout = computeDAGLayout({
284
- nodes: state.tasks.map((t) => ({ id: t.id })),
285
- edges: state.tasks.flatMap((t) =>
286
- t.depends_on.map((d) => ({ from: d, to: t.id })),
287
- ),
288
- direction: 'vertical',
289
- nodeWidth: NODE_W,
290
- nodeHeight: NODE_H,
291
- rankGap: 72,
292
- nodeGap: 40,
293
- padding: 24,
294
- });
295
- const byId = new Map(state.tasks.map((t) => [t.id, t]));
296
-
297
- return (
298
- <svg width={layout.width} height={layout.height} style={{ display: 'block', maxWidth: '100%' }}>
299
- {layout.edges.map((e, i) => (
300
- <line
301
- key={'e' + i}
302
- x1={e.sourceX}
303
- y1={e.sourceY}
304
- x2={e.targetX}
305
- y2={e.targetY}
306
- stroke={theme.stroke.secondary}
307
- strokeWidth={1.25}
308
- markerEnd="url(#dag-arrow)"
309
- />
310
- ))}
311
- <defs>
312
- <marker id="dag-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
313
- <path d="M 0 0 L 10 5 L 0 10 z" fill={theme.stroke.secondary} />
314
- </marker>
315
- </defs>
316
- {layout.nodes.map((n) => {
317
- const t = byId.get(n.id);
318
- if (!t) return null;
319
- return (
320
- <g key={n.id} transform={'translate(' + n.x + ',' + n.y + ')'}>
321
- <rect
322
- width={NODE_W}
323
- height={NODE_H}
324
- rx={8}
325
- ry={8}
326
- fill={theme.fill.secondary}
327
- stroke={theme.stroke.secondary}
328
- strokeWidth={t.status === 'RUNNING' ? 2 : 1}
329
- />
330
- <text x={12} y={22} fontSize={13} fontWeight={600} fill={theme.text.primary}>
331
- {t.id.length > 22 ? t.id.slice(0, 21) + '…' : t.id}
332
- </text>
333
- <text x={12} y={42} fontSize={10.5} fill={theme.text.secondary}>
334
- {t.executor} · {t.complexity} · {t.status}
335
- </text>
336
- <text x={12} y={58} fontSize={10.5} fill={theme.text.tertiary}>
337
- {formatDuration(t.durationMs)}
338
- </text>
339
- </g>
340
- );
341
- })}
342
- </svg>
343
- );
344
- }
345
-
346
- export default function DagRun(): JSX.Element {
347
- const counts = useMemo(
348
- () =>
349
- STATE.tasks.reduce(
350
- (acc, t) => {
351
- acc.total += 1;
352
- const key = t.status.toLowerCase() as 'pending' | 'running' | 'finished' | 'error' | 'skipped';
353
- if (key in acc) acc[key] += 1;
354
- return acc;
355
- },
356
- { total: 0, pending: 0, running: 0, finished: 0, error: 0, skipped: 0 },
357
- ),
358
- [],
359
- );
360
- const isFinal = STATE.finishedAt !== undefined;
361
- const statusLabel =
362
- STATE.runOutcome === 'PAUSED'
363
- ? 'PAUSED'
364
- : STATE.runOutcome === 'FAILED'
365
- ? 'FAILED'
366
- : isFinal
367
- ? 'COMPLETE'
368
- : 'RUNNING';
369
- const statusTone =
370
- STATE.runOutcome === 'FAILED'
371
- ? 'danger'
372
- : STATE.runOutcome === 'PAUSED'
373
- ? 'warning'
374
- : isFinal
375
- ? 'success'
376
- : 'info';
377
-
378
- return (
379
- <Stack gap={20}>
380
- <Stack gap={6}>
381
- <H1>{STATE.title}</H1>
382
- <Text tone="secondary" size="small">
383
- run {STATE.runId}
384
- </Text>
385
- <Row gap={10} align="center">
386
- <Pill tone={statusTone} active size="sm">
387
- {statusLabel}
388
- </Pill>
389
- <Text tone="secondary" size="small">
390
- {counts.total} tasks · elapsed {formatDuration(elapsed(STATE))}
391
- </Text>
392
- </Row>
393
- {STATE.runMessage ? (
394
- <Text tone="secondary" size="small">
395
- {STATE.runMessage}
396
- </Text>
397
- ) : null}
398
- </Stack>
399
-
400
- <Grid columns={6} gap={12}>
401
- <Stat value={String(counts.total)} label="Total" />
402
- <Stat value={String(counts.pending)} label="Pending" />
403
- <Stat value={String(counts.running)} label="Running" tone={counts.running > 0 ? 'info' : undefined} />
404
- <Stat value={String(counts.finished)} label="Finished" tone={counts.finished > 0 ? 'success' : undefined} />
405
- <Stat value={String(counts.error)} label="Errored" tone={counts.error > 0 ? 'danger' : undefined} />
406
- <Stat value={String(counts.skipped)} label="Skipped" />
407
- </Grid>
408
-
409
- <Divider />
410
-
411
- <Stack gap={12}>
412
- <H2>Graph</H2>
413
- <DAGGraph state={STATE} />
414
- </Stack>
415
-
416
- <Divider />
417
-
418
- <Stack gap={12}>
419
- <H2>Tasks</H2>
420
- <Stack gap={10}>
421
- {STATE.tasks.map((t) => (
422
- <Card key={t.id} collapsible defaultOpen={t.status !== 'PENDING'}>
423
- <CardHeader
424
- trailing={
425
- <Row gap={6} align="center">
426
- <Pill tone="neutral" size="sm">
427
- {t.executor}
428
- </Pill>
429
- <Pill tone={pillToneFor(t.status)} active={t.status !== 'PENDING'} size="sm">
430
- {t.status}
431
- </Pill>
432
- </Row>
433
- }
434
- >
435
- {t.id}
436
- </CardHeader>
437
- <CardBody>
438
- <Stack gap={8}>
439
- <Text tone="secondary" size="small">
440
- Model {t.model}
441
- {t.depends_on.length > 0 ? ' · depends on ' + t.depends_on.join(', ') : ''}
442
- {t.durationMs !== undefined ? ' · ' + formatDuration(t.durationMs) : ''}
443
- </Text>
444
- <Text size="small">
445
- <Text as="span" weight="semibold">
446
- Prompt:{' '}
447
- </Text>
448
- {t.subtask_prompt}
449
- </Text>
450
- {t.resultText ? (
451
- <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
452
- {t.resultText}
453
- {t.status === 'RUNNING' ? '\u2588' : ''}
454
- </pre>
455
- ) : null}
456
- {t.skippedReason ? (
457
- <Text size="small" tone="secondary">
458
- Skipped: {t.skippedReason}
459
- </Text>
460
- ) : null}
461
- {t.errorMessage ? (
462
- <Text size="small" tone="secondary">
463
- {t.errorMessage}
464
- </Text>
465
- ) : null}
466
- </Stack>
467
- </CardBody>
468
- </Card>
469
- ))}
470
- </Stack>
471
- </Stack>
472
- </Stack>
473
- );
248
+ const CANVAS_BODY = String.raw `const NODE_W = 200;
249
+ const NODE_H = 72;
250
+
251
+ function pillToneFor(status: TaskStatus): 'neutral' | 'info' | 'success' | 'warning' {
252
+ switch (status) {
253
+ case 'PENDING':
254
+ return 'neutral';
255
+ case 'RUNNING':
256
+ return 'info';
257
+ case 'FINISHED':
258
+ return 'success';
259
+ case 'ERROR':
260
+ return 'warning';
261
+ case 'SKIPPED':
262
+ return 'neutral';
263
+ }
264
+ }
265
+
266
+ function formatDuration(ms?: number): string {
267
+ if (ms === undefined) return '—';
268
+ if (ms < 1000) return ms + 'ms';
269
+ const s = ms / 1000;
270
+ if (s < 60) return s.toFixed(1) + 's';
271
+ const m = Math.floor(s / 60);
272
+ const rem = Math.round(s - m * 60);
273
+ return m + 'm ' + rem + 's';
274
+ }
275
+
276
+ function elapsed(state: RunState): number {
277
+ const end = state.finishedAt ?? Date.now();
278
+ return end - state.startedAt;
279
+ }
280
+
281
+ function DAGGraph({ state }: { state: RunState }): JSX.Element {
282
+ const theme = useHostTheme();
283
+ const layout = computeDAGLayout({
284
+ nodes: state.tasks.map((t) => ({ id: t.id })),
285
+ edges: state.tasks.flatMap((t) =>
286
+ t.depends_on.map((d) => ({ from: d, to: t.id })),
287
+ ),
288
+ direction: 'vertical',
289
+ nodeWidth: NODE_W,
290
+ nodeHeight: NODE_H,
291
+ rankGap: 72,
292
+ nodeGap: 40,
293
+ padding: 24,
294
+ });
295
+ const byId = new Map(state.tasks.map((t) => [t.id, t]));
296
+
297
+ return (
298
+ <svg width={layout.width} height={layout.height} style={{ display: 'block', maxWidth: '100%' }}>
299
+ {layout.edges.map((e, i) => (
300
+ <line
301
+ key={'e' + i}
302
+ x1={e.sourceX}
303
+ y1={e.sourceY}
304
+ x2={e.targetX}
305
+ y2={e.targetY}
306
+ stroke={theme.stroke.secondary}
307
+ strokeWidth={1.25}
308
+ markerEnd="url(#dag-arrow)"
309
+ />
310
+ ))}
311
+ <defs>
312
+ <marker id="dag-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
313
+ <path d="M 0 0 L 10 5 L 0 10 z" fill={theme.stroke.secondary} />
314
+ </marker>
315
+ </defs>
316
+ {layout.nodes.map((n) => {
317
+ const t = byId.get(n.id);
318
+ if (!t) return null;
319
+ return (
320
+ <g key={n.id} transform={'translate(' + n.x + ',' + n.y + ')'}>
321
+ <rect
322
+ width={NODE_W}
323
+ height={NODE_H}
324
+ rx={8}
325
+ ry={8}
326
+ fill={theme.fill.secondary}
327
+ stroke={theme.stroke.secondary}
328
+ strokeWidth={t.status === 'RUNNING' ? 2 : 1}
329
+ />
330
+ <text x={12} y={22} fontSize={13} fontWeight={600} fill={theme.text.primary}>
331
+ {t.id.length > 22 ? t.id.slice(0, 21) + '…' : t.id}
332
+ </text>
333
+ <text x={12} y={42} fontSize={10.5} fill={theme.text.secondary}>
334
+ {t.executor} · {t.complexity} · {t.status}
335
+ </text>
336
+ <text x={12} y={58} fontSize={10.5} fill={theme.text.tertiary}>
337
+ {formatDuration(t.durationMs)}
338
+ </text>
339
+ </g>
340
+ );
341
+ })}
342
+ </svg>
343
+ );
344
+ }
345
+
346
+ export default function DagRun(): JSX.Element {
347
+ const counts = useMemo(
348
+ () =>
349
+ STATE.tasks.reduce(
350
+ (acc, t) => {
351
+ acc.total += 1;
352
+ const key = t.status.toLowerCase() as 'pending' | 'running' | 'finished' | 'error' | 'skipped';
353
+ if (key in acc) acc[key] += 1;
354
+ return acc;
355
+ },
356
+ { total: 0, pending: 0, running: 0, finished: 0, error: 0, skipped: 0 },
357
+ ),
358
+ [],
359
+ );
360
+ const isFinal = STATE.finishedAt !== undefined;
361
+ const statusLabel =
362
+ STATE.runOutcome === 'PAUSED'
363
+ ? 'PAUSED'
364
+ : STATE.runOutcome === 'FAILED'
365
+ ? 'FAILED'
366
+ : isFinal
367
+ ? 'COMPLETE'
368
+ : 'RUNNING';
369
+ const statusTone =
370
+ STATE.runOutcome === 'FAILED'
371
+ ? 'danger'
372
+ : STATE.runOutcome === 'PAUSED'
373
+ ? 'warning'
374
+ : isFinal
375
+ ? 'success'
376
+ : 'info';
377
+
378
+ return (
379
+ <Stack gap={20}>
380
+ <Stack gap={6}>
381
+ <H1>{STATE.title}</H1>
382
+ <Text tone="secondary" size="small">
383
+ run {STATE.runId}
384
+ </Text>
385
+ <Row gap={10} align="center">
386
+ <Pill tone={statusTone} active size="sm">
387
+ {statusLabel}
388
+ </Pill>
389
+ <Text tone="secondary" size="small">
390
+ {counts.total} tasks · elapsed {formatDuration(elapsed(STATE))}
391
+ </Text>
392
+ </Row>
393
+ {STATE.runMessage ? (
394
+ <Text tone="secondary" size="small">
395
+ {STATE.runMessage}
396
+ </Text>
397
+ ) : null}
398
+ </Stack>
399
+
400
+ <Grid columns={6} gap={12}>
401
+ <Stat value={String(counts.total)} label="Total" />
402
+ <Stat value={String(counts.pending)} label="Pending" />
403
+ <Stat value={String(counts.running)} label="Running" tone={counts.running > 0 ? 'info' : undefined} />
404
+ <Stat value={String(counts.finished)} label="Finished" tone={counts.finished > 0 ? 'success' : undefined} />
405
+ <Stat value={String(counts.error)} label="Errored" tone={counts.error > 0 ? 'danger' : undefined} />
406
+ <Stat value={String(counts.skipped)} label="Skipped" />
407
+ </Grid>
408
+
409
+ <Divider />
410
+
411
+ <Stack gap={12}>
412
+ <H2>Graph</H2>
413
+ <DAGGraph state={STATE} />
414
+ </Stack>
415
+
416
+ <Divider />
417
+
418
+ <Stack gap={12}>
419
+ <H2>Tasks</H2>
420
+ <Stack gap={10}>
421
+ {STATE.tasks.map((t) => (
422
+ <Card key={t.id} collapsible defaultOpen={t.status !== 'PENDING'}>
423
+ <CardHeader
424
+ trailing={
425
+ <Row gap={6} align="center">
426
+ <Pill tone="neutral" size="sm">
427
+ {t.executor}
428
+ </Pill>
429
+ <Pill tone={pillToneFor(t.status)} active={t.status !== 'PENDING'} size="sm">
430
+ {t.status}
431
+ </Pill>
432
+ </Row>
433
+ }
434
+ >
435
+ {t.id}
436
+ </CardHeader>
437
+ <CardBody>
438
+ <Stack gap={8}>
439
+ <Text tone="secondary" size="small">
440
+ Model {t.model}
441
+ {t.depends_on.length > 0 ? ' · depends on ' + t.depends_on.join(', ') : ''}
442
+ {t.durationMs !== undefined ? ' · ' + formatDuration(t.durationMs) : ''}
443
+ </Text>
444
+ <Text size="small">
445
+ <Text as="span" weight="semibold">
446
+ Prompt:{' '}
447
+ </Text>
448
+ {t.subtask_prompt}
449
+ </Text>
450
+ {t.resultText ? (
451
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
452
+ {t.resultText}
453
+ {t.status === 'RUNNING' ? '\u2588' : ''}
454
+ </pre>
455
+ ) : null}
456
+ {t.skippedReason ? (
457
+ <Text size="small" tone="secondary">
458
+ Skipped: {t.skippedReason}
459
+ </Text>
460
+ ) : null}
461
+ {t.errorMessage ? (
462
+ <Text size="small" tone="secondary">
463
+ {t.errorMessage}
464
+ </Text>
465
+ ) : null}
466
+ </Stack>
467
+ </CardBody>
468
+ </Card>
469
+ ))}
470
+ </Stack>
471
+ </Stack>
472
+ </Stack>
473
+ );
474
474
  }`;