@tea-agent/loop-agent 0.1.0 → 0.2.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 (100) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +127 -92
  3. package/dist/adapters/index.js +3 -2
  4. package/dist/adapters/loop-agent.js +44 -2
  5. package/dist/application/dag/args.js +420 -0
  6. package/dist/application/dag/generate-task-dag.js +280 -0
  7. package/dist/application/dag/report-dag.js +14 -0
  8. package/dist/application/dag/run-dag.js +93 -0
  9. package/dist/application/dag/validate-dag.js +101 -0
  10. package/dist/application/loop/run-action.js +23 -0
  11. package/dist/cli/catalog.js +2 -237
  12. package/dist/cli/command-definitions.js +571 -0
  13. package/dist/cli/index.js +2 -0
  14. package/dist/cli/program.js +65 -1
  15. package/dist/cli/router.js +13 -0
  16. package/dist/cli-governance/active-residue-check.js +38 -0
  17. package/dist/commands/dag-report.js +6 -107
  18. package/dist/commands/dag-run-task.js +8 -466
  19. package/dist/commands/dag-validate.js +7 -179
  20. package/dist/commands/examples.js +90 -0
  21. package/dist/commands/init.js +1495 -0
  22. package/dist/commands/loop.js +57 -31
  23. package/dist/commands/pi-prompt.js +2 -9
  24. package/dist/commands/run-dag.js +7 -180
  25. package/dist/executors/cursor-executor-artifacts.js +3 -4
  26. package/dist/executors/cursor-worker-client.js +13 -3
  27. package/dist/executors/dag-cursor-executor.js +2 -3
  28. package/dist/executors/dag-pi-executor.js +3 -4
  29. package/dist/executors/dag-static-executor.js +2 -5
  30. package/dist/executors/pi-defaults.js +9 -0
  31. package/dist/executors/shell-executor.js +12 -20
  32. package/dist/governance/manifest-types.js +1 -0
  33. package/dist/infrastructure/harness/active-residue-policy.js +73 -0
  34. package/dist/infrastructure/harness/artifact-store.js +72 -0
  35. package/dist/infrastructure/harness/atomic-write.js +49 -0
  36. package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
  37. package/dist/infrastructure/harness/loop-action-store.js +23 -0
  38. package/dist/infrastructure/harness/loop-store.js +41 -0
  39. package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
  40. package/dist/infrastructure/harness/task-store.js +77 -0
  41. package/dist/records/one-shot-runs.js +26 -61
  42. package/dist/records/promotion.js +3 -4
  43. package/dist/shared/artifacts-core.js +5 -5
  44. package/dist/shared/logger.js +9 -15
  45. package/dist/task/delegate.js +4 -4
  46. package/dist/task/runtime.js +5 -7
  47. package/dist/task/state.js +6 -20
  48. package/dist/workflows/dag/convergence/controller.js +277 -0
  49. package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
  50. package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
  51. package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
  52. package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
  53. package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
  54. package/dist/workflows/dag/lifecycle.js +6 -5
  55. package/dist/workflows/dag/node-execution.js +262 -0
  56. package/dist/workflows/dag/run-store.js +36 -0
  57. package/dist/workflows/dag/runner.js +82 -1341
  58. package/dist/workflows/dag/scheduler.js +84 -0
  59. package/dist/workflows/dag/upstream-artifacts.js +20 -18
  60. package/dist/workflows/loop/actions/cursor-fix.js +191 -0
  61. package/dist/workflows/loop/actions/dag-action.js +130 -0
  62. package/dist/workflows/loop/actions/pi-review.js +267 -0
  63. package/dist/workflows/loop/actions/shared.js +157 -0
  64. package/dist/workflows/loop/actions/shell-verify.js +82 -0
  65. package/dist/workflows/loop/actions/types.js +1 -0
  66. package/dist/workflows/loop/actions/workflow-action.js +255 -0
  67. package/dist/workflows/loop/actions.js +55 -1212
  68. package/dist/workflows/loop/closeout.js +5 -4
  69. package/dist/workflows/loop/context.js +2 -3
  70. package/dist/workflows/loop/events.js +3 -2
  71. package/dist/workflows/loop/policy/auto-policy.js +104 -0
  72. package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
  73. package/dist/workflows/loop/rounds.js +3 -3
  74. package/dist/workflows/loop/signals.js +4 -7
  75. package/dist/workflows/loop/state.js +11 -11
  76. package/docs/README.md +3 -2
  77. package/docs/architecture/runtime-boundaries.md +147 -0
  78. package/docs/exec-plans/active/README.md +4 -0
  79. package/docs/exec-plans/completed/README.md +6 -2
  80. package/package.json +2 -1
  81. package/skills/ai-engineering-context/SKILL.md +21 -21
  82. package/skills/loop-agent/SKILL.md +73 -188
  83. package/skills/loop-agent/references/README.md +6 -2
  84. package/skills/loop-agent/references/harness-policy.md +113 -113
  85. package/skills/loop-agent/references/learned/README.md +13 -13
  86. package/skills/loop-agent/references/long-running-loop.md +59 -0
  87. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
  88. package/skills/loop-agent/references/verification-and-failure-handling.md +18 -0
  89. package/skills/requesting-code-review/SKILL.md +40 -40
  90. package/skills/requesting-code-review/code-reviewer.md +4 -4
  91. package/skills/systematic-debugging/CREATION-LOG.md +43 -43
  92. package/skills/systematic-debugging/SKILL.md +113 -113
  93. package/skills/systematic-debugging/condition-based-waiting.md +20 -20
  94. package/skills/systematic-debugging/defense-in-depth.md +27 -27
  95. package/skills/systematic-debugging/root-cause-tracing.md +38 -38
  96. package/skills/systematic-debugging/test-academic.md +6 -6
  97. package/skills/systematic-debugging/test-pressure-1.md +6 -6
  98. package/skills/systematic-debugging/test-pressure-2.md +2 -2
  99. package/skills/systematic-debugging/test-pressure-3.md +6 -6
  100. package/skills/verification-before-completion/SKILL.md +37 -37
@@ -1,30 +1,23 @@
1
- import { createHash } from "node:crypto";
2
- import { access, appendFile, cp, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
1
+ import { readdir, readFile } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
5
- import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, isPauseOnHumanDecisionGate, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
6
- import { repoRelativePath } from "../../shared/path-refs.js";
7
- import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
4
+ import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
5
+ import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
8
6
  import { createDagNodeExecutor } from "./executor-registry.js";
9
7
  import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
10
- import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
11
8
  import { assertValidDagSpec } from "./validate.js";
12
9
  import { normalizeDagPromptSources } from "./prompt-source.js";
13
- import { buildDagNodePromptEnvelope } from "./prompt.js";
14
- import { persistLongNodeOutputArtifacts, relocateRunArtifactPaths, } from "./upstream-artifacts.js";
15
- import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
16
- import { resolveDagNodeSkills } from "./skills.js";
10
+ import { relocateConvergenceArtifactPaths, relocateRunArtifactPaths, } from "./upstream-artifacts.js";
11
+ import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
12
+ import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
13
+ import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
17
14
  import { topoSortToRanks } from "./topo.js";
18
- import { parseDagSpec, resolveModelForTask, } from "./types.js";
19
- import { buildVerifiedFindingsReport, findingSchema, verificationSchema, } from "../dynamic/artifacts.js";
20
- import { parseRepairArtifactFromText, validateRepairArtifactScope, } from "./repair-artifact.js";
21
- async function prepareDagRunDir(runDir) {
22
- await rm(runDir, { recursive: true, force: true });
23
- await mkdir(runDir, { recursive: true });
24
- }
25
- async function writeDagRunSpec(runDir, spec) {
26
- await writeFile(path.join(runDir, "run.json"), `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
27
- }
15
+ import { parseDagSpec, } from "./types.js";
16
+ import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
17
+ import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
18
+ import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
19
+ import { executeDynamicReduction } from "./dynamic-runtime/reduction.js";
20
+ export { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions };
28
21
  export async function loadDagSpecFromFile(dagPath) {
29
22
  const raw = JSON.parse(await readFile(dagPath, "utf-8"));
30
23
  const normalized = await normalizeDagPromptSources(raw, path.dirname(dagPath));
@@ -108,33 +101,6 @@ export async function buildDagRunId(spec, options) {
108
101
  const existing = await listExistingDagRunIds(options.cwd);
109
102
  return collisionSafeDagRunId(baseId, existing, now);
110
103
  }
111
- export function buildNodePrompt(spec, task, upstream) {
112
- return buildDagNodePromptEnvelope({
113
- spec,
114
- task,
115
- upstream,
116
- resolvedSkills: resolveDagNodeSkills(spec, task),
117
- });
118
- }
119
- export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
120
- const skillNames = resolveDagNodeSkills(spec, task);
121
- const resolvedSkillInstructions = task.executor === "cursor" || task.executor === "pi"
122
- ? await resolveDagSkillInstructions(skillNames, {
123
- cwd,
124
- includeLearnedPatterns: task.role === "implementer",
125
- })
126
- : [];
127
- return {
128
- prompt: buildDagNodePromptEnvelope({
129
- spec,
130
- task,
131
- upstream,
132
- resolvedSkills: skillNames,
133
- resolvedSkillInstructions,
134
- }),
135
- resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
136
- };
137
- }
138
104
  export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "dag") {
139
105
  const nodes = {};
140
106
  for (const task of spec.tasks) {
@@ -184,9 +150,9 @@ export async function runDag(spec, opts) {
184
150
  const completedRunDir = getDagRunDir(opts.cwd, "completed", state.runId);
185
151
  const pausedRunDir = getDagRunDir(opts.cwd, "paused", state.runId);
186
152
  const runDir = activeRunDir;
187
- await prepareDagRunDir(runDir);
188
- await writeDagRunSpec(runDir, spec);
189
- await writeDagRunState(runDir, state);
153
+ await prepareActiveRunDir(runDir);
154
+ await writeRunSpec(runDir, spec);
155
+ await writeRunState(runDir, state);
190
156
  await notifyRunObserver(opts.observer, "onRunStart", state);
191
157
  if (opts.dryRun || opts.initOnly) {
192
158
  return {
@@ -273,8 +239,8 @@ async function executeDagCheckpoint(input) {
273
239
  const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
274
240
  let runDir = initialRunDir;
275
241
  let stateWriteQueue = Promise.resolve();
276
- const persistState = async () => {
277
- stateWriteQueue = stateWriteQueue.then(() => writeDagRunState(runDir, state));
242
+ const persistState = async (options) => {
243
+ stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
278
244
  await stateWriteQueue;
279
245
  };
280
246
  const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
@@ -284,20 +250,65 @@ async function executeDagCheckpoint(input) {
284
250
  runId: state.runId,
285
251
  spec,
286
252
  });
253
+ const executeDynamicNode = async (dynamicInput) => {
254
+ const { task } = dynamicInput;
255
+ if (task.dynamicExpansion) {
256
+ return executeDynamicMapExpansion({
257
+ ...dynamicInput,
258
+ expansion: task.dynamicExpansion,
259
+ executeDynamicNode,
260
+ });
261
+ }
262
+ if (task.dynamicCondition) {
263
+ return executeDynamicCondition({
264
+ task,
265
+ condition: task.dynamicCondition,
266
+ tasksById: dynamicInput.tasksById,
267
+ state: dynamicInput.state,
268
+ });
269
+ }
270
+ if (task.dynamicLoopUntil) {
271
+ return executeDynamicLoopUntil({
272
+ ...dynamicInput,
273
+ loop: task.dynamicLoopUntil,
274
+ executeDynamicNode,
275
+ });
276
+ }
277
+ return executeDynamicReduction({
278
+ task,
279
+ reduction: task.dynamicReduction,
280
+ state: dynamicInput.state,
281
+ runDir: dynamicInput.runDir,
282
+ });
283
+ };
287
284
  let pausedByNodeId;
288
285
  while (true) {
289
286
  pausedByNodeId = await executeDagRanksOnce({
290
- spec,
291
287
  state,
292
288
  ranks,
293
289
  tasksById,
294
- cwd,
295
- runDir,
296
290
  maxConcurrent,
297
- baseExecuteNode,
298
- customExecuteNode: input.executeNode,
299
- observer: input.observer,
300
291
  persistState,
292
+ createExecuteNodeForRank: (rankCursorNodeIds) => buildRankAwareExecuteNode({
293
+ baseExecuteNode,
294
+ customExecuteNode: input.executeNode,
295
+ rankCursorNodeIds,
296
+ tasksById,
297
+ meta: { runDir, runId: state.runId, spec },
298
+ }),
299
+ executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
300
+ nodeId,
301
+ tasksById,
302
+ state,
303
+ spec,
304
+ cwd,
305
+ runDir,
306
+ executeNode,
307
+ executeDynamicNode,
308
+ observer: input.observer,
309
+ persistState,
310
+ onPause,
311
+ }),
301
312
  });
302
313
  if (pausedByNodeId) {
303
314
  break;
@@ -325,21 +336,23 @@ async function executeDagCheckpoint(input) {
325
336
  state.status = "paused";
326
337
  await persistState();
327
338
  await notifyRunObserver(input.observer, "onRunFinish", state);
328
- runDir = await transferDagRunDir(runDir, pausedRunDir);
339
+ runDir = await moveToPausedRunDir(runDir, pausedRunDir);
329
340
  }
330
341
  else {
331
342
  finalizeTerminalRunStatus(state, spec.tasks.length);
332
343
  await persistState();
333
344
  await notifyRunObserver(input.observer, "onRunFinish", state);
334
- runDir = await transferDagRunDir(runDir, completedRunDir);
345
+ runDir = await moveToCompletedRunDir(runDir, completedRunDir);
335
346
  }
336
347
  await relocateRunArtifactPaths({
337
348
  runDir,
338
349
  oldRunDir: runDirBeforeTransfer,
339
350
  nodes: state.nodes,
340
351
  });
341
- relocateConvergenceArtifactPaths(state, runDirBeforeTransfer, runDir);
342
- await persistState();
352
+ if (state.convergence) {
353
+ relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
354
+ }
355
+ await persistState({ allowCompletedFactsWrite: true });
343
356
  return {
344
357
  title: spec.title,
345
358
  runId: state.runId,
@@ -349,1223 +362,6 @@ async function executeDagCheckpoint(input) {
349
362
  runDir,
350
363
  };
351
364
  }
352
- async function executeDagRanksOnce(input) {
353
- let pausedByNodeId;
354
- for (const rank of input.ranks) {
355
- const runnable = rank.filter((id) => {
356
- const node = input.state.nodes[id];
357
- const task = input.tasksById.get(id);
358
- if (!node || node.status !== "PENDING")
359
- return false;
360
- return !shouldSkipNode(node, task, input.state.nodes);
361
- });
362
- const blocked = rank.filter((id) => {
363
- const node = input.state.nodes[id];
364
- const task = input.tasksById.get(id);
365
- return (node?.status === "PENDING" &&
366
- shouldSkipNode(node, task, input.state.nodes));
367
- });
368
- for (const id of blocked) {
369
- const node = input.state.nodes[id];
370
- node.status = "SKIPPED";
371
- node.skippedReason = conditionSkippedByAncestor(input.tasksById.get(id), input.state.nodes)
372
- ? "condition ancestor branch not selected"
373
- : "upstream dependency failed or was skipped";
374
- }
375
- if (blocked.length > 0) {
376
- await input.persistState();
377
- }
378
- const pauseGateRunnable = runnable.filter((id) => {
379
- const task = input.tasksById.get(id);
380
- return isPauseOnHumanDecisionGate(task);
381
- });
382
- const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
383
- const rankWriteGuardedNodeIds = runnable.filter((id) => {
384
- const task = input.tasksById.get(id);
385
- return task ? isWriteGuardedAgentExecutor(task) : false;
386
- });
387
- const executeNode = buildRankAwareExecuteNode({
388
- baseExecuteNode: input.baseExecuteNode,
389
- customExecuteNode: input.customExecuteNode,
390
- rankWriteGuardedNodeIds,
391
- tasksById: input.tasksById,
392
- meta: {
393
- runDir: input.runDir,
394
- runId: input.state.runId,
395
- spec: input.spec,
396
- },
397
- });
398
- for (const nodeId of pauseGateRunnable) {
399
- await executeDagNode({
400
- nodeId,
401
- tasksById: input.tasksById,
402
- state: input.state,
403
- spec: input.spec,
404
- cwd: input.cwd,
405
- runDir: input.runDir,
406
- executeNode,
407
- observer: input.observer,
408
- persistState: input.persistState,
409
- onPause: (id, pausedAt, pauseReason) => {
410
- pausedByNodeId = id;
411
- input.state.pausedAt = pausedAt;
412
- input.state.pausedByNodeId = id;
413
- input.state.pauseReason = pauseReason;
414
- },
415
- });
416
- if (pausedByNodeId)
417
- break;
418
- }
419
- if (pausedByNodeId)
420
- break;
421
- await mapConcurrent(regularRunnable, input.maxConcurrent, async (nodeId) => {
422
- await executeDagNode({
423
- nodeId,
424
- tasksById: input.tasksById,
425
- state: input.state,
426
- spec: input.spec,
427
- cwd: input.cwd,
428
- runDir: input.runDir,
429
- executeNode,
430
- observer: input.observer,
431
- persistState: input.persistState,
432
- onPause: (id, pausedAt, pauseReason) => {
433
- pausedByNodeId = id;
434
- input.state.pausedAt = pausedAt;
435
- input.state.pausedByNodeId = id;
436
- input.state.pauseReason = pauseReason;
437
- },
438
- });
439
- });
440
- if (pausedByNodeId)
441
- break;
442
- }
443
- return pausedByNodeId;
444
- }
445
- function convergenceChainNodeIds(tasksById) {
446
- const repairId = tasksById.has("repair-pi") ? "repair-pi" : "repair-cursor";
447
- return [
448
- "process-supervisor-pi",
449
- "process-gate-shell",
450
- repairId,
451
- "hard-verify-shell",
452
- ];
453
- }
454
- const CONVERGENCE_NON_RETRY_FAILURES = new Set([
455
- "timeout",
456
- "spawn-error",
457
- "write-guard",
458
- "missing-api-key",
459
- "auth",
460
- "human-rejected",
461
- "decision-gate-requires-human",
462
- ]);
463
- function shouldEnableDagConvergence(spec) {
464
- return (process.env.HARNESS_DAG_CONVERGENCE !== "off" &&
465
- spec.convergence?.enabled === true);
466
- }
467
- async function runConvergencePassController(input) {
468
- const convergence = input.state.convergence;
469
- if (!convergence?.enabled)
470
- return { retry: false };
471
- if (process.env.HARNESS_DAG_CONVERGENCE === "off") {
472
- convergence.terminalReason = "feature-flag-off";
473
- return { retry: false };
474
- }
475
- if (!hasConvergenceChain(input.tasksById)) {
476
- convergence.terminalReason = "unsupported-dag-shape";
477
- return { retry: false };
478
- }
479
- const hardVerify = input.state.nodes["hard-verify-shell"];
480
- if (!hardVerify)
481
- return { retry: false };
482
- if (hardVerify.status === "FINISHED") {
483
- convergence.terminalReason = "hard-verify-pass";
484
- await appendConvergenceKnowledgePattern({
485
- cwd: input.cwd,
486
- state: input.state,
487
- });
488
- return { retry: false };
489
- }
490
- if (hardVerify.status !== "ERROR")
491
- return { retry: false };
492
- const currentPass = convergence.currentPass || 1;
493
- const hardFailure = hardVerify.failureCategory ?? "unknown";
494
- const passRecord = await buildConvergencePassRecord({
495
- pass: currentPass,
496
- status: "retrying",
497
- reason: "hard-verify-failed",
498
- state: input.state,
499
- runDir: input.runDir,
500
- });
501
- if (CONVERGENCE_NON_RETRY_FAILURES.has(hardFailure)) {
502
- passRecord.status = "terminal";
503
- passRecord.reason = "non-retry-failure";
504
- convergence.passHistory.push(passRecord);
505
- convergence.terminalReason = "non-retry-failure";
506
- await input.persistState();
507
- return { retry: false };
508
- }
509
- if (input.spec.convergence?.pauseOnRegression !== false &&
510
- detectConvergenceRegression(convergence.passHistory, passRecord)) {
511
- passRecord.status = "paused";
512
- passRecord.reason = "regression";
513
- convergence.passHistory.push(passRecord);
514
- convergence.terminalReason = "regression";
515
- const pausedAt = new Date().toISOString();
516
- input.state.status = "paused";
517
- input.state.pausedAt = pausedAt;
518
- input.state.pausedByNodeId = "hard-verify-shell";
519
- input.state.pauseReason = "convergence-regression";
520
- input.state.failureCategory = "convergence-regression";
521
- await input.persistState();
522
- return { retry: false, pausedByNodeId: "hard-verify-shell" };
523
- }
524
- if (currentPass >= convergence.maxPasses) {
525
- passRecord.status = "terminal";
526
- passRecord.reason = "max-passes";
527
- convergence.passHistory.push(passRecord);
528
- convergence.terminalReason = "max-passes";
529
- await input.persistState();
530
- return { retry: false };
531
- }
532
- convergence.passHistory.push(passRecord);
533
- convergence.currentPass = currentPass + 1;
534
- await resetConvergenceNodesForNextPass({
535
- spec: input.spec,
536
- state: input.state,
537
- tasksById: input.tasksById,
538
- });
539
- await input.persistState();
540
- return { retry: true };
541
- }
542
- function hasConvergenceChain(tasksById) {
543
- return convergenceChainNodeIds(tasksById).every((id) => tasksById.has(id));
544
- }
545
- async function buildConvergencePassRecord(input) {
546
- const hardVerify = input.state.nodes["hard-verify-shell"];
547
- const processSupervisor = input.state.nodes["process-supervisor-pi"];
548
- const verifyEvidence = hardVerify?.verifyEvidence;
549
- return {
550
- pass: input.pass,
551
- startedAt: hardVerify?.startedAt,
552
- finishedAt: new Date().toISOString(),
553
- status: input.status,
554
- reason: input.reason,
555
- hardVerifyStatus: hardVerify?.status,
556
- hardVerifyFailureCategory: hardVerify?.failureCategory,
557
- processVerdict: parseProcessVerdict(processSupervisor),
558
- repairArtifact: processSupervisor?.repairArtifact,
559
- verifyPhase: verifyEvidence?.phase,
560
- verifyQuota: verifyEvidence?.quota,
561
- verifyCommandCount: verifyEvidence?.commandCount,
562
- verifyCommandLabels: verifyEvidence?.commandLabels,
563
- shellSuccessCount: countSuccessfulShellCommands(hardVerify?.stdout),
564
- artifactRefs: await preserveConvergencePassArtifacts({
565
- pass: input.pass,
566
- state: input.state,
567
- runDir: input.runDir,
568
- }),
569
- };
570
- }
571
- async function preserveConvergencePassArtifacts(input) {
572
- const passDir = path.join(input.runDir, "convergence", `pass-${input.pass}`);
573
- await mkdir(passDir, { recursive: true });
574
- const nodeIds = nodesToPreserveForConvergence(input.state);
575
- const refs = [];
576
- for (const nodeId of nodeIds) {
577
- const node = input.state.nodes[nodeId];
578
- if (!node || node.status === "PENDING")
579
- continue;
580
- const preservedNodeRecordPath = path.join(passDir, `${nodeId}.json`);
581
- const preservedNodeDir = path.join(passDir, nodeId);
582
- await copyIfExists(path.join(input.runDir, `${nodeId}.json`), preservedNodeRecordPath);
583
- await copyIfExists(path.join(input.runDir, nodeId), preservedNodeDir);
584
- refs.push({
585
- nodeId,
586
- status: node.status,
587
- failureCategory: node.failureCategory,
588
- nodeRecordPath: node.nodeRecordPath ?? path.join(input.runDir, `${nodeId}.json`),
589
- stdoutArtifactPath: node.stdoutArtifactPath,
590
- assistantArtifactPath: node.assistantArtifactPath,
591
- preservedNodeRecordPath,
592
- preservedNodeDir,
593
- });
594
- }
595
- return refs;
596
- }
597
- function nodesToPreserveForConvergence(state) {
598
- const repairId = state.nodes["repair-pi"] ? "repair-pi" : "repair-cursor";
599
- return [
600
- "process-supervisor-pi",
601
- "process-gate-shell",
602
- repairId,
603
- "hard-verify-shell",
604
- ].filter((nodeId) => state.nodes[nodeId]);
605
- }
606
- async function copyIfExists(from, to) {
607
- try {
608
- await access(from);
609
- await cp(from, to, { recursive: true, force: true });
610
- }
611
- catch {
612
- // Missing artifacts are represented in passHistory by absent files.
613
- }
614
- }
615
- function parseProcessVerdict(node) {
616
- const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
617
- for (const line of text.split("\n")) {
618
- const trimmed = line.trim();
619
- if (trimmed === "VERDICT: pass")
620
- return "pass";
621
- if (trimmed === "VERDICT: request-revision")
622
- return "request-revision";
623
- }
624
- return "unknown";
625
- }
626
- function assertRepairArtifactVerdictMatchesSupervisor(input) {
627
- const supervisorVerdict = parseProcessVerdict(input.node);
628
- if (supervisorVerdict === "unknown")
629
- return;
630
- if (supervisorVerdict !== input.artifactVerdict) {
631
- throw new Error(`repair artifact gate failed: verdict mismatch between supervisor ${supervisorVerdict} and REPAIR_ARTIFACT_JSON ${input.artifactVerdict}`);
632
- }
633
- }
634
- function countSuccessfulShellCommands(stdout) {
635
- if (!stdout)
636
- return undefined;
637
- const matches = stdout.match(/\|\s*\d+\s*\|\s*true\s*\|/g);
638
- if (matches)
639
- return matches.length;
640
- if (/\|\s*\d+\s*\|\s*false\s*\|/.test(stdout))
641
- return 0;
642
- const explicit = stdout.match(/(?:passed|success(?:es)?|ok)\s*[=:]\s*(\d+)/i);
643
- return explicit ? Number.parseInt(explicit[1], 10) : undefined;
644
- }
645
- function detectConvergenceRegression(history, current) {
646
- const previous = [...history]
647
- .reverse()
648
- .find((pass) => pass.shellSuccessCount !== undefined);
649
- if (previous?.shellSuccessCount !== undefined &&
650
- current.shellSuccessCount !== undefined &&
651
- current.shellSuccessCount < previous.shellSuccessCount) {
652
- return true;
653
- }
654
- return isWorseFailureCategory(previous?.hardVerifyFailureCategory, current.hardVerifyFailureCategory);
655
- }
656
- function isWorseFailureCategory(previous, current) {
657
- const severity = new Map([
658
- ["success", 0],
659
- ["nonzero-exit", 1],
660
- ["unknown", 2],
661
- ["timeout", 3],
662
- ["spawn-error", 3],
663
- ["write-guard", 4],
664
- ]);
665
- if (!previous || !current)
666
- return false;
667
- return (severity.get(current) ?? 2) > (severity.get(previous) ?? 2);
668
- }
669
- async function appendConvergenceKnowledgePattern(input) {
670
- const convergence = input.state.convergence;
671
- if (!convergence || convergence.passHistory.length === 0)
672
- return;
673
- const supervisor = input.state.nodes["process-supervisor-pi"];
674
- const hardVerify = input.state.nodes["hard-verify-shell"];
675
- const pattern = {
676
- schemaVersion: 1,
677
- type: "dag-convergence-repair",
678
- runId: input.state.runId,
679
- recordedAt: new Date().toISOString(),
680
- passCount: convergence.currentPass,
681
- terminalReason: convergence.terminalReason ?? "hard-verify-pass",
682
- processVerdict: parseProcessVerdict(supervisor),
683
- repairArtifact: supervisor?.repairArtifact,
684
- supervisorStructuredBlock: extractSupervisorStructuredBlock(supervisor),
685
- hardVerify: {
686
- status: hardVerify?.status,
687
- failureCategory: hardVerify?.failureCategory,
688
- nodeRecordPath: hardVerify?.nodeRecordPath,
689
- },
690
- passHistory: convergence.passHistory.map((pass) => ({
691
- pass: pass.pass,
692
- reason: pass.reason,
693
- hardVerifyFailureCategory: pass.hardVerifyFailureCategory,
694
- shellSuccessCount: pass.shellSuccessCount,
695
- })),
696
- };
697
- const knowledgeDir = path.join(input.cwd, ".harness", "knowledge");
698
- await mkdir(knowledgeDir, { recursive: true });
699
- await appendFile(path.join(knowledgeDir, "patterns.jsonl"), `${JSON.stringify(pattern)}\n`, "utf-8");
700
- }
701
- function extractSupervisorStructuredBlock(node) {
702
- // Legacy compatibility only. New supervisor prompts must emit REPAIR_ARTIFACT_JSON.
703
- const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
704
- const block = {};
705
- for (const key of ["FAILURE_CLASS", "FIX_SCOPE", "INVARIANT"]) {
706
- const match = text.match(new RegExp(`^${key}:\\s*(.+)$`, "im"));
707
- if (match?.[1])
708
- block[key] = match[1].trim();
709
- }
710
- return block;
711
- }
712
- async function resetConvergenceNodesForNextPass(input) {
713
- const resetIds = new Set(convergenceChainNodeIds(input.tasksById));
714
- for (const id of collectTransitiveDescendantTaskIds(input.spec, "hard-verify-shell")) {
715
- const node = input.state.nodes[id];
716
- if (node?.status === "SKIPPED")
717
- resetIds.add(id);
718
- }
719
- for (const id of resetIds) {
720
- const task = input.tasksById.get(id);
721
- if (!task)
722
- continue;
723
- input.state.nodes[id] = freshNodeRecord(task);
724
- }
725
- }
726
- function collectTransitiveDescendantTaskIds(spec, rootNodeId) {
727
- const childrenByParent = new Map();
728
- for (const task of spec.tasks) {
729
- for (const parent of task.depends_on) {
730
- const children = childrenByParent.get(parent) ?? [];
731
- children.push(task.id);
732
- childrenByParent.set(parent, children);
733
- }
734
- }
735
- const descendants = new Set();
736
- const queue = [...(childrenByParent.get(rootNodeId) ?? [])];
737
- while (queue.length > 0) {
738
- const id = queue.shift();
739
- if (descendants.has(id))
740
- continue;
741
- descendants.add(id);
742
- queue.push(...(childrenByParent.get(id) ?? []));
743
- }
744
- return descendants;
745
- }
746
- function freshNodeRecord(task) {
747
- return {
748
- id: task.id,
749
- status: "PENDING",
750
- executor: task.executor,
751
- complexity: task.complexity,
752
- };
753
- }
754
- function relocateConvergenceArtifactPaths(state, oldRunDir, newRunDir) {
755
- const convergence = state.convergence;
756
- if (!convergence || oldRunDir === newRunDir)
757
- return;
758
- const relocate = (value) => value?.startsWith(oldRunDir)
759
- ? path.join(newRunDir, path.relative(oldRunDir, value))
760
- : value;
761
- for (const pass of convergence.passHistory) {
762
- for (const ref of pass.artifactRefs) {
763
- ref.nodeRecordPath = relocate(ref.nodeRecordPath);
764
- ref.stdoutArtifactPath = relocate(ref.stdoutArtifactPath);
765
- ref.assistantArtifactPath = relocate(ref.assistantArtifactPath);
766
- ref.preservedNodeRecordPath = relocate(ref.preservedNodeRecordPath);
767
- ref.preservedNodeDir = relocate(ref.preservedNodeDir);
768
- }
769
- }
770
- }
771
- function findRepairTaskForGate(input) {
772
- return Array.from(input.tasksById.values()).find((candidate) => candidate.depends_on.includes(input.gateTask.id) &&
773
- candidate.role === "implementer" &&
774
- candidate.writePolicy === "exclusive" &&
775
- (candidate.executor === "cursor" ||
776
- (candidate.executor === "pi" && candidate.toolProfile === "write")));
777
- }
778
- function parseSupervisorRepairArtifact(node) {
779
- const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
780
- return parseRepairArtifactFromText(text);
781
- }
782
- function validateRepairArtifactGateBeforeShell(input) {
783
- const gate = input.task.shell?.repairArtifactGate;
784
- if (!gate)
785
- return;
786
- const upstream = input.state.nodes[gate.fromNodeId];
787
- const parsed = parseSupervisorRepairArtifact(upstream);
788
- if (!parsed.ok) {
789
- throw new Error(`repair artifact gate failed: ${parsed.reason}`);
790
- }
791
- assertRepairArtifactVerdictMatchesSupervisor({
792
- node: upstream,
793
- artifactVerdict: parsed.artifact.verdict,
794
- });
795
- upstream.repairArtifact = parsed.artifact;
796
- const scoped = validateRepairArtifactScope({
797
- artifact: parsed.artifact,
798
- repairTask: findRepairTaskForGate({
799
- tasksById: input.tasksById,
800
- gateTask: input.task,
801
- }),
802
- });
803
- if (!scoped.ok) {
804
- throw new Error(`repair artifact gate failed: ${scoped.reason}`);
805
- }
806
- }
807
- function recordRepairArtifactForSupervisorNode(input) {
808
- const dependentGate = Object.values(input.state.nodes).find((record) => record.id === "process-gate-shell");
809
- if (input.task.id !== "process-supervisor-pi" || !dependentGate)
810
- return;
811
- const parsed = parseSupervisorRepairArtifact(input.node);
812
- if (parsed.ok) {
813
- input.node.repairArtifact = parsed.artifact;
814
- }
815
- }
816
- async function executeDagNode(input) {
817
- const { nodeId, tasksById, state, spec, cwd, runDir, executeNode } = input;
818
- const task = tasksById.get(nodeId);
819
- const node = state.nodes[nodeId];
820
- node.status = "RUNNING";
821
- node.startedAt = new Date().toISOString();
822
- if (task.shell?.verifyEvidence) {
823
- node.verifyEvidence = task.shell.verifyEvidence;
824
- }
825
- await input.persistState();
826
- await notifyNodeObserver(input.observer, "onNodeStart", nodeId, state);
827
- if (task.dynamicExpansion ||
828
- task.dynamicReduction ||
829
- task.dynamicCondition ||
830
- task.dynamicLoopUntil) {
831
- const started = Date.now();
832
- try {
833
- const result = task.dynamicExpansion
834
- ? await executeDynamicMapExpansion({
835
- task,
836
- expansion: task.dynamicExpansion,
837
- tasksById,
838
- state,
839
- spec,
840
- cwd,
841
- runDir,
842
- executeNode,
843
- observer: input.observer,
844
- persistState: input.persistState,
845
- })
846
- : task.dynamicCondition
847
- ? await executeDynamicCondition({
848
- task,
849
- condition: task.dynamicCondition,
850
- tasksById,
851
- state,
852
- })
853
- : task.dynamicLoopUntil
854
- ? await executeDynamicLoopUntil({
855
- task,
856
- loop: task.dynamicLoopUntil,
857
- tasksById,
858
- state,
859
- spec,
860
- cwd,
861
- runDir,
862
- executeNode,
863
- observer: input.observer,
864
- persistState: input.persistState,
865
- })
866
- : await executeDynamicReduction({
867
- task,
868
- reduction: task.dynamicReduction,
869
- state,
870
- runDir,
871
- });
872
- node.durationMs = result.durationMs ?? Date.now() - started;
873
- node.stdout = result.stdout;
874
- node.stderr = result.stderr;
875
- node.failureCategory = result.failureCategory;
876
- node.finishedAt = new Date().toISOString();
877
- node.status = result.ok ? "FINISHED" : "ERROR";
878
- }
879
- catch (error) {
880
- node.status = "ERROR";
881
- node.stderr = error instanceof Error ? error.message : String(error);
882
- node.finishedAt = new Date().toISOString();
883
- node.durationMs = Date.now() - started;
884
- }
885
- if (node.status === "FINISHED") {
886
- const artifactMeta = await persistLongNodeOutputArtifacts({
887
- runDir,
888
- nodeId,
889
- stdout: node.stdout,
890
- assistantText: node.assistantText,
891
- });
892
- Object.assign(node, artifactMeta);
893
- }
894
- state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
895
- await appendNodeLog(runDir, nodeId, state.nodes[nodeId]);
896
- await input.persistState();
897
- await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
898
- return;
899
- }
900
- const { prompt, resolvedSkills } = await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd);
901
- node.resolvedSkills = resolvedSkills;
902
- await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
903
- const model = resolveModelForTask(task, spec.executorModels);
904
- const started = Date.now();
905
- try {
906
- validateRepairArtifactGateBeforeShell({
907
- task,
908
- tasksById,
909
- state,
910
- });
911
- const result = await executeNode({ task, cwd, model, prompt });
912
- node.durationMs = result.durationMs ?? Date.now() - started;
913
- node.stdout = result.stdout;
914
- node.stderr = result.stderr;
915
- node.failureCategory = result.failureCategory;
916
- if (result.assistantText !== undefined) {
917
- node.assistantText = result.assistantText;
918
- }
919
- if (result.backend !== undefined)
920
- node.backend = result.backend;
921
- if (result.sdkAttempted !== undefined) {
922
- node.sdkAttempted = result.sdkAttempted;
923
- }
924
- if (result.tokensUsed !== undefined)
925
- node.tokensUsed = result.tokensUsed;
926
- if (result.parsedEvents !== undefined) {
927
- node.parsedEvents = result.parsedEvents;
928
- }
929
- recordRepairArtifactForSupervisorNode({
930
- task,
931
- node,
932
- state,
933
- });
934
- const outputChunk = result.assistantText ?? result.stdout;
935
- if (outputChunk.trim()) {
936
- await notifyNodeObserver(input.observer, "onNodeOutput", nodeId, state, outputChunk);
937
- }
938
- node.finishedAt = new Date().toISOString();
939
- node.status = result.ok ? "FINISHED" : "ERROR";
940
- if (result.ok) {
941
- const decisionRecord = await recordDecisionEnvelopeForNode({
942
- task,
943
- runDir,
944
- nodeId,
945
- assistantText: node.assistantText ?? result.assistantText,
946
- });
947
- if (decisionRecord) {
948
- node.decisionEnvelope = decisionRecord.nodeRecord;
949
- if (!decisionRecord.nodeRecord.parseOk) {
950
- node.status = "ERROR";
951
- node.failureCategory = "decision-envelope-invalid";
952
- node.stderr = [
953
- node.stderr,
954
- ...(decisionRecord.nodeRecord.errors ?? []),
955
- ]
956
- .filter(Boolean)
957
- .join("\n");
958
- }
959
- else if (shouldPauseOnHumanEscalation(task, decisionRecord.nodeRecord) &&
960
- decisionRecord.envelope) {
961
- const pausedAt = new Date().toISOString();
962
- const nodeArtifactsDir = path.join(runDir, nodeId);
963
- const { jsonPath } = await writeHumanEscalationArtifacts({
964
- nodeArtifactsDir,
965
- runId: state.runId,
966
- nodeId,
967
- envelope: decisionRecord.envelope,
968
- pausedAt,
969
- });
970
- node.pauseReason = "decision-gate-requires-human";
971
- node.escalationArtifactPath = jsonPath;
972
- input.onPause(nodeId, pausedAt, node.pauseReason);
973
- }
974
- }
975
- }
976
- }
977
- catch (error) {
978
- node.status = "ERROR";
979
- node.stderr = error instanceof Error ? error.message : String(error);
980
- node.finishedAt = new Date().toISOString();
981
- node.durationMs = Date.now() - started;
982
- }
983
- if (node.status === "FINISHED") {
984
- const artifactMeta = await persistLongNodeOutputArtifacts({
985
- runDir,
986
- nodeId,
987
- stdout: node.stdout,
988
- assistantText: node.assistantText,
989
- });
990
- Object.assign(node, artifactMeta);
991
- }
992
- state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
993
- await appendNodeLog(runDir, nodeId, state.nodes[nodeId]);
994
- await input.persistState();
995
- await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
996
- }
997
- function sha256Json(value) {
998
- return `sha256:${createHash("sha256")
999
- .update(JSON.stringify(value))
1000
- .digest("hex")}`;
1001
- }
1002
- function getNodeOutputAsJson(state, nodeId) {
1003
- const record = state.nodes[nodeId];
1004
- if (!record || record.status !== "FINISHED") {
1005
- throw new Error(`itemsFrom upstream node "${nodeId}" is not finished`);
1006
- }
1007
- const raw = record.stdout?.trim() || record.assistantText?.trim();
1008
- if (!raw) {
1009
- throw new Error(`itemsFrom upstream node "${nodeId}" has no JSON output`);
1010
- }
1011
- try {
1012
- return JSON.parse(raw);
1013
- }
1014
- catch (error) {
1015
- throw new Error(`itemsFrom upstream node "${nodeId}" output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1016
- }
1017
- }
1018
- function resolveOutputSelector(selector, state) {
1019
- const match = selector.match(/^\$\.nodes\[['"]([^'"]+)['"]\]\.output(?:\.(.+))?$/);
1020
- if (!match?.[1]) {
1021
- throw new Error(`unsupported itemsFrom selector: ${selector}`);
1022
- }
1023
- const nodeId = match[1];
1024
- const pathSuffix = match[2];
1025
- let current = getNodeOutputAsJson(state, nodeId);
1026
- if (pathSuffix) {
1027
- for (const segment of pathSuffix.split(".")) {
1028
- if (current &&
1029
- typeof current === "object" &&
1030
- segment in current) {
1031
- current = current[segment];
1032
- continue;
1033
- }
1034
- throw new Error(`output selector path not found: ${selector} (missing "${segment}")`);
1035
- }
1036
- }
1037
- return current;
1038
- }
1039
- function resolveItemsFromSelector(selector, state) {
1040
- let current;
1041
- try {
1042
- current = resolveOutputSelector(selector, state);
1043
- }
1044
- catch (error) {
1045
- const message = error instanceof Error ? error.message : String(error);
1046
- if (message.startsWith("output selector path not found:")) {
1047
- throw new Error(`itemsFrom path not found: ${selector}`);
1048
- }
1049
- throw error;
1050
- }
1051
- if (!Array.isArray(current)) {
1052
- throw new Error(`itemsFrom selector did not resolve to an array: ${selector}`);
1053
- }
1054
- return current;
1055
- }
1056
- function parseConditionLiteral(raw) {
1057
- const trimmed = raw.trim();
1058
- if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
1059
- (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
1060
- return trimmed.slice(1, -1);
1061
- }
1062
- if (trimmed === "true")
1063
- return true;
1064
- if (trimmed === "false")
1065
- return false;
1066
- if (trimmed === "null")
1067
- return null;
1068
- const numeric = Number(trimmed);
1069
- return Number.isFinite(numeric) ? numeric : trimmed;
1070
- }
1071
- function evaluateConditionExpression(expression, state) {
1072
- const equality = expression.match(/^\s*(\$\..+?)\s*==\s*(.+?)\s*$/);
1073
- if (!equality?.[1]) {
1074
- throw new Error(`unsupported condition expression: ${expression}`);
1075
- }
1076
- const actual = resolveOutputSelector(equality[1], state);
1077
- const expected = parseConditionLiteral(equality[2] ?? "");
1078
- return actual === expected;
1079
- }
1080
- function evaluateLoopStopExpression(input) {
1081
- const equality = input.expression.match(/^\s*(\$\..+?)\s*==\s*(.+?)\s*$/);
1082
- if (!equality?.[1]) {
1083
- throw new Error(`unsupported loop_until stop expression: ${input.expression}`);
1084
- }
1085
- const selector = equality[1];
1086
- let actual;
1087
- if (selector.startsWith("$.last.output.")) {
1088
- actual = lookupPath(input.lastOutput, selector.slice("$.last.output.".length));
1089
- }
1090
- else if (selector === "$.iteration") {
1091
- actual = input.iteration;
1092
- }
1093
- else {
1094
- throw new Error(`unsupported loop_until stop selector: ${selector}`);
1095
- }
1096
- const expected = parseConditionLiteral(equality[2] ?? "");
1097
- return actual === expected;
1098
- }
1099
- function executeDynamicCondition(input) {
1100
- const started = Date.now();
1101
- const matchedIndex = input.condition.cases.findIndex((conditionCase) => evaluateConditionExpression(conditionCase.when, input.state));
1102
- const selected = matchedIndex >= 0
1103
- ? input.condition.cases[matchedIndex]?.then
1104
- : input.condition.default;
1105
- if (!selected) {
1106
- throw new Error(`condition ${input.task.id} did not match and has no default`);
1107
- }
1108
- if (!input.tasksById.has(selected)) {
1109
- throw new Error(`condition ${input.task.id} selected missing target ${selected}`);
1110
- }
1111
- const branchTargets = new Set([
1112
- ...input.condition.cases.map((conditionCase) => conditionCase.then),
1113
- ...(input.condition.default ? [input.condition.default] : []),
1114
- ]);
1115
- for (const target of branchTargets) {
1116
- if (target === selected)
1117
- continue;
1118
- const record = input.state.nodes[target];
1119
- if (record?.status === "PENDING") {
1120
- record.status = "SKIPPED";
1121
- record.skippedReason = `condition ${input.task.id} selected ${selected}; branch not selected`;
1122
- }
1123
- }
1124
- return {
1125
- ok: true,
1126
- stdout: JSON.stringify({
1127
- workflowNodeId: input.condition.workflowNodeId,
1128
- selected,
1129
- matchedCaseIndex: matchedIndex >= 0 ? matchedIndex : undefined,
1130
- defaulted: matchedIndex < 0,
1131
- }),
1132
- stderr: "",
1133
- failureCategory: "success",
1134
- durationMs: Date.now() - started,
1135
- };
1136
- }
1137
- function renderLoopTemplate(template, iteration) {
1138
- const iterationStatus = iteration >= 2 ? "passed" : "failed";
1139
- return template
1140
- .replace(/\{\{\s*iteration\s*\}\}/g, String(iteration))
1141
- .replace(/\{\{\s*iterationStatus\s*\}\}/g, iterationStatus);
1142
- }
1143
- function buildLoopBodyChildTask(input) {
1144
- const { parent, bodyTask, iteration, nodeId, bodyIdMap } = input;
1145
- const mappedDepends = bodyTask.dependsOn.map((depId) => bodyIdMap.get(depId) ?? depId);
1146
- return {
1147
- id: nodeId,
1148
- depends_on: mappedDepends.length > 0 ? mappedDepends : parent.depends_on,
1149
- complexity: bodyTask.complexity,
1150
- subtask_prompt: renderLoopTemplate(bodyTask.subtaskPromptTemplate, iteration),
1151
- executor: bodyTask.executor,
1152
- role: bodyTask.role,
1153
- writePolicy: bodyTask.writePolicy,
1154
- allowedPaths: bodyTask.allowedPaths,
1155
- forbiddenPaths: bodyTask.forbiddenPaths,
1156
- writeSet: bodyTask.writeSet,
1157
- outputContract: bodyTask.outputContract,
1158
- static: bodyTask.executor === "static"
1159
- ? {
1160
- resultMarkdown: bodyTask.staticResultTemplate
1161
- ? renderLoopTemplate(bodyTask.staticResultTemplate, iteration)
1162
- : "dynamic loop child completed",
1163
- }
1164
- : undefined,
1165
- };
1166
- }
1167
- async function executeDynamicLoopUntil(input) {
1168
- const started = Date.now();
1169
- const executedChildren = [];
1170
- let stopped = false;
1171
- let stopReason = "";
1172
- let iterations = 0;
1173
- for (let iteration = 1; iteration <= input.loop.maxIterations; iteration += 1) {
1174
- iterations = iteration;
1175
- const bodyIdMap = new Map(input.loop.bodyTasks.map((bodyTask) => [
1176
- bodyTask.id,
1177
- `${input.task.id}-r${iteration}-${bodyTask.id}`,
1178
- ]));
1179
- const children = input.loop.bodyTasks.map((bodyTask) => buildLoopBodyChildTask({
1180
- parent: input.task,
1181
- loop: input.loop,
1182
- bodyTask,
1183
- iteration,
1184
- nodeId: bodyIdMap.get(bodyTask.id),
1185
- bodyIdMap,
1186
- }));
1187
- for (const child of children) {
1188
- if (!input.tasksById.has(child.id)) {
1189
- input.tasksById.set(child.id, child);
1190
- input.spec.tasks.push(child);
1191
- }
1192
- if (!input.state.nodes[child.id]) {
1193
- input.state.nodes[child.id] = freshNodeRecord(child);
1194
- }
1195
- }
1196
- input.state.ranks.push(children.map((child) => child.id));
1197
- await writeDagRunSpec(input.runDir, input.spec);
1198
- await input.persistState();
1199
- for (const child of children) {
1200
- await executeDagNode({
1201
- nodeId: child.id,
1202
- tasksById: input.tasksById,
1203
- state: input.state,
1204
- spec: input.spec,
1205
- cwd: input.cwd,
1206
- runDir: input.runDir,
1207
- executeNode: input.executeNode,
1208
- observer: input.observer,
1209
- persistState: input.persistState,
1210
- onPause: () => {
1211
- throw new Error(`dynamic child node ${child.id} requested a human pause; loop_until children do not support pause in v0`);
1212
- },
1213
- });
1214
- executedChildren.push(child.id);
1215
- }
1216
- const failedChild = children.find((child) => input.state.nodes[child.id]?.status !== "FINISHED");
1217
- if (failedChild) {
1218
- return {
1219
- ok: false,
1220
- stdout: JSON.stringify({
1221
- workflowNodeId: input.loop.workflowNodeId,
1222
- iterations,
1223
- stopped: false,
1224
- children: executedChildren,
1225
- }),
1226
- stderr: `loop_until child failed: ${failedChild.id}`,
1227
- failureCategory: "dynamic-loop-child-failed",
1228
- durationMs: Date.now() - started,
1229
- };
1230
- }
1231
- const lastChild = children[children.length - 1];
1232
- const lastOutput = parseJsonFromText(input.state.nodes[lastChild.id]?.stdout);
1233
- const matched = input.loop.stopWhenAny.find((expression) => evaluateLoopStopExpression({
1234
- expression,
1235
- lastOutput,
1236
- iteration,
1237
- }));
1238
- if (matched) {
1239
- stopped = true;
1240
- stopReason = matched;
1241
- break;
1242
- }
1243
- }
1244
- const manifest = {
1245
- workflowNodeId: input.loop.workflowNodeId,
1246
- expandedAt: new Date().toISOString(),
1247
- maxIterations: input.loop.maxIterations,
1248
- iterations,
1249
- stopped,
1250
- stopReason: stopped ? stopReason : undefined,
1251
- children: executedChildren.map((nodeId) => ({ nodeId })),
1252
- };
1253
- const expansionDir = path.join(input.runDir, "expansions");
1254
- await mkdir(expansionDir, { recursive: true });
1255
- await writeFile(path.join(expansionDir, `${input.task.id}.expansion.json`), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
1256
- return {
1257
- ok: true,
1258
- stdout: JSON.stringify({
1259
- workflowNodeId: input.loop.workflowNodeId,
1260
- iterations,
1261
- stopped,
1262
- stopReason: stopped ? stopReason : undefined,
1263
- maxIterationsReached: !stopped,
1264
- children: executedChildren,
1265
- }),
1266
- stderr: "",
1267
- failureCategory: "success",
1268
- durationMs: Date.now() - started,
1269
- };
1270
- }
1271
- function lookupPath(value, pathExpression) {
1272
- let current = value;
1273
- for (const segment of pathExpression.split(".")) {
1274
- if (current &&
1275
- typeof current === "object" &&
1276
- segment in current) {
1277
- current = current[segment];
1278
- continue;
1279
- }
1280
- return undefined;
1281
- }
1282
- return current;
1283
- }
1284
- function templateValueToString(value) {
1285
- if (typeof value === "string" ||
1286
- typeof value === "number" ||
1287
- typeof value === "boolean") {
1288
- return String(value);
1289
- }
1290
- if (value === null || value === undefined)
1291
- return "";
1292
- return JSON.stringify(value);
1293
- }
1294
- function parseJsonFromText(value) {
1295
- if (!value?.trim())
1296
- return undefined;
1297
- try {
1298
- return JSON.parse(value);
1299
- }
1300
- catch {
1301
- return undefined;
1302
- }
1303
- }
1304
- function renderDynamicTemplate(template, item, index, itemName) {
1305
- const primitive = typeof item === "string" ||
1306
- typeof item === "number" ||
1307
- typeof item === "boolean"
1308
- ? String(item)
1309
- : JSON.stringify(item);
1310
- return template
1311
- .replace(/\{\{\s*item\s*\}\}/g, primitive)
1312
- .replace(new RegExp(`\\{\\{\\s*${itemName}\\s*\\}\\}`, "g"), primitive)
1313
- .replace(new RegExp(`\\{\\{\\s*${itemName}\\.([A-Za-z0-9_.-]+)\\s*\\}\\}`, "g"), (_match, pathExpression) => templateValueToString(lookupPath(item, pathExpression)))
1314
- .replace(/\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, pathExpression) => templateValueToString(lookupPath(item, pathExpression)))
1315
- .replace(/\{\{\s*itemJson\s*\}\}/g, JSON.stringify(item))
1316
- .replace(/\{\{\s*index\s*\}\}/g, String(index));
1317
- }
1318
- function renderDynamicPatternList(patterns, item, index, itemName) {
1319
- return patterns?.map((pattern) => renderDynamicTemplate(pattern, item, index, itemName));
1320
- }
1321
- function buildExpandedChildTask(input) {
1322
- const { parent, expansion, item, index, nodeId } = input;
1323
- const child = expansion.childTask;
1324
- const subtaskPrompt = renderDynamicTemplate(child.subtaskPromptTemplate, item, index, expansion.itemName);
1325
- const staticResult = child.staticResultTemplate
1326
- ? renderDynamicTemplate(child.staticResultTemplate, item, index, expansion.itemName)
1327
- : undefined;
1328
- return {
1329
- id: nodeId,
1330
- depends_on: parent.depends_on,
1331
- complexity: child.complexity,
1332
- subtask_prompt: subtaskPrompt,
1333
- executor: child.executor,
1334
- role: child.role,
1335
- writePolicy: child.writePolicy,
1336
- allowedPaths: renderDynamicPatternList(child.allowedPaths, item, index, expansion.itemName) ?? [],
1337
- forbiddenPaths: renderDynamicPatternList(child.forbiddenPaths, item, index, expansion.itemName) ?? [],
1338
- writeSet: renderDynamicPatternList(child.writeSet, item, index, expansion.itemName),
1339
- outputContract: child.outputContract,
1340
- static: child.executor === "static"
1341
- ? { resultMarkdown: staticResult ?? "dynamic child completed" }
1342
- : undefined,
1343
- };
1344
- }
1345
- function addExpansionRank(input) {
1346
- if (input.state.ranks.some((rank) => input.childNodeIds.every((nodeId) => rank.includes(nodeId)))) {
1347
- return;
1348
- }
1349
- const parentRankIndex = input.state.ranks.findIndex((rank) => rank.includes(input.parentNodeId));
1350
- const insertAt = parentRankIndex >= 0 ? parentRankIndex + 1 : input.state.ranks.length;
1351
- input.state.ranks.splice(insertAt, 0, input.childNodeIds);
1352
- }
1353
- function normalizedWritePattern(pattern) {
1354
- return pattern.replace(/\\/g, "/").replace(/^\.\//, "");
1355
- }
1356
- function writePatternsOverlap(left, right) {
1357
- const normalizedLeft = normalizedWritePattern(left);
1358
- const normalizedRight = normalizedWritePattern(right);
1359
- if (normalizedLeft === normalizedRight)
1360
- return true;
1361
- const leftPrefix = normalizedLeft.replace(/\*\*.*$/, "");
1362
- const rightPrefix = normalizedRight.replace(/\*\*.*$/, "");
1363
- return (leftPrefix.length > 0 &&
1364
- rightPrefix.length > 0 &&
1365
- (leftPrefix.startsWith(rightPrefix) || rightPrefix.startsWith(leftPrefix)));
1366
- }
1367
- function collectDynamicChildWriteSetConflicts(children) {
1368
- const conflicts = [];
1369
- const exclusiveChildren = children.filter((child) => child.writePolicy === "exclusive" && (child.writeSet?.length ?? 0) > 0);
1370
- for (let i = 0; i < exclusiveChildren.length; i += 1) {
1371
- for (let j = i + 1; j < exclusiveChildren.length; j += 1) {
1372
- const left = exclusiveChildren[i];
1373
- const right = exclusiveChildren[j];
1374
- for (const leftEntry of left.writeSet ?? []) {
1375
- for (const rightEntry of right.writeSet ?? []) {
1376
- if (writePatternsOverlap(leftEntry, rightEntry)) {
1377
- conflicts.push(`${left.id}:${leftEntry} overlaps ${right.id}:${rightEntry}`);
1378
- }
1379
- }
1380
- }
1381
- }
1382
- }
1383
- return conflicts;
1384
- }
1385
- function resolveRunLocalPath(runDir, ref, label) {
1386
- if (path.isAbsolute(ref)) {
1387
- throw new Error(`${label} must be relative to the DAG run directory: ${ref}`);
1388
- }
1389
- const runRoot = path.resolve(runDir);
1390
- const resolved = path.resolve(runRoot, ref);
1391
- if (resolved !== runRoot && !resolved.startsWith(`${runRoot}${path.sep}`)) {
1392
- throw new Error(`${label} escapes the DAG run directory: ${ref}`);
1393
- }
1394
- return resolved;
1395
- }
1396
- async function executeDynamicMapExpansion(input) {
1397
- const started = Date.now();
1398
- const items = resolveItemsFromSelector(input.expansion.itemsFrom, input.state);
1399
- if (items.length > input.expansion.maxExpandedNodes) {
1400
- throw new Error(`map_agent ${input.task.id} item count ${items.length} exceeds maxExpandedNodes ${input.expansion.maxExpandedNodes}`);
1401
- }
1402
- if (items.length > input.expansion.maxItems) {
1403
- throw new Error(`map_agent ${input.task.id} item count ${items.length} exceeds maxItems ${input.expansion.maxItems}`);
1404
- }
1405
- const childNodeIds = items.map((_, index) => `${input.expansion.childIdPrefix}-${String(index + 1).padStart(4, "0")}`);
1406
- const children = childNodeIds.map((nodeId, index) => buildExpandedChildTask({
1407
- parent: input.task,
1408
- expansion: input.expansion,
1409
- item: items[index],
1410
- index,
1411
- nodeId,
1412
- }));
1413
- const writeSetConflicts = collectDynamicChildWriteSetConflicts(children);
1414
- if (writeSetConflicts.length > 0) {
1415
- throw new Error(`dynamic map_agent ${input.task.id} expanded overlapping writeSets: ${writeSetConflicts.join("; ")}`);
1416
- }
1417
- const workspaceRefs = items.map((item, index) => input.expansion.workspaceTemplate
1418
- ? renderDynamicTemplate(input.expansion.workspaceTemplate, item, index, input.expansion.itemName)
1419
- : undefined);
1420
- for (const workspaceRef of workspaceRefs) {
1421
- if (!workspaceRef)
1422
- continue;
1423
- await mkdir(resolveRunLocalPath(input.runDir, workspaceRef, "workspaceRef"), {
1424
- recursive: true,
1425
- });
1426
- }
1427
- for (const child of children) {
1428
- if (!input.tasksById.has(child.id)) {
1429
- input.tasksById.set(child.id, child);
1430
- input.spec.tasks.push(child);
1431
- }
1432
- if (!input.state.nodes[child.id]) {
1433
- input.state.nodes[child.id] = freshNodeRecord(child);
1434
- }
1435
- }
1436
- addExpansionRank({
1437
- state: input.state,
1438
- parentNodeId: input.task.id,
1439
- childNodeIds,
1440
- });
1441
- await writeDagRunSpec(input.runDir, input.spec);
1442
- const manifest = {
1443
- workflowNodeId: input.expansion.workflowNodeId,
1444
- expandedAt: new Date().toISOString(),
1445
- itemsFrom: input.expansion.itemsFrom,
1446
- itemCount: items.length,
1447
- children: childNodeIds.map((nodeId, index) => ({
1448
- nodeId,
1449
- itemRef: `$.items[${index}]`,
1450
- itemValueHash: sha256Json(items[index]),
1451
- workspaceRef: workspaceRefs[index],
1452
- })),
1453
- };
1454
- const expansionDir = path.join(input.runDir, "expansions");
1455
- await mkdir(expansionDir, { recursive: true });
1456
- await writeFile(path.join(expansionDir, `${input.task.id}.expansion.json`), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
1457
- await input.persistState();
1458
- for (const child of children) {
1459
- const childRecord = input.state.nodes[child.id];
1460
- if (childRecord?.status === "FINISHED")
1461
- continue;
1462
- await executeDagNode({
1463
- nodeId: child.id,
1464
- tasksById: input.tasksById,
1465
- state: input.state,
1466
- spec: input.spec,
1467
- cwd: input.cwd,
1468
- runDir: input.runDir,
1469
- executeNode: input.executeNode,
1470
- observer: input.observer,
1471
- persistState: input.persistState,
1472
- onPause: () => {
1473
- throw new Error(`dynamic child node ${child.id} requested a human pause; map_agent children do not support pause in v0`);
1474
- },
1475
- });
1476
- }
1477
- const failedChildren = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status !== "FINISHED");
1478
- const aggregate = {
1479
- workflowNodeId: input.expansion.workflowNodeId,
1480
- itemCount: items.length,
1481
- children: childNodeIds.map((nodeId, index) => ({
1482
- nodeId,
1483
- item: items[index],
1484
- workspaceRef: workspaceRefs[index],
1485
- status: input.state.nodes[nodeId]?.status,
1486
- stdout: input.state.nodes[nodeId]?.stdout,
1487
- output: parseJsonFromText(input.state.nodes[nodeId]?.stdout),
1488
- assistantText: input.state.nodes[nodeId]?.assistantText,
1489
- })),
1490
- };
1491
- return {
1492
- ok: failedChildren.length === 0,
1493
- stdout: JSON.stringify(aggregate),
1494
- stderr: failedChildren.length > 0
1495
- ? `dynamic map children failed: ${failedChildren.join(", ")}`
1496
- : "",
1497
- failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
1498
- durationMs: Date.now() - started,
1499
- };
1500
- }
1501
- function parseFindingsFromSelector(selector, state) {
1502
- const raw = resolveOutputSelector(selector, state);
1503
- if (!Array.isArray(raw)) {
1504
- throw new Error(`findings selector did not resolve to an array: ${selector}`);
1505
- }
1506
- return raw.map((entry) => {
1507
- const direct = findingSchema.safeParse(entry);
1508
- if (direct.success)
1509
- return direct.data;
1510
- if (entry && typeof entry === "object") {
1511
- const output = entry.output;
1512
- const fromOutput = findingSchema.safeParse(output);
1513
- if (fromOutput.success)
1514
- return fromOutput.data;
1515
- const stdout = entry.stdout;
1516
- if (typeof stdout === "string") {
1517
- const fromStdout = findingSchema.safeParse(parseJsonFromText(stdout));
1518
- if (fromStdout.success)
1519
- return fromStdout.data;
1520
- }
1521
- }
1522
- return findingSchema.parse(entry);
1523
- });
1524
- }
1525
- function parseVerificationFromChildEntry(entry) {
1526
- if (!entry || typeof entry !== "object")
1527
- return undefined;
1528
- const stdout = entry.stdout;
1529
- if (typeof stdout !== "string" || stdout.trim().length === 0) {
1530
- return undefined;
1531
- }
1532
- return verificationSchema.parse(JSON.parse(stdout));
1533
- }
1534
- function parseVerificationsFromSelector(selector, state) {
1535
- const raw = resolveOutputSelector(selector, state);
1536
- if (!Array.isArray(raw)) {
1537
- throw new Error(`verifications selector did not resolve to an array: ${selector}`);
1538
- }
1539
- return raw
1540
- .map(parseVerificationFromChildEntry)
1541
- .filter((entry) => Boolean(entry));
1542
- }
1543
- async function executeDynamicReduction(input) {
1544
- const started = Date.now();
1545
- if (input.reduction.type !== "verified_findings_report") {
1546
- throw new Error(`unsupported dynamic reduction: ${input.reduction.type}`);
1547
- }
1548
- const findings = parseFindingsFromSelector(input.reduction.findingsFrom, input.state);
1549
- const verifications = parseVerificationsFromSelector(input.reduction.verificationsFrom, input.state);
1550
- const nodeDir = path.join(input.runDir, input.task.id);
1551
- await mkdir(nodeDir, { recursive: true });
1552
- const refutedFindingsPath = path.join(nodeDir, input.reduction.refutedFindingsArtifactName);
1553
- const { report, refutedFindings } = buildVerifiedFindingsReport({
1554
- findings,
1555
- verifications,
1556
- refutedFindingsRef: refutedFindingsPath.startsWith(input.runDir)
1557
- ? repoRelativePath(input.runDir, refutedFindingsPath)
1558
- : refutedFindingsPath,
1559
- });
1560
- await writeFile(refutedFindingsPath, `${JSON.stringify(refutedFindings, null, 2)}\n`, "utf-8");
1561
- return {
1562
- ok: true,
1563
- stdout: JSON.stringify(report),
1564
- stderr: "",
1565
- failureCategory: "success",
1566
- durationMs: Date.now() - started,
1567
- };
1568
- }
1569
365
  async function notifyRunObserver(observer, event, state) {
1570
366
  try {
1571
367
  await observer?.[event]?.(state);
@@ -1574,18 +370,6 @@ async function notifyRunObserver(observer, event, state) {
1574
370
  // Observers are derived views; they must not affect canonical DAG execution.
1575
371
  }
1576
372
  }
1577
- async function notifyNodeObserver(observer, event, nodeId, state, chunk) {
1578
- try {
1579
- if (event === "onNodeOutput") {
1580
- await observer?.onNodeOutput?.(nodeId, chunk ?? "", state);
1581
- return;
1582
- }
1583
- await observer?.[event]?.(nodeId, state);
1584
- }
1585
- catch {
1586
- // Observers are derived views; they must not affect canonical DAG execution.
1587
- }
1588
- }
1589
373
  function finalizeTerminalRunStatus(state, taskCount) {
1590
374
  const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
1591
375
  const errorCount = Object.values(state.nodes).filter((n) => n.status === "ERROR").length;
@@ -1606,14 +390,10 @@ async function stopCursorWorkerIfRunning() {
1606
390
  await stopCursorWorker();
1607
391
  }
1608
392
  }
1609
- function isWriteGuardedAgentExecutor(task) {
1610
- return (task.executor === "cursor" ||
1611
- (task.executor === "pi" && task.toolProfile === "write"));
1612
- }
1613
- function concurrentSiblingWriteSetsForNode(rankWriteGuardedNodeIds, nodeId, tasksById) {
1614
- if (rankWriteGuardedNodeIds.length <= 1)
393
+ function concurrentSiblingWriteSetsForNode(rankCursorNodeIds, nodeId, tasksById) {
394
+ if (rankCursorNodeIds.length <= 1)
1615
395
  return [];
1616
- return rankWriteGuardedNodeIds
396
+ return rankCursorNodeIds
1617
397
  .filter((id) => id !== nodeId)
1618
398
  .map((id) => tasksById.get(id)?.writeSet ?? [])
1619
399
  .filter((writeSet) => writeSet.length > 0);
@@ -1623,55 +403,16 @@ function buildRankAwareExecuteNode(input) {
1623
403
  return input.baseExecuteNode;
1624
404
  }
1625
405
  return async (nodeInput) => {
1626
- if (!isWriteGuardedAgentExecutor(nodeInput.task) || !nodeInput.model) {
406
+ if (nodeInput.task.executor !== "cursor" || !nodeInput.model) {
1627
407
  return input.baseExecuteNode(nodeInput);
1628
408
  }
1629
409
  const siblingWriteSets = nodeInput.task.writePolicy === "exclusive"
1630
- ? concurrentSiblingWriteSetsForNode(input.rankWriteGuardedNodeIds, nodeInput.task.id, input.tasksById)
410
+ ? concurrentSiblingWriteSetsForNode(input.rankCursorNodeIds, nodeInput.task.id, input.tasksById)
1631
411
  : [];
1632
- const meta = {
412
+ return executeDagCursorNode({ ...nodeInput, model: nodeInput.model }, {
1633
413
  ...input.meta,
1634
414
  concurrentSiblingWriteSets: siblingWriteSets.length > 0 ? siblingWriteSets : undefined,
1635
415
  writeGuardAttribution: siblingWriteSets.length > 0 ? "best-effort" : "per-node",
1636
- };
1637
- if (nodeInput.task.executor === "pi") {
1638
- return executeDagPiNode({ ...nodeInput, model: nodeInput.model }, meta);
1639
- }
1640
- return executeDagCursorNode({ ...nodeInput, model: nodeInput.model }, meta);
416
+ });
1641
417
  };
1642
418
  }
1643
- function shouldSkipNode(node, task, nodes) {
1644
- if (!node || node.status !== "PENDING")
1645
- return false;
1646
- return task.depends_on.some((depId) => {
1647
- const dep = nodes[depId];
1648
- return !dep || dep.status === "ERROR" || dep.status === "SKIPPED";
1649
- });
1650
- }
1651
- function isConditionSkippedReason(reason) {
1652
- return Boolean(reason?.startsWith("condition "));
1653
- }
1654
- function conditionSkippedByAncestor(task, nodes) {
1655
- return task.depends_on.some((depId) => isConditionSkippedReason(nodes[depId]?.skippedReason));
1656
- }
1657
- async function mapConcurrent(items, limit, fn) {
1658
- const executing = new Set();
1659
- for (const item of items) {
1660
- const job = fn(item).finally(() => executing.delete(job));
1661
- executing.add(job);
1662
- if (executing.size >= limit) {
1663
- await Promise.race(executing);
1664
- }
1665
- }
1666
- await Promise.all(executing);
1667
- }
1668
- async function writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills) {
1669
- if (resolvedSkills.length === 0)
1670
- return;
1671
- const nodeDir = path.join(runDir, nodeId);
1672
- await mkdir(nodeDir, { recursive: true });
1673
- await writeFile(path.join(nodeDir, "skills.json"), `${JSON.stringify({ resolvedSkills }, null, 2)}\n`, "utf-8");
1674
- }
1675
- async function appendNodeLog(runDir, nodeId, record) {
1676
- await writeFile(path.join(runDir, `${nodeId}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf-8");
1677
- }