@tea-agent/loop-agent 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +142 -142
- package/CHANGELOG.md +132 -98
- package/README.md +195 -195
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/args.js +9 -1
- package/dist/application/dag/run-dag.js +16 -2
- package/dist/cli/command-definitions.js +22 -4
- package/dist/cli/help.js +3 -2
- package/dist/cli/program.js +7 -5
- package/dist/commands/import-prd.js +76 -0
- package/dist/commands/init.js +467 -457
- package/dist/commands/instructions.js +90 -58
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/cursor-executor.js +1 -1
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/executors/pi-sdk-executor.js +63 -1
- package/dist/shared/preview.js +39 -0
- package/dist/task/config-types.js +3 -0
- package/dist/task/runtime.js +27 -27
- package/dist/task/source-references.js +221 -0
- package/dist/worker/cli.js +62 -1
- package/dist/worker/loop-agent/loop-agent-client.js +97 -5
- package/dist/worker/materialize/harness-task-materializer.js +166 -5
- package/dist/worker/observability/event-store.js +82 -0
- package/dist/worker/observability/events.js +79 -0
- package/dist/worker/observability/progress-composite.js +33 -0
- package/dist/worker/observability/read-model.js +1013 -0
- package/dist/worker/observability/snapshot-store.js +43 -0
- package/dist/worker/observability/types.js +1 -0
- package/dist/worker/observe/paths.js +64 -0
- package/dist/worker/observe/routes.js +423 -0
- package/dist/worker/observe/server.js +61 -0
- package/dist/worker/observe/static/app.js +1419 -0
- package/dist/worker/observe/static/index.html +63 -0
- package/dist/worker/observe/static/styles.css +613 -0
- package/dist/worker/pool/failure-routing.js +41 -6
- package/dist/worker/pool/run-store.js +50 -0
- package/dist/worker/progress-reporter.js +0 -18
- package/dist/worker/run-task/run-task.js +327 -92
- package/dist/worker/runner/run-ready.js +112 -4
- package/dist/worker/task-spec/schema.js +2 -1
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/event-observer.js +132 -0
- package/dist/workflows/dag/init-hybrid.js +182 -21
- package/dist/workflows/dag/observer-compose.js +52 -0
- package/docs/README.md +75 -72
- package/docs/agent-dag-recovery-playbook.md +184 -184
- package/docs/agent-dag-runner.md +42 -42
- package/docs/architecture/runtime-boundaries.md +162 -147
- package/docs/cursor-executor-usage.md +25 -25
- package/docs/decisions/README.md +3 -3
- package/docs/design/README.md +49 -36
- package/docs/development-principles.md +73 -73
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +12 -7
- package/docs/exec-plans/completed/README.md +32 -19
- package/docs/feature-workflow.md +186 -186
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +208 -199
- package/docs/loop-agent-harness.md +55 -42
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +3 -3
- package/docs/reports/README.md +9 -5
- package/docs/skills/README.md +6 -6
- package/docs/skills/vetted-skill-registry.md +26 -26
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +454 -454
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +316 -316
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/init-evolution-review.md +33 -33
- package/docs/templates/interactive-ui-round2-experiment.md +66 -0
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +52 -0
- package/docs/templates/worker-dogfood-setup.md +48 -0
- package/docs/verification-matrix.md +41 -41
- package/examples/decision-gate-agent-dag.json +123 -123
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +70 -69
- package/package.json +66 -66
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/init-capability-evolution/SKILL.md +69 -69
- package/skills/loop-agent/SKILL.md +149 -147
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +412 -403
- package/skills/loop-agent/references/harness-policy.md +263 -259
- package/skills/loop-agent/references/hybrid-dag.md +216 -216
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +59 -59
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -84
- package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/verification-before-completion/SKILL.md +154 -154
- 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 =
|
|
56
|
-
|
|
57
|
-
readProductCategory(primaryFailure, "
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
20
|
+
const preflightStep = "preflight";
|
|
17
21
|
const preflightStartedAt = Date.now();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
36
|
+
const materializeStep = "materialize";
|
|
30
37
|
const materializeStartedAt = Date.now();
|
|
31
|
-
const materializeManifest = await
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
53
|
+
const dagGenStep = "dag-run-task";
|
|
44
54
|
const dagGenStartedAt = Date.now();
|
|
45
|
-
await
|
|
46
|
-
"dag"
|
|
47
|
-
"run-task",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
await
|
|
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
|
-
|
|
79
|
+
const validateStep = "dag-validate";
|
|
64
80
|
const validateStartedAt = Date.now();
|
|
65
|
-
await
|
|
66
|
-
"dag"
|
|
67
|
-
"validate",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
96
|
+
const runDagStep = "run-dag";
|
|
78
97
|
const runDagStartedAt = Date.now();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
+
}
|