@tea-agent/loop-agent 0.9.0 → 0.10.0-alpha.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 (183) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +92 -11
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +4 -14
  5. package/dist/application/dag/generate-task-dag.js +32 -2
  6. package/dist/application/dag/run-dag.js +1 -27
  7. package/dist/application/dag/validate-dag.js +2 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +7 -11
  10. package/dist/cli/program.js +9 -21
  11. package/dist/commands/cursor-prompt.js +42 -82
  12. package/dist/commands/dag-approve.js +36 -0
  13. package/dist/commands/dag-reconcile-run.js +118 -0
  14. package/dist/commands/delegate.js +75 -77
  15. package/dist/commands/doctor.js +0 -18
  16. package/dist/commands/init.js +60 -40
  17. package/dist/commands/instructions.js +7 -10
  18. package/dist/commands/loop.js +4 -20
  19. package/dist/executors/config-core.js +0 -51
  20. package/dist/executors/dag-pi-executor.js +1 -1
  21. package/dist/executors/dag.js +0 -1
  22. package/dist/executors/index.js +0 -2
  23. package/dist/executors/model-routing.js +9 -9
  24. package/dist/executors/shell-executor.js +1 -1
  25. package/dist/governance/checks.js +6 -3
  26. package/dist/governance/manifest-types.js +33 -2
  27. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  28. package/dist/records/harvest.js +2 -23
  29. package/dist/records/one-shot-runs.js +1 -1
  30. package/dist/shared/artifacts-core.js +24 -5
  31. package/dist/shared/output-truncation.js +37 -0
  32. package/dist/shared/package-metadata.js +353 -0
  33. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  34. package/dist/sidecars/cursor-prompt/index.js +3 -0
  35. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  36. package/dist/task/config-types.js +29 -12
  37. package/dist/task/delegate.js +9 -21
  38. package/dist/task/runtime.js +1 -2
  39. package/dist/worker/cli.js +243 -0
  40. package/dist/worker/closeout/apply.js +73 -0
  41. package/dist/worker/closeout/preview.js +30 -0
  42. package/dist/worker/delivery/final-verification.js +194 -0
  43. package/dist/worker/delivery/git-transaction.js +354 -0
  44. package/dist/worker/delivery/package.js +502 -0
  45. package/dist/worker/feature/decision-loader.js +68 -0
  46. package/dist/worker/feature/discover.js +14 -0
  47. package/dist/worker/feature/next-action.js +74 -0
  48. package/dist/worker/feature/reducer.js +133 -0
  49. package/dist/worker/feature/review.js +502 -0
  50. package/dist/worker/feature/run.js +365 -0
  51. package/dist/worker/feature/types.js +1 -0
  52. package/dist/worker/follow-up/approve.js +270 -0
  53. package/dist/worker/follow-up/factory.js +234 -0
  54. package/dist/worker/follow-up/paths.js +25 -0
  55. package/dist/worker/follow-up/policy.js +26 -0
  56. package/dist/worker/follow-up/schema.js +93 -0
  57. package/dist/worker/follow-up/store.js +96 -0
  58. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  59. package/dist/worker/metrics/projector.js +139 -0
  60. package/dist/worker/observability/read-model.js +282 -15
  61. package/dist/worker/observe/paths.js +17 -5
  62. package/dist/worker/observe/static/app.js +443 -61
  63. package/dist/worker/observe/static/index.html +3 -1
  64. package/dist/worker/observe/static/styles.css +85 -18
  65. package/dist/worker/pool/run-store.js +14 -2
  66. package/dist/worker/pool/validation.js +59 -0
  67. package/dist/worker/preflight.js +49 -1
  68. package/dist/worker/report/morning-report.js +41 -6
  69. package/dist/worker/run-task/run-task.js +23 -13
  70. package/dist/worker/runner/run-ready.js +89 -11
  71. package/dist/worker/task-spec/schema.js +0 -1
  72. package/dist/workflows/dag/convergence/controller.js +1 -1
  73. package/dist/workflows/dag/executor-registry.js +0 -2
  74. package/dist/workflows/dag/governance-profile.js +10 -0
  75. package/dist/workflows/dag/init-hybrid.js +601 -26
  76. package/dist/workflows/dag/lifecycle.js +146 -0
  77. package/dist/workflows/dag/node-execution.js +64 -7
  78. package/dist/workflows/dag/prompt.js +16 -0
  79. package/dist/workflows/dag/report.js +2 -0
  80. package/dist/workflows/dag/runner.js +176 -119
  81. package/dist/workflows/dag/scheduler.js +7 -2
  82. package/dist/workflows/dag/skill-snapshot.js +527 -0
  83. package/dist/workflows/dag/types.js +45 -9
  84. package/dist/workflows/dag/validate.js +5 -8
  85. package/dist/workflows/loop/actions/dag-action.js +0 -2
  86. package/dist/workflows/loop/actions/shared.js +1 -1
  87. package/dist/workflows/loop/actions.js +14 -31
  88. package/dist/workflows/loop/benchmark.js +1 -1
  89. package/dist/workflows/loop/index.js +1 -1
  90. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  91. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  92. package/docs/README.md +35 -16
  93. package/docs/agent-dag-recovery-playbook.md +1 -1
  94. package/docs/architecture/README.md +26 -0
  95. package/docs/architecture/dag-execution.md +134 -0
  96. package/docs/architecture/evolution.md +52 -0
  97. package/docs/architecture/facts-and-state.md +58 -0
  98. package/docs/architecture/runtime-boundaries.md +41 -15
  99. package/docs/architecture/system-overview.md +93 -0
  100. package/docs/architecture/worker-and-feature.md +81 -0
  101. package/docs/cursor-prompt-sidecar.md +36 -0
  102. package/docs/decisions/README.md +13 -1
  103. package/docs/design/README.md +39 -13
  104. package/docs/development-principles.md +1 -1
  105. package/docs/exec-plans/active/README.md +2 -2
  106. package/docs/exec-plans/completed/README.md +14 -1
  107. package/docs/feature-workflow.md +44 -4
  108. package/docs/init-surface.manifest.json +63 -1
  109. package/docs/loop-agent-harness.md +65 -3
  110. package/docs/progress/README.md +27 -0
  111. package/docs/reports/README.md +74 -8
  112. package/docs/skills/README.md +2 -1
  113. package/docs/skills/vetted-skill-registry.md +2 -1
  114. package/docs/templates/agent-dag-report.schema.json +4 -2
  115. package/docs/templates/agent-dag.base.json +0 -5
  116. package/docs/templates/agent-dag.final-verification.json +0 -5
  117. package/docs/templates/agent-dag.schema.json +1 -2
  118. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  119. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  120. package/docs/templates/backend-test-dag.json +213 -0
  121. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  122. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  123. package/docs/templates/frontend-design-contract.md +33 -0
  124. package/docs/templates/frontend-task-constraints.md +25 -0
  125. package/docs/templates/frontend-task-requirement.md +61 -0
  126. package/docs/templates/harness.schema.json +8 -5
  127. package/docs/templates/hybrid-dag.json +1 -6
  128. package/docs/templates/init-evolution-review.md +4 -2
  129. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  130. package/docs/templates/product-line/task.yaml +0 -1
  131. package/docs/templates/worker-dogfood-evidence.md +28 -0
  132. package/docs/templates/worker-dogfood-setup.md +20 -0
  133. package/docs/verification-matrix.md +17 -0
  134. package/examples/decision-gate-agent-dag.json +87 -33
  135. package/examples/example-dag.json +0 -5
  136. package/examples/hybrid-loop-agent-dag.json +0 -5
  137. package/harness.json +6 -11
  138. package/package.json +22 -44
  139. package/scripts/check-product-line-docs.sh +10 -3
  140. package/skills/agent-worker/SKILL.md +37 -0
  141. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  142. package/skills/frontend-design-review/SKILL.md +59 -0
  143. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  144. package/skills/frontend-implementation/SKILL.md +48 -0
  145. package/skills/frontend-implementation/references/code-standards.md +34 -0
  146. package/skills/frontend-implementation/references/design-spec.md +46 -0
  147. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  148. package/skills/frontend-review/SKILL.md +53 -0
  149. package/skills/frontend-review/references/review-findings.md +42 -0
  150. package/skills/frontend-verification/SKILL.md +40 -0
  151. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  152. package/skills/grill-me/SKILL.md +10 -0
  153. package/skills/grill-with-docs/SKILL.md +88 -0
  154. package/skills/grill-with-docs/adr-format.md +47 -0
  155. package/skills/grill-with-docs/context-format.md +60 -0
  156. package/skills/init-capability-evolution/SKILL.md +1 -0
  157. package/skills/loop-agent/SKILL.md +11 -9
  158. package/skills/loop-agent/references/command-reference.md +28 -15
  159. package/skills/loop-agent/references/docs-converge.md +126 -0
  160. package/skills/loop-agent/references/harness-policy.md +7 -7
  161. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  162. package/skills/loop-agent/references/long-running-loop.md +4 -6
  163. package/skills/loop-agent/references/multi-worktree.md +6 -6
  164. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  165. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  166. package/skills/using-git-worktrees/SKILL.md +215 -0
  167. package/dist/commands/cursor-worker.js +0 -43
  168. package/dist/cursor-worker-entry.js +0 -8
  169. package/dist/executors/cursor-artifacts.js +0 -33
  170. package/dist/executors/cursor-execution-log.js +0 -81
  171. package/dist/executors/cursor-executor-artifacts.js +0 -134
  172. package/dist/executors/cursor-run.js +0 -115
  173. package/dist/executors/cursor-tool.js +0 -94
  174. package/dist/executors/cursor-worker-client.js +0 -223
  175. package/dist/executors/cursor-worker-protocol.js +0 -18
  176. package/dist/executors/cursor-worker-server.js +0 -54
  177. package/dist/executors/cursor-worker.js +0 -3
  178. package/dist/executors/cursor.js +0 -6
  179. package/dist/executors/dag-cursor-executor.js +0 -87
  180. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  181. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  182. package/docs/cursor-executor-usage.md +0 -25
  183. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -1,18 +1,20 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
+ import { hostname } from "node:os";
2
3
  import path from "node:path";
3
4
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
4
5
  import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
5
6
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
6
7
  import { createDagNodeExecutor } from "./executor-registry.js";
7
- import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
8
+ import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
8
9
  import { assertValidDagSpec } from "./validate.js";
9
10
  import { normalizeDagPromptSources } from "./prompt-source.js";
10
11
  import { relocateConvergenceArtifactPaths, relocateRunArtifactPaths, } from "./upstream-artifacts.js";
12
+ import { createSkillSnapshot, prepareSkillSnapshotForContinuation, writeSkillSnapshot, } from "./skill-snapshot.js";
11
13
  import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
12
14
  import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
13
15
  import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
14
16
  import { topoSortToRanks } from "./topo.js";
15
- import { parseDagSpec, } from "./types.js";
17
+ import { parseDagSpec, resolveModelForTask, } from "./types.js";
16
18
  import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
17
19
  import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
18
20
  import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
@@ -109,6 +111,9 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
109
111
  status: "PENDING",
110
112
  executor: task.executor,
111
113
  complexity: task.complexity,
114
+ ...(resolveModelForTask(task, spec.executorModels)
115
+ ? { model: resolveModelForTask(task, spec.executorModels) }
116
+ : {}),
112
117
  };
113
118
  }
114
119
  return {
@@ -117,6 +122,12 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
117
122
  runId,
118
123
  cwd: opts.cwd,
119
124
  startedAt: new Date().toISOString(),
125
+ runner: {
126
+ pid: process.pid,
127
+ hostname: hostname(),
128
+ startedAt: new Date().toISOString(),
129
+ heartbeatAt: new Date().toISOString(),
130
+ },
120
131
  status: opts.initOnly || opts.dryRun ? "pending" : "running",
121
132
  ranks,
122
133
  nodes,
@@ -152,6 +163,19 @@ export async function runDag(spec, opts) {
152
163
  const runDir = activeRunDir;
153
164
  await prepareActiveRunDir(runDir);
154
165
  await writeRunSpec(runDir, spec);
166
+ // Resolve and snapshot all skill profiles before any node executes.
167
+ try {
168
+ const snapshot = await createSkillSnapshot({
169
+ runId: state.runId,
170
+ spec,
171
+ cwd: opts.cwd,
172
+ mode: "run-start",
173
+ });
174
+ state.skillSnapshotRef = await writeSkillSnapshot(runDir, snapshot);
175
+ }
176
+ catch (error) {
177
+ throw new Error(`failed to create run-start skill snapshot: ${error instanceof Error ? error.message : String(error)}`);
178
+ }
155
179
  await writeRunState(runDir, state);
156
180
  await notifyRunObserver(opts.observer, "onRunStart", state);
157
181
  if (opts.dryRun || opts.initOnly) {
@@ -180,7 +204,6 @@ export async function runDag(spec, opts) {
180
204
  });
181
205
  }
182
206
  finally {
183
- await stopCursorWorkerIfRunning();
184
207
  }
185
208
  }
186
209
  export async function resumeDagRun(opts) {
@@ -211,8 +234,34 @@ export async function resumeDagRun(opts) {
211
234
  catch {
212
235
  throw new Error(`dag run ${opts.runId} is missing human-approval.json for node ${decisionNodeId}`);
213
236
  }
237
+ // Validate or backfill before mutating runner/status for this resume attempt.
238
+ try {
239
+ const prepared = await prepareSkillSnapshotForContinuation({
240
+ runDir,
241
+ runId: state.runId,
242
+ spec,
243
+ state,
244
+ cwd: state.cwd,
245
+ ref: state.skillSnapshotRef,
246
+ });
247
+ if (prepared.backfilled) {
248
+ console.warn(`[dag resume] warning: run ${state.runId} has no skill snapshot ref; creating a legacy-resume-backfill that freezes remaining nodes only`);
249
+ state.skillSnapshotRef = prepared.ref;
250
+ await writeRunState(runDir, state);
251
+ }
252
+ }
253
+ catch (error) {
254
+ throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
255
+ }
214
256
  const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
215
257
  state.status = "running";
258
+ const resumedAt = new Date().toISOString();
259
+ state.runner = {
260
+ pid: process.pid,
261
+ hostname: hostname(),
262
+ startedAt: resumedAt,
263
+ heartbeatAt: resumedAt,
264
+ };
216
265
  const activeRunDir = getDagRunDir(opts.cwd, "active", state.runId);
217
266
  const completedRunDir = getDagRunDir(opts.cwd, "completed", state.runId);
218
267
  const pausedRunDir = getDagRunDir(opts.cwd, "paused", state.runId);
@@ -232,7 +281,6 @@ export async function resumeDagRun(opts) {
232
281
  });
233
282
  }
234
283
  finally {
235
- await stopCursorWorkerIfRunning();
236
284
  }
237
285
  }
238
286
  async function executeDagCheckpoint(input) {
@@ -243,124 +291,136 @@ async function executeDagCheckpoint(input) {
243
291
  stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
244
292
  await stateWriteQueue;
245
293
  };
246
- const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
247
- const baseExecuteNode = input.executeNode ??
248
- createDagNodeExecutor({
249
- runDir,
250
- runId: state.runId,
251
- spec,
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,
294
+ const heartbeatTimer = setInterval(() => {
295
+ if (!state.runner)
296
+ return;
297
+ state.runner.heartbeatAt = new Date().toISOString();
298
+ void persistState().catch(() => { });
299
+ }, 15_000);
300
+ heartbeatTimer.unref();
301
+ try {
302
+ const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
303
+ const baseExecuteNode = input.executeNode ??
304
+ createDagNodeExecutor({
305
+ runDir,
306
+ runId: state.runId,
307
+ spec,
260
308
  });
261
- }
262
- if (task.dynamicCondition) {
263
- return executeDynamicCondition({
309
+ const executeDynamicNode = async (dynamicInput) => {
310
+ const { task } = dynamicInput;
311
+ if (task.dynamicExpansion) {
312
+ return executeDynamicMapExpansion({
313
+ ...dynamicInput,
314
+ expansion: task.dynamicExpansion,
315
+ executeDynamicNode,
316
+ });
317
+ }
318
+ if (task.dynamicCondition) {
319
+ return executeDynamicCondition({
320
+ task,
321
+ condition: task.dynamicCondition,
322
+ tasksById: dynamicInput.tasksById,
323
+ state: dynamicInput.state,
324
+ });
325
+ }
326
+ if (task.dynamicLoopUntil) {
327
+ return executeDynamicLoopUntil({
328
+ ...dynamicInput,
329
+ loop: task.dynamicLoopUntil,
330
+ executeDynamicNode,
331
+ });
332
+ }
333
+ return executeDynamicReduction({
264
334
  task,
265
- condition: task.dynamicCondition,
266
- tasksById: dynamicInput.tasksById,
335
+ reduction: task.dynamicReduction,
267
336
  state: dynamicInput.state,
337
+ runDir: dynamicInput.runDir,
268
338
  });
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
- };
284
- let pausedByNodeId;
285
- while (true) {
286
- pausedByNodeId = await executeDagRanksOnce({
287
- state,
288
- ranks,
289
- tasksById,
290
- maxConcurrent,
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,
339
+ };
340
+ let pausedByNodeId;
341
+ while (true) {
342
+ pausedByNodeId = await executeDagRanksOnce({
302
343
  state,
344
+ ranks,
345
+ tasksById,
346
+ maxConcurrent,
347
+ persistState,
348
+ createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
349
+ baseExecuteNode,
350
+ customExecuteNode: input.executeNode,
351
+ rankWriterNodeIds,
352
+ tasksById,
353
+ meta: { runDir, runId: state.runId, spec },
354
+ }),
355
+ executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
356
+ nodeId,
357
+ tasksById,
358
+ state,
359
+ spec,
360
+ cwd,
361
+ runDir,
362
+ executeNode,
363
+ executeDynamicNode,
364
+ observer: input.observer,
365
+ persistState,
366
+ onPause,
367
+ }),
368
+ });
369
+ if (pausedByNodeId) {
370
+ break;
371
+ }
372
+ const convergenceDecision = await runConvergencePassController({
303
373
  spec,
304
- cwd,
374
+ state,
375
+ tasksById,
305
376
  runDir,
306
- executeNode,
307
- executeDynamicNode,
308
- observer: input.observer,
377
+ cwd,
309
378
  persistState,
310
- onPause,
311
- }),
312
- });
313
- if (pausedByNodeId) {
379
+ });
380
+ if (convergenceDecision.pausedByNodeId) {
381
+ pausedByNodeId = convergenceDecision.pausedByNodeId;
382
+ break;
383
+ }
384
+ if (convergenceDecision.retry) {
385
+ continue;
386
+ }
314
387
  break;
315
388
  }
316
- const convergenceDecision = await runConvergencePassController({
317
- spec,
318
- state,
319
- tasksById,
389
+ state.finishedAt = new Date().toISOString();
390
+ const runDirBeforeTransfer = runDir;
391
+ if (pausedByNodeId) {
392
+ state.status = "paused";
393
+ await persistState();
394
+ await notifyRunObserver(input.observer, "onRunFinish", state);
395
+ runDir = await moveToPausedRunDir(runDir, pausedRunDir);
396
+ }
397
+ else {
398
+ finalizeTerminalRunStatus(state, spec.tasks.length);
399
+ await persistState();
400
+ await notifyRunObserver(input.observer, "onRunFinish", state);
401
+ runDir = await moveToCompletedRunDir(runDir, completedRunDir);
402
+ }
403
+ await relocateRunArtifactPaths({
320
404
  runDir,
321
- cwd,
322
- persistState,
405
+ oldRunDir: runDirBeforeTransfer,
406
+ nodes: state.nodes,
323
407
  });
324
- if (convergenceDecision.pausedByNodeId) {
325
- pausedByNodeId = convergenceDecision.pausedByNodeId;
326
- break;
327
- }
328
- if (convergenceDecision.retry) {
329
- continue;
408
+ if (state.convergence) {
409
+ relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
330
410
  }
331
- break;
411
+ await persistState({ allowCompletedFactsWrite: true });
412
+ return {
413
+ title: spec.title,
414
+ runId: state.runId,
415
+ status: state.status,
416
+ ranks,
417
+ nodes: state.nodes,
418
+ runDir,
419
+ };
332
420
  }
333
- state.finishedAt = new Date().toISOString();
334
- const runDirBeforeTransfer = runDir;
335
- if (pausedByNodeId) {
336
- state.status = "paused";
337
- await persistState();
338
- await notifyRunObserver(input.observer, "onRunFinish", state);
339
- runDir = await moveToPausedRunDir(runDir, pausedRunDir);
421
+ finally {
422
+ clearInterval(heartbeatTimer);
340
423
  }
341
- else {
342
- finalizeTerminalRunStatus(state, spec.tasks.length);
343
- await persistState();
344
- await notifyRunObserver(input.observer, "onRunFinish", state);
345
- runDir = await moveToCompletedRunDir(runDir, completedRunDir);
346
- }
347
- await relocateRunArtifactPaths({
348
- runDir,
349
- oldRunDir: runDirBeforeTransfer,
350
- nodes: state.nodes,
351
- });
352
- if (state.convergence) {
353
- relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
354
- }
355
- await persistState({ allowCompletedFactsWrite: true });
356
- return {
357
- title: spec.title,
358
- runId: state.runId,
359
- status: state.status,
360
- ranks,
361
- nodes: state.nodes,
362
- runDir,
363
- };
364
424
  }
365
425
  async function notifyRunObserver(observer, event, state) {
366
426
  try {
@@ -384,16 +444,10 @@ function finalizeTerminalRunStatus(state, taskCount) {
384
444
  state.status = "failed";
385
445
  }
386
446
  }
387
- async function stopCursorWorkerIfRunning() {
388
- const { isCursorWorkerRunning, stopCursorWorker } = await import("../../executors/cursor-worker-client.js");
389
- if (isCursorWorkerRunning()) {
390
- await stopCursorWorker();
391
- }
392
- }
393
- function concurrentSiblingWriteSetsForNode(rankCursorNodeIds, nodeId, tasksById) {
394
- if (rankCursorNodeIds.length <= 1)
447
+ function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
448
+ if (rankWriterNodeIds.length <= 1)
395
449
  return [];
396
- return rankCursorNodeIds
450
+ return rankWriterNodeIds
397
451
  .filter((id) => id !== nodeId)
398
452
  .map((id) => tasksById.get(id)?.writeSet ?? [])
399
453
  .filter((writeSet) => writeSet.length > 0);
@@ -403,13 +457,16 @@ function buildRankAwareExecuteNode(input) {
403
457
  return input.baseExecuteNode;
404
458
  }
405
459
  return async (nodeInput) => {
406
- if (nodeInput.task.executor !== "cursor" || !nodeInput.model) {
460
+ const isPiWriter = nodeInput.task.executor === "pi" &&
461
+ nodeInput.task.toolProfile === "write" &&
462
+ Boolean(nodeInput.model);
463
+ if (!isPiWriter) {
407
464
  return input.baseExecuteNode(nodeInput);
408
465
  }
409
466
  const siblingWriteSets = nodeInput.task.writePolicy === "exclusive"
410
- ? concurrentSiblingWriteSetsForNode(input.rankCursorNodeIds, nodeInput.task.id, input.tasksById)
467
+ ? concurrentSiblingWriteSetsForNode(input.rankWriterNodeIds, nodeInput.task.id, input.tasksById)
411
468
  : [];
412
- return executeDagCursorNode({ ...nodeInput, model: nodeInput.model }, {
469
+ return executeDagPiNode({ ...nodeInput, model: nodeInput.model }, {
413
470
  ...input.meta,
414
471
  concurrentSiblingWriteSets: siblingWriteSets.length > 0 ? siblingWriteSets : undefined,
415
472
  writeGuardAttribution: siblingWriteSets.length > 0 ? "best-effort" : "per-node",
@@ -55,8 +55,13 @@ export async function executeDagRanksOnce(input) {
55
55
  return isPauseOnHumanDecisionGate(task);
56
56
  });
57
57
  const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
58
- const rankCursorNodeIds = runnable.filter((id) => input.tasksById.get(id)?.executor === "cursor");
59
- const executeNode = input.createExecuteNodeForRank(rankCursorNodeIds);
58
+ const rankWriterNodeIds = runnable.filter((id) => {
59
+ const task = input.tasksById.get(id);
60
+ return (task?.executor === "pi" &&
61
+ task.toolProfile === "write" &&
62
+ task.writePolicy === "exclusive");
63
+ });
64
+ const executeNode = input.createExecuteNodeForRank(rankWriterNodeIds);
60
65
  for (const nodeId of pauseGateRunnable) {
61
66
  await input.executeScheduledNode(nodeId, executeNode, (id, pausedAt, pauseReason) => {
62
67
  pausedByNodeId = id;