@open-everywhere/provider-github 0.0.0-stage → 0.1.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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +50 -2
- package/agent/channels/github-catch-up.ts +134 -0
- package/agent/channels/github-engagement.ts +381 -0
- package/agent/channels/github-identity.ts +376 -0
- package/agent/channels/github-installations.ts +152 -0
- package/agent/channels/github-message-ops.ts +62 -0
- package/agent/channels/github-origin-codec.ts +42 -0
- package/agent/channels/github-post-arm.ts +422 -0
- package/agent/channels/github-reaction-feedback.ts +489 -0
- package/agent/channels/github-reaction-scope.ts +312 -0
- package/agent/channels/github-read.ts +471 -0
- package/agent/channels/github-stop.ts +175 -0
- package/agent/channels/github-supervisor.ts +645 -0
- package/agent/channels/github-webhook-verifier.ts +56 -0
- package/agent/channels/github.ts +1458 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/memory/memory.ts +265 -0
- package/agent/ops.ts +17 -0
- package/agent/provider-events.ts +114 -0
- package/agent/register.ts +130 -0
- package/agent/surface-parity.ts +116 -0
- package/dist/agent/channels/github-catch-up.d.ts +29 -0
- package/dist/agent/channels/github-catch-up.d.ts.map +1 -0
- package/dist/agent/channels/github-catch-up.js +105 -0
- package/dist/agent/channels/github-catch-up.js.map +1 -0
- package/dist/agent/channels/github-engagement.d.ts +223 -0
- package/dist/agent/channels/github-engagement.d.ts.map +1 -0
- package/dist/agent/channels/github-engagement.js +317 -0
- package/dist/agent/channels/github-engagement.js.map +1 -0
- package/dist/agent/channels/github-identity.d.ts +164 -0
- package/dist/agent/channels/github-identity.d.ts.map +1 -0
- package/dist/agent/channels/github-identity.js +295 -0
- package/dist/agent/channels/github-identity.js.map +1 -0
- package/dist/agent/channels/github-installations.d.ts +49 -0
- package/dist/agent/channels/github-installations.d.ts.map +1 -0
- package/dist/agent/channels/github-installations.js +114 -0
- package/dist/agent/channels/github-installations.js.map +1 -0
- package/dist/agent/channels/github-message-ops.d.ts +3 -0
- package/dist/agent/channels/github-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/github-message-ops.js +58 -0
- package/dist/agent/channels/github-message-ops.js.map +1 -0
- package/dist/agent/channels/github-origin-codec.d.ts +7 -0
- package/dist/agent/channels/github-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/github-origin-codec.js +36 -0
- package/dist/agent/channels/github-origin-codec.js.map +1 -0
- package/dist/agent/channels/github-post-arm.d.ts +54 -0
- package/dist/agent/channels/github-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/github-post-arm.js +339 -0
- package/dist/agent/channels/github-post-arm.js.map +1 -0
- package/dist/agent/channels/github-reaction-feedback.d.ts +97 -0
- package/dist/agent/channels/github-reaction-feedback.d.ts.map +1 -0
- package/dist/agent/channels/github-reaction-feedback.js +380 -0
- package/dist/agent/channels/github-reaction-feedback.js.map +1 -0
- package/dist/agent/channels/github-reaction-scope.d.ts +102 -0
- package/dist/agent/channels/github-reaction-scope.d.ts.map +1 -0
- package/dist/agent/channels/github-reaction-scope.js +296 -0
- package/dist/agent/channels/github-reaction-scope.js.map +1 -0
- package/dist/agent/channels/github-read.d.ts +135 -0
- package/dist/agent/channels/github-read.d.ts.map +1 -0
- package/dist/agent/channels/github-read.js +371 -0
- package/dist/agent/channels/github-read.js.map +1 -0
- package/dist/agent/channels/github-stop.d.ts +52 -0
- package/dist/agent/channels/github-stop.d.ts.map +1 -0
- package/dist/agent/channels/github-stop.js +145 -0
- package/dist/agent/channels/github-stop.js.map +1 -0
- package/dist/agent/channels/github-supervisor.d.ts +308 -0
- package/dist/agent/channels/github-supervisor.d.ts.map +1 -0
- package/dist/agent/channels/github-supervisor.js +339 -0
- package/dist/agent/channels/github-supervisor.js.map +1 -0
- package/dist/agent/channels/github-webhook-verifier.d.ts +5 -0
- package/dist/agent/channels/github-webhook-verifier.d.ts.map +1 -0
- package/dist/agent/channels/github-webhook-verifier.js +55 -0
- package/dist/agent/channels/github-webhook-verifier.js.map +1 -0
- package/dist/agent/channels/github.d.ts +139 -0
- package/dist/agent/channels/github.d.ts.map +1 -0
- package/dist/agent/channels/github.js +1232 -0
- package/dist/agent/channels/github.js.map +1 -0
- package/dist/agent/contributions.d.ts +12 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +31 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/memory/memory.d.ts +54 -0
- package/dist/agent/memory/memory.d.ts.map +1 -0
- package/dist/agent/memory/memory.js +224 -0
- package/dist/agent/memory/memory.js.map +1 -0
- package/dist/agent/ops.d.ts +3 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +16 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +112 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +4 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +116 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +110 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +156 -4
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// THE GITHUB AMBIENT ENGAGEMENT SEAM (chat-surface contract §2.5) — the GitHub wiring over core's
|
|
2
|
+
// pure engagement policy (`packages/core/agent/channels/engagement.ts`). It gives GitHub the middle
|
|
3
|
+
// posture Slack, Discord and Teams already have: an @mention marks the issue or pull-request thread
|
|
4
|
+
// ENGAGED, a later PLAIN comment in that thread wakes the agent, and `stand_down` stores the stop.
|
|
5
|
+
//
|
|
6
|
+
// THE ANCHOR is `githubConversationKey(owner, repo, number)` — `owner/repo#N`, the same string the
|
|
7
|
+
// mapper already publishes as `anchorChannelId` for the stop registry. One issue or pull request IS
|
|
8
|
+
// the GitHub conversation, so that is the grain a person can address a stop to, and it is the grain
|
|
9
|
+
// engagement is kept per. The surface partition stays the REPO (`github/<owner>/<repo>`), so the
|
|
10
|
+
// anchor distinguishes the containers inside it.
|
|
11
|
+
//
|
|
12
|
+
// 🔴 WHICH LEG OWNS AN ENGAGED FOLLOW-UP IS THE SAME QUESTION AS WHICH LEG OWNS A MENTION, AND IT
|
|
13
|
+
// GETS THE SAME ANSWER. GitHub runs two legs over one webhook: the NATIVE mention session
|
|
14
|
+
// (`githubOnComment`) and the ambient supervisor (`ambientDelivery`). Today a mention goes to
|
|
15
|
+
// exactly one of them — the native leg, unless the repo is single-window and operator-vouched, where
|
|
16
|
+
// the native session is suppressed and the unified window answers instead. This seam follows that
|
|
17
|
+
// split rather than inventing a second one: on a repo where the native leg owns mentions it also
|
|
18
|
+
// owns engaged follow-ups, and the ambient leg drops them; on a single-window vouched repo the
|
|
19
|
+
// ambient leg owns them, because the native handler stands down for EVERY comment there
|
|
20
|
+
// (`shouldSuppressNativeMention` asks `singleWindowMentionPolicy` with `isMention` pinned true).
|
|
21
|
+
// Two legs answering one comment is the failure this ordering exists to prevent.
|
|
22
|
+
//
|
|
23
|
+
// 🔴 "THE WINDOW ALREADY ANSWERS EVERY COMMENT" WAS THE CLAIM THAT BROKE IT (OPE-1740/OPE-1745). It
|
|
24
|
+
// held only while a seat REQUIRED `ambientSupervisor`. Seats are universal since OPE-1695, so a
|
|
25
|
+
// seated, vouched repo on an ADDRESSED-ONLY domain answers addressed comments and nothing else — and
|
|
26
|
+
// an engaged follow-up there reached neither leg: the native handler stood down, and the ambient
|
|
27
|
+
// leg's chattiness gate refused the comment as `domain-refused` before this seam was read. Deploy
|
|
28
|
+
// #201 measured exactly that on `oe-e2e-addressed-github`. The ambient leg now reads the seam FIRST
|
|
29
|
+
// and feeds its verdict to the gate (`githubAmbientLegAdmission`, github.ts): enrolment is an
|
|
30
|
+
// address, and the domain opt-in governs UNINVITED talk only.
|
|
31
|
+
//
|
|
32
|
+
// 🔴 HOW FAR THE NON-MENTION ARM REACHES — PRIVATE REPOS, AND NEVER AN INLINE REVIEW COMMENT.
|
|
33
|
+
// `githubEngagedFollowUpReach` is the one predicate for it, and BOTH legs ask it. A plain follow-up
|
|
34
|
+
// wakes a native turn (a sandbox checkout, a judged spend), so it is refused on a public or
|
|
35
|
+
// unknown-privacy repo, and refused inside a review thread because the anchor is the pull request
|
|
36
|
+
// rather than the thread. An @mention is unaffected by either floor.
|
|
37
|
+
//
|
|
38
|
+
// 🔴 IT IS A STRICTER FLOOR THAN THE AMBIENT LEG'S, NOT THE SAME ONE. `classifyGitHubAmbientComment`
|
|
39
|
+
// step 5 admits a VOUCHED public repo (`repoPrivate !== true && !vouched ⇒ drop`); this one is
|
|
40
|
+
// vouch-blind and refuses every public repo. Do not "align" the two by adding the vouch here — that
|
|
41
|
+
// re-opens an uninvited native turn on a public repo, which is the thing this floor exists to close.
|
|
42
|
+
//
|
|
43
|
+
// 🔴 WHAT NO TEST REACHES, STATED RATHER THAN IMPLIED, AND ONE OF THE THREE IS COVERED BY NOTHING.
|
|
44
|
+
// `github.ts`'s `ambientDelivery` is module-private and only runs behind the route's `waitUntil`, so
|
|
45
|
+
// no in-repo test drives the LEG itself. Obligation by obligation:
|
|
46
|
+
// • THE CHATTINESS GATE reading the enrolment fact IS driven offline, over the real store and a
|
|
47
|
+
// real declared addressed-only domain, because `githubAmbientLegAdmission` is exported for it
|
|
48
|
+
// (`scripts/engaged-seam-wiring-test.ts`, the addressed-only block) — reach floors included.
|
|
49
|
+
// That the leg CALLS it is `J-ENGAGED-github`'s to prove: step (b)'s follow-up reaches a turn on
|
|
50
|
+
// that journey's repo only through this gate.
|
|
51
|
+
// • THE STOOD-DOWN SUPPRESSION is covered by NOTHING, and the gate above it is why. On an
|
|
52
|
+
// ADDRESSED-ONLY repo a stopped row resolves `hold`, never `deliver`, so the gate refuses the
|
|
53
|
+
// comment first and this branch never runs — `J-ENGAGED-github`'s stand-down step passes on the
|
|
54
|
+
// domain flag, not on the stop. The branch is reachable only on an ambient-ON repo, and no
|
|
55
|
+
// journey drives a `stand_down` on one. Deleting it would red nothing.
|
|
56
|
+
// • THE `!shouldSuppressNativeMention(singleWindow, vouched)` DROP GUARD is covered on its
|
|
57
|
+
// NON-FIRING side only: `J-ENGAGED-github`'s repo stands the native leg down, so a guard that
|
|
58
|
+
// wrongly dropped there reds the journey. Its FIRING side — a repo the native leg owns, where
|
|
59
|
+
// the guard is what stops two legs answering one comment — is covered by nothing.
|
|
60
|
+
// `scripts/github-external-policy-room-test.ts` records the same limit for its own leg.
|
|
61
|
+
// What IS pinned: this module's decision table
|
|
62
|
+
// (`packages/providers/github/scripts/github-engagement-test.ts`), the NATIVE leg's whole loop over
|
|
63
|
+
// the real store — mark, deliver, stop, the way back in, and the redelivery guard
|
|
64
|
+
// (`scripts/engaged-seam-wiring-test.ts`), and the anchor round trip through `stand_down`
|
|
65
|
+
// (`scripts/stand-down-anchor-test.ts` LEG 17). On the AMBIENT leg, the `J-ENGAGED-github` e2e
|
|
66
|
+
// journey covers exactly what the three bullets above say it covers, and no more.
|
|
67
|
+
|
|
68
|
+
import {
|
|
69
|
+
getOperationalStore,
|
|
70
|
+
getThreadEngagement,
|
|
71
|
+
markThreadEngaged,
|
|
72
|
+
resolvePartition,
|
|
73
|
+
shouldDeliverReply,
|
|
74
|
+
type EngagementRow,
|
|
75
|
+
type Partition,
|
|
76
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
77
|
+
import { buildGitHubAuth, githubConversationKey } from "./github-identity.js";
|
|
78
|
+
import type { GitHubAmbientAddressing } from "./github-supervisor.js";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The engagement-registry anchor for one GitHub container, or `undefined` when the delivery names
|
|
82
|
+
* no container number.
|
|
83
|
+
*
|
|
84
|
+
* 🔴 IT IS THE ONE PREDICATE THE MAPPER AND THE SEAM SHARE. `bindGitHub` publishes exactly this as
|
|
85
|
+
* `engagementKey`, so `stand_down`'s mark and this seam's read address the same row. Two spellings
|
|
86
|
+
* of "which container this is" is how a stop comes to mark a row no gate reads.
|
|
87
|
+
*/
|
|
88
|
+
export function githubEngagementAnchor(owner: string | undefined, repo: string | undefined, num: number | undefined): string | undefined {
|
|
89
|
+
return githubConversationKey(owner, repo, num);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The partition one container's engagement row lives in — the REPO grain
|
|
93
|
+
* (`github/<owner>/<repo>`), which is what `resolvePartition` gives a bound GitHub session and
|
|
94
|
+
* therefore what `stand_down` writes under. It is built from THIS delivery's own facts, through the
|
|
95
|
+
* same `buildGitHubAuth` the wake dispatches, so the seam can never key a row the turn would not.
|
|
96
|
+
* Null on an unresolvable identity: the caller then keeps today's path, the fail-open posture every
|
|
97
|
+
* carriage leg on this surface takes. */
|
|
98
|
+
export function githubSeamPartition(f: GitHubSeamFacts): Partition | null {
|
|
99
|
+
try {
|
|
100
|
+
const auth = buildGitHubAuth({
|
|
101
|
+
owner: f.owner,
|
|
102
|
+
repo: f.repo,
|
|
103
|
+
isPrivate: f.isPrivate,
|
|
104
|
+
actorLogin: f.actorLogin,
|
|
105
|
+
});
|
|
106
|
+
return resolvePartition({ channel: { kind: "github" }, session: { auth: { current: auth } } });
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The facts one comment delivery contributes to the seam. Both legs can build it — the native
|
|
113
|
+
* handler from eve's inbound context, the ambient leg from the raw webhook event — so neither
|
|
114
|
+
* re-states the policy below. */
|
|
115
|
+
export interface GitHubSeamFacts {
|
|
116
|
+
readonly owner: string;
|
|
117
|
+
readonly repo: string;
|
|
118
|
+
/** The issue or pull-request number the comment landed on. */
|
|
119
|
+
readonly number: number | undefined;
|
|
120
|
+
readonly isPrivate?: boolean;
|
|
121
|
+
/** The comment author's login — the principal this delivery binds. It rides the facts rather than
|
|
122
|
+
* being stubbed here so the partition the seam keys is built from the delivery's own identity. */
|
|
123
|
+
readonly actorLogin: string;
|
|
124
|
+
/** The comment @mentions this deployment's app (the caller's own `githubBotMentioned` verdict). */
|
|
125
|
+
readonly isMention: boolean;
|
|
126
|
+
/** The comment was written by a bot or by this app itself — never delivered (loop guard). */
|
|
127
|
+
readonly authorIsBot: boolean;
|
|
128
|
+
/** The container this comment landed in, in GitHub's own vocabulary — `issue`, `pull_request` or
|
|
129
|
+
* `review_thread`. The seam reads it for one question, in
|
|
130
|
+
* {@link githubEngagedFollowUpReach}: an INLINE review comment shares its pull request's anchor,
|
|
131
|
+
* so a review thread is not a container of its own. Required rather than optional, because a
|
|
132
|
+
* caller that forgets it would silently widen that arm. */
|
|
133
|
+
readonly conversationKind: string | undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* May the NON-MENTION arm of the seam reach this delivery at all? Read BEFORE any registry read, by
|
|
138
|
+
* both legs, so the two never disagree about which comments the native leg owns.
|
|
139
|
+
*
|
|
140
|
+
* TWO FLOORS. Each refuses on an unknown rather than admitting, and each is about REACH — whether
|
|
141
|
+
* this kind of delivery may wake at all — never about whether the container is engaged.
|
|
142
|
+
*
|
|
143
|
+
* • 🔴 THE REPO MUST BE POSITIVELY PRIVATE. An engaged container's plain comment wakes a FULL
|
|
144
|
+
* native turn: a sandbox checkout and a judged spend. On a public repo that lets ANY GitHub
|
|
145
|
+
* account buy one, by writing in an issue somebody once @mentioned the app in — the same
|
|
146
|
+
* drive-by spend and prompt-injection surface `classifyGitHubAmbientComment` step 5 refuses for,
|
|
147
|
+
* on the louder of the two legs. 🔴 THIS FLOOR IS STRICTER THAN THAT ONE, deliberately: step 5
|
|
148
|
+
* admits a VOUCHED public repo, and this one refuses every public repo. A vouch widens the
|
|
149
|
+
* ambient supervisor's judged reply; widening an uninvited NATIVE turn to a public repo is a
|
|
150
|
+
* bigger step that needs its own ruling.
|
|
151
|
+
* • 🔴 A REVIEW THREAD IS NOT A CONTAINER OF ITS OWN. {@link githubEngagementAnchor} collapses to
|
|
152
|
+
* `owner/repo#N`, so every INLINE review comment on a pull request reads the row that one
|
|
153
|
+
* @mention anywhere on that pull request engaged — and one review leaves dozens of them.
|
|
154
|
+
*
|
|
155
|
+
* AN @MENTION IS UNAFFECTED BY BOTH, on a public repo and inside a review thread alike: it is
|
|
156
|
+
* answered by the caller's own mention gate, which this seam never narrows.
|
|
157
|
+
*/
|
|
158
|
+
export function githubEngagedFollowUpReach(f: GitHubSeamFacts): { readonly reachable: boolean; readonly reason: string } {
|
|
159
|
+
if (f.isPrivate !== true) {
|
|
160
|
+
return { reachable: false, reason: "the repo is public or of unknown privacy — an engaged follow-up wakes a native turn, which is private-repo only" };
|
|
161
|
+
}
|
|
162
|
+
if (f.conversationKind === "review_thread") {
|
|
163
|
+
return { reachable: false, reason: "an inline review comment shares its pull request's anchor — a review thread is not a container of its own" };
|
|
164
|
+
}
|
|
165
|
+
return { reachable: true, reason: "a private repo, and not an inline review comment" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* What the seam says about one inbound comment. Four outcomes, each naming what the CALLER owes:
|
|
170
|
+
*
|
|
171
|
+
* • `unanchored` — the delivery names no container, so there is no row to address and the caller
|
|
172
|
+
* keeps today's path exactly.
|
|
173
|
+
* • `engage` — an @mention. The caller marks the container engaged ({@link markGitHubEngaged}); the
|
|
174
|
+
* mention itself is answered by the caller's own path, never by the seam.
|
|
175
|
+
* • `deliver` — a plain comment in an engaged container. The caller wakes a turn.
|
|
176
|
+
* • `hold` — a plain comment the seam does not admit. `stopped` says WHY, and it is the one field a
|
|
177
|
+
* caller must branch on: `stopped: true` means the agent was asked to stand down in this
|
|
178
|
+
* container, so the comment must be SUPPRESSED outright — including on the ambient leg, which
|
|
179
|
+
* does not otherwise read this registry. `stopped: false` means the container simply is not
|
|
180
|
+
* engaged, which is not a suppression: the caller keeps today's path.
|
|
181
|
+
*/
|
|
182
|
+
export type GitHubEngagementVerdict =
|
|
183
|
+
| { readonly kind: "unanchored"; readonly reason: string }
|
|
184
|
+
| { readonly kind: "engage"; readonly anchor: string }
|
|
185
|
+
| { readonly kind: "deliver"; readonly anchor: string; readonly reason: string }
|
|
186
|
+
| { readonly kind: "hold"; readonly anchor: string; readonly reason: string; readonly stopped: boolean };
|
|
187
|
+
|
|
188
|
+
/** Injectable seams so the whole verdict is unit-tested without a store. */
|
|
189
|
+
export interface GitHubEngagementDeps {
|
|
190
|
+
readonly getEngagement?: (p: Partition, anchor: string) => Promise<EngagementRow | null>;
|
|
191
|
+
readonly markEngaged?: (p: Partition, anchor: string, atIso: string) => Promise<void>;
|
|
192
|
+
/** The mark's durable once-guard — see {@link markGitHubEngaged}. Injected so a test can drive a
|
|
193
|
+
* redelivered @mention without an operational store. */
|
|
194
|
+
readonly markEventSeen?: (org: string, key: string, atIso: string) => Promise<boolean>;
|
|
195
|
+
readonly now?: () => string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resolve the seam for one comment — READ-ONLY. The mark is a separate call
|
|
200
|
+
* ({@link markGitHubEngaged}) so exactly one caller writes the row: two writers is how one comment
|
|
201
|
+
* comes to engage a container twice, once on each leg.
|
|
202
|
+
*
|
|
203
|
+
* ORDERING. A mention is settled before any read: it is always answered by the caller's own path, so
|
|
204
|
+
* its verdict costs no store round-trip. Only a plain comment pays for a read, and it pays for
|
|
205
|
+
* exactly one — the ROW, not a boolean, because the row carries both facts the decision needs (is
|
|
206
|
+
* this engaged, and was the agent asked to stand down here) and two reads could disagree.
|
|
207
|
+
*
|
|
208
|
+
* THE DECISION ITSELF IS CORE'S. `shouldDeliverReply` owns the disqualifier order and the reasons;
|
|
209
|
+
* this function only translates GitHub facts into its input. Nothing here re-states the policy.
|
|
210
|
+
*
|
|
211
|
+
* FAIL-CLOSED on a store error: `getThreadEngagement` reads a failure as `null`, so an unreadable
|
|
212
|
+
* registry holds rather than delivers — the agent does not barge into a conversation it cannot prove
|
|
213
|
+
* it was invited to.
|
|
214
|
+
*/
|
|
215
|
+
export async function resolveGitHubEngagement(f: GitHubSeamFacts, deps: GitHubEngagementDeps = {}): Promise<GitHubEngagementVerdict> {
|
|
216
|
+
const anchor = githubEngagementAnchor(f.owner, f.repo, f.number);
|
|
217
|
+
if (!anchor) return { kind: "unanchored", reason: "the delivery names no issue or pull-request number — no row to address" };
|
|
218
|
+
const partition = githubSeamPartition(f);
|
|
219
|
+
if (!partition) return { kind: "unanchored", reason: "the delivery has no resolvable partition — no row to address" };
|
|
220
|
+
|
|
221
|
+
if (f.isMention) return { kind: "engage", anchor };
|
|
222
|
+
|
|
223
|
+
const read = deps.getEngagement ?? ((p, key) => getThreadEngagement(p, key));
|
|
224
|
+
const row = await read(partition, anchor);
|
|
225
|
+
const decision = shouldDeliverReply({
|
|
226
|
+
isEngaged: row !== null,
|
|
227
|
+
stopped: row?.stopped ?? false,
|
|
228
|
+
authorIsBot: f.authorIsBot,
|
|
229
|
+
isAppMention: false, // settled above — a mention never reaches this branch
|
|
230
|
+
inThread: true, // an anchored GitHub container IS the thread engagement is kept per
|
|
231
|
+
});
|
|
232
|
+
if (decision.deliver) return { kind: "deliver", anchor, reason: decision.reason };
|
|
233
|
+
return { kind: "hold", anchor, reason: decision.reason, stopped: row?.stopped === true };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Mark one container ENGAGED on an @mention, ONCE PER COMMENT.
|
|
238
|
+
*
|
|
239
|
+
* `participating: true` is the re-invitation: it promotes the row and CLEARS a previous stand-down,
|
|
240
|
+
* which is what "an @mention brings the agent back" means on every surface that wires this seam.
|
|
241
|
+
*
|
|
242
|
+
* 🔴 THE MARK IS DEDUPED, AND THE STOP IS WHY. Clearing the flag is right for a person's second
|
|
243
|
+
* @mention and WRONG for GitHub redelivering the first one — a redelivered webhook would quietly
|
|
244
|
+
* lift a stand-down nobody asked to lift. `commentId` is the comment's own id, so the guard keys on
|
|
245
|
+
* the thing that must happen once. It is the DURABLE layer only, unlike Teams' front: this route
|
|
246
|
+
* holds no in-process dedupe object, and a serverless invocation is a fresh process, so an
|
|
247
|
+
* in-memory layer would catch almost nothing here. FAIL-OPEN on a store error, the same posture
|
|
248
|
+
* every dedupe in this repository takes — a dedupe prevents a double write, not a security hole.
|
|
249
|
+
*
|
|
250
|
+
* Best-effort throughout — `markThreadEngaged` swallows a store failure, because engagement
|
|
251
|
+
* bookkeeping must never fail a turn. Returns the anchor it marked, or null when it marked nothing.
|
|
252
|
+
*/
|
|
253
|
+
export async function markGitHubEngaged(f: GitHubSeamFacts, commentId: string | number | undefined, deps: GitHubEngagementDeps = {}): Promise<string | null> {
|
|
254
|
+
const anchor = githubEngagementAnchor(f.owner, f.repo, f.number);
|
|
255
|
+
if (!anchor) return null;
|
|
256
|
+
const partition = githubSeamPartition(f);
|
|
257
|
+
if (!partition) return null;
|
|
258
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
259
|
+
const atIso = now();
|
|
260
|
+
if (commentId !== undefined) {
|
|
261
|
+
const seen = deps.markEventSeen ?? liveMarkEngageSeen;
|
|
262
|
+
if (!(await seen(partition.org, `github-engage:${anchor}:${String(commentId)}`, atIso))) return null;
|
|
263
|
+
}
|
|
264
|
+
const mark = deps.markEngaged ?? ((p, key, at) => markThreadEngaged(p, key, at, { participating: true }));
|
|
265
|
+
await mark(partition, anchor, atIso);
|
|
266
|
+
return anchor;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The durable half of the mark dedupe. FAIL-OPEN: an unreachable store answers "not seen", so a
|
|
270
|
+
* store outage costs a redundant mark rather than a lost engagement. */
|
|
271
|
+
const ENGAGE_DEDUPE_TTL_MS = 10 * 60_000;
|
|
272
|
+
let warnedEngageDedupe = false;
|
|
273
|
+
async function liveMarkEngageSeen(org: string, key: string, atIso: string): Promise<boolean> {
|
|
274
|
+
try {
|
|
275
|
+
return await getOperationalStore().markEventSeen(org, key, atIso, ENGAGE_DEDUPE_TTL_MS);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (!warnedEngageDedupe) {
|
|
278
|
+
warnedEngageDedupe = true;
|
|
279
|
+
console.warn(`[open-everywhere:github-engagement] engage dedupe unavailable — a redelivered @mention may re-mark: ${String(err)}`);
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Is this comment MACHINE-AUTHORED — a GitHub App, a bot account, or this deployment's own reply?
|
|
287
|
+
*
|
|
288
|
+
* 🔴 ONE PREDICATE, AND IT MUST FAIL CLOSED ON AN UNRESOLVED SLUG. `githubBotMentioned` needs the
|
|
289
|
+
* app slug to recognise a mention at all, so before this seam a deployment with no `GITHUB_APP_SLUG`
|
|
290
|
+
* simply never woke natively. A plain comment now reaches the seam, and without the slug the app
|
|
291
|
+
* cannot tell its OWN reply from a person's — so "unknown" answers `true` and nothing is delivered,
|
|
292
|
+
* the same posture `classifyGitHubAmbientComment` takes with its own no-slug drop.
|
|
293
|
+
*/
|
|
294
|
+
export function githubMachineAuthored(authorLogin: string | undefined, authorType: string | undefined, botSlug: string | undefined): boolean {
|
|
295
|
+
const slug = botSlug?.trim();
|
|
296
|
+
if (!slug) return true; // cannot identify self ⇒ cannot prove a human wrote this
|
|
297
|
+
if (authorType === "Bot") return true;
|
|
298
|
+
return authorLogin !== undefined && authorLogin.toLowerCase() === `${slug}[bot]`.toLowerCase();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The NATIVE leg's one-line admission (`github.ts`'s mention gate): may this plain, non-mention
|
|
303
|
+
* comment run a turn?
|
|
304
|
+
*
|
|
305
|
+
* It exists because the native channel is where a GitHub turn is minted — the wake is the webhook
|
|
306
|
+
* itself, so admitting the comment through the mention gate IS the wake. The ambient leg owns the
|
|
307
|
+
* complementary halves above it (the STOP suppression, and dropping a comment this leg now owns), so
|
|
308
|
+
* this call only ever answers the delivery question, and it never writes.
|
|
309
|
+
*
|
|
310
|
+
* 🔴 THE REACH FLOORS RUN FIRST, above the registry read: a public repo and an inline review comment
|
|
311
|
+
* are refused whatever the row says ({@link githubEngagedFollowUpReach}). Putting them here rather
|
|
312
|
+
* than in {@link resolveGitHubEngagement} is deliberate — the ambient leg reads that function for the
|
|
313
|
+
* STOP, and a stood-down container must stay suppressed on a public repo too.
|
|
314
|
+
*/
|
|
315
|
+
export async function githubEngagedAdmits(f: GitHubSeamFacts, deps: GitHubEngagementDeps = {}): Promise<boolean> {
|
|
316
|
+
if (f.isMention) return true; // the caller's own mention gate already admitted it
|
|
317
|
+
if (!githubEngagedFollowUpReach(f).reachable) return false;
|
|
318
|
+
const verdict = await resolveGitHubEngagement(f, deps);
|
|
319
|
+
return verdict.kind === "deliver";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* WHAT WOKE ONE AMBIENT-LEG DELIVERY, for the `github.dispatched` audit row (OPE-1752).
|
|
324
|
+
*
|
|
325
|
+
* 🔴 THE CLASSIFIER'S OWN WORD CANNOT ANSWER THIS, AND WRITING IT WAS THE DEFECT.
|
|
326
|
+
* `classifyGitHubAmbientComment` answers a narrower question — did this comment ADDRESS the agent —
|
|
327
|
+
* and an engaged follow-up did not, so it stays `channel-watch`. That is the right answer to its own
|
|
328
|
+
* question, and `channel-watch` is what makes the turn a JUDGED one. But `channel-watch` ALSO names
|
|
329
|
+
* unowned chatter in an un-enrolled issue, and the audit row is the one place those two must not
|
|
330
|
+
* read alike. This is the same promotion OPE-1510 made on Notion (`notionWakeAddressing`) and
|
|
331
|
+
* OPE-1578 made on Linear (`linearWakeAddressing`) — one concept, one spelling, across three
|
|
332
|
+
* surfaces.
|
|
333
|
+
*
|
|
334
|
+
* 🔴 THE MAPPING IS THE ROW'S ALONE, and nothing else has to be re-derived from it. The classifier's
|
|
335
|
+
* value reaches five places in `github.ts`, and TWO of them are behavioural: the `drop` return, the
|
|
336
|
+
* `githubAmbientLegAdmission` gate input, the seam facts' `isMention`, `directMention` — which is
|
|
337
|
+
* what the wake envelope and `singleWindowMentionPolicy` are built from — and this row. The gate
|
|
338
|
+
* input and `directMention` are the behavioural two, and NEITHER reads the PROMOTED value: both take
|
|
339
|
+
* the classifier's word directly, above this mapping. `github-supervisor.ts` derives the wake
|
|
340
|
+
* envelope's own `addressing`
|
|
341
|
+
* from `directMention` (`directMention ? "direct-mention" : "channel-watch"`), and the
|
|
342
|
+
* unified-window wake takes `singleWindowMentionPolicy(singleWindow, directMention, vouched)`.
|
|
343
|
+
* Neither reads this value. So the turn is judged the same, and the SILENCE BAR is what it always
|
|
344
|
+
* was, whatever this function returns.
|
|
345
|
+
*
|
|
346
|
+
* 🔴 A ROW THAT CANNOT TELL THEM APART REDS `J-ENGAGED-github` STEP (b) AGAINST A HEALTHY
|
|
347
|
+
* DEPLOYMENT. `gateEngagedWake` (packages/core/scripts/e2e/scenarios.ts) skips on an EMPTY dispatch
|
|
348
|
+
* read only, so a non-empty read carrying `channel-watch` is a hard gate failure, not an ignored
|
|
349
|
+
* value. Measured on deploy #203: the follow-up woke, dispatched and was ANSWERED (`reply.post`,
|
|
350
|
+
* 2026-09-12T20:27:25Z), and only the label disagreed. What the promotion DOES oblige is every
|
|
351
|
+
* reader that filtered the ambient leg by the literal `"channel-watch"` — `AMBIENT_LEG_ADDRESSINGS`
|
|
352
|
+
* (packages/core/scripts/e2e/engaged-rows.ts) is the pair such a reader takes.
|
|
353
|
+
*
|
|
354
|
+
* 🔴 IT REPORTS THE ENGAGED DOOR, AND THAT DOOR CARRIES A REACH FLOOR LINEAR'S DOES NOT. Linear's
|
|
355
|
+
* input is the bare seam verdict; GitHub's is `admission.engagedFollowUp`, which is
|
|
356
|
+
* `seam.kind === "deliver" && githubEngagedFollowUpReach(f).reachable`. So the promotion needs BOTH
|
|
357
|
+
* an enrolled container and a reachable one, and {@link githubEngagedFollowUpReach} refuses a public
|
|
358
|
+
* or unknown-privacy repo and an inline review comment. An engaged issue on a VOUCHED PUBLIC repo
|
|
359
|
+
* with ambient ON therefore still writes `channel-watch`: the classifier keeps that comment, but the
|
|
360
|
+
* engaged door is shut and the native leg would not have woken a turn for it either. That is the
|
|
361
|
+
* right answer — the row reports the door the delivery actually came through — and it is a narrower
|
|
362
|
+
* claim than Linear's, so do not read the two mappings as identical in reach.
|
|
363
|
+
*
|
|
364
|
+
* 🔴 IT REPORTS THE REGISTRY, NOT THE AMBIENT OPT-IN. `githubAmbientLegAdmission` resolves the seam
|
|
365
|
+
* for every kept comment, ABOVE the `githubAmbientEnabled` gate and independently of it, so on an
|
|
366
|
+
* ambient-ENABLED private repo an engaged issue's plain comment also reads `thread-follow` — the
|
|
367
|
+
* opt-in would have admitted it either way. The value therefore separates an engaged wake from an
|
|
368
|
+
* ambient one only where the domain has ambient OFF, which is the addressed-only posture the
|
|
369
|
+
* journeys drive.
|
|
370
|
+
*
|
|
371
|
+
* `engagedFollowUp` is the caller's own admission verdict, and it is the only door a plain comment
|
|
372
|
+
* has on a repo that never opted in. The `channel-watch` guard is belt-and-braces:
|
|
373
|
+
* `drop` returns before the row is written and can never reach here, and `direct` is an address
|
|
374
|
+
* rather than a follow-up.
|
|
375
|
+
*/
|
|
376
|
+
export function githubWakeAddressing(
|
|
377
|
+
addressing: GitHubAmbientAddressing,
|
|
378
|
+
engagedFollowUp: boolean | undefined,
|
|
379
|
+
): GitHubAmbientAddressing | "thread-follow" {
|
|
380
|
+
return addressing === "channel-watch" && engagedFollowUp === true ? "thread-follow" : addressing;
|
|
381
|
+
}
|