@tea-agent/loop-agent 0.3.0 → 0.5.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 (57) hide show
  1. package/AGENTS.md +16 -14
  2. package/CHANGELOG.md +70 -53
  3. package/README.md +28 -25
  4. package/bin/agent-worker.js +22 -0
  5. package/dist/application/dag/validate-dag.js +14 -1
  6. package/dist/commands/init.js +220 -32
  7. package/dist/executors/config-core.js +3 -2
  8. package/dist/executors/dag-pi-executor.js +8 -1
  9. package/dist/executors/model-routing.js +43 -0
  10. package/dist/governance/manifest-types.js +9 -1
  11. package/dist/worker/cli.js +119 -0
  12. package/dist/worker/loop-agent/command-result.js +1 -0
  13. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  14. package/dist/worker/loop-agent/parse-json.js +14 -0
  15. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  16. package/dist/worker/pool/failure-routing.js +98 -0
  17. package/dist/worker/pool/run-store.js +125 -0
  18. package/dist/worker/pool/types.js +1 -0
  19. package/dist/worker/preflight.js +108 -0
  20. package/dist/worker/profile-mapping.js +76 -0
  21. package/dist/worker/progress-reporter.js +81 -0
  22. package/dist/worker/report/morning-report.js +69 -0
  23. package/dist/worker/repos/repo-resolver.js +23 -0
  24. package/dist/worker/run-task/run-task.js +359 -0
  25. package/dist/worker/runner/run-ready.js +216 -0
  26. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  27. package/dist/worker/task-graph/ready-queue.js +23 -0
  28. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  29. package/dist/worker/task-graph/types.js +1 -0
  30. package/dist/worker/task-graph/validate.js +188 -0
  31. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  32. package/dist/worker/task-spec/schema.js +116 -0
  33. package/dist/worker/task-spec/types.js +1 -0
  34. package/dist/worker/task-spec/validate.js +352 -0
  35. package/dist/workflows/dag/init-hybrid.js +4 -13
  36. package/dist/workflows/dag/skill-instructions.js +4 -0
  37. package/dist/workflows/dag/types.js +1 -1
  38. package/dist/workflows/dag/validate.js +3 -2
  39. package/docs/README.md +11 -7
  40. package/docs/development-principles.md +2 -0
  41. package/docs/exec-plans/active/README.md +1 -1
  42. package/docs/exec-plans/completed/README.md +8 -0
  43. package/docs/init-surface.manifest.json +199 -175
  44. package/docs/skills/vetted-skill-registry.md +4 -4
  45. package/docs/templates/agent-dag.base.json +1 -1
  46. package/docs/templates/agent-dag.final-verification.json +1 -1
  47. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  48. package/docs/templates/hybrid-dag.json +1 -1
  49. package/docs/templates/init-evolution-review.md +33 -33
  50. package/examples/example-dag.json +1 -1
  51. package/examples/hybrid-loop-agent-dag.json +1 -1
  52. package/harness.json +7 -32
  53. package/package.json +14 -12
  54. package/skills/init-capability-evolution/SKILL.md +69 -69
  55. package/skills/loop-agent/SKILL.md +2 -0
  56. package/skills/loop-agent/references/command-reference.md +63 -35
  57. package/skills/loop-agent/references/harness-policy.md +2 -1
@@ -0,0 +1,81 @@
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
+ const ANSI = {
20
+ bold: "\x1b[1m",
21
+ green: "\x1b[32m",
22
+ red: "\x1b[31m",
23
+ yellow: "\x1b[33m",
24
+ cyan: "\x1b[36m",
25
+ dim: "\x1b[2m",
26
+ reset: "\x1b[0m",
27
+ };
28
+ function stamp(now) {
29
+ const iso = now.toISOString();
30
+ // YYYY-MM-DDTHH:MM:SS — drop millis and trailing Z noise for compact lines.
31
+ return `[${iso.slice(0, 19)}]`;
32
+ }
33
+ export function createProgressReporter(options = {}) {
34
+ if (options.quiet)
35
+ return noopProgressReporter;
36
+ const sink = options.sink ?? ((chunk) => process.stderr.write(chunk));
37
+ const isTty = options.isTty ?? process.stderr.isTTY === true;
38
+ const now = options.now ?? (() => new Date());
39
+ function decorate(prefix, message, color) {
40
+ return isTty ? `${color}${prefix}${ANSI.reset} ${message}` : `${prefix} ${message}`;
41
+ }
42
+ return {
43
+ batch(message) {
44
+ sink(`${stamp(now())} ${decorate("==", message, ANSI.bold + ANSI.cyan)}\n`);
45
+ },
46
+ task(message) {
47
+ sink(`${stamp(now())} ${decorate("▶", message, ANSI.bold)}\n`);
48
+ },
49
+ step(message) {
50
+ const body = isTty ? `${ANSI.dim}${message}${ANSI.reset}` : message;
51
+ sink(`${stamp(now())} ${body}\n`);
52
+ },
53
+ note(message) {
54
+ sink(`${stamp(now())} ${decorate("•", message, ANSI.dim)}\n`);
55
+ },
56
+ };
57
+ }
58
+ export const noopProgressReporter = {
59
+ batch() { },
60
+ task() { },
61
+ step() { },
62
+ note() { },
63
+ };
64
+ /** Format a millisecond duration as a compact human string: "6m 35s", "1h 2m 3s", "450ms". */
65
+ export function formatDuration(ms) {
66
+ if (!Number.isFinite(ms) || ms < 0)
67
+ return "?";
68
+ if (ms < 1000)
69
+ return `${Math.round(ms)}ms`;
70
+ const totalSeconds = Math.round(ms / 1000);
71
+ const hours = Math.floor(totalSeconds / 3600);
72
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
73
+ const seconds = totalSeconds % 60;
74
+ const parts = [];
75
+ if (hours > 0)
76
+ parts.push(`${hours}h`);
77
+ if (minutes > 0 || hours > 0)
78
+ parts.push(`${minutes}m`);
79
+ parts.push(`${seconds}s`);
80
+ return parts.join(" ");
81
+ }
@@ -0,0 +1,69 @@
1
+ import { access, writeFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getRunsJsonlPath, readAllTaskPoolStates, readJsonlFile, } from "../pool/run-store.js";
4
+ export async function renderMorningReport(options) {
5
+ const runs = (await readJsonlFile(getRunsJsonlPath(options.repoRoot))).filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
6
+ const total = runs.length;
7
+ const succeeded = runs.filter((run) => run.status === "succeeded").length;
8
+ const failed = runs.filter((run) => run.status === "failed").length;
9
+ const followUps = runs.filter((run) => run.failure).length;
10
+ const states = await readAllTaskPoolStates(options.repoRoot);
11
+ const blocked = Object.values(states).filter((state) => state.status === "Blocked").length;
12
+ const lines = [
13
+ "# Nightly Worker Report",
14
+ "",
15
+ "## Summary",
16
+ "",
17
+ `- Total: ${total}`,
18
+ `- Succeeded: ${succeeded}`,
19
+ `- Failed: ${failed}`,
20
+ `- Blocked: ${blocked}`,
21
+ `- Follow-ups: ${followUps}`,
22
+ "",
23
+ "## Results",
24
+ "",
25
+ "| Task | Status | Run | Failure | Next | Artifacts |",
26
+ "|---|---|---|---|---|---|",
27
+ ];
28
+ for (const run of runs) {
29
+ lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
30
+ }
31
+ if (followUps > 0) {
32
+ lines.push("", "## Human Actions", "");
33
+ for (const run of runs.filter((candidate) => candidate.failure)) {
34
+ lines.push(`- ${run.failure?.derivedFollowUpTaskId}: ${run.failure?.recommendedFollowUpKind} for ${run.taskId}`);
35
+ }
36
+ }
37
+ return `${lines.join("\n")}\n`;
38
+ }
39
+ export async function writeMorningReport(input) {
40
+ const markdown = await renderMorningReport(input);
41
+ await mkdir(path.dirname(input.outputPath), { recursive: true });
42
+ await writeFile(input.outputPath, markdown, "utf-8");
43
+ return input.outputPath;
44
+ }
45
+ async function artifactSummary(run) {
46
+ const artifacts = [
47
+ ["dag", run.dagPath],
48
+ ["record", run.runRecordPath],
49
+ ["report", run.failureArtifacts?.reportMarkdownArtifactPath],
50
+ ["doctor", run.failureArtifacts?.doctorMarkdownArtifactPath],
51
+ ["closeout", run.failureArtifacts?.closeoutDraftPath],
52
+ ].filter((entry) => Boolean(entry[1]));
53
+ if (artifacts.length === 0)
54
+ return "-";
55
+ const parts = [];
56
+ for (const [label, artifactPath] of artifacts) {
57
+ parts.push(`${label}:${(await exists(artifactPath)) ? artifactPath : `missing artifact ${artifactPath}`}`);
58
+ }
59
+ return parts.join("<br>");
60
+ }
61
+ async function exists(filePath) {
62
+ try {
63
+ await access(filePath);
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
@@ -0,0 +1,23 @@
1
+ import path from "node:path";
2
+ export function resolveRepoPath(config, pathRef) {
3
+ const entry = config.repos?.[pathRef];
4
+ if (!entry) {
5
+ return {
6
+ ok: false,
7
+ code: "repo-path-ref-missing",
8
+ message: `repo path_ref is not configured: ${pathRef}`,
9
+ };
10
+ }
11
+ if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
12
+ return {
13
+ ok: false,
14
+ code: "repo-path-invalid",
15
+ message: `repo path_ref has invalid path: ${pathRef}`,
16
+ };
17
+ }
18
+ return {
19
+ ok: true,
20
+ repoRoot: path.resolve(entry.path),
21
+ defaultBranch: typeof entry.default_branch === "string" ? entry.default_branch : undefined,
22
+ };
23
+ }
@@ -0,0 +1,359 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
5
+ import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
6
+ import { preflightTargetRepo } from "../preflight.js";
7
+ export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
8
+ export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
9
+ export async function runTaskSpec(options) {
10
+ const now = options.now ?? new Date();
11
+ const workerRunId = options.workerRunId ?? buildWorkerRunId(options.taskSpec.id, now);
12
+ const commands = [];
13
+ const client = new RecordingRunTaskClient(options.client, commands);
14
+ const progress = options.progress ?? noopProgressReporter;
15
+ 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" : ""));
17
+ const preflightStartedAt = Date.now();
18
+ const preflight = await preflightTargetRepo({
19
+ repoRoot: options.repoRoot,
20
+ client,
21
+ ...(typeof options.preflight === "object" ? options.preflight : {}),
22
+ });
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
+ progress.step(`preflight ok in ${formatDuration(Date.now() - preflightStartedAt)}`);
28
+ }
29
+ progress.step("materialize harness task");
30
+ 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,
37
+ });
38
+ progress.step(`materialized ${materializeManifest.harnessTaskId} in ${formatDuration(Date.now() - materializeStartedAt)}`);
39
+ const taskArtifactsDir = path.join(options.repoRoot, ".harness", "tasks", materializeManifest.harnessTaskId, "artifacts");
40
+ await mkdir(taskArtifactsDir, { recursive: true });
41
+ const dagPath = path.join(taskArtifactsDir, `${workerRunId}-dag.json`);
42
+ const runRecordPath = path.join(taskArtifactsDir, "worker-run-record.json");
43
+ progress.step("dag run-task: generate DAG");
44
+ 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
+ ]);
58
+ progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
59
+ if (options.piModel) {
60
+ progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
61
+ await applyPiModelOverride(dagPath, options.piModel);
62
+ }
63
+ progress.step("dag validate");
64
+ 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
+ ]);
76
+ progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
77
+ progress.step("run-dag: executing DAG nodes (this is the long step)");
78
+ 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",
116
+ });
117
+ const status = reportDecision.succeeded ? "succeeded" : "failed";
118
+ progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
119
+ const failureArtifacts = reportDecision.succeeded
120
+ ? undefined
121
+ : await collectFailureArtifacts({
122
+ client,
123
+ repoRoot: options.repoRoot,
124
+ workerRunId,
125
+ taskArtifactsDir,
126
+ });
127
+ 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);
131
+ }
132
+ const record = {
133
+ schemaVersion: 1,
134
+ status,
135
+ workerRunId,
136
+ businessId: options.taskSpec.id,
137
+ harnessTaskId: materializeManifest.harnessTaskId,
138
+ featureId: options.taskSpec.feature_id,
139
+ loopAgentProfile: materializeManifest.loopAgentProfile,
140
+ dagPath,
141
+ runRecordPath,
142
+ materializeManifest,
143
+ reportDecision,
144
+ commands,
145
+ ...(failureArtifacts ? { failureArtifacts } : {}),
146
+ };
147
+ await writeFile(runRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
148
+ return {
149
+ status,
150
+ workerRunId,
151
+ businessId: options.taskSpec.id,
152
+ harnessTaskId: materializeManifest.harnessTaskId,
153
+ runRecordPath,
154
+ dagPath,
155
+ reportDecision,
156
+ ...(failureArtifacts ? { failureArtifacts } : {}),
157
+ };
158
+ }
159
+ export function buildWorkerRunId(businessId, now) {
160
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
161
+ const hash = createHash("sha256")
162
+ .update(`${businessId}:${now.toISOString()}`)
163
+ .digest("hex")
164
+ .slice(0, 6);
165
+ return `wr-${date}-${businessId}-${hash}`;
166
+ }
167
+ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs) {
168
+ const result = await client.run(args, {
169
+ cwd: repoRoot,
170
+ artifactName,
171
+ expectJson,
172
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
173
+ });
174
+ if (!result.ok) {
175
+ throw new Error(`loop-agent command failed: ${args.join(" ")}`);
176
+ }
177
+ return result;
178
+ }
179
+ function resolveRunDagTimeoutMs(taskSpec) {
180
+ return Math.min(taskSpec.worker.timeout_ms ?? DEFAULT_RUN_DAG_TIMEOUT_MS, MAX_WORKER_TIMEOUT_MS);
181
+ }
182
+ async function collectFailureArtifacts(input) {
183
+ const doctor = await input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
184
+ cwd: input.repoRoot,
185
+ artifactName: "dag-doctor-markdown",
186
+ });
187
+ const closeoutDraftPath = path.join(input.repoRoot, ".task-pool", "failure-handoffs", `${input.workerRunId}-failure-closeout-draft.md`);
188
+ await input.client.run([
189
+ "dag",
190
+ "closeout-draft",
191
+ "--run-id",
192
+ input.workerRunId,
193
+ "--output",
194
+ closeoutDraftPath,
195
+ ], {
196
+ cwd: input.repoRoot,
197
+ artifactName: "dag-closeout-draft",
198
+ });
199
+ const reportMarkdown = input.client instanceof RecordingRunTaskClient
200
+ ? input.client.findArtifact("dag-report-markdown")
201
+ : undefined;
202
+ return {
203
+ reportMarkdownArtifactPath: reportMarkdown?.result.artifacts.stdoutPath ?? "",
204
+ doctorMarkdownArtifactPath: doctor.artifacts.stdoutPath,
205
+ closeoutDraftPath,
206
+ };
207
+ }
208
+ function decideFromReport(workerRunId, result) {
209
+ if (!result.ok || !result.json) {
210
+ return {
211
+ succeeded: false,
212
+ reason: "report-json-unavailable",
213
+ };
214
+ }
215
+ const runs = readObjectArray(result.json, "runs");
216
+ const run = runs.find((candidate) => readString(candidate, "runId") === workerRunId) ??
217
+ runs[0];
218
+ if (!run) {
219
+ return {
220
+ succeeded: false,
221
+ reason: "report-run-missing",
222
+ };
223
+ }
224
+ const runStatus = readString(run, "status");
225
+ const nodes = readObjectArray(run, "nodes");
226
+ const failedNode = nodes.find((node) => {
227
+ const status = readString(node, "status");
228
+ const failureCategory = readString(node, "failureCategory");
229
+ return (status === "ERROR" ||
230
+ status === "SKIPPED" ||
231
+ (Boolean(failureCategory) && failureCategory !== "success"));
232
+ });
233
+ if (runStatus !== "completed" && runStatus !== "finished") {
234
+ return {
235
+ succeeded: false,
236
+ reason: "run-not-completed",
237
+ runStatus,
238
+ primaryFailure: readProperty(run, "primaryFailure"),
239
+ };
240
+ }
241
+ if (failedNode) {
242
+ return {
243
+ succeeded: false,
244
+ reason: "node-failure",
245
+ runStatus,
246
+ primaryFailure: readProperty(run, "primaryFailure") ?? failedNode,
247
+ };
248
+ }
249
+ return {
250
+ succeeded: true,
251
+ reason: "report-completed",
252
+ runStatus,
253
+ primaryFailure: readProperty(run, "primaryFailure"),
254
+ };
255
+ }
256
+ class RecordingRunTaskClient {
257
+ delegate;
258
+ records;
259
+ constructor(delegate, records) {
260
+ this.delegate = delegate;
261
+ this.records = records;
262
+ }
263
+ async run(args, options) {
264
+ const result = await this.delegate.run(args, options);
265
+ this.records.push({
266
+ name: commandRecordName(args, options.artifactName),
267
+ artifactName: options.artifactName,
268
+ args,
269
+ result: {
270
+ ok: result.ok,
271
+ exitCode: result.exitCode,
272
+ durationMs: result.durationMs,
273
+ timedOut: result.timedOut,
274
+ artifacts: result.artifacts,
275
+ ...(result.parseFailure ? { parseFailure: result.parseFailure } : {}),
276
+ },
277
+ });
278
+ return result;
279
+ }
280
+ async runExternal(command, args, options) {
281
+ const result = await this.delegate.runExternal(command, args, options);
282
+ this.records.push({
283
+ name: options.artifactName,
284
+ artifactName: options.artifactName,
285
+ args: [command, ...args],
286
+ result: {
287
+ ok: result.ok,
288
+ exitCode: result.exitCode,
289
+ durationMs: result.durationMs,
290
+ timedOut: result.timedOut,
291
+ artifacts: result.artifacts,
292
+ ...(result.parseFailure ? { parseFailure: result.parseFailure } : {}),
293
+ },
294
+ });
295
+ return result;
296
+ }
297
+ findArtifact(artifactName) {
298
+ return this.records.find((record) => record.artifactName === artifactName);
299
+ }
300
+ }
301
+ function commandRecordName(args, artifactName) {
302
+ if (args[0] === "new-task")
303
+ return "new-task";
304
+ return artifactName;
305
+ }
306
+ function noCursorArgs(taskSpec) {
307
+ return taskSpec.loop_agent.no_cursor ? ["--no-cursor"] : [];
308
+ }
309
+ function forbidCursorArgs(taskSpec) {
310
+ return taskSpec.loop_agent.no_cursor ? ["--forbid-executor", "cursor"] : [];
311
+ }
312
+ function maxConcurrentArgs(taskSpec) {
313
+ return ["--max-concurrent", String(taskSpec.loop_agent.max_concurrent)];
314
+ }
315
+ /**
316
+ * When a smoke `piModel` override is active, drop `--strict-models` so the
317
+ * non-canonical `executorModels.pi` rewrite passes `dag validate`. Governance,
318
+ * forbidden-executor and spine audits are still enforced by their own flags.
319
+ */
320
+ function strictModelsArgs(piModel) {
321
+ return piModel ? [] : ["--strict-models"];
322
+ }
323
+ /**
324
+ * Rewrite `executorModels.pi.{LOW,MED,HIGH}` in an already-generated DAG JSON
325
+ * so every pi executor node resolves to the smoke override model. Cursor / shell
326
+ * slots are left untouched.
327
+ */
328
+ export async function applyPiModelOverride(dagPath, piModel) {
329
+ const raw = await readFile(dagPath, "utf-8");
330
+ const spec = JSON.parse(raw);
331
+ spec.executorModels = {
332
+ ...(spec.executorModels?.cursor ? { cursor: spec.executorModels.cursor } : {}),
333
+ pi: {
334
+ LOW: piModel,
335
+ MED: piModel,
336
+ HIGH: piModel,
337
+ },
338
+ };
339
+ await writeFile(dagPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
340
+ }
341
+ function readObjectArray(value, key) {
342
+ if (!value || typeof value !== "object")
343
+ return [];
344
+ const child = value[key];
345
+ if (!Array.isArray(child))
346
+ return [];
347
+ return child.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
348
+ }
349
+ function readString(value, key) {
350
+ if (!value || typeof value !== "object")
351
+ return undefined;
352
+ const child = value[key];
353
+ return typeof child === "string" ? child : undefined;
354
+ }
355
+ function readProperty(value, key) {
356
+ if (!value || typeof value !== "object")
357
+ return undefined;
358
+ return value[key];
359
+ }