@tea-agent/loop-agent 0.7.5 → 0.8.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 +143 -142
  2. package/CHANGELOG.md +148 -164
  3. package/README.md +206 -204
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/init.js +518 -488
  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/governance/manifest-types.js +1 -1
  11. package/dist/task/runtime.js +27 -27
  12. package/dist/worker/cli.js +3 -3
  13. package/dist/worker/observability/event-store.js +2 -1
  14. package/dist/worker/observability/read-model.js +13 -11
  15. package/dist/worker/observe/paths.js +2 -2
  16. package/dist/worker/observe/routes.js +4 -3
  17. package/dist/worker/observe/static/app.js +1479 -1480
  18. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  19. package/dist/worker/observe/static/dag-layout.js +83 -83
  20. package/dist/worker/observe/static/index.html +63 -63
  21. package/dist/worker/observe/static/styles.css +722 -722
  22. package/dist/worker/pool/run-store.js +7 -8
  23. package/dist/worker/run-task/run-task.js +11 -2
  24. package/dist/worker/runner/run-ready.js +1 -1
  25. package/dist/workflows/dag/canvas-observer.js +275 -275
  26. package/docs/README.md +80 -79
  27. package/docs/agent-dag-recovery-playbook.md +184 -184
  28. package/docs/agent-dag-runner.md +42 -42
  29. package/docs/architecture/runtime-boundaries.md +162 -162
  30. package/docs/cursor-executor-usage.md +25 -25
  31. package/docs/decisions/README.md +3 -3
  32. package/docs/design/README.md +49 -49
  33. package/docs/development-principles.md +73 -73
  34. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  35. package/docs/exec-plans/README.md +6 -6
  36. package/docs/exec-plans/active/README.md +11 -11
  37. package/docs/exec-plans/completed/README.md +35 -34
  38. package/docs/feature-workflow.md +187 -187
  39. package/docs/harness-methodology-debugging.md +153 -153
  40. package/docs/harness-methodology-tdd.md +130 -130
  41. package/docs/harness-methodology-verification.md +27 -27
  42. package/docs/init-surface.manifest.json +245 -241
  43. package/docs/loop-agent-harness.md +63 -55
  44. package/docs/production-readiness.md +96 -96
  45. package/docs/progress/README.md +3 -3
  46. package/docs/reports/README.md +9 -9
  47. package/docs/skills/README.md +6 -6
  48. package/docs/skills/vetted-skill-registry.md +26 -26
  49. package/docs/templates/adr.md +60 -60
  50. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  51. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  52. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  53. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  54. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  55. package/docs/templates/agent-dag-report.schema.json +454 -454
  56. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  57. package/docs/templates/agent-dag.base.json +195 -195
  58. package/docs/templates/agent-dag.final-verification.json +190 -190
  59. package/docs/templates/agent-dag.schema.json +316 -316
  60. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  61. package/docs/templates/exec-plan.md +64 -64
  62. package/docs/templates/feature-spec.md +53 -53
  63. package/docs/templates/harness.schema.json +218 -0
  64. package/docs/templates/hybrid-dag.json +193 -193
  65. package/docs/templates/init-evolution-review.md +33 -33
  66. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  67. package/docs/templates/product-line/AGENTS.md +8 -8
  68. package/docs/templates/product-line/README.md +9 -9
  69. package/docs/templates/product-line/acceptance.yaml +14 -14
  70. package/docs/templates/product-line/closeout.yaml +9 -9
  71. package/docs/templates/product-line/design.md +13 -13
  72. package/docs/templates/product-line/links.md +10 -10
  73. package/docs/templates/product-line/requirement.md +17 -17
  74. package/docs/templates/product-line/task-graph.yaml +15 -15
  75. package/docs/templates/product-line/task.yaml +65 -65
  76. package/docs/templates/product-line/test-plan.md +7 -7
  77. package/docs/templates/production-readiness-checklist.md +57 -57
  78. package/docs/templates/progress-log.md +17 -17
  79. package/docs/templates/project-start-checklist.md +9 -9
  80. package/docs/templates/qa-report.md +48 -48
  81. package/docs/templates/sprint-contract.md +29 -29
  82. package/docs/templates/worker-dogfood-evidence.md +52 -52
  83. package/docs/templates/worker-dogfood-setup.md +48 -48
  84. package/docs/verification-matrix.md +49 -49
  85. package/examples/decision-gate-agent-dag.json +123 -123
  86. package/examples/example-dag.json +51 -51
  87. package/examples/hybrid-loop-agent-dag.json +194 -194
  88. package/harness.json +73 -71
  89. package/package.json +68 -67
  90. package/scripts/check-product-line-docs.sh +22 -22
  91. package/scripts/check-task-pool-root.sh +32 -0
  92. package/skills/ai-engineering-context/SKILL.md +48 -48
  93. package/skills/code-review-core/SKILL.md +20 -20
  94. package/skills/codebase-scout/SKILL.md +19 -19
  95. package/skills/init-capability-evolution/SKILL.md +69 -69
  96. package/skills/loop-agent/SKILL.md +149 -149
  97. package/skills/loop-agent/references/README.md +67 -67
  98. package/skills/loop-agent/references/command-reference.md +432 -412
  99. package/skills/loop-agent/references/harness-policy.md +263 -263
  100. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  101. package/skills/loop-agent/references/learned/README.md +21 -21
  102. package/skills/loop-agent/references/long-running-loop.md +59 -59
  103. package/skills/loop-agent/references/model-routing.md +36 -36
  104. package/skills/loop-agent/references/multi-worktree.md +54 -54
  105. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  106. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  107. package/skills/loop-agent/references/pi-prompt.md +23 -23
  108. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  109. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  110. package/skills/loop-agent/references/task-workflow.md +89 -89
  111. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  112. package/skills/requesting-code-review/SKILL.md +101 -101
  113. package/skills/requesting-code-review/code-reviewer.md +168 -168
  114. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  115. package/skills/systematic-debugging/SKILL.md +296 -296
  116. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  117. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  118. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  119. package/skills/systematic-debugging/find-polluter.sh +63 -63
  120. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  121. package/skills/systematic-debugging/test-academic.md +14 -14
  122. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  123. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  124. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  125. package/skills/test-driven-development/SKILL.md +20 -20
  126. package/skills/verification-before-completion/SKILL.md +154 -154
  127. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,16 +1,17 @@
1
1
  import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ export const TASK_POOL_RELATIVE_ROOT = ".harness/task-pool";
3
4
  export function getTaskPoolRoot(repoRoot) {
4
- return path.join(repoRoot, ".task-pool");
5
+ return path.join(repoRoot, ...TASK_POOL_RELATIVE_ROOT.split("/"));
5
6
  }
6
7
  export function getRunsJsonlPath(repoRoot) {
7
- return path.join(getTaskPoolRoot(repoRoot), "runs", "runs.jsonl");
8
+ return path.join(getTaskPoolRoot(repoRoot), "runs.jsonl");
8
9
  }
9
10
  export function getEventsJsonlPath(repoRoot) {
10
- return path.join(getTaskPoolRoot(repoRoot), "events", "events.jsonl");
11
+ return path.join(getTaskPoolRoot(repoRoot), "events.jsonl");
11
12
  }
12
13
  export function getTaskStatePath(repoRoot, taskId) {
13
- return path.join(getTaskPoolRoot(repoRoot), "state", `${taskId}.json`);
14
+ return path.join(getTaskPoolRoot(repoRoot), "states", `${taskId}.json`);
14
15
  }
15
16
  export async function recordTaskPoolRun(input) {
16
17
  await ensurePoolDirs(input.repoRoot);
@@ -76,7 +77,7 @@ export async function readTaskPoolState(repoRoot, taskId) {
76
77
  }
77
78
  }
78
79
  export async function readAllTaskPoolStates(repoRoot) {
79
- const stateDir = path.join(getTaskPoolRoot(repoRoot), "state");
80
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
80
81
  try {
81
82
  const entries = await readdir(stateDir);
82
83
  const states = {};
@@ -127,9 +128,7 @@ async function appendJsonlFile(filePath, value) {
127
128
  }
128
129
  }
129
130
  async function ensurePoolDirs(repoRoot) {
130
- await mkdir(path.join(getTaskPoolRoot(repoRoot), "runs"), { recursive: true });
131
- await mkdir(path.join(getTaskPoolRoot(repoRoot), "events"), { recursive: true });
132
- await mkdir(path.join(getTaskPoolRoot(repoRoot), "state"), { recursive: true });
131
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "states"), { recursive: true });
133
132
  }
134
133
  function stateFromRun(run) {
135
134
  return {
@@ -5,6 +5,7 @@ import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
5
5
  import { redactForPreview, truncatePreview } from "../observability/events.js";
6
6
  import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
7
7
  import { preflightTargetRepo } from "../preflight.js";
8
+ import { getTaskPoolRoot } from "../pool/run-store.js";
8
9
  export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
9
10
  export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
10
11
  export async function runTaskSpec(options) {
@@ -95,7 +96,7 @@ export async function runTaskSpec(options) {
95
96
  progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
96
97
  const runDagStep = "run-dag";
97
98
  const runDagStartedAt = Date.now();
98
- const dagEventsPath = path.join(options.repoRoot, ".task-pool", "observability", "runs", workerRunId, "dag-events.jsonl");
99
+ const dagEventsPath = path.join(getTaskPoolRoot(options.repoRoot), "observability", "runs", workerRunId, "dag-events.jsonl");
99
100
  await mkdir(path.dirname(dagEventsPath), { recursive: true }).catch(() => { });
100
101
  await runObservedStep(eventCtx, runDagStep, async () => {
101
102
  progress.step("run-dag: executing DAG nodes (this is the long step)");
@@ -321,7 +322,11 @@ async function collectFailureArtifacts(input) {
321
322
  cwd: input.repoRoot,
322
323
  artifactName: "dag-doctor-markdown",
323
324
  }), { statusForResult: (result) => (result.ok ? "succeeded" : "failed") });
324
- const closeoutDraftPath = path.join(input.repoRoot, ".task-pool", "failure-handoffs", `${input.workerRunId}-failure-closeout-draft.md`);
325
+ const failureHandoffRoot = path.resolve(getTaskPoolRoot(input.repoRoot), "failure-handoffs");
326
+ const closeoutDraftPath = path.resolve(failureHandoffRoot, `${input.workerRunId}-failure-closeout-draft.md`);
327
+ if (!isPathInside(failureHandoffRoot, closeoutDraftPath)) {
328
+ throw new Error("closeout draft output must be under .harness/task-pool/failure-handoffs/");
329
+ }
325
330
  await runObservedStep(input.eventCtx, "closeout-draft", () => input.client.run([
326
331
  "dag",
327
332
  "closeout-draft",
@@ -343,6 +348,10 @@ async function collectFailureArtifacts(input) {
343
348
  closeoutDraftPath,
344
349
  };
345
350
  }
351
+ function isPathInside(root, target) {
352
+ const relative = path.relative(root, target);
353
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
354
+ }
346
355
  function decideFromReport(workerRunId, result) {
347
356
  if (!result.ok || !result.json) {
348
357
  return {
@@ -227,7 +227,7 @@ export async function runReadyTasks(options) {
227
227
  }
228
228
  }
229
229
  const summary = summarize(tasks);
230
- const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "runs", `${batchRunId}.json`);
230
+ const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "batch-run.json");
231
231
  const output = {
232
232
  schemaVersion: 1,
233
233
  status: summary.recordErrors > 0 || summary.runErrors > 0 ? "failed" : "completed",
@@ -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
  }`;