agents-relay 2.0.169 → 2.0.171

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 (74) hide show
  1. package/README.md +13 -19
  2. package/dist/adapters.js +2 -8
  3. package/dist/blockers.js +3 -2
  4. package/dist/cli-help.js +3 -3
  5. package/dist/cli-job-create.js +2 -4
  6. package/dist/cli-support.js +5 -16
  7. package/dist/cli.js +9 -11
  8. package/dist/dashboard-html.js +5 -9
  9. package/dist/dashboard-style.js +0 -1
  10. package/dist/execution-recovery.js +1 -1
  11. package/dist/force-restate-validation.js +11 -1
  12. package/dist/interactive-resume.js +1 -1
  13. package/dist/job-actions.js +1 -1
  14. package/dist/job-pause.js +1 -1
  15. package/dist/legacy-blocked-task-migration.js +1 -1
  16. package/dist/local-authority-store.js +0 -57
  17. package/dist/local-git-merge.js +1 -2
  18. package/dist/local-merge-delivery.js +1 -1
  19. package/dist/markers.js +5 -6
  20. package/dist/planned-review-head.js +2 -2
  21. package/dist/planner-runtime.js +3 -2
  22. package/dist/planner.js +2 -7
  23. package/dist/pool.js +7 -36
  24. package/dist/reconciler-review.js +4 -5
  25. package/dist/reconciler.js +11 -36
  26. package/dist/relay-create.js +3 -3
  27. package/dist/relay-job-snapshot-reads.js +6 -2
  28. package/dist/relay-store.js +2 -2
  29. package/dist/relay-workflow-operations.js +2 -2
  30. package/dist/relayd-control.js +9 -7
  31. package/dist/relayd-utilities.js +0 -4
  32. package/dist/relayd.js +7 -24
  33. package/dist/runtime-status.js +0 -19
  34. package/dist/single-job-task-control.js +1 -1
  35. package/dist/store.js +1 -1
  36. package/dist/task-actions.js +3 -3
  37. package/dist/task-recovery.js +2 -2
  38. package/dist/task-transitions.js +61 -0
  39. package/dist/troubleshooter-fallback.js +1 -1
  40. package/dist/troubleshooter.js +1 -4
  41. package/dist/types.js +1 -2
  42. package/dist/unblock.js +5 -9
  43. package/dist/worker-rejection.js +1 -1
  44. package/dist/workflow/authoritative-conditions.js +1 -1
  45. package/dist/workflow/blocker-resolution-v2.js +2 -4
  46. package/dist/workflow/contracts.js +0 -1
  47. package/dist/workflow/definitions-v2.js +3 -6
  48. package/dist/workflow/durable-transition.js +1 -1
  49. package/dist/workflow/force-restate-validation-v2.js +2 -1
  50. package/dist/workflow/orchestrator-rescue.js +1 -1
  51. package/dist/workflow/planner-orchestration-v2.js +2 -5
  52. package/dist/workflow/planner-trigger-v2.js +1 -2
  53. package/dist/workflow/review-v2.js +0 -1
  54. package/dist/workflow/task-reconcile.js +1 -7
  55. package/dist/workflow-effect-failures.js +1 -1
  56. package/dist/workspace.js +1 -1
  57. package/package.json +3 -4
  58. package/skills/agents-relay/SKILL.md +6 -6
  59. package/skills/agents-relay/agents/planner.agent.md +3 -3
  60. package/skills/agents-relay/contracts/event-contract.md +10 -0
  61. package/skills/agents-relay/contracts/orchestrator-workflows.md +2 -2
  62. package/skills/agents-relay/contracts/task-job-contract.md +5 -5
  63. package/workflow/blocker-resolution.mmd +1 -7
  64. package/workflow/force-restate-validation.mmd +7 -2
  65. package/workflow/planner-orchestration.mmd +2 -5
  66. package/workflow/planner-trigger.mmd +2 -9
  67. package/workflow/review-lifecycle.mmd +2 -8
  68. package/dist/e2e-injection.js +0 -76
  69. package/dist/scheduled-jobs.js +0 -67
  70. package/dist/scheduler.js +0 -214
  71. package/dist/workflow/schedule-cycle-v2.js +0 -25
  72. package/dist/workflow/scheduled-run-v2.js +0 -30
  73. package/workflow/schedule-cycle.mmd +0 -23
  74. package/workflow/scheduled-run.mmd +0 -59
package/dist/relayd.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { escalationNotifierFromEnvironment } from './escalation-notifier.js';
3
- import { consumeMethodInjection, withTestInjection } from './e2e-injection.js';
4
3
  import { once } from 'node:events';
5
4
  import { spawnOwnedProcess } from './owned-process.js';
6
5
  import { readFile } from 'node:fs/promises';
@@ -28,7 +27,7 @@ import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, star
28
27
  import { eventBusResolutionMessage, publishTaskUnblocked, resolveEventBus } from './cli-support.js';
29
28
  import { eventFor } from './events.js';
30
29
  import { dispatchTaskAction } from './task-actions.js';
31
- import { isEffectivelyBlocked, unblockAutonomousTasks, unblockTask } from './unblock.js';
30
+ import { isEffectivelyBlocked, unblockTask } from './unblock.js';
32
31
  import { registerBlockerDependency } from './blockers.js';
33
32
  import { ensureManagedGitHubJob } from './cli-job-create.js';
34
33
  import { applyJobActivateAction } from './job-actions.js';
@@ -49,8 +48,8 @@ import { routeExecution } from './execution-routing.js';
49
48
  import { classifyFailure, errorMessage, observability } from './observability.js';
50
49
  import { createWorkspaceControl } from './relayd-control.js';
51
50
  import { createDashboardActions } from './relayd-dashboard-actions.js';
52
- export { daemonHelp, normalizeDaemonArguments, overlayOperationalOverviews, mergeOperationalInventory, recoveryScanRepositories, shouldCoalescePullRequestRefresh, SCHEDULE_HEARTBEAT_MS, dueScheduledOverviews, configureExecutionRouterRulesPath } from './relayd-utilities.js';
53
- import { configureDaemonObservability, firstStageChoices, value, daemonHelp, normalizeDaemonArguments, SCHEDULE_HEARTBEAT_MS, dueScheduledOverviews, configureExecutionRouterRulesPath } from './relayd-utilities.js';
51
+ export { daemonHelp, normalizeDaemonArguments, overlayOperationalOverviews, mergeOperationalInventory, recoveryScanRepositories, shouldCoalescePullRequestRefresh, configureExecutionRouterRulesPath } from './relayd-utilities.js';
52
+ import { configureDaemonObservability, firstStageChoices, value, daemonHelp, normalizeDaemonArguments, configureExecutionRouterRulesPath } from './relayd-utilities.js';
54
53
  export async function runDaemon(argv) {
55
54
  if (argv.includes('--help') || argv.includes('-h')) {
56
55
  console.log(daemonHelp);
@@ -323,7 +322,7 @@ export async function runDaemon(argv) {
323
322
  if (job.state === 'COMPLETED' && job.localMerge?.status === 'succeeded' && job.localMerge.projectionStrategy === 'target_ref_push') {
324
323
  const projection = await projectBindingLocalMerge(job, relay, {
325
324
  projectPath: canonical,
326
- project: async (projectedJob) => withTestInjection('github.projectLocalMerge', await consumeMethodInjection(relay, job.repository, job.id, 'github.projectLocalMerge'), () => projectAuthoritativeLocalMerge(canonical, projectedJob)),
325
+ project: async (projectedJob) => projectAuthoritativeLocalMerge(canonical, projectedJob),
327
326
  verifyObservedRemote: async (sha) => {
328
327
  try {
329
328
  await gitText(canonical, ['merge-base', '--is-ancestor', sha, 'refs/heads/main']);
@@ -384,7 +383,7 @@ export async function runDaemon(argv) {
384
383
  const fallback = initial[0]?.job;
385
384
  const dashboardStore = fallback ? (await requireLocalJobBinding(fallback.id)).store : new InMemoryStore();
386
385
  const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskAction, workspaceTaskSetState } = createDashboardActions({ getOverviews: () => cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, bus, dashboardTaskAction, dashboardTaskSetState });
387
- const workspaceControl = createWorkspaceControl({ client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockAutonomousTasks, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob });
386
+ const workspaceControl = createWorkspaceControl({ client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob });
388
387
  const secretFile = value(argv, '--github-webhook-secret-file');
389
388
  const secret = noGitHub ? '' : value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
390
389
  const webhook = secret ? {
@@ -474,7 +473,7 @@ export async function runDaemon(argv) {
474
473
  job: {
475
474
  id: job.id, title: job.title, objective: job.objective ?? null,
476
475
  description: job.description ?? null, priority: job.priority ?? null,
477
- executionMode: job.executionMode, repository: job.repository,
476
+ repository: job.repository,
478
477
  prNumber: job.prNumber, state: job.state,
479
478
  },
480
479
  task: {
@@ -525,17 +524,6 @@ export async function runDaemon(argv) {
525
524
  observability().record({ stage: 'daemon', event: 'daemon.reconcile_failed', level: 'warn', outcome: 'failed', message: `Workspace reconcile unavailable: ${errorMessage(error)}`, data: { classification: classifyFailure(error).classification, reason: errorMessage(error) } });
526
525
  }
527
526
  };
528
- const scheduleHeartbeatSafely = async () => {
529
- const now = new Date();
530
- for (const item of dueScheduledOverviews(cachedOverviews, now)) {
531
- try {
532
- await pool.scheduleTickSignal(bus, item.job, now);
533
- }
534
- catch (error) {
535
- observability().record({ stage: 'daemon', event: 'daemon.schedule_heartbeat_failed', level: 'warn', outcome: 'failed', message: `Scheduled heartbeat unavailable: ${errorMessage(error)}`, correlation: { repository: item.job.repository, prNumber: item.job.prNumber, jobId: item.job.id }, data: { classification: classifyFailure(error).classification, reason: errorMessage(error) } });
536
- }
537
- }
538
- };
539
527
  const flushProjectionsSafely = async () => {
540
528
  let externalActionsCompleted = 0;
541
529
  for (const writer of projectionWriters.values()) {
@@ -552,18 +540,15 @@ export async function runDaemon(argv) {
552
540
  };
553
541
  const watchdogDefault = webhook ? SERVICE_DEFAULTS.webhookWatchdogMs : SERVICE_DEFAULTS.watchdogMs;
554
542
  let interval;
555
- let scheduleInterval;
556
543
  let projectionInterval;
557
544
  const stop = () => { stopRuntimeStatusEvents(); if (interval)
558
- clearInterval(interval); if (scheduleInterval)
559
- clearInterval(scheduleInterval); if (projectionInterval)
545
+ clearInterval(interval); if (projectionInterval)
560
546
  clearInterval(projectionInterval); void Promise.allSettled([stopWatch(), shutdownOwnedProcesses('parent-shutdown')]).finally(() => { for (const store of relayStores.values())
561
547
  store.close(); server.close(() => process.exit(0)); }); };
562
548
  process.once('SIGINT', stop);
563
549
  process.once('SIGTERM', stop);
564
550
  await syncGitHubBindingsSafely();
565
551
  await pool.reconcileKnown(cachedOverviews);
566
- await scheduleHeartbeatSafely();
567
552
  await flushProjectionsSafely();
568
553
  interval = setInterval(() => void reconcileSafely().finally(flushProjectionsSafely), Number(value(argv, '--interval', String(watchdogDefault))));
569
554
  interval.unref();
@@ -571,8 +556,6 @@ export async function runDaemon(argv) {
571
556
  projectionInterval = setInterval(() => void syncGitHubBindingsSafely().then(flushProjectionsSafely), Number(value(argv, '--projection-ms', String(SERVICE_DEFAULTS.projectionFlushMs))));
572
557
  projectionInterval.unref();
573
558
  }
574
- scheduleInterval = setInterval(() => void scheduleHeartbeatSafely(), SCHEDULE_HEARTBEAT_MS);
575
- scheduleInterval.unref();
576
559
  console.log('Dashboard listening; repository worker pool active');
577
560
  void refreshAll(true)
578
561
  .then(items => pool.reconcileKnown(items))
@@ -26,23 +26,6 @@ export function deriveJobRuntimeStatus(job, _prState = 'OPEN') {
26
26
  // Active task execution still takes precedence below.
27
27
  const plannerFailed = currentPlan?.status === 'FAILED' && !job.tasks.some(task => ['QUEUED', 'RUNNING'].includes(task.state));
28
28
  const plannerRunning = currentPlan?.status === 'PLANNING';
29
- if (job.schedule) {
30
- if (job.tasks.some(task => task.state === 'RUNNING'))
31
- return 'RUNNING';
32
- if (job.tasks.some(task => task.state === 'QUEUED'))
33
- return 'QUEUED';
34
- if (job.tasks.some(task => task.state === 'BLOCKED'))
35
- return 'BLOCKED';
36
- if (job.tasks.some(task => task.state === 'FAILED'))
37
- return 'FAILED';
38
- if (job.tasks.some(task => task.state === 'DRAFT'))
39
- return 'WAITING';
40
- if (plannerFailed)
41
- return 'FAILED';
42
- if (plannerRunning)
43
- return 'RUNNING';
44
- return 'WAITING';
45
- }
46
29
  // Draft or queued reviewers must not mask implementation that is still active.
47
30
  const implementation = implementationTasks(job);
48
31
  if (implementation.some(task => task.state === 'RUNNING'))
@@ -68,8 +51,6 @@ export function deriveJobRuntimeStatus(job, _prState = 'OPEN') {
68
51
  return 'FAILED';
69
52
  if (plannerRunning)
70
53
  return 'RUNNING';
71
- if (job.executionMode === 'autonomous' && job.autoMerge === false && job.reviewEnabled === false && implementationComplete(job))
72
- return 'WAITING';
73
54
  if (job.reviewEnabled === false && implementationComplete(job))
74
55
  return 'MERGE_PENDING';
75
56
  return 'IDLE';
@@ -14,7 +14,7 @@ export async function createSingleJobTaskControl(jobId, store, reconciler, bus)
14
14
  await reconciler.idle();
15
15
  } },
16
16
  bus, isEffectivelyBlocked: unblock.isEffectivelyBlocked,
17
- unblockAutonomousTasks: unblock.unblockAutonomousTasks, unblockTask: unblock.unblockTask,
17
+ unblockTask: unblock.unblockTask,
18
18
  publishTaskUnblocked, dispatchTaskAction,
19
19
  });
20
20
  const allowed = new Set(['task.unblock', 'task.recover', 'task.escalate', 'task.resume-troubleshooter', 'task.classify']);
package/dist/store.js CHANGED
@@ -92,7 +92,7 @@ export class InMemoryStore {
92
92
  }
93
93
  }
94
94
  async load(jobId) { const job = await this.findJob(jobId); if (!job)
95
- throw new Error(`Job ${jobId} not found`); job.plans = this.plansForJob(jobId); job.tasks = this.tasksForJob(jobId).map(task => normalizeLegacyTaskLifecycle(task, job.executionMode)); return job; }
95
+ throw new Error(`Job ${jobId} not found`); job.plans = this.plansForJob(jobId); job.tasks = this.tasksForJob(jobId).map(task => normalizeLegacyTaskLifecycle(task)); return job; }
96
96
  async mutateJob(jobId, mutation) {
97
97
  const current = await this.load(jobId);
98
98
  const next = mutation(structuredClone(current));
@@ -1,4 +1,4 @@
1
- import { extendRetryBudget, transitionTaskEvent, tryTransitionTaskEvent } from './scheduler.js';
1
+ import { extendRetryBudget, transitionTaskEvent, tryTransitionTaskEvent } from './task-transitions.js';
2
2
  import { selectRelatedTaskMutations } from './workflow/task-action-policy.js';
3
3
  import { dispatchTaskResolutionSync, materializeResolution } from './workflow/task-resolution.js';
4
4
  import { classifyTaskResolution } from './workflow/task-resolution.js';
@@ -59,7 +59,7 @@ function applyTaskRetrySnapshot(task, policy = {}) {
59
59
  }
60
60
  /** Fixed-job authoritative unblock continuation, applied inside one Job snapshot mutation. */
61
61
  export function applyTaskUnblockRetrySnapshot(task) {
62
- if (task.state === 'BLOCKED' && !tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: false }).accepted)
62
+ if (task.state === 'BLOCKED' && !tryTransitionTaskEvent(task, 'task.unblocked', {}).accepted)
63
63
  throw lifecycleRejection(task, 'retryable after unblock');
64
64
  if (!['QUEUED', 'DRAFT'].includes(task.state))
65
65
  throw lifecycleRejection(task, 'retryable after unblock');
@@ -81,7 +81,7 @@ export async function applyTaskRetryAction(task, policy = {}) {
81
81
  /** Fixed-job authoritative unblock continuation: legality is owned by the lifecycle graph. */
82
82
  export async function applyTaskUnblockRetryAction(task) {
83
83
  if (task.state === 'BLOCKED')
84
- await transitionTaskAction(task, 'task.unblocked', 'retryable after unblock', { autonomousJob: false });
84
+ await transitionTaskAction(task, 'task.unblocked', 'retryable after unblock', {});
85
85
  if (!['QUEUED', 'DRAFT'].includes(task.state))
86
86
  throw lifecycleRejection(task, 'retryable after unblock');
87
87
  task.attempt = 0;
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { eventFor } from './events.js';
3
- import { tryTransitionTaskEvent } from './scheduler.js';
3
+ import { tryTransitionTaskEvent } from './task-transitions.js';
4
4
  import { dispatchTaskResolutionSync, materializeResolution } from './workflow/task-resolution.js';
5
5
  function activeRecovery(job, original) {
6
6
  return job.tasks.find(task => task.agentName === 'troubleshooter'
@@ -112,7 +112,7 @@ export async function resumeTroubleshooter(store, jobId, recoveryTaskId, respons
112
112
  throw new Error(`Escalation ${task.escalation.id} already has a response`);
113
113
  if (task.state !== 'BLOCKED')
114
114
  throw new Error(`Task ${recoveryTaskId} is not a blocked Troubleshooter escalation`);
115
- if (!tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: false }).accepted)
115
+ if (!tryTransitionTaskEvent(task, 'task.unblocked', {}).accepted)
116
116
  throw new Error(`Troubleshooter ${recoveryTaskId} cannot be resumed`);
117
117
  task.escalation = { ...task.escalation, status: 'responded', response: response.trim(), respondedAt: now };
118
118
  task.result = null;
@@ -0,0 +1,61 @@
1
+ import { implementationTasks } from './runtime-status.js';
2
+ import { dispatchJobLifecycle, dispatchTaskLifecycle, dispatchTaskLifecycleSync } from './workflow/lifecycle.js';
3
+ import { reconcileTaskDependencies } from './workflow/task-reconcile.js';
4
+ export { dependencyReady, detectCycles } from './workflow/task-reconcile.js';
5
+ import { observability } from './observability.js';
6
+ export function completionGateSatisfied(job) {
7
+ const latest = [...(job.plans ?? [])].sort((a, b) => a.round - b.round).at(-1);
8
+ const planSatisfied = latest?.status === 'SUCCEEDED' && latest.objectiveStatus === 'satisfied';
9
+ const legacySatisfied = (job.plans?.length ?? 0) === 0 && job.tasks.some(task => task.kind === 'planner' && task.state === 'SUCCEEDED' && task.plannerResult?.objective_status === 'satisfied');
10
+ const implementation = implementationTasks(job);
11
+ return (planSatisfied || legacySatisfied) && implementation.length > 0 && implementation.every(task => task.state === 'SUCCEEDED');
12
+ }
13
+ export function refreshTaskDependencies(job) {
14
+ if (job.pause)
15
+ return job;
16
+ reconcileTaskDependencies(job);
17
+ return job;
18
+ }
19
+ export function extendRetryBudget(task, windowSize = 3) {
20
+ if (!Number.isInteger(windowSize) || windowSize < 1)
21
+ throw new Error('Retry window size must be a positive integer');
22
+ const usedOrBudgeted = Math.max(task.maxAttempts, task.attempt);
23
+ return Math.max(windowSize, (Math.floor(usedOrBudgeted / windowSize) + 1) * windowSize);
24
+ }
25
+ export function tryTransitionTaskEvent(task, event, context = {}) {
26
+ return dispatchTaskLifecycleSync(task, event, context);
27
+ }
28
+ export async function transitionTaskEvent(task, event, context = {}, executeEffect, persist) {
29
+ const from = task.state;
30
+ const decision = await dispatchTaskLifecycle(task, event, context, executeEffect, persist);
31
+ if (!decision.accepted || !decision.target)
32
+ throw new Error(decision.reason === 'ambiguous'
33
+ ? `Ambiguous workflow transition from ${from} on ${event}`
34
+ : `No enabled workflow transition from ${from} on ${event}`);
35
+ observability().record({
36
+ stage: 'transition',
37
+ event,
38
+ outcome: 'succeeded',
39
+ message: `Task ${task.id} ${from} -> ${decision.target}`,
40
+ correlation: { jobId: task.jobId, taskId: task.id, executionId: task.executionId ?? undefined },
41
+ transition: { graph: 'task-lifecycle', from, to: decision.target, actions: decision.actions },
42
+ });
43
+ return decision;
44
+ }
45
+ export async function transitionJobEvent(job, event, context = {}, executeEffect, persist) {
46
+ const from = job.state;
47
+ const decision = await dispatchJobLifecycle(job, event, context, executeEffect, persist);
48
+ if (!decision.accepted || !decision.target)
49
+ throw new Error(decision.reason === 'ambiguous'
50
+ ? `Ambiguous workflow transition from ${from} on ${event}`
51
+ : `No enabled workflow transition from ${from} on ${event}`);
52
+ observability().record({
53
+ stage: 'transition',
54
+ event,
55
+ outcome: 'succeeded',
56
+ message: `Job ${job.id} ${from} -> ${decision.target}`,
57
+ correlation: { repository: job.repository, prNumber: job.prNumber, jobId: job.id },
58
+ transition: { graph: 'job-lifecycle', from, to: decision.target, actions: decision.actions },
59
+ });
60
+ return decision;
61
+ }
@@ -1,4 +1,4 @@
1
- import { tryTransitionTaskEvent } from './scheduler.js';
1
+ import { tryTransitionTaskEvent } from './task-transitions.js';
2
2
  import { requestTroubleshooterEscalation } from './task-recovery.js';
3
3
  const MARKER = '[Relay execution fallback attempted]';
4
4
  const RUNTIME_FAILURE = /worker_process_failed|worker_start_timeout|terminal_event_timeout|model.*not supported|unsupported.model|delivery|process.exited|provider/i;
@@ -28,10 +28,7 @@ Inspect event order, type/status, source, data/phase, duplicate events, missing
28
28
 
29
29
  Follow the selected Troubleshooter agent contract. Diagnose root cause from durable task state, append-only comments, task-scoped event history, and relevant runtime logs before retry. The final task.error string alone is never sufficient. Independently verify every human response against evidence from the exact original failing execution context; a response or historical resolution condition alone never proves recovery. Classify evidence as model/provider/runtime, worker/tool/infrastructure, semantic task failure, or timeout/unknown. Select one graph-enabled resolution path: exact_original_retry, same_job_dependency, blocker_job_dependency, escalation_gate, or cancel_supersede. For confirmed adapter/model/provider failure, include --failure-kind, --reason, --evidence and --reroute in task recover; use --exclude-current-route only if the exact failed route should not be selected again. For semantic task failure use --failure-kind task_failure without rerouting. For uncertain failures use --failure-kind unknown and preserve routing unless evidence justifies rerouting. Request the existing agents-relay task recover or agents-relay escalation operation; submission alone is not recovery. Reload authoritative SQLite state and verify the requested resolution and exact-original retry postconditions before reporting success. Bound self-healing retries to the durable Task retry budget. For a shared worker/runtime/tool defect, use its owning repository's P0/self-job workflow and verify the durable fix before requesting recovery. Never use ordinary task retry from a Troubleshooter because manual retry intentionally supersedes active recovery. Do not manually choose a provider/model or create a replacement task. If diagnosis suggests a genuine human/external blocker, invoke the escalation command; do not publish task.blocked or implement a workflow branch procedurally. The canonical escalation graph invokes Neo escalation and owns the resolved/failed/needs_user branch; Relay applies the selected outcome. Retry this exact original task only after the blocker is removed, reconcile the original job, and verify durable progress.`;
30
30
  }
31
- function belongsToCurrentRun(job, task) {
32
- const slot = job.schedule?.currentRunSlot;
33
- return !slot || task.createdAt >= slot;
34
- }
31
+ function belongsToCurrentRun(_job, _task) { return true; }
35
32
  const ACTIVE_RECOVERY_STATES = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED']);
36
33
  function recoveryTargetsTask(recovery, original) {
37
34
  if (recovery.agentName !== 'troubleshooter')
package/dist/types.js CHANGED
@@ -4,8 +4,7 @@ export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
4
4
  export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
5
5
  export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(600000, Math.max(120000, Math.floor(overallTimeoutMs / 3))); }
6
6
  /** Normalize snapshots written before UNBLOCKED was removed from the lifecycle graph. */
7
- export function normalizeLegacyTaskLifecycle(task, executionMode) {
8
- void executionMode;
7
+ export function normalizeLegacyTaskLifecycle(task) {
9
8
  const legacy = task;
10
9
  if (legacy.state === 'UNBLOCKED') {
11
10
  legacy.state = task.agentName === 'troubleshooter' ? 'QUEUED' : 'FAILED';
package/dist/unblock.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { applyTaskUnblockRetrySnapshot } from './task-actions.js';
2
- import { tryTransitionTaskEvent } from './scheduler.js';
2
+ import { tryTransitionTaskEvent } from './task-transitions.js';
3
3
  import { dispatchAuthoritativeUnblock } from './workflow/authoritative-unblock-v2.js';
4
4
  import { hasPendingTroubleshooterEscalation } from './task-recovery.js';
5
5
  export function isEffectivelyBlocked(task) { return task.kind !== 'planner' && task.agentName === 'troubleshooter' && task.state === 'BLOCKED'; }
@@ -31,7 +31,7 @@ function unblockFacts(job, tasks, currentHeadSha, reason) {
31
31
  return [...new Set(facts)];
32
32
  }
33
33
  export function unconsumedUnblockTrigger(job) {
34
- const consumed = new Set((job.plans ?? []).filter(plan => plan.status === 'SUCCEEDED').flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
34
+ const consumed = new Set((job.plans ?? []).filter(plan => plan.status === 'SUCCEEDED').flatMap(plan => plan.trigger?.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
35
35
  const tasks = job.tasks.filter(task => task.agentName !== 'troubleshooter' && task.unblock && !consumed.has(task.id) && ['DRAFT', 'QUEUED'].includes(task.state));
36
36
  if (tasks.length === 0) {
37
37
  const legacy = job.changeCycle;
@@ -56,12 +56,8 @@ export function unblockNeedsNewPlan(job) {
56
56
  if (!pending)
57
57
  return false;
58
58
  const latest = job.plans?.at(-1);
59
- return !(latest?.status === 'FAILED' && latest.trigger.source === 'unblock'
60
- && latest.trigger.taskIds?.some(id => pending.taskIds?.includes(id)));
61
- }
62
- export async function unblockAutonomousTasks(options) {
63
- void options;
64
- throw new Error('Ordinary Tasks cannot be authoritatively unblocked; only a blocked Troubleshooter can resume');
59
+ return !(latest?.status === 'FAILED' && latest.trigger?.source === 'unblock'
60
+ && latest.trigger?.taskIds?.some(id => pending.taskIds?.includes(id)));
65
61
  }
66
62
  export async function unblockTask(options) {
67
63
  if (!isEffectivelyBlocked(options.task))
@@ -83,7 +79,7 @@ export async function unblockTask(options) {
83
79
  const task = current.tasks.find(item => item.id === options.task.id);
84
80
  if (!task || !isEffectivelyBlocked(task))
85
81
  throw new Error(`Task ${options.task.id} changed before authoritative unblock`);
86
- if (!tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: false }).accepted)
82
+ if (!tryTransitionTaskEvent(task, 'task.unblocked', {}).accepted)
87
83
  throw new Error(`Task ${task.id} cannot be authoritatively unblocked`);
88
84
  const baseline = baselineFor(task);
89
85
  task.blockedBaseline = baseline;
@@ -1,4 +1,4 @@
1
- /** Classify genuine worker-process rejection text, regardless of E2E mode. */
1
+ /** Classify genuine worker-process rejection text. */
2
2
  export function classifyWorkerRejection(message) {
3
3
  if (/rate[_ .-]?limit[_ .-]?exceeded|usage limit reached|quota[_ .-]?exceeded|insufficient[_ .-]?quota/i.test(message))
4
4
  return 'rate_limit_exceeded';
@@ -1,4 +1,4 @@
1
- import { completionGateSatisfied } from '../scheduler.js';
1
+ import { completionGateSatisfied } from '../task-transitions.js';
2
2
  export function verifyAuthoritativeCondition(db, jobId, predicate, taskId, executionId) {
3
3
  if (!jobId)
4
4
  throw new Error('job ID required');
@@ -5,13 +5,12 @@ const source = readFileSync(new URL('../../workflow/blocker-resolution.mmd', imp
5
5
  const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
6
  export const blockerResolutionV2 = new GraphRuntime(source, {
7
7
  states: new Set([
8
- 'waiting', 'check_target', 'check_autonomous', 'check_effective', 'check_already_unblocked',
8
+ 'waiting', 'check_target', 'check_effective', 'check_already_unblocked',
9
9
  'check_draft', 'check_blocked', 'check_failed', 'check_unblocked', 'wait', 'obsolete', 'resumed',
10
10
  ]),
11
11
  guards: {
12
12
  resolutionAllowed: boolGuard('resolutionAllowed'),
13
13
  targetMatches: boolGuard('targetMatches'),
14
- autonomousJob: boolGuard('autonomousJob'),
15
14
  effectivelyBlocked: boolGuard('effectivelyBlocked'),
16
15
  alreadyUnblocked: boolGuard('alreadyUnblocked'),
17
16
  isDraft: boolGuard('isDraft'),
@@ -20,8 +19,7 @@ export const blockerResolutionV2 = new GraphRuntime(source, {
20
19
  isUnblocked: boolGuard('isUnblocked'),
21
20
  },
22
21
  actions: Object.fromEntries([
23
- 'blocker.autonomousUnblock', 'blocker.acceptExistingUnblock', 'blocker.releaseDraft',
24
- 'blocker.unblockTask', 'blocker.retryFailed', 'blocker.retryUnblocked',
22
+ 'blocker.releaseDraft',
25
23
  ].map(id => [id, { id }])),
26
24
  events: { 'blocker.resolved': { id: 'blocker.resolved' } },
27
25
  });
@@ -56,7 +56,6 @@ export const WORKFLOW_FACT_NAMES = [
56
56
  'retryAvailable',
57
57
  'reviewUnblocked',
58
58
  'initialPlan',
59
- 'scheduledRun',
60
59
  'hasUnblock',
61
60
  'manualPlan',
62
61
  'reviewDisabled',
@@ -5,7 +5,7 @@ import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES, }
5
5
  const TASK_STATES = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED'];
6
6
  const JOB_STATES = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'];
7
7
  const conditionRegistry = new ConditionRegistry([
8
- ...new Set(['dependenciesSatisfied', 'dependenciesUnsatisfied', 'runnable', 'dispatchable', 'recoverableExecution', 'troubleshooterRecovery', 'troubleshooterTask', 'awaitingInteractiveDecision', 'retryAvailable', 'autonomousJob', 'notMerged', 'autoMerge', 'prOpen', 'reviewDisabled', 'implementationComplete', 'headAvailable', 'mergeCapable', 'localCompletionAllowed', 'localMergeReady'])
8
+ ...new Set(['dependenciesSatisfied', 'dependenciesUnsatisfied', 'runnable', 'dispatchable', 'recoverableExecution', 'troubleshooterRecovery', 'troubleshooterTask', 'awaitingInteractiveDecision', 'retryAvailable', 'notMerged', 'autoMerge', 'prOpen', 'reviewDisabled', 'implementationComplete', 'headAvailable', 'mergeCapable', 'localCompletionAllowed', 'localMergeReady'])
9
9
  ].map(booleanCondition));
10
10
  const boolGuard = (name) => conditionRegistry.guard(name);
11
11
  export const lifecycleConditions = conditionRegistry;
@@ -34,7 +34,6 @@ export const taskLifecycleV2 = load('task-lifecycle', TASK_STATES, {
34
34
  troubleshooterTask: boolGuard('troubleshooterTask'),
35
35
  awaitingInteractiveDecision: facts => facts.durableFacts.awaitingInteractiveDecision === true,
36
36
  retryAvailable: boolGuard('retryAvailable'),
37
- autonomousJob: boolGuard('autonomousJob'),
38
37
  });
39
38
  export const jobLifecycleV2 = load('job-lifecycle', JOB_STATES, {
40
39
  notMerged: boolGuard('notMerged'),
@@ -46,15 +45,13 @@ export const jobLifecycleV2 = load('job-lifecycle', JOB_STATES, {
46
45
  mergeCapable: boolGuard('mergeCapable'),
47
46
  localCompletionAllowed: (facts) => {
48
47
  const values = facts.durableFacts;
49
- return values.jobActive === true && values.autoMergeEnabled === true && values.schedulePresent !== true
50
- && values.implementationComplete === true && values.completionGateSatisfied === true
48
+ return values.jobActive === true && values.autoMergeEnabled === true && values.implementationComplete === true && values.completionGateSatisfied === true
51
49
  && values.reviewSatisfied === true
52
50
  && (values.localMergeRequired !== true || values.localMergeSucceeded === true);
53
51
  },
54
52
  localMergeReady: (facts) => {
55
53
  const values = facts.durableFacts;
56
- return values.jobActive === true && values.autoMergeEnabled === true && values.schedulePresent !== true
57
- && values.implementationComplete === true && values.completionGateSatisfied === true
54
+ return values.jobActive === true && values.autoMergeEnabled === true && values.implementationComplete === true && values.completionGateSatisfied === true
58
55
  && values.reviewSatisfied === true && values.localMergePresent === true
59
56
  && values.reviewHeadCurrent === true && values.mergeAttemptReady === true;
60
57
  },
@@ -1,4 +1,4 @@
1
- import { tryTransitionTaskEvent } from '../scheduler.js';
1
+ import { tryTransitionTaskEvent } from '../task-transitions.js';
2
2
  import { observability } from '../observability.js';
3
3
  /**
4
4
  * Runtime primitive for applying one canonical Task-graph event to fresh durable state.
@@ -3,11 +3,12 @@ import { GraphRuntime } from './graph-v2.js';
3
3
  import { dispatchWorkflowGraphSync } from './dispatcher.js';
4
4
  const source = readFileSync(new URL('../../workflow/force-restate-validation.mmd', import.meta.url), 'utf8');
5
5
  export const forceRestateValidationV2 = new GraphRuntime(source, {
6
- states: new Set(['idle', 'check_history_mode', 'check_history', 'check_normal_history', 'check_forward', 'rejected', 'committed']),
6
+ states: new Set(['idle', 'check_history_mode', 'check_history', 'check_normal_history', 'check_satisfied', 'check_forward', 'rejected', 'committed']),
7
7
  guards: {
8
8
  forceRestateRequested: facts => facts.durableFacts.forceRestateRequested === true,
9
9
  historyConsistencyValid: facts => facts.durableFacts.historyConsistencyValid === true,
10
10
  normalHistoryMutationAbsent: facts => facts.durableFacts.normalHistoryMutationAbsent === true,
11
+ objectiveSatisfied: facts => facts.durableFacts.objectiveSatisfied === true,
11
12
  forwardConvergenceValid: facts => facts.durableFacts.forwardConvergenceValid === true,
12
13
  },
13
14
  actions: {
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { dispatchWorkflowGraphSync } from './dispatcher.js';
3
3
  import { GraphRuntime } from './graph-v2.js';
4
- import { tryTransitionTaskEvent } from '../scheduler.js';
4
+ import { tryTransitionTaskEvent } from '../task-transitions.js';
5
5
  import { taskOverallTimeoutMs } from '../types.js';
6
6
  export const STALE_ORCHESTRATOR_TASK_MS = 30 * 60 * 1000;
7
7
  const source = readFileSync(new URL('../../workflow/orchestrator-rescue.mmd', import.meta.url), 'utf8');
@@ -10,7 +10,6 @@ export const plannerOrchestrationV2 = new GraphRuntime(source, {
10
10
  'check_blocked', 'check_work', 'check_materialized', 'check_objective', 'wait', 'recovered', 'created',
11
11
  ]),
12
12
  guards: {
13
- autonomousJob: boolGuard('autonomousJob'),
14
13
  legacyPlanningPending: boolGuard('legacyPlanningPending'),
15
14
  recoverablePendingPlan: boolGuard('recoverablePendingPlan'),
16
15
  terminalPlanFailure: boolGuard('terminalPlanFailure'),
@@ -38,9 +37,8 @@ export function decidePlannerOrchestration(facts) {
38
37
  return dispatchWorkflowGraphSync(plannerOrchestrationV2, { initialState: 'idle', input: { transport: 'event', id: 'planner.reconcile', payload: {} }, durableFacts: facts, graphName: 'planner-orchestration', isTransientState: state => state.startsWith('check_'), maxSteps: 14 });
39
38
  }
40
39
  export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
41
- const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
42
- const scopedTasks = Number.isFinite(slot) ? job.tasks.filter(task => Date.parse(task.createdAt) >= slot) : job.tasks;
43
- const scopedPlans = (job.plans ?? []).filter(plan => !Number.isFinite(slot) || Date.parse(plan.createdAt) >= slot)
40
+ const scopedTasks = job.tasks;
41
+ const scopedPlans = (job.plans ?? [])
44
42
  .sort((a, b) => a.round - b.round || a.createdAt.localeCompare(b.createdAt));
45
43
  const latestPlan = scopedPlans.at(-1);
46
44
  const failedPlan = latestPlan?.status === 'FAILED' ? latestPlan : null;
@@ -56,7 +54,6 @@ export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
56
54
  const latestId = latestPlan?.id ?? legacyLatest?.id;
57
55
  const latestObjective = latestPlan?.objectiveStatus ?? legacyLatest?.plannerResult?.objective_status;
58
56
  return {
59
- autonomousJob: job.executionMode === 'autonomous' || latestPlan?.forceRestate === true,
60
57
  legacyPlanningPending: job.initialPlanning?.status === 'pending',
61
58
  recoverablePendingPlan,
62
59
  terminalPlanFailure: Boolean(failedPlan && (!failedPlan.manualPlanning || failedPlan.forceRestate) && failedPlan.trigger.source !== 'unblock'),
@@ -4,11 +4,10 @@ import { dispatchWorkflowGraphSync } from './dispatcher.js';
4
4
  const source = readFileSync(new URL('../../workflow/planner-trigger.mmd', import.meta.url), 'utf8');
5
5
  const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
6
  export const plannerTriggerV2 = new GraphRuntime(source, {
7
- states: new Set(['idle', 'check_unblock', 'check_schedule', 'check_initial', 'manual', 'unblock', 'schedule', 'initial', 'continuation']),
7
+ states: new Set(['idle', 'check_unblock', 'check_initial', 'manual', 'unblock', 'initial', 'continuation']),
8
8
  guards: {
9
9
  manualPlan: boolGuard('manualPlan'),
10
10
  hasUnblock: boolGuard('hasUnblock'),
11
- scheduledRun: boolGuard('scheduledRun'),
12
11
  initialPlan: boolGuard('initialPlan'),
13
12
  },
14
13
  actions: {},
@@ -6,7 +6,6 @@ const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
6
  export const reviewLifecycleV2 = new GraphRuntime(source, {
7
7
  states: new Set(['NONE', 'PENDING', 'STARTED', 'APPROVED', 'CHANGES_REQUIRED', 'BLOCKED', 'STALE']),
8
8
  guards: {
9
- unscheduled: boolGuard('unscheduled'),
10
9
  reviewEnabled: boolGuard('reviewEnabled'),
11
10
  prOpen: boolGuard('prOpen'),
12
11
  jobActive: boolGuard('jobActive'),
@@ -20,15 +20,9 @@ export function detectCycles(tasks) {
20
20
  for (const task of tasks)
21
21
  visit(task.id);
22
22
  }
23
- function scheduledCycleTasks(job) {
24
- const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
25
- if (!Number.isFinite(slot))
26
- return job.tasks;
27
- return job.tasks.filter(task => Date.parse(task.createdAt) >= slot);
28
- }
29
23
  export function reconcileTaskDependencies(job) {
30
24
  detectCycles(job.tasks);
31
- const scopedTasks = scheduledCycleTasks(job);
25
+ const scopedTasks = job.tasks;
32
26
  const tasks = new Map(job.tasks.map(task => [task.id, task]));
33
27
  for (const task of scopedTasks) {
34
28
  if (task.kind === 'planner')
@@ -1,4 +1,4 @@
1
- import { transitionJobEvent, transitionTaskEvent } from './scheduler.js';
1
+ import { transitionJobEvent, transitionTaskEvent } from './task-transitions.js';
2
2
  import { workflowEffectFailureEvent } from './workflow/contracts.js';
3
3
  import { eventFor } from './events.js';
4
4
  export async function applyJobEffectOutcomes(store, emit, job, outcomes) {
package/dist/workspace.js CHANGED
@@ -191,7 +191,7 @@ export function managedLocalJobWorktreePath(repository, jobId, workspaceRoot = d
191
191
  }
192
192
  /** Only code artifacts need an isolated Git source branch and worktree. */
193
193
  export function jobRequiresManagedWorktree(job) {
194
- return !(job.prNumber > 0) && !job.schedule && (Boolean(job.localMerge?.sourceRef) || job.tasks.some(task => task.output?.kind === 'task_pr'));
194
+ return !(job.prNumber > 0) && (Boolean(job.localMerge?.sourceRef) || job.tasks.some(task => task.output?.kind === 'task_pr'));
195
195
  }
196
196
  /** Bind an ordinary SQLite Job to one deterministic local work branch.
197
197
  * The target is always main, and branch creation never switches the user's checkout.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.169",
3
+ "version": "2.0.171",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,12 +17,11 @@
17
17
  "test:chatgpt-temp-quality": "AGENTS_RELAY_RUN_CHATGPT_TEMP_QUALITY=1 sh -c \"npm run build && node test/quality/run-temporary-chat.mjs\"",
18
18
  "dev": "tsx src/cli.ts",
19
19
  "test:publish": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 --test-force-exit test/*.test.js",
20
- "test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
20
+ "test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
21
21
  "test:full": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 test/*.test.js && npm run test:verification:l3",
22
22
  "test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs test/verification/task-resolution-classification.test.mjs test/verification/planner-contract.test.mjs test/verification/force-restate-validation.test.mjs test/planner-child-recovery.test.js",
23
- "test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/l3-planner-cli.test.mjs test/verification/l3-task-resolution.test.mjs test/verification/l3-plan-apply-convergence.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs test/verification/l3-daemon-startup-recovery.test.mjs",
23
+ "test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/local-sqlite-jobid.test.js test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/l3-planner-cli.test.mjs test/verification/l3-task-resolution.test.mjs test/verification/l3-plan-apply-convergence.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs test/verification/l3-daemon-startup-recovery.test.mjs",
24
24
  "test:verification": "npm run test:verification:l1 && npm run test:verification:l2 && npm run test:verification:l3 && npm run test:verification:cross-a && node --test test/workflow-production-outbox.test.js && npm run test:verification:matrix",
25
- "e2e": "node e2e/runner/run.mjs",
26
25
  "test:verification:cross-a": "npm run build && RELAY_A_DIST=$PWD/dist node --test test/verification/action-ledger-cross-branch.test.mjs test/verification/outbox-cross-a.test.mjs",
27
26
  "test:verification:matrix": "node test/verification/recovery-report.mjs /tmp/agents-relay-job481-evidence",
28
27
  "test:verification:l2": "npm run build && node --test test/verification/l2/*.test.mjs",