@tea-agent/loop-agent 0.5.0 → 0.6.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 (133) hide show
  1. package/AGENTS.md +142 -142
  2. package/CHANGELOG.md +116 -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/runtime.js +27 -27
  21. package/dist/task/source-references.js +221 -0
  22. package/dist/worker/cli.js +62 -1
  23. package/dist/worker/loop-agent/loop-agent-client.js +97 -5
  24. package/dist/worker/materialize/harness-task-materializer.js +162 -5
  25. package/dist/worker/observability/event-store.js +82 -0
  26. package/dist/worker/observability/events.js +79 -0
  27. package/dist/worker/observability/progress-composite.js +33 -0
  28. package/dist/worker/observability/read-model.js +1013 -0
  29. package/dist/worker/observability/snapshot-store.js +43 -0
  30. package/dist/worker/observability/types.js +1 -0
  31. package/dist/worker/observe/paths.js +64 -0
  32. package/dist/worker/observe/routes.js +423 -0
  33. package/dist/worker/observe/server.js +61 -0
  34. package/dist/worker/observe/static/app.js +1419 -0
  35. package/dist/worker/observe/static/index.html +63 -0
  36. package/dist/worker/observe/static/styles.css +613 -0
  37. package/dist/worker/pool/failure-routing.js +41 -6
  38. package/dist/worker/pool/run-store.js +50 -0
  39. package/dist/worker/progress-reporter.js +0 -18
  40. package/dist/worker/run-task/run-task.js +327 -92
  41. package/dist/worker/runner/run-ready.js +112 -4
  42. package/dist/workflows/dag/canvas-observer.js +275 -275
  43. package/dist/workflows/dag/event-observer.js +132 -0
  44. package/dist/workflows/dag/init-hybrid.js +146 -13
  45. package/dist/workflows/dag/observer-compose.js +52 -0
  46. package/docs/README.md +74 -72
  47. package/docs/agent-dag-recovery-playbook.md +184 -184
  48. package/docs/agent-dag-runner.md +42 -42
  49. package/docs/architecture/runtime-boundaries.md +162 -147
  50. package/docs/cursor-executor-usage.md +25 -25
  51. package/docs/decisions/README.md +3 -3
  52. package/docs/design/README.md +49 -36
  53. package/docs/development-principles.md +73 -73
  54. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  55. package/docs/exec-plans/README.md +6 -6
  56. package/docs/exec-plans/active/README.md +12 -7
  57. package/docs/exec-plans/completed/README.md +31 -19
  58. package/docs/feature-workflow.md +186 -186
  59. package/docs/harness-methodology-debugging.md +153 -153
  60. package/docs/harness-methodology-tdd.md +130 -130
  61. package/docs/harness-methodology-verification.md +27 -27
  62. package/docs/init-surface.manifest.json +205 -199
  63. package/docs/loop-agent-harness.md +55 -42
  64. package/docs/production-readiness.md +96 -96
  65. package/docs/progress/README.md +3 -3
  66. package/docs/reports/README.md +9 -5
  67. package/docs/skills/README.md +6 -6
  68. package/docs/skills/vetted-skill-registry.md +26 -26
  69. package/docs/templates/adr.md +60 -60
  70. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  71. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  72. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  73. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  74. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  75. package/docs/templates/agent-dag-report.schema.json +454 -454
  76. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  77. package/docs/templates/agent-dag.base.json +195 -195
  78. package/docs/templates/agent-dag.final-verification.json +190 -190
  79. package/docs/templates/agent-dag.schema.json +316 -316
  80. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  81. package/docs/templates/exec-plan.md +64 -64
  82. package/docs/templates/feature-spec.md +53 -53
  83. package/docs/templates/hybrid-dag.json +193 -193
  84. package/docs/templates/init-evolution-review.md +33 -33
  85. package/docs/templates/production-readiness-checklist.md +57 -57
  86. package/docs/templates/progress-log.md +17 -17
  87. package/docs/templates/project-start-checklist.md +9 -9
  88. package/docs/templates/qa-report.md +48 -48
  89. package/docs/templates/sprint-contract.md +29 -29
  90. package/docs/templates/worker-dogfood-evidence.md +52 -0
  91. package/docs/templates/worker-dogfood-setup.md +48 -0
  92. package/docs/verification-matrix.md +41 -41
  93. package/examples/decision-gate-agent-dag.json +123 -123
  94. package/examples/example-dag.json +51 -51
  95. package/examples/hybrid-loop-agent-dag.json +194 -194
  96. package/harness.json +70 -69
  97. package/package.json +66 -66
  98. package/skills/ai-engineering-context/SKILL.md +48 -48
  99. package/skills/code-review-core/SKILL.md +20 -20
  100. package/skills/codebase-scout/SKILL.md +19 -19
  101. package/skills/init-capability-evolution/SKILL.md +69 -69
  102. package/skills/loop-agent/SKILL.md +149 -147
  103. package/skills/loop-agent/references/README.md +67 -67
  104. package/skills/loop-agent/references/command-reference.md +412 -403
  105. package/skills/loop-agent/references/harness-policy.md +263 -259
  106. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  107. package/skills/loop-agent/references/learned/README.md +21 -21
  108. package/skills/loop-agent/references/long-running-loop.md +59 -59
  109. package/skills/loop-agent/references/model-routing.md +36 -36
  110. package/skills/loop-agent/references/multi-worktree.md +54 -54
  111. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  112. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  113. package/skills/loop-agent/references/pi-prompt.md +23 -23
  114. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  115. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  116. package/skills/loop-agent/references/task-workflow.md +89 -84
  117. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  118. package/skills/requesting-code-review/SKILL.md +101 -101
  119. package/skills/requesting-code-review/code-reviewer.md +168 -168
  120. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  121. package/skills/systematic-debugging/SKILL.md +296 -296
  122. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  123. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  124. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  125. package/skills/systematic-debugging/find-polluter.sh +63 -63
  126. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  127. package/skills/systematic-debugging/test-academic.md +14 -14
  128. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  129. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  130. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  131. package/skills/test-driven-development/SKILL.md +20 -20
  132. package/skills/verification-before-completion/SKILL.md +154 -154
  133. package/skills/webapp-testing/SKILL.md +19 -19
@@ -52,12 +52,14 @@ export function deriveFailureRoute(result) {
52
52
  if (result.status === "succeeded")
53
53
  return undefined;
54
54
  const primaryFailure = readObject(result.reportDecision.primaryFailure);
55
- const category = readProductCategory(primaryFailure, "productLineFailureCategory") ??
56
- readProductCategory(primaryFailure, "productLineCategory") ??
57
- readProductCategory(primaryFailure, "product_line_failure_category") ??
58
- mapDagCategory(readString(primaryFailure, "failureCategory")) ??
59
- mapDagCategory(readString(primaryFailure, "normalizedFailureCategory")) ??
60
- categoryFromDecisionReason(result.reportDecision.reason);
55
+ const category = looksLikeExecutorConfigurationFailure(primaryFailure)
56
+ ? "EnvFailure"
57
+ : readProductCategory(primaryFailure, "productLineFailureCategory") ??
58
+ readProductCategory(primaryFailure, "productLineCategory") ??
59
+ readProductCategory(primaryFailure, "product_line_failure_category") ??
60
+ mapDagCategory(readString(primaryFailure, "failureCategory")) ??
61
+ mapDagCategory(readString(primaryFailure, "normalizedFailureCategory")) ??
62
+ categoryFromDecisionReason(result.reportDecision.reason);
61
63
  const recommendedFollowUpKind = readString(primaryFailure, "recommendedFollowUpKind") ??
62
64
  readString(primaryFailure, "recommendedFollowUp") ??
63
65
  FOLLOW_UP_BY_CATEGORY[category];
@@ -68,6 +70,18 @@ export function deriveFailureRoute(result) {
68
70
  source: primaryFailure ? "report-primary-failure" : "fallback",
69
71
  };
70
72
  }
73
+ /** Route a failure raised before a DAG report can be produced. */
74
+ export function deriveFailureRouteFromError(message, businessId) {
75
+ const category = looksLikeExecutorConfigurationText(message)
76
+ ? "EnvFailure"
77
+ : "Unknown";
78
+ return {
79
+ category,
80
+ recommendedFollowUpKind: FOLLOW_UP_BY_CATEGORY[category],
81
+ derivedFollowUpTaskId: `${FOLLOW_UP_PREFIX_BY_CATEGORY[category]}-${businessId}`,
82
+ source: "runner-error",
83
+ };
84
+ }
71
85
  function readProductCategory(value, key) {
72
86
  const candidate = readString(value, key);
73
87
  return candidate && PRODUCT_CATEGORIES.has(candidate)
@@ -85,6 +99,27 @@ function categoryFromDecisionReason(reason) {
85
99
  }
86
100
  return "Unknown";
87
101
  }
102
+ function looksLikeExecutorConfigurationFailure(value) {
103
+ if (!value)
104
+ return false;
105
+ return looksLikeExecutorConfigurationText([
106
+ "failureCategory",
107
+ "normalizedFailureCategory",
108
+ "message",
109
+ "error",
110
+ "stderr",
111
+ "detail",
112
+ ]
113
+ .map((key) => readString(value, key))
114
+ .filter((candidate) => Boolean(candidate))
115
+ .join("\n"));
116
+ }
117
+ function looksLikeExecutorConfigurationText(message) {
118
+ const value = message.toLowerCase();
119
+ const executorTerms = /provider|model|executor|api[ _-]?key|credential|thinking level/;
120
+ const configurationTerms = /unknown|invalid|unsupported|missing|not configured|configuration|unavailable/;
121
+ return executorTerms.test(value) && configurationTerms.test(value);
122
+ }
88
123
  function readObject(value) {
89
124
  if (!value || typeof value !== "object" || Array.isArray(value))
90
125
  return undefined;
@@ -22,6 +22,48 @@ export async function findRunByWorkerRunId(repoRoot, workerRunId) {
22
22
  const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
23
23
  return runs.find((run) => run.workerRunId === workerRunId);
24
24
  }
25
+ /**
26
+ * Explicitly requeue a failed task without deleting or rewriting prior evidence.
27
+ * The runner consumes `retryOfWorkerRunId` and derives a guaranteed fresh run id.
28
+ */
29
+ export async function prepareTaskPoolRetry(input) {
30
+ const current = await readTaskPoolState(input.repoRoot, input.taskId);
31
+ if (!current) {
32
+ throw new Error(`cannot retry ${input.taskId}: Task Pool state does not exist`);
33
+ }
34
+ if (current.status !== "Failed") {
35
+ throw new Error(`cannot retry ${input.taskId}: expected Failed state, found ${current.status}`);
36
+ }
37
+ if (!current.workerRunId) {
38
+ throw new Error(`cannot retry ${input.taskId}: failed state is missing workerRunId`);
39
+ }
40
+ const retryRequestedAt = (input.now ?? new Date()).toISOString();
41
+ const result = {
42
+ taskId: input.taskId,
43
+ status: "Ready",
44
+ previousWorkerRunId: current.workerRunId,
45
+ retryRequestedAt,
46
+ ...(input.reason ? { reason: input.reason } : {}),
47
+ };
48
+ await writeTaskPoolState(input.repoRoot, {
49
+ taskId: current.taskId,
50
+ status: "Ready",
51
+ updatedAt: retryRequestedAt,
52
+ retryOfWorkerRunId: current.workerRunId,
53
+ retryRequestedAt,
54
+ ...(input.reason ? { retryReason: input.reason } : {}),
55
+ });
56
+ await ensurePoolDirs(input.repoRoot);
57
+ await appendJsonlFile(getEventsJsonlPath(input.repoRoot), {
58
+ schemaVersion: 1,
59
+ at: retryRequestedAt,
60
+ type: "task-retry-requested",
61
+ taskId: input.taskId,
62
+ previousWorkerRunId: current.workerRunId,
63
+ ...(input.reason ? { reason: input.reason } : {}),
64
+ });
65
+ return result;
66
+ }
25
67
  export async function readTaskPoolState(repoRoot, taskId) {
26
68
  const statePath = getTaskStatePath(repoRoot, taskId);
27
69
  try {
@@ -97,11 +139,19 @@ function stateFromRun(run) {
97
139
  workerRunId: run.workerRunId,
98
140
  ...(run.runRecordPath ? { lastRunRecordPath: run.runRecordPath } : {}),
99
141
  ...(run.failure ? { failure: run.failure } : {}),
142
+ ...(run.retryOfWorkerRunId
143
+ ? { retryOfWorkerRunId: run.retryOfWorkerRunId }
144
+ : {}),
100
145
  };
101
146
  }
102
147
  function stateStatusFromRun(run) {
103
148
  if (run.status === "succeeded")
104
149
  return "Done";
150
+ // A pre-DAG provider/executor configuration failure is retryable after the
151
+ // operator corrects the environment; do not bury it in a non-retryable Blocked state.
152
+ if (run.status === "run-error" && run.failure?.category === "EnvFailure") {
153
+ return "Failed";
154
+ }
105
155
  if (run.status === "run-error")
106
156
  return "Blocked";
107
157
  return "Failed";
@@ -1,21 +1,3 @@
1
- /**
2
- * Human-readable progress reporting for long-running worker commands.
3
- *
4
- * Why this exists: agent-worker batch runs can take many minutes per task with
5
- * zero stdout/stderr feedback, which looks indistinguishable from a hang. The
6
- * system writes rich state to disk (state.json, run.json, .task-pool artifacts)
7
- * but never echoes it to the triggering terminal, so users assume it is dead.
8
- *
9
- * Design:
10
- * - Progress goes to STDERR only. STDOUT stays reserved for the final
11
- * machine-readable JSON payload, so `... | jq` and pipes keep working.
12
- * - Default ON. `--quiet` disables it. Non-interactive use that only wants JSON
13
- * can pass `--quiet` (or redirect stderr).
14
- * - TTY-aware for ANSI only: if stderr is not a TTY, we still print the lines
15
- * (they are line-buffered text and flush fine) but skip ANSI decoration.
16
- * - A `noop` reporter keeps library callers (and tests that don't care) quiet
17
- * with zero changes to their call sites beyond accepting the option.
18
- */
19
1
  const ANSI = {
20
2
  bold: "\x1b[1m",
21
3
  green: "\x1b[32m",
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
5
+ import { redactForPreview, truncatePreview } from "../observability/events.js";
5
6
  import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
6
7
  import { preflightTargetRepo } from "../preflight.js";
7
8
  export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
@@ -10,110 +11,139 @@ export async function runTaskSpec(options) {
10
11
  const now = options.now ?? new Date();
11
12
  const workerRunId = options.workerRunId ?? buildWorkerRunId(options.taskSpec.id, now);
12
13
  const commands = [];
13
- const client = new RecordingRunTaskClient(options.client, commands);
14
14
  const progress = options.progress ?? noopProgressReporter;
15
+ const taskId = options.taskSpec.id;
16
+ const eventCtx = { progress, workerRunId, taskId };
17
+ const recordingClient = new RecordingRunTaskClient(options.client, commands);
18
+ const client = new ObservedRunTaskClient(recordingClient, eventCtx);
15
19
  if (options.preflight !== false) {
16
- progress.step("preflight: version + inspect + docs-audit + git-status" + (options.preflight && typeof options.preflight === "object" && options.preflight.runCheckRepo ? " + check-repo" : ""));
20
+ const preflightStep = "preflight";
17
21
  const preflightStartedAt = Date.now();
18
- const preflight = await preflightTargetRepo({
19
- repoRoot: options.repoRoot,
20
- client,
21
- ...(typeof options.preflight === "object" ? options.preflight : {}),
22
+ await runObservedStep(eventCtx, preflightStep, async () => {
23
+ progress.step("preflight: version + inspect + docs-audit + git-status" + (options.preflight && typeof options.preflight === "object" && options.preflight.runCheckRepo ? " + check-repo" : ""));
24
+ const preflight = await preflightTargetRepo({
25
+ repoRoot: options.repoRoot,
26
+ client,
27
+ ...(typeof options.preflight === "object" ? options.preflight : {}),
28
+ });
29
+ if (!preflight.ok) {
30
+ progress.note(`preflight FAILED: ${preflight.code}`);
31
+ throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
32
+ }
22
33
  });
23
- if (!preflight.ok) {
24
- progress.note(`preflight FAILED: ${preflight.code}`);
25
- throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
26
- }
27
34
  progress.step(`preflight ok in ${formatDuration(Date.now() - preflightStartedAt)}`);
28
35
  }
29
- progress.step("materialize harness task");
36
+ const materializeStep = "materialize";
30
37
  const materializeStartedAt = Date.now();
31
- const materializeManifest = await materializeTaskSpec({
32
- repoRoot: options.repoRoot,
33
- taskSpec: options.taskSpec,
34
- taskSpecPath: options.taskSpecPath,
35
- client,
36
- now,
38
+ const materializeManifest = await runObservedStep(eventCtx, materializeStep, async () => {
39
+ progress.step("materialize harness task");
40
+ return materializeTaskSpec({
41
+ repoRoot: options.repoRoot,
42
+ taskSpec: options.taskSpec,
43
+ taskSpecPath: options.taskSpecPath,
44
+ client,
45
+ now,
46
+ });
37
47
  });
38
48
  progress.step(`materialized ${materializeManifest.harnessTaskId} in ${formatDuration(Date.now() - materializeStartedAt)}`);
39
49
  const taskArtifactsDir = path.join(options.repoRoot, ".harness", "tasks", materializeManifest.harnessTaskId, "artifacts");
40
50
  await mkdir(taskArtifactsDir, { recursive: true });
41
51
  const dagPath = path.join(taskArtifactsDir, `${workerRunId}-dag.json`);
42
52
  const runRecordPath = path.join(taskArtifactsDir, "worker-run-record.json");
43
- progress.step("dag run-task: generate DAG");
53
+ const dagGenStep = "dag-run-task";
44
54
  const dagGenStartedAt = Date.now();
45
- await runRequiredCommand(options.repoRoot, client, "dag-run-task", [
46
- "dag",
47
- "run-task",
48
- materializeManifest.harnessTaskId,
49
- "--profile",
50
- materializeManifest.loopAgentProfile,
51
- "--strict-models",
52
- "--output",
53
- dagPath,
54
- "--cwd",
55
- options.repoRoot,
56
- ...noCursorArgs(options.taskSpec),
57
- ]);
55
+ await runObservedStep(eventCtx, dagGenStep, async () => {
56
+ progress.step("dag run-task: generate DAG");
57
+ await runRequiredCommand(options.repoRoot, client, "dag-run-task", [
58
+ "dag",
59
+ "run-task",
60
+ materializeManifest.harnessTaskId,
61
+ "--profile",
62
+ materializeManifest.loopAgentProfile,
63
+ "--strict-models",
64
+ "--output",
65
+ dagPath,
66
+ "--cwd",
67
+ options.repoRoot,
68
+ ...noCursorArgs(options.taskSpec),
69
+ ], true, undefined, eventCtx);
70
+ });
58
71
  progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
59
72
  if (options.piModel) {
60
- progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
61
- await applyPiModelOverride(dagPath, options.piModel);
73
+ const piModelStep = "pi-model-override";
74
+ await runObservedStep(eventCtx, piModelStep, async () => {
75
+ progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
76
+ await applyPiModelOverride(dagPath, options.piModel);
77
+ });
62
78
  }
63
- progress.step("dag validate");
79
+ const validateStep = "dag-validate";
64
80
  const validateStartedAt = Date.now();
65
- await runRequiredCommand(options.repoRoot, client, "dag-validate", [
66
- "dag",
67
- "validate",
68
- "--dag",
69
- dagPath,
70
- ...strictModelsArgs(options.piModel),
71
- "--strict-governance",
72
- "--spine-task",
73
- materializeManifest.harnessTaskId,
74
- ...forbidCursorArgs(options.taskSpec),
75
- ]);
81
+ await runObservedStep(eventCtx, validateStep, async () => {
82
+ progress.step("dag validate");
83
+ await runRequiredCommand(options.repoRoot, client, "dag-validate", [
84
+ "dag",
85
+ "validate",
86
+ "--dag",
87
+ dagPath,
88
+ ...strictModelsArgs(options.piModel),
89
+ "--strict-governance",
90
+ "--spine-task",
91
+ materializeManifest.harnessTaskId,
92
+ ...forbidCursorArgs(options.taskSpec),
93
+ ], true, undefined, eventCtx);
94
+ });
76
95
  progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
77
- progress.step("run-dag: executing DAG nodes (this is the long step)");
96
+ const runDagStep = "run-dag";
78
97
  const runDagStartedAt = Date.now();
79
- await runRequiredCommand(options.repoRoot, client, "run-dag", [
80
- "run-dag",
81
- "--dag",
82
- dagPath,
83
- "--cwd",
84
- options.repoRoot,
85
- "--run-id",
86
- workerRunId,
87
- ...maxConcurrentArgs(options.taskSpec),
88
- ...noCursorArgs(options.taskSpec),
89
- ], true, resolveRunDagTimeoutMs(options.taskSpec));
90
- progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
91
- const reportJson = await client.run([
92
- "dag",
93
- "report",
94
- "--run-id",
95
- workerRunId,
96
- "--lifecycle",
97
- "all",
98
- "--json",
99
- ], {
100
- cwd: options.repoRoot,
101
- artifactName: "dag-report-json",
102
- expectJson: true,
103
- });
104
- const reportDecision = decideFromReport(workerRunId, reportJson);
105
- await client.run([
106
- "dag",
107
- "report",
108
- "--run-id",
109
- workerRunId,
110
- "--lifecycle",
111
- "all",
112
- "--markdown",
113
- ], {
114
- cwd: options.repoRoot,
115
- artifactName: "dag-report-markdown",
98
+ const dagEventsPath = path.join(options.repoRoot, ".task-pool", "observability", "runs", workerRunId, "dag-events.jsonl");
99
+ await mkdir(path.dirname(dagEventsPath), { recursive: true }).catch(() => { });
100
+ await runObservedStep(eventCtx, runDagStep, async () => {
101
+ progress.step("run-dag: executing DAG nodes (this is the long step)");
102
+ await runDagWithEventsFallback(options.repoRoot, client, [
103
+ "run-dag",
104
+ "--dag",
105
+ dagPath,
106
+ "--cwd",
107
+ options.repoRoot,
108
+ "--run-id",
109
+ workerRunId,
110
+ "--events-jsonl",
111
+ dagEventsPath,
112
+ ...maxConcurrentArgs(options.taskSpec),
113
+ ...noCursorArgs(options.taskSpec),
114
+ ], resolveRunDagTimeoutMs(options.taskSpec), eventCtx);
116
115
  });
116
+ progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
117
+ const reportStep = "report-decision";
118
+ const reportDecision = await runObservedStep(eventCtx, reportStep, async () => {
119
+ const reportJson = await client.run([
120
+ "dag",
121
+ "report",
122
+ "--run-id",
123
+ workerRunId,
124
+ "--lifecycle",
125
+ "all",
126
+ "--json",
127
+ ], {
128
+ cwd: options.repoRoot,
129
+ artifactName: "dag-report-json",
130
+ expectJson: true,
131
+ });
132
+ const decision = decideFromReport(workerRunId, reportJson);
133
+ await client.run([
134
+ "dag",
135
+ "report",
136
+ "--run-id",
137
+ workerRunId,
138
+ "--lifecycle",
139
+ "all",
140
+ "--markdown",
141
+ ], {
142
+ cwd: options.repoRoot,
143
+ artifactName: "dag-report-markdown",
144
+ });
145
+ return decision;
146
+ }, { statusForResult: (decision) => decision.succeeded ? "succeeded" : "failed" });
117
147
  const status = reportDecision.succeeded ? "succeeded" : "failed";
118
148
  progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
119
149
  const failureArtifacts = reportDecision.succeeded
@@ -123,11 +153,17 @@ export async function runTaskSpec(options) {
123
153
  repoRoot: options.repoRoot,
124
154
  workerRunId,
125
155
  taskArtifactsDir,
156
+ eventCtx,
126
157
  });
127
158
  if (reportDecision.succeeded) {
128
- progress.step("promote + closeout");
129
- await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true);
130
- await runRequiredCommand(options.repoRoot, client, "closeout-task", ["closeout", "task", materializeManifest.harnessTaskId], true);
159
+ await runObservedStep(eventCtx, "promote-run", async () => {
160
+ progress.step("promote run");
161
+ await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true, undefined, eventCtx);
162
+ });
163
+ await runObservedStep(eventCtx, "closeout-task", async () => {
164
+ progress.step("closeout task");
165
+ await runRequiredCommand(options.repoRoot, client, "closeout-task", ["closeout", "task", materializeManifest.harnessTaskId], true, undefined, eventCtx);
166
+ });
131
167
  }
132
168
  const record = {
133
169
  schemaVersion: 1,
@@ -145,6 +181,24 @@ export async function runTaskSpec(options) {
145
181
  ...(failureArtifacts ? { failureArtifacts } : {}),
146
182
  };
147
183
  await writeFile(runRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
184
+ emit(progress, {
185
+ type: "artifact.written",
186
+ source: "artifact",
187
+ label: "worker-run-record",
188
+ workerRunId,
189
+ taskId,
190
+ artifactRefs: {
191
+ runRecordPath,
192
+ dagPath,
193
+ ...(failureArtifacts
194
+ ? {
195
+ reportMarkdown: failureArtifacts.reportMarkdownArtifactPath,
196
+ doctorMarkdown: failureArtifacts.doctorMarkdownArtifactPath,
197
+ closeoutDraft: failureArtifacts.closeoutDraftPath,
198
+ }
199
+ : {}),
200
+ },
201
+ });
148
202
  return {
149
203
  status,
150
204
  workerRunId,
@@ -164,7 +218,7 @@ export function buildWorkerRunId(businessId, now) {
164
218
  .slice(0, 6);
165
219
  return `wr-${date}-${businessId}-${hash}`;
166
220
  }
167
- async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs) {
221
+ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs, eventCtx) {
168
222
  const result = await client.run(args, {
169
223
  cwd: repoRoot,
170
224
  artifactName,
@@ -176,16 +230,99 @@ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJs
176
230
  }
177
231
  return result;
178
232
  }
233
+ /**
234
+ * `--events-jsonl` is additive observability. Older published controllers do
235
+ * not know the flag; their parser rejects before any DAG node can execute, so
236
+ * retrying the exact command without it is safe and preserves the same run id.
237
+ */
238
+ async function runDagWithEventsFallback(repoRoot, client, args, timeoutMs, eventCtx) {
239
+ const result = await client.run(args, {
240
+ cwd: repoRoot,
241
+ artifactName: "run-dag",
242
+ expectJson: true,
243
+ timeoutMs,
244
+ });
245
+ if (result.ok)
246
+ return result;
247
+ if (!rejectsEventsJsonl(result) || !args.includes("--events-jsonl")) {
248
+ throw new Error(`loop-agent command failed: ${args.join(" ")}`);
249
+ }
250
+ const fallbackArgs = withoutFlagAndValue(args, "--events-jsonl");
251
+ eventCtx.progress.note("run-dag controller does not support --events-jsonl; retrying without DAG event stream");
252
+ const fallback = await client.run(fallbackArgs, {
253
+ cwd: repoRoot,
254
+ artifactName: "run-dag-events-fallback",
255
+ expectJson: true,
256
+ timeoutMs,
257
+ });
258
+ if (!fallback.ok) {
259
+ throw new Error(`loop-agent command failed: ${fallbackArgs.join(" ")}`);
260
+ }
261
+ return fallback;
262
+ }
263
+ function rejectsEventsJsonl(result) {
264
+ return /unknown run-dag flag:\s*--events-jsonl/i.test(`${result.stderr}\n${result.stdout}`);
265
+ }
266
+ function withoutFlagAndValue(args, flag) {
267
+ const index = args.indexOf(flag);
268
+ if (index < 0)
269
+ return args;
270
+ return [...args.slice(0, index), ...args.slice(index + 2)];
271
+ }
272
+ function emit(progress, input) {
273
+ const structured = progress;
274
+ if (typeof structured.event !== "function")
275
+ return;
276
+ void structured.event(input).catch(() => { });
277
+ }
278
+ function emitStepStarted(ctx, stepName) {
279
+ emit(ctx.progress, {
280
+ type: "step.started",
281
+ source: "worker",
282
+ label: stepName,
283
+ spanId: `step:${ctx.workerRunId}:${stepName}`,
284
+ parentSpanId: ctx.workerRunId,
285
+ workerRunId: ctx.workerRunId,
286
+ taskId: ctx.taskId,
287
+ status: "running",
288
+ });
289
+ }
290
+ function emitStepFinished(ctx, stepName, status, durationMs) {
291
+ emit(ctx.progress, {
292
+ type: "step.finished",
293
+ source: "worker",
294
+ label: stepName,
295
+ spanId: `step:${ctx.workerRunId}:${stepName}`,
296
+ parentSpanId: ctx.workerRunId,
297
+ workerRunId: ctx.workerRunId,
298
+ taskId: ctx.taskId,
299
+ status,
300
+ durationMs,
301
+ });
302
+ }
303
+ async function runObservedStep(ctx, stepName, run, options) {
304
+ const startedAt = Date.now();
305
+ emitStepStarted(ctx, stepName);
306
+ try {
307
+ const result = await run();
308
+ emitStepFinished(ctx, stepName, options?.statusForResult?.(result) ?? "succeeded", Date.now() - startedAt);
309
+ return result;
310
+ }
311
+ catch (error) {
312
+ emitStepFinished(ctx, stepName, "failed", Date.now() - startedAt);
313
+ throw error;
314
+ }
315
+ }
179
316
  function resolveRunDagTimeoutMs(taskSpec) {
180
317
  return Math.min(taskSpec.worker.timeout_ms ?? DEFAULT_RUN_DAG_TIMEOUT_MS, MAX_WORKER_TIMEOUT_MS);
181
318
  }
182
319
  async function collectFailureArtifacts(input) {
183
- const doctor = await input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
320
+ const doctor = await runObservedStep(input.eventCtx, "dag-doctor", () => input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
184
321
  cwd: input.repoRoot,
185
322
  artifactName: "dag-doctor-markdown",
186
- });
323
+ }), { statusForResult: (result) => (result.ok ? "succeeded" : "failed") });
187
324
  const closeoutDraftPath = path.join(input.repoRoot, ".task-pool", "failure-handoffs", `${input.workerRunId}-failure-closeout-draft.md`);
188
- await input.client.run([
325
+ await runObservedStep(input.eventCtx, "closeout-draft", () => input.client.run([
189
326
  "dag",
190
327
  "closeout-draft",
191
328
  "--run-id",
@@ -195,8 +332,9 @@ async function collectFailureArtifacts(input) {
195
332
  ], {
196
333
  cwd: input.repoRoot,
197
334
  artifactName: "dag-closeout-draft",
198
- });
199
- const reportMarkdown = input.client instanceof RecordingRunTaskClient
335
+ }), { statusForResult: (result) => (result.ok ? "succeeded" : "failed") });
336
+ const reportMarkdown = input.client instanceof ObservedRunTaskClient ||
337
+ input.client instanceof RecordingRunTaskClient
200
338
  ? input.client.findArtifact("dag-report-markdown")
201
339
  : undefined;
202
340
  return {
@@ -298,6 +436,100 @@ class RecordingRunTaskClient {
298
436
  return this.records.find((record) => record.artifactName === artifactName);
299
437
  }
300
438
  }
439
+ class ObservedRunTaskClient {
440
+ delegate;
441
+ eventCtx;
442
+ constructor(delegate, eventCtx) {
443
+ this.delegate = delegate;
444
+ this.eventCtx = eventCtx;
445
+ }
446
+ run(args, options) {
447
+ return this.runObserved(args, options, (observedOptions) => this.delegate.run(args, observedOptions));
448
+ }
449
+ runExternal(command, args, options) {
450
+ return this.runObserved([command, ...args], options, (observedOptions) => this.delegate.runExternal(command, args, observedOptions));
451
+ }
452
+ findArtifact(artifactName) {
453
+ return this.delegate.findArtifact(artifactName);
454
+ }
455
+ async runObserved(command, options, run) {
456
+ const observedOptions = {
457
+ ...options,
458
+ onSpawn: (info) => {
459
+ options.onSpawn?.(info);
460
+ emit(this.eventCtx.progress, {
461
+ type: "command.started",
462
+ source: "loop-agent-command",
463
+ label: options.artifactName,
464
+ workerRunId: this.eventCtx.workerRunId,
465
+ taskId: this.eventCtx.taskId,
466
+ pid: info.pid,
467
+ timeoutMs: options.timeoutMs,
468
+ command,
469
+ artifactRefs: info.artifactRefs,
470
+ status: "running",
471
+ });
472
+ },
473
+ onStdout: (chunk) => {
474
+ options.onStdout?.(chunk);
475
+ this.emitOutput(options.artifactName, chunk);
476
+ },
477
+ onStderr: (chunk) => {
478
+ options.onStderr?.(chunk);
479
+ this.emitOutput(options.artifactName, chunk);
480
+ },
481
+ onHeartbeat: (info) => {
482
+ options.onHeartbeat?.(info);
483
+ emit(this.eventCtx.progress, {
484
+ type: "command.heartbeat",
485
+ source: "loop-agent-command",
486
+ label: options.artifactName,
487
+ workerRunId: this.eventCtx.workerRunId,
488
+ taskId: this.eventCtx.taskId,
489
+ message: `elapsed ${info.elapsedMs}ms`,
490
+ });
491
+ },
492
+ };
493
+ try {
494
+ const result = await run(observedOptions);
495
+ emit(this.eventCtx.progress, {
496
+ type: "command.finished",
497
+ source: "loop-agent-command",
498
+ label: options.artifactName,
499
+ workerRunId: this.eventCtx.workerRunId,
500
+ taskId: this.eventCtx.taskId,
501
+ exitCode: result.exitCode,
502
+ timedOut: result.timedOut,
503
+ durationMs: result.durationMs,
504
+ artifactRefs: result.artifacts,
505
+ status: result.ok ? "succeeded" : "failed",
506
+ });
507
+ return result;
508
+ }
509
+ catch (error) {
510
+ emit(this.eventCtx.progress, {
511
+ type: "command.finished",
512
+ source: "loop-agent-command",
513
+ label: options.artifactName,
514
+ workerRunId: this.eventCtx.workerRunId,
515
+ taskId: this.eventCtx.taskId,
516
+ status: "failed",
517
+ message: errorMessage(error),
518
+ });
519
+ throw error;
520
+ }
521
+ }
522
+ emitOutput(label, chunk) {
523
+ emit(this.eventCtx.progress, {
524
+ type: "command.output",
525
+ source: "loop-agent-command",
526
+ label,
527
+ workerRunId: this.eventCtx.workerRunId,
528
+ taskId: this.eventCtx.taskId,
529
+ outputPreview: redactForPreview(truncatePreview(chunk)),
530
+ });
531
+ }
532
+ }
301
533
  function commandRecordName(args, artifactName) {
302
534
  if (args[0] === "new-task")
303
535
  return "new-task";
@@ -357,3 +589,6 @@ function readProperty(value, key) {
357
589
  return undefined;
358
590
  return value[key];
359
591
  }
592
+ function errorMessage(error) {
593
+ return error instanceof Error ? error.message : String(error);
594
+ }