agents-relay 1.0.177 → 2.0.1

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 (66) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters.js +26 -2
  3. package/dist/blockers.js +60 -65
  4. package/dist/cli-support.js +33 -1
  5. package/dist/cli.js +72 -86
  6. package/dist/dashboard-actions.js +13 -35
  7. package/dist/dashboard-html.js +1 -1
  8. package/dist/dashboard.js +39 -0
  9. package/dist/execution-recovery.js +37 -0
  10. package/dist/graph-runtime.js +1 -0
  11. package/dist/job-actions.js +53 -0
  12. package/dist/manual-plan.js +15 -17
  13. package/dist/planner-runtime.js +135 -0
  14. package/dist/planner.js +76 -60
  15. package/dist/pool.js +54 -10
  16. package/dist/projected-store.js +314 -0
  17. package/dist/reconciler-review.js +185 -110
  18. package/dist/reconciler-runtime.js +67 -7
  19. package/dist/reconciler.js +134 -282
  20. package/dist/relay-store.js +380 -0
  21. package/dist/relayd-main.js +6 -0
  22. package/dist/relayd.js +122 -29
  23. package/dist/runtime-status.js +2 -1
  24. package/dist/scheduled-jobs.js +40 -31
  25. package/dist/scheduler.js +19 -18
  26. package/dist/store.js +1 -1
  27. package/dist/task-actions.js +139 -0
  28. package/dist/troubleshooter.js +14 -8
  29. package/dist/types.js +3 -3
  30. package/dist/unblock.js +31 -21
  31. package/dist/workflow/authoritative-unblock-v2.js +20 -0
  32. package/dist/workflow/blocker-resolution-v2.js +33 -0
  33. package/dist/workflow/contracts.js +21 -0
  34. package/dist/workflow/definitions-v2.js +75 -0
  35. package/dist/workflow/dispatcher.js +56 -0
  36. package/dist/workflow/entity-creation.js +32 -0
  37. package/dist/workflow/graph-v2.js +391 -0
  38. package/dist/workflow/job-reconcile-v2.js +19 -0
  39. package/dist/workflow/lifecycle.js +82 -4
  40. package/dist/workflow/planner-orchestration-v2.js +58 -0
  41. package/dist/workflow/planner-trigger-v2.js +24 -0
  42. package/dist/workflow/review-v2.js +42 -0
  43. package/dist/workflow/runtime.js +15 -1
  44. package/dist/workflow/schedule-cycle-v2.js +25 -0
  45. package/dist/workflow/scheduled-run-v2.js +30 -0
  46. package/dist/workflow/task-action-policy.js +19 -0
  47. package/dist/workflow/task-reconcile.js +6 -6
  48. package/dist/workflow/task-related-action-v2.js +42 -0
  49. package/dist/workflow/troubleshooter-v2.js +29 -0
  50. package/dist/workspace.js +6 -1
  51. package/package.json +3 -3
  52. package/skills/agents-relay/agents/planner.agent.md +5 -5
  53. package/skills/agents-relay/contracts/task-job-contract.md +2 -51
  54. package/workflow/authoritative-unblock-v2.mmd +15 -0
  55. package/workflow/blocker-resolution-v2.mmd +73 -0
  56. package/workflow/job-lifecycle-v2.mmd +70 -0
  57. package/workflow/job-lifecycle.mmd +2 -2
  58. package/workflow/planner-orchestration-v2.mmd +65 -0
  59. package/workflow/planner-trigger-v2.mmd +32 -0
  60. package/workflow/review-lifecycle-v2.mmd +124 -0
  61. package/workflow/schedule-cycle-v2.mmd +19 -0
  62. package/workflow/scheduled-run-v2.mmd +55 -0
  63. package/workflow/task-lifecycle-v2.mmd +201 -0
  64. package/workflow/task-lifecycle.mmd +3 -0
  65. package/workflow/task-related-action-v2.mmd +28 -0
  66. package/workflow/troubleshooter-v2.mmd +43 -0
@@ -1,4 +1,6 @@
1
1
  import { taskOverallTimeoutMs } from './types.js';
2
+ import { dispatchTroubleshooter } from './workflow/troubleshooter-v2.js';
3
+ import { createTaskEntity } from './workflow/entity-creation.js';
2
4
  function slug(value) {
3
5
  return value.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 72) || 'task';
4
6
  }
@@ -46,15 +48,20 @@ export function pendingTroubleshooterTasks(job) {
46
48
  const existing = new Set(job.tasks.filter(task => task.agentName === 'troubleshooter').map(task => task.id));
47
49
  const recoveries = [];
48
50
  for (const task of job.tasks) {
49
- if (!['FAILED', 'BLOCKED'].includes(task.state) || task.kind === 'planner' || task.agentName === 'troubleshooter' || !belongsToCurrentRun(job, task))
50
- continue;
51
- if (task.state === 'BLOCKED' && task.unblock)
52
- continue;
53
51
  const id = troubleshooterTaskId(task);
54
- if (existing.has(id) || hasRecoveryBarrier(job, task))
52
+ let create = false;
53
+ const decision = dispatchTroubleshooter({
54
+ failedOrBlocked: ['FAILED', 'BLOCKED'].includes(task.state),
55
+ ordinaryWork: task.kind !== 'planner' && task.agentName !== 'troubleshooter',
56
+ currentRun: belongsToCurrentRun(job, task),
57
+ needsRecovery: !(task.state === 'BLOCKED' && Boolean(task.unblock)),
58
+ idUnused: !existing.has(id),
59
+ noRecoveryBarrier: !hasRecoveryBarrier(job, task),
60
+ }, () => { create = true; });
61
+ if (!decision.accepted || !create)
55
62
  continue;
56
63
  const now = new Date().toISOString();
57
- recoveries.push({
64
+ recoveries.push(createTaskEntity(job, {
58
65
  jobId: job.id,
59
66
  id,
60
67
  kind: 'work',
@@ -71,7 +78,6 @@ export function pendingTroubleshooterTasks(job) {
71
78
  output: { kind: 'task_pr' },
72
79
  continuation: null,
73
80
  continuationDeliveredAt: null,
74
- state: 'QUEUED',
75
81
  attempt: 0,
76
82
  maxAttempts: 1,
77
83
  leaseOwner: null,
@@ -84,7 +90,7 @@ export function pendingTroubleshooterTasks(job) {
84
90
  timeoutMs: taskOverallTimeoutMs('M'),
85
91
  createdAt: now,
86
92
  updatedAt: now,
87
- });
93
+ }));
88
94
  }
89
95
  return recoveries;
90
96
  }
package/dist/types.js CHANGED
@@ -1,7 +1,7 @@
1
- import { jobLifecycleWorkflow, taskLifecycleWorkflow } from './workflow/definitions.js';
1
+ import { assertV2StateTransition, jobLifecycleV2, taskLifecycleV2 } from './workflow/definitions-v2.js';
2
2
  export const STORY_SIZE_TIMEOUT_MS = { S: 300000, M: 900000, L: 2700000 };
3
3
  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
- export function assertJobLifecycleTransition(from, to) { jobLifecycleWorkflow.assertStateTransition(from, to, 'job'); }
7
- export function assertTaskTransition(from, to) { taskLifecycleWorkflow.assertStateTransition(from, to, 'task'); }
6
+ export function assertJobLifecycleTransition(from, to) { assertV2StateTransition(jobLifecycleV2.graph, from, to, 'job'); }
7
+ export function assertTaskTransition(from, to) { assertV2StateTransition(taskLifecycleV2.graph, from, to, 'task'); }
package/dist/unblock.js CHANGED
@@ -1,4 +1,6 @@
1
- import { extendRetryBudget, transitionTaskEvent } from './scheduler.js';
1
+ import { transitionTaskEvent } from './scheduler.js';
2
+ import { applyTaskUnblockRetryAction } from './task-actions.js';
3
+ import { dispatchAuthoritativeUnblock } from './workflow/authoritative-unblock-v2.js';
2
4
  export function isEffectivelyBlocked(task) { return task.kind !== 'planner' && task.state === 'BLOCKED'; }
3
5
  function baselineFor(task) {
4
6
  return task.blockedBaseline ?? { blockedAt: task.updatedAt, reason: task.error ?? undefined };
@@ -69,24 +71,32 @@ export async function unblockAutonomousTasks(options) {
69
71
  export async function unblockTask(options) {
70
72
  if (!isEffectivelyBlocked(options.task))
71
73
  throw new Error(`Task ${options.task.id} is not an effective BLOCKED work task`);
72
- if (options.job.executionMode === 'autonomous')
73
- return { replanning: true, planTrigger: await unblockAutonomousTasks({ store: options.store, job: options.job, tasks: [options.task], trigger: options.trigger, now: options.now }) };
74
- const now = options.now ?? new Date().toISOString();
75
- const currentHeadSha = await options.store.headSha?.() ?? options.job.headSha ?? null;
76
- const baseline = baselineFor(options.task);
77
- options.task.blockedBaseline = baseline;
78
- const facts = unblockFacts(options.job, [options.task], currentHeadSha, options.trigger.reason);
79
- options.task.unblock = { at: now, source: options.trigger.source, reason: options.trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: options.trigger.blockerJobId, resolution: options.trigger.resolution };
80
- await transitionTaskEvent(options.task, 'task.unblocked');
81
- await options.store.saveTask(options.task);
82
- options.task.maxAttempts = extendRetryBudget(options.task);
83
- await transitionTaskEvent(options.task, 'retry');
84
- options.task.error = null;
85
- options.task.leaseOwner = null;
86
- options.task.leaseExpiresAt = null;
87
- options.task.executionId = null;
88
- options.task.updatedAt = now;
89
- delete options.task.blockedBy;
90
- await options.store.saveTask(options.task);
91
- return { replanning: false };
74
+ let result;
75
+ const decision = await dispatchAuthoritativeUnblock(options.job.executionMode === 'autonomous', async (actionId) => {
76
+ if (actionId === 'unblock.replan') {
77
+ result = { replanning: true, planTrigger: await unblockAutonomousTasks({ store: options.store, job: options.job, tasks: [options.task], trigger: options.trigger, now: options.now }) };
78
+ return {};
79
+ }
80
+ if (actionId === 'unblock.retry') {
81
+ const now = options.now ?? new Date().toISOString();
82
+ const currentHeadSha = await options.store.headSha?.() ?? options.job.headSha ?? null;
83
+ const baseline = baselineFor(options.task);
84
+ options.task.blockedBaseline = baseline;
85
+ const facts = unblockFacts(options.job, [options.task], currentHeadSha, options.trigger.reason);
86
+ options.task.unblock = { at: now, source: options.trigger.source, reason: options.trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: options.trigger.blockerJobId, resolution: options.trigger.resolution };
87
+ await transitionTaskEvent(options.task, 'task.unblocked');
88
+ await options.store.saveTask(options.task);
89
+ await applyTaskUnblockRetryAction(options.task);
90
+ options.task.updatedAt = now;
91
+ await options.store.saveTask(options.task);
92
+ result = { replanning: false };
93
+ return {};
94
+ }
95
+ throw new Error('Unsupported authoritative unblock action ' + actionId);
96
+ });
97
+ if (!decision.accepted)
98
+ throw new Error('Authoritative unblock graph rejected task.authoritative_unblock');
99
+ if (!result)
100
+ throw new Error('Authoritative unblock graph completed without an action result');
101
+ return result;
92
102
  }
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { GraphRuntime } from './graph-v2.js';
3
+ import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
4
+ const source = readFileSync(new URL('../../workflow/authoritative-unblock-v2.mmd', import.meta.url), 'utf8');
5
+ const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
+ export const authoritativeUnblockV2 = new GraphRuntime(source, {
7
+ states: new Set(['blocked', 'autonomous_replan', 'fixed_retry']),
8
+ guards: { autonomousJob: boolGuard('autonomousJob') },
9
+ actions: {
10
+ 'unblock.replan': { id: 'unblock.replan' },
11
+ 'unblock.retry': { id: 'unblock.retry' },
12
+ },
13
+ events: { 'task.authoritative_unblock': { id: 'task.authoritative_unblock' } },
14
+ });
15
+ export async function dispatchAuthoritativeUnblock(autonomousJob, executeAction) {
16
+ return dispatchWorkflowGraph(authoritativeUnblockV2, { initialState: 'blocked', input: { transport: 'event', id: 'task.authoritative_unblock', payload: {} }, durableFacts: { autonomousJob }, executeAction });
17
+ }
18
+ export function decideAuthoritativeUnblock(autonomousJob) {
19
+ return dispatchWorkflowGraphSync(authoritativeUnblockV2, { initialState: 'blocked', input: { transport: 'event', id: 'task.authoritative_unblock', payload: {} }, durableFacts: { autonomousJob } });
20
+ }
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { GraphRuntime } from './graph-v2.js';
3
+ import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
4
+ const source = readFileSync(new URL('../../workflow/blocker-resolution-v2.mmd', import.meta.url), 'utf8');
5
+ const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
+ export const blockerResolutionV2 = new GraphRuntime(source, {
7
+ states: new Set([
8
+ 'waiting', 'check_target', 'check_autonomous', 'check_effective', 'check_already_unblocked',
9
+ 'check_draft', 'check_blocked', 'check_failed', 'check_unblocked', 'wait', 'obsolete', 'resumed',
10
+ ]),
11
+ guards: {
12
+ resolutionAllowed: boolGuard('resolutionAllowed'),
13
+ targetMatches: boolGuard('targetMatches'),
14
+ autonomousJob: boolGuard('autonomousJob'),
15
+ effectivelyBlocked: boolGuard('effectivelyBlocked'),
16
+ alreadyUnblocked: boolGuard('alreadyUnblocked'),
17
+ isDraft: boolGuard('isDraft'),
18
+ isBlocked: boolGuard('isBlocked'),
19
+ isFailed: boolGuard('isFailed'),
20
+ isUnblocked: boolGuard('isUnblocked'),
21
+ },
22
+ actions: Object.fromEntries([
23
+ 'blocker.autonomousUnblock', 'blocker.acceptExistingUnblock', 'blocker.releaseDraft',
24
+ 'blocker.unblockTask', 'blocker.retryFailed', 'blocker.retryUnblocked',
25
+ ].map(id => [id, { id }])),
26
+ events: { 'blocker.resolved': { id: 'blocker.resolved' } },
27
+ });
28
+ export async function dispatchBlockerResolution(facts, executeAction) {
29
+ return dispatchWorkflowGraph(blockerResolutionV2, { initialState: 'waiting', input: { transport: 'event', id: 'blocker.resolved', payload: {} }, durableFacts: facts, isTransientState: state => state.startsWith('check_'), maxSteps: 10, executeAction });
30
+ }
31
+ export function decideBlockerResolution(facts) {
32
+ return dispatchWorkflowGraphSync(blockerResolutionV2, { initialState: 'waiting', input: { transport: 'event', id: 'blocker.resolved', payload: {} }, durableFacts: facts, isTransientState: state => state.startsWith('check_'), maxSteps: 10 });
33
+ }
@@ -6,11 +6,17 @@ export const WORKFLOW_EVENT_NAMES = [
6
6
  'effect.cancelActiveTasks.failed',
7
7
  'effect.launchTask.failed',
8
8
  'effect.mergePullRequest.failed',
9
+ 'execution.orphaned',
9
10
  'fail',
10
11
  'hold',
12
+ 'lease.expired',
11
13
  'implementation.completed',
12
14
  'pr.closed',
13
15
  'pr.merged',
16
+ 'queue',
17
+ 'plan.requested',
18
+ 'planner.failed',
19
+ 'planner.unavailable',
14
20
  'reconcile',
15
21
  'release',
16
22
  'retry',
@@ -36,10 +42,25 @@ export const WORKFLOW_FACT_NAMES = [
36
42
  'notMerged',
37
43
  'autoMerge',
38
44
  'recoverableExecution',
45
+ 'retryAvailable',
46
+ 'initialPlan',
47
+ 'scheduledRun',
48
+ 'hasUnblock',
49
+ 'manualPlan',
50
+ 'prOpen',
51
+ 'reviewDisabled',
52
+ 'implementationComplete',
53
+ 'headAvailable',
54
+ 'mergeCapable',
39
55
  ];
40
56
  export const WORKFLOW_MUTATION_NAMES = [
41
57
  'mutation.clearError',
42
58
  'mutation.clearFailureReason',
59
+ 'mutation.clearLease',
60
+ 'mutation.clearExecution',
61
+ 'mutation.markLeaseExpired',
62
+ 'mutation.markStartupOrphanedExhausted',
63
+ 'mutation.markPlannerUnavailable',
43
64
  ];
44
65
  export const WORKFLOW_EFFECT_NAMES = [
45
66
  'effect.launchTask',
@@ -0,0 +1,75 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { GraphRuntime } from './graph-v2.js';
3
+ import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES, } from './contracts.js';
4
+ const TASK_STATES = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'UNBLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED'];
5
+ const JOB_STATES = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'];
6
+ const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
7
+ function eventRegistry() {
8
+ return Object.fromEntries(WORKFLOW_EVENT_NAMES.map(id => [id, { id }]));
9
+ }
10
+ function actionRegistry() {
11
+ return Object.fromEntries([...WORKFLOW_MUTATION_NAMES, ...WORKFLOW_EFFECT_NAMES].map(id => [id, { id }]));
12
+ }
13
+ function load(name, states, guards) {
14
+ const url = new URL(`../../workflow/${name}-v2.mmd`, import.meta.url);
15
+ return new GraphRuntime(readFileSync(url, 'utf8'), {
16
+ states: new Set(states),
17
+ guards,
18
+ actions: actionRegistry(),
19
+ events: eventRegistry(),
20
+ });
21
+ }
22
+ export const taskLifecycleV2 = load('task-lifecycle', TASK_STATES, {
23
+ dependenciesSatisfied: boolGuard('dependenciesSatisfied'),
24
+ dependenciesUnsatisfied: boolGuard('dependenciesUnsatisfied'),
25
+ runnable: boolGuard('runnable'),
26
+ dispatchable: boolGuard('dispatchable'),
27
+ recoverableExecution: boolGuard('recoverableExecution'),
28
+ retryAvailable: boolGuard('retryAvailable'),
29
+ });
30
+ export const jobLifecycleV2 = load('job-lifecycle', JOB_STATES, {
31
+ notMerged: boolGuard('notMerged'),
32
+ autoMerge: boolGuard('autoMerge'),
33
+ prOpen: boolGuard('prOpen'),
34
+ reviewDisabled: boolGuard('reviewDisabled'),
35
+ implementationComplete: boolGuard('implementationComplete'),
36
+ headAvailable: boolGuard('headAvailable'),
37
+ mergeCapable: boolGuard('mergeCapable'),
38
+ });
39
+ function topologyTargets(graph, from) {
40
+ const source = [...graph.nodes.values()].find(node => node.kind === 'state' && node.semanticId === from);
41
+ if (!source)
42
+ throw new Error(`Unknown workflow state ${from}`);
43
+ const targets = new Set();
44
+ const visit = (nodeId, seen) => {
45
+ if (seen.has(nodeId))
46
+ return;
47
+ seen.add(nodeId);
48
+ const node = graph.nodes.get(nodeId);
49
+ if (!node)
50
+ return;
51
+ if (node.kind === 'state') {
52
+ targets.add(node.semanticId);
53
+ return;
54
+ }
55
+ for (const edge of graph.edges) {
56
+ if (edge.from === nodeId && edge.kind !== 'emit')
57
+ visit(edge.to, new Set(seen));
58
+ }
59
+ };
60
+ for (const edge of graph.edges) {
61
+ if (edge.from === source.id && (edge.kind === 'on' || edge.kind === 'request')) {
62
+ visit(edge.to, new Set([source.id]));
63
+ }
64
+ }
65
+ return targets;
66
+ }
67
+ export function assertV2StateTransition(graph, from, to, kind) {
68
+ if (from === to)
69
+ return;
70
+ const states = new Set([...graph.nodes.values()].filter(node => node.kind === 'state').map(node => node.semanticId));
71
+ if (!states.has(from) || !states.has(to))
72
+ throw new Error(`Unknown ${kind} state ${!states.has(from) ? from : to}`);
73
+ if (!topologyTargets(graph, from).has(to))
74
+ throw new Error(`Invalid ${kind} transition ${from} -> ${to}`);
75
+ }
@@ -0,0 +1,56 @@
1
+ import { GraphDispatcher } from './graph-v2.js';
2
+ /**
3
+ * The single production graph execution boundary.
4
+ * Ingress adapters normalize input/facts, while GraphDispatcher alone evaluates,
5
+ * persists graph transitions, executes graph-selected actions, and emits events.
6
+ */
7
+ export async function dispatchWorkflowGraph(runtime, options) {
8
+ let current = options.initialState;
9
+ const maxSteps = options.maxSteps ?? 1;
10
+ for (let step = 0; step < maxSteps; step += 1) {
11
+ const dispatcher = new GraphDispatcher(runtime, {
12
+ persistTransition: async (transition) => { await options.persistTransition?.(transition.source, transition.target); },
13
+ executeAction: options.executeAction ? async (actionId) => await options.executeAction(actionId) : undefined,
14
+ publishEvent: async (event) => { await options.publishEvent?.(event.id, event.payload); },
15
+ });
16
+ const result = await dispatcher.dispatch({
17
+ currentState: current,
18
+ input: options.input,
19
+ durableFacts: options.durableFacts ?? {},
20
+ });
21
+ if (!result.decision.accepted)
22
+ return result.decision;
23
+ current = result.decision.target;
24
+ if (!options.isTransientState?.(current))
25
+ return result.decision;
26
+ }
27
+ return { accepted: false, source: current, reason: 'no_transition' };
28
+ }
29
+ export function dispatchWorkflowGraphSync(runtime, options) {
30
+ let current = options.initialState;
31
+ const maxSteps = options.maxSteps ?? 1;
32
+ for (let step = 0; step < maxSteps; step += 1) {
33
+ const dispatcher = new GraphDispatcher(runtime, {
34
+ persistTransition: transition => { const result = options.persistTransition?.(transition.source, transition.target); if (result instanceof Promise)
35
+ throw new Error('sync workflow persistTransition returned Promise'); },
36
+ executeAction: options.executeAction ? actionId => { const result = options.executeAction(actionId); if (result instanceof Promise)
37
+ throw new Error('sync workflow executeAction returned Promise'); return result; } : undefined,
38
+ publishEvent: event => { const result = options.publishEvent?.(event.id, event.payload); if (result instanceof Promise)
39
+ throw new Error('sync workflow publishEvent returned Promise'); },
40
+ });
41
+ const result = dispatcher.dispatchSync({ currentState: current, input: options.input, durableFacts: options.durableFacts ?? {} });
42
+ if (!result.decision.accepted)
43
+ return result.decision;
44
+ current = result.decision.target;
45
+ if (!options.isTransientState?.(current))
46
+ return result.decision;
47
+ }
48
+ return { accepted: false, source: current, reason: 'no_transition' };
49
+ }
50
+ export async function dispatchWorkflowTargetOverride(runtime, options) {
51
+ // Administrative correction still derives legality from the canonical Mermaid graph.
52
+ // It intentionally executes no workflow actions/effects: this is a state repair, not an event replay.
53
+ const { assertV2StateTransition } = await import('./definitions-v2.js');
54
+ assertV2StateTransition(runtime.graph, options.currentState, options.targetState, options.kind);
55
+ await options.persistTransition(options.currentState, options.targetState);
56
+ }
@@ -0,0 +1,32 @@
1
+ import { dispatchJobLifecycleSync, dispatchTaskLifecycleSync } from './lifecycle.js';
2
+ function dependenciesSatisfied(job, dependencies) {
3
+ if (dependencies.length === 0)
4
+ return true;
5
+ const byId = new Map(job.tasks.map(task => [task.id, task]));
6
+ return dependencies.every(id => byId.get(id)?.state === 'SUCCEEDED');
7
+ }
8
+ /**
9
+ * Canonical creation boundary for new Task entities.
10
+ * Constructors provide metadata/facts only; the Task lifecycle graph derives
11
+ * whether a fresh Task remains DRAFT or becomes QUEUED.
12
+ */
13
+ export function createTaskEntity(job, value, options = {}) {
14
+ const task = { ...value, state: 'DRAFT' };
15
+ if (!options.defer) {
16
+ dispatchTaskLifecycleSync(task, 'reconcile', {
17
+ dependenciesSatisfied: dependenciesSatisfied(job, task.dependencies),
18
+ });
19
+ }
20
+ return task;
21
+ }
22
+ /**
23
+ * Canonical creation boundary for new Job entities.
24
+ * New Jobs begin at the graph initial state and scheduled creation is expressed
25
+ * as the normal activate lifecycle event.
26
+ */
27
+ export function createJobEntity(value, options = {}) {
28
+ const job = { ...value, state: 'DRAFT' };
29
+ if (options.activate)
30
+ dispatchJobLifecycleSync(job, 'activate');
31
+ return job;
32
+ }