@tea-agent/loop-agent 0.4.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 (67) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +48 -38
  3. package/README.md +3 -3
  4. package/dist/application/dag/args.js +9 -1
  5. package/dist/application/dag/run-dag.js +16 -2
  6. package/dist/application/dag/validate-dag.js +14 -1
  7. package/dist/cli/command-definitions.js +22 -4
  8. package/dist/cli/help.js +3 -2
  9. package/dist/cli/program.js +7 -5
  10. package/dist/commands/import-prd.js +76 -0
  11. package/dist/commands/init.js +230 -32
  12. package/dist/commands/instructions.js +90 -58
  13. package/dist/executors/config-core.js +3 -2
  14. package/dist/executors/dag-pi-executor.js +1 -0
  15. package/dist/executors/model-routing.js +43 -0
  16. package/dist/executors/pi-sdk-executor.js +63 -1
  17. package/dist/governance/manifest-types.js +9 -1
  18. package/dist/shared/preview.js +39 -0
  19. package/dist/task/source-references.js +221 -0
  20. package/dist/worker/cli.js +62 -1
  21. package/dist/worker/loop-agent/loop-agent-client.js +97 -5
  22. package/dist/worker/materialize/harness-task-materializer.js +162 -5
  23. package/dist/worker/observability/event-store.js +82 -0
  24. package/dist/worker/observability/events.js +79 -0
  25. package/dist/worker/observability/progress-composite.js +33 -0
  26. package/dist/worker/observability/read-model.js +1013 -0
  27. package/dist/worker/observability/snapshot-store.js +43 -0
  28. package/dist/worker/observability/types.js +1 -0
  29. package/dist/worker/observe/paths.js +64 -0
  30. package/dist/worker/observe/routes.js +423 -0
  31. package/dist/worker/observe/server.js +61 -0
  32. package/dist/worker/observe/static/app.js +1419 -0
  33. package/dist/worker/observe/static/index.html +63 -0
  34. package/dist/worker/observe/static/styles.css +613 -0
  35. package/dist/worker/pool/failure-routing.js +41 -6
  36. package/dist/worker/pool/run-store.js +59 -1
  37. package/dist/worker/progress-reporter.js +0 -18
  38. package/dist/worker/run-task/run-task.js +327 -92
  39. package/dist/worker/runner/run-ready.js +112 -4
  40. package/dist/workflows/dag/event-observer.js +132 -0
  41. package/dist/workflows/dag/init-hybrid.js +150 -26
  42. package/dist/workflows/dag/observer-compose.js +52 -0
  43. package/dist/workflows/dag/skill-instructions.js +4 -0
  44. package/dist/workflows/dag/types.js +1 -1
  45. package/dist/workflows/dag/validate.js +3 -2
  46. package/docs/README.md +2 -0
  47. package/docs/architecture/runtime-boundaries.md +18 -3
  48. package/docs/design/README.md +22 -9
  49. package/docs/exec-plans/active/README.md +6 -1
  50. package/docs/exec-plans/completed/README.md +12 -0
  51. package/docs/init-surface.manifest.json +32 -2
  52. package/docs/loop-agent-harness.md +13 -0
  53. package/docs/reports/README.md +4 -0
  54. package/docs/templates/agent-dag.base.json +1 -1
  55. package/docs/templates/agent-dag.final-verification.json +1 -1
  56. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  57. package/docs/templates/hybrid-dag.json +1 -1
  58. package/docs/templates/worker-dogfood-evidence.md +52 -0
  59. package/docs/templates/worker-dogfood-setup.md +48 -0
  60. package/examples/example-dag.json +1 -1
  61. package/examples/hybrid-loop-agent-dag.json +1 -1
  62. package/harness.json +5 -29
  63. package/package.json +6 -6
  64. package/skills/loop-agent/SKILL.md +5 -3
  65. package/skills/loop-agent/references/command-reference.md +12 -3
  66. package/skills/loop-agent/references/harness-policy.md +7 -3
  67. package/skills/loop-agent/references/task-workflow.md +8 -3
@@ -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
+ }