@tea-agent/loop-agent 0.3.0 → 0.4.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 (119) hide show
  1. package/AGENTS.md +135 -133
  2. package/CHANGELOG.md +88 -63
  3. package/README.md +171 -168
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/init.js +457 -457
  7. package/dist/commands/loop-benchmark.js +11 -11
  8. package/dist/commands/pi-reuse-benchmark.js +16 -16
  9. package/dist/executors/cursor-executor.js +1 -1
  10. package/dist/executors/dag-pi-executor.js +8 -1
  11. package/dist/task/runtime.js +27 -27
  12. package/dist/worker/cli.js +119 -0
  13. package/dist/worker/loop-agent/command-result.js +1 -0
  14. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  15. package/dist/worker/loop-agent/parse-json.js +14 -0
  16. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  17. package/dist/worker/pool/failure-routing.js +98 -0
  18. package/dist/worker/pool/run-store.js +117 -0
  19. package/dist/worker/pool/types.js +1 -0
  20. package/dist/worker/preflight.js +108 -0
  21. package/dist/worker/profile-mapping.js +76 -0
  22. package/dist/worker/progress-reporter.js +81 -0
  23. package/dist/worker/report/morning-report.js +69 -0
  24. package/dist/worker/repos/repo-resolver.js +23 -0
  25. package/dist/worker/run-task/run-task.js +359 -0
  26. package/dist/worker/runner/run-ready.js +216 -0
  27. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  28. package/dist/worker/task-graph/ready-queue.js +23 -0
  29. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  30. package/dist/worker/task-graph/types.js +1 -0
  31. package/dist/worker/task-graph/validate.js +188 -0
  32. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  33. package/dist/worker/task-spec/schema.js +116 -0
  34. package/dist/worker/task-spec/types.js +1 -0
  35. package/dist/worker/task-spec/validate.js +352 -0
  36. package/dist/workflows/dag/canvas-observer.js +275 -275
  37. package/docs/README.md +65 -61
  38. package/docs/agent-dag-recovery-playbook.md +184 -184
  39. package/docs/agent-dag-runner.md +42 -42
  40. package/docs/architecture/runtime-boundaries.md +147 -147
  41. package/docs/cursor-executor-usage.md +25 -25
  42. package/docs/decisions/README.md +3 -3
  43. package/docs/design/README.md +36 -36
  44. package/docs/development-principles.md +73 -71
  45. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  46. package/docs/exec-plans/README.md +6 -6
  47. package/docs/exec-plans/active/README.md +7 -7
  48. package/docs/exec-plans/completed/README.md +19 -11
  49. package/docs/feature-workflow.md +186 -186
  50. package/docs/harness-methodology-debugging.md +153 -153
  51. package/docs/harness-methodology-tdd.md +130 -130
  52. package/docs/harness-methodology-verification.md +27 -27
  53. package/docs/loop-agent-harness.md +42 -42
  54. package/docs/production-readiness.md +96 -96
  55. package/docs/progress/README.md +3 -3
  56. package/docs/reports/README.md +5 -5
  57. package/docs/skills/README.md +6 -6
  58. package/docs/skills/vetted-skill-registry.md +22 -22
  59. package/docs/templates/adr.md +60 -60
  60. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  61. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  62. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  63. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  64. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  65. package/docs/templates/agent-dag-report.schema.json +454 -454
  66. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  67. package/docs/templates/agent-dag.base.json +195 -195
  68. package/docs/templates/agent-dag.final-verification.json +190 -190
  69. package/docs/templates/agent-dag.schema.json +316 -316
  70. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  71. package/docs/templates/exec-plan.md +64 -64
  72. package/docs/templates/feature-spec.md +53 -53
  73. package/docs/templates/hybrid-dag.json +193 -193
  74. package/docs/templates/production-readiness-checklist.md +57 -57
  75. package/docs/templates/progress-log.md +17 -17
  76. package/docs/templates/project-start-checklist.md +9 -9
  77. package/docs/templates/qa-report.md +48 -48
  78. package/docs/templates/sprint-contract.md +29 -29
  79. package/docs/verification-matrix.md +41 -41
  80. package/examples/decision-gate-agent-dag.json +123 -123
  81. package/examples/example-dag.json +51 -51
  82. package/examples/hybrid-loop-agent-dag.json +194 -194
  83. package/harness.json +89 -89
  84. package/package.json +60 -58
  85. package/skills/ai-engineering-context/SKILL.md +48 -48
  86. package/skills/code-review-core/SKILL.md +20 -20
  87. package/skills/codebase-scout/SKILL.md +19 -19
  88. package/skills/loop-agent/SKILL.md +147 -145
  89. package/skills/loop-agent/references/README.md +67 -67
  90. package/skills/loop-agent/references/command-reference.md +368 -340
  91. package/skills/loop-agent/references/harness-policy.md +259 -258
  92. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  93. package/skills/loop-agent/references/learned/README.md +21 -21
  94. package/skills/loop-agent/references/long-running-loop.md +59 -59
  95. package/skills/loop-agent/references/model-routing.md +36 -36
  96. package/skills/loop-agent/references/multi-worktree.md +54 -54
  97. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  98. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  99. package/skills/loop-agent/references/pi-prompt.md +23 -23
  100. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  101. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  102. package/skills/loop-agent/references/task-workflow.md +84 -84
  103. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  104. package/skills/requesting-code-review/SKILL.md +101 -101
  105. package/skills/requesting-code-review/code-reviewer.md +168 -168
  106. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  107. package/skills/systematic-debugging/SKILL.md +296 -296
  108. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  109. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  110. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  111. package/skills/systematic-debugging/find-polluter.sh +63 -63
  112. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  113. package/skills/systematic-debugging/test-academic.md +14 -14
  114. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  115. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  116. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  117. package/skills/test-driven-development/SKILL.md +20 -20
  118. package/skills/verification-before-completion/SKILL.md +154 -154
  119. 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
  }`;