@tea-agent/loop-agent 0.5.0 → 0.7.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 (136) hide show
  1. package/AGENTS.md +142 -142
  2. package/CHANGELOG.md +132 -98
  3. package/README.md +195 -195
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +9 -1
  7. package/dist/application/dag/run-dag.js +16 -2
  8. package/dist/cli/command-definitions.js +22 -4
  9. package/dist/cli/help.js +3 -2
  10. package/dist/cli/program.js +7 -5
  11. package/dist/commands/import-prd.js +76 -0
  12. package/dist/commands/init.js +467 -457
  13. package/dist/commands/instructions.js +90 -58
  14. package/dist/commands/loop-benchmark.js +11 -11
  15. package/dist/commands/pi-reuse-benchmark.js +16 -16
  16. package/dist/executors/cursor-executor.js +1 -1
  17. package/dist/executors/dag-pi-executor.js +1 -0
  18. package/dist/executors/pi-sdk-executor.js +63 -1
  19. package/dist/shared/preview.js +39 -0
  20. package/dist/task/config-types.js +3 -0
  21. package/dist/task/runtime.js +27 -27
  22. package/dist/task/source-references.js +221 -0
  23. package/dist/worker/cli.js +62 -1
  24. package/dist/worker/loop-agent/loop-agent-client.js +97 -5
  25. package/dist/worker/materialize/harness-task-materializer.js +166 -5
  26. package/dist/worker/observability/event-store.js +82 -0
  27. package/dist/worker/observability/events.js +79 -0
  28. package/dist/worker/observability/progress-composite.js +33 -0
  29. package/dist/worker/observability/read-model.js +1013 -0
  30. package/dist/worker/observability/snapshot-store.js +43 -0
  31. package/dist/worker/observability/types.js +1 -0
  32. package/dist/worker/observe/paths.js +64 -0
  33. package/dist/worker/observe/routes.js +423 -0
  34. package/dist/worker/observe/server.js +61 -0
  35. package/dist/worker/observe/static/app.js +1419 -0
  36. package/dist/worker/observe/static/index.html +63 -0
  37. package/dist/worker/observe/static/styles.css +613 -0
  38. package/dist/worker/pool/failure-routing.js +41 -6
  39. package/dist/worker/pool/run-store.js +50 -0
  40. package/dist/worker/progress-reporter.js +0 -18
  41. package/dist/worker/run-task/run-task.js +327 -92
  42. package/dist/worker/runner/run-ready.js +112 -4
  43. package/dist/worker/task-spec/schema.js +2 -1
  44. package/dist/workflows/dag/canvas-observer.js +275 -275
  45. package/dist/workflows/dag/event-observer.js +132 -0
  46. package/dist/workflows/dag/init-hybrid.js +182 -21
  47. package/dist/workflows/dag/observer-compose.js +52 -0
  48. package/docs/README.md +75 -72
  49. package/docs/agent-dag-recovery-playbook.md +184 -184
  50. package/docs/agent-dag-runner.md +42 -42
  51. package/docs/architecture/runtime-boundaries.md +162 -147
  52. package/docs/cursor-executor-usage.md +25 -25
  53. package/docs/decisions/README.md +3 -3
  54. package/docs/design/README.md +49 -36
  55. package/docs/development-principles.md +73 -73
  56. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  57. package/docs/exec-plans/README.md +6 -6
  58. package/docs/exec-plans/active/README.md +12 -7
  59. package/docs/exec-plans/completed/README.md +32 -19
  60. package/docs/feature-workflow.md +186 -186
  61. package/docs/harness-methodology-debugging.md +153 -153
  62. package/docs/harness-methodology-tdd.md +130 -130
  63. package/docs/harness-methodology-verification.md +27 -27
  64. package/docs/init-surface.manifest.json +208 -199
  65. package/docs/loop-agent-harness.md +55 -42
  66. package/docs/production-readiness.md +96 -96
  67. package/docs/progress/README.md +3 -3
  68. package/docs/reports/README.md +9 -5
  69. package/docs/skills/README.md +6 -6
  70. package/docs/skills/vetted-skill-registry.md +26 -26
  71. package/docs/templates/adr.md +60 -60
  72. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  73. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  74. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  75. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  76. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  77. package/docs/templates/agent-dag-report.schema.json +454 -454
  78. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  79. package/docs/templates/agent-dag.base.json +195 -195
  80. package/docs/templates/agent-dag.final-verification.json +190 -190
  81. package/docs/templates/agent-dag.schema.json +316 -316
  82. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  83. package/docs/templates/exec-plan.md +64 -64
  84. package/docs/templates/feature-spec.md +53 -53
  85. package/docs/templates/hybrid-dag.json +193 -193
  86. package/docs/templates/init-evolution-review.md +33 -33
  87. package/docs/templates/interactive-ui-round2-experiment.md +66 -0
  88. package/docs/templates/production-readiness-checklist.md +57 -57
  89. package/docs/templates/progress-log.md +17 -17
  90. package/docs/templates/project-start-checklist.md +9 -9
  91. package/docs/templates/qa-report.md +48 -48
  92. package/docs/templates/sprint-contract.md +29 -29
  93. package/docs/templates/worker-dogfood-evidence.md +52 -0
  94. package/docs/templates/worker-dogfood-setup.md +48 -0
  95. package/docs/verification-matrix.md +41 -41
  96. package/examples/decision-gate-agent-dag.json +123 -123
  97. package/examples/example-dag.json +51 -51
  98. package/examples/hybrid-loop-agent-dag.json +194 -194
  99. package/harness.json +70 -69
  100. package/package.json +66 -66
  101. package/skills/ai-engineering-context/SKILL.md +48 -48
  102. package/skills/code-review-core/SKILL.md +20 -20
  103. package/skills/codebase-scout/SKILL.md +19 -19
  104. package/skills/init-capability-evolution/SKILL.md +69 -69
  105. package/skills/loop-agent/SKILL.md +149 -147
  106. package/skills/loop-agent/references/README.md +67 -67
  107. package/skills/loop-agent/references/command-reference.md +412 -403
  108. package/skills/loop-agent/references/harness-policy.md +263 -259
  109. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  110. package/skills/loop-agent/references/learned/README.md +21 -21
  111. package/skills/loop-agent/references/long-running-loop.md +59 -59
  112. package/skills/loop-agent/references/model-routing.md +36 -36
  113. package/skills/loop-agent/references/multi-worktree.md +54 -54
  114. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  115. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  116. package/skills/loop-agent/references/pi-prompt.md +23 -23
  117. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  118. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  119. package/skills/loop-agent/references/task-workflow.md +89 -84
  120. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  121. package/skills/requesting-code-review/SKILL.md +101 -101
  122. package/skills/requesting-code-review/code-reviewer.md +168 -168
  123. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  124. package/skills/systematic-debugging/SKILL.md +296 -296
  125. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  126. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  127. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  128. package/skills/systematic-debugging/find-polluter.sh +63 -63
  129. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  130. package/skills/systematic-debugging/test-academic.md +14 -14
  131. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  132. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  133. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  134. package/skills/test-driven-development/SKILL.md +20 -20
  135. package/skills/verification-before-completion/SKILL.md +154 -154
  136. package/skills/webapp-testing/SKILL.md +19 -19
@@ -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
  }`;