@nanobpm/nano-workforce 0.101.0 → 0.102.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.
@@ -12,9 +12,9 @@
12
12
  // shapes the escalation payload on a block.
13
13
  import type { AppJobHandler } from "@nanobpm/urban";
14
14
  import { matchTags, tag } from "@nanobpm/urban/effect";
15
- import { abandonTokenFromUrl } from "../../app/abandon.ts";
15
+ import { ABANDONED_STATUS, abandonTokenFromUrl } from "../../app/abandon.ts";
16
16
  import { checkBaseTarget, classifyBaseGuard } from "../../app/baseGuard.ts";
17
- import { enqueueViaComment, fetchPrState, mergePr } from "../../app/github.ts";
17
+ import { classifyPrLiveness, enqueueViaComment, fetchPrState, mergePr } from "../../app/github.ts";
18
18
  import { classifyMergeLanding, DEFAULT_MERGE_PROTOCOL, loadMergeProtocol } from "../../app/mergeProtocol.ts";
19
19
  import { ensurePr, MERGE_ADMIN, MERGE_METHOD } from "../../app/service.ts";
20
20
  import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
@@ -23,7 +23,7 @@ import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
23
23
  type In = WorkerInputs["pr.merge"];
24
24
 
25
25
  interface Out extends Record<string, unknown> {
26
- mergeStatus: "merged" | "queued" | "blocked" | "retry";
26
+ mergeStatus: "merged" | "queued" | "blocked" | "retry" | "abandoned";
27
27
  status?: string;
28
28
  question?: string;
29
29
  }
@@ -54,7 +54,8 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
54
54
  // FK parent, and BEFORE the base-guard/protocol logic. Best-effort: a transport hiccup falls through
55
55
  // to the normal path rather than blocking a genuine merge.
56
56
  const pre = await fetchPrState(repo, prNumber, token).catch(() => null);
57
- if (pre?.merged) {
57
+ const liveness = classifyPrLiveness(pre);
58
+ if (liveness === "merged") {
58
59
  await app.data.table("merges", "id").insert({
59
60
  pr_key: prKey,
60
61
  outcome: "merged",
@@ -65,6 +66,33 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
65
66
  return { mergeStatus: "merged" };
66
67
  }
67
68
 
69
+ // Closed-not-merged short-circuit (#342). A PR CLOSED on GitHub without merging (e.g. superseded
70
+ // by a newer PR) can never land: falling through to the land protocol would return
71
+ // blocked/conflict and escalate a merge no human can complete, orphaning the process on a dead
72
+ // PR (#350). Instead record a terminal `abandoned` audit row and drive the loop down its
73
+ // terminate/abandon end event — NOT `merge-esc-conflict`. This opens NO escalation and parks NO
74
+ // user task: a closed PR is terminal state, not a human decision. Symmetric with the merged
75
+ // short-circuit above and runs on the same live-state read, so one `fetchPrState` classifies both.
76
+ if (liveness === "closed") {
77
+ await app.data.table("merges", "id").insert({
78
+ pr_key: prKey,
79
+ outcome: "abandoned",
80
+ method: "pr-closed",
81
+ detail: "PR was closed on GitHub without merging (e.g. superseded) — abandoning the merge loop",
82
+ at: now,
83
+ });
84
+ // Terminal status write. This branch drives the model's terminate/abandon end event, which runs
85
+ // NO mark-merged worker (the merged path's `pr.mark-merged` is what sets `status:"merged"`). If
86
+ // we don't flip the row here it lingers on its in-flight status (e.g. the transient `merging`),
87
+ // so `activePrs`/delivery keep treating a dead PR as live. Symmetric with mark-merged's write;
88
+ // `ensurePr` above guarantees the row exists to update.
89
+ await app.data.table("pull_requests", "pr_key").update(prKey, {
90
+ status: ABANDONED_STATUS,
91
+ updated_at: now,
92
+ });
93
+ return { mergeStatus: "abandoned" };
94
+ }
95
+
68
96
  // Dead-end-base guard (#60): never land a PR into a base branch that has itself already merged
69
97
  // to the default branch — the merge would land into a dead branch and never reach `main`.
70
98
  // GitHub only auto-retargets a PR when its base is *deleted* on merge; a merged-but-undeleted