agents-relay 2.0.38 → 2.0.39

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.
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { storySizeTimeoutMs, chatGptStartAckTimeoutMs } from './types.js';
3
- import { dependencyReady, schedule, transitionJobEvent, transitionTaskEvent } from './scheduler.js';
3
+ import { dependencyReady, schedule, transitionTaskEvent } from './scheduler.js';
4
4
  import { applyTaskCancelAction } from './task-actions.js';
5
5
  import { LaunchFailure, StartAckTimeout } from './adapters.js';
6
6
  import { eventFor } from './events.js';
@@ -13,7 +13,6 @@ import { executeManualPlan } from './manual-plan.js';
13
13
  import { executionEventSource, withExecutionBoundary } from './reconciler-runtime.js';
14
14
  export { executionEventSource } from './reconciler-runtime.js';
15
15
  import { CallbackWorkflowEffectHandler } from './workflow/effects.js';
16
- import { applyHistoricalJobCompatibilityRepair } from './workflow/runtime.js';
17
16
  import { pendingTroubleshooterTasks } from './troubleshooter.js';
18
17
  import { prepareManagedExecutionWorktree } from './workspace.js';
19
18
  import { createReviewChairExecutionPlan } from './review-chair.js';
@@ -119,23 +118,10 @@ export class Reconciler extends ReconcilerReviewBase {
119
118
  await this.invalidateStaleReview(job, headSha);
120
119
  const now = (this.options.now ?? new Date()).getTime();
121
120
  if (prState !== 'OPEN') {
122
- const reason = prState === 'MERGED' ? 'Pull request merged' : 'Pull request closed';
123
- const terminalState = prState === 'MERGED' ? 'COMPLETED' : 'CANCELLED';
124
- if (job.state !== terminalState) {
125
- if (!['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'].includes(job.state)) {
126
- // Compatibility repair boundary for historical records that persisted runtime status as lifecycle.
127
- applyHistoricalJobCompatibilityRepair(job, terminalState);
128
- await this.store.saveJob(job);
129
- const outcomes = await this.workflowEffects.execute(['effect.cancelActiveTasks'], { jobId: job.id, transitionKey: `compatibility:${prState}`, data: { reason } });
130
- await this.applyJobEffectOutcomes(job, outcomes);
131
- }
132
- else {
133
- await transitionJobEvent(job, prState === 'MERGED' ? 'pr.merged' : 'pr.closed', prState === 'CLOSED' ? { notMerged: true } : {}, async (actionId) => {
134
- const outcomes = await this.workflowEffects.execute([actionId], { jobId: job.id, transitionKey: `terminal:${prState}`, data: { reason } });
135
- await this.applyJobEffectOutcomes(job, outcomes);
136
- }, async () => { await this.store.saveJob(job); });
137
- }
138
- }
121
+ await dispatchJobReconcile(job, { prState, headSha, mergeCapable: Boolean(this.store.mergePullRequest) }, async (actionId) => {
122
+ const outcomes = await this.workflowEffects.execute([actionId], { jobId: job.id, transitionKey: `terminal:${prState}`, data: { reason: `Pull request ${prState.toLowerCase()}` } });
123
+ await this.applyJobEffectOutcomes(job, outcomes);
124
+ }, async () => { await this.store.saveJob(job); });
139
125
  if (prState === 'MERGED' && this.store.resolveBlockerDependents) {
140
126
  const resolved = await this.store.resolveBlockerDependents(job.id, 'merge');
141
127
  for (const target of resolved.unblocked)
@@ -1,19 +1,65 @@
1
1
  import { decideJobLifecycle, dispatchJobLifecycle } from './lifecycle.js';
2
2
  import { implementationComplete } from '../runtime-status.js';
3
- export function decideJobReconcile(job, facts) {
4
- return decideJobLifecycle(job, 'reconcile', {
5
- prOpen: facts.prState === 'OPEN',
3
+ import { applyHistoricalJobCompatibilityRepair } from './runtime.js';
4
+ function lifecycleInput(facts) {
5
+ if (facts.prState === 'MERGED')
6
+ return { event: 'pr.merged', context: {} };
7
+ if (facts.prState === 'CLOSED')
8
+ return { event: 'pr.closed', context: { notMerged: true } };
9
+ return {
10
+ event: 'reconcile',
11
+ context: {
12
+ prOpen: true,
13
+ headAvailable: Boolean(facts.headSha),
14
+ mergeCapable: facts.mergeCapable,
15
+ },
16
+ };
17
+ }
18
+ function graphFacts(job, facts) {
19
+ const input = lifecycleInput(facts);
20
+ if (input.event !== 'reconcile')
21
+ return input.context;
22
+ return {
23
+ ...input.context,
6
24
  reviewDisabled: job.reviewEnabled === false,
7
25
  implementationComplete: implementationComplete(job),
8
26
  autoMerge: job.autoMerge !== false,
9
- headAvailable: Boolean(facts.headSha),
10
- mergeCapable: facts.mergeCapable,
11
- });
27
+ };
28
+ }
29
+ /**
30
+ * Historical compatibility only. Normal Job lifecycle is always decided by
31
+ * workflow/job-lifecycle.mmd from the PR fact-derived event.
32
+ */
33
+ function repairHistoricalTerminalState(job, facts) {
34
+ if (['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'].includes(job.state))
35
+ return null;
36
+ if (facts.prState === 'MERGED')
37
+ applyHistoricalJobCompatibilityRepair(job, 'COMPLETED');
38
+ else if (facts.prState === 'CLOSED')
39
+ applyHistoricalJobCompatibilityRepair(job, 'CANCELLED');
40
+ else
41
+ return null;
42
+ return job.state;
43
+ }
44
+ export function decideJobReconcile(job, facts) {
45
+ const input = lifecycleInput(facts);
46
+ return decideJobLifecycle(job, input.event, graphFacts(job, facts));
12
47
  }
13
48
  export async function dispatchJobReconcile(job, facts, executeEffect, persist) {
14
- return dispatchJobLifecycle(job, 'reconcile', {
15
- prOpen: facts.prState === 'OPEN', reviewDisabled: job.reviewEnabled === false,
16
- implementationComplete: implementationComplete(job), autoMerge: job.autoMerge !== false,
17
- headAvailable: Boolean(facts.headSha), mergeCapable: facts.mergeCapable,
18
- }, executeEffect, persist);
49
+ const repaired = repairHistoricalTerminalState(job, facts);
50
+ if (repaired) {
51
+ await persist?.();
52
+ await executeEffect?.('effect.cancelActiveTasks');
53
+ return {
54
+ accepted: true,
55
+ target: repaired,
56
+ guard: null,
57
+ actions: ['effect.cancelActiveTasks'],
58
+ mutations: [],
59
+ effects: ['effect.cancelActiveTasks'],
60
+ reason: 'accepted',
61
+ };
62
+ }
63
+ const input = lifecycleInput(facts);
64
+ return dispatchJobLifecycle(job, input.event, graphFacts(job, facts), executeEffect, persist);
19
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.38",
3
+ "version": "2.0.39",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,3 +37,11 @@ A **P0 job** (alias: **self job**) is executed directly by the current active or
37
37
  6. After implementation and local verification, the orchestrator merges the job itself and completes normal post-merge reconciliation.
38
38
 
39
39
  `job create` still only creates durable job state; the P0 workflow requires the orchestrator to follow it immediately with task creation and execution.
40
+
41
+ ## Merge completion vs deployment evidence
42
+
43
+ For a normal one-shot managed Job, the canonical Job lifecycle graph owns completion: a durable `pr.merged` fact transitions the Job to `COMPLETED`. Reconciler and deployment code must not invent a second completion gate after merge.
44
+
45
+ Post-merge publication and deployment activity is separate from Job lifecycle. Events or evidence such as `package.published`, published version metadata, service restart, and deployment verification may be attached after the Job is already `COMPLETED`; they must not hold the Job open, reopen it, or redefine completion. If post-merge deployment needs remediation, record that as separate deployment/recovery work rather than mutating the completed implementation Job lifecycle.
46
+
47
+ A PR closed without merge follows the same graph-owned rule through `pr.closed -> CANCELLED`.