@snappedly-tools/shipyard 0.7.0 → 0.9.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.
Files changed (63) hide show
  1. package/README.md +46 -43
  2. package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
  3. package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
  4. package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
  5. package/dist/chunk-57EEKW3R.js.map +1 -0
  6. package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
  7. package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
  8. package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
  9. package/dist/chunk-JZUBT4WG.js.map +1 -0
  10. package/dist/chunk-NQRFVKCU.js +1840 -0
  11. package/dist/chunk-NQRFVKCU.js.map +1 -0
  12. package/dist/chunk-Z5C4LHVP.js +137 -0
  13. package/dist/chunk-Z5C4LHVP.js.map +1 -0
  14. package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
  15. package/dist/index.d.ts +99 -2606
  16. package/dist/index.js +412 -7487
  17. package/dist/index.js.map +1 -1
  18. package/dist/integrations/github.d.ts +52 -0
  19. package/dist/integrations/github.js +1049 -0
  20. package/dist/integrations/github.js.map +1 -0
  21. package/dist/integrations/releases.d.ts +136 -0
  22. package/dist/integrations/releases.js +500 -0
  23. package/dist/integrations/releases.js.map +1 -0
  24. package/dist/main.js +826 -472
  25. package/dist/main.js.map +1 -1
  26. package/dist/publication-BPoy_M9M.d.ts +1200 -0
  27. package/dist/sandboxes/docker.d.ts +2 -3
  28. package/dist/sandboxes/docker.js +2 -4
  29. package/dist/templates/parallel-planner/main.mts +78 -16
  30. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  31. package/dist/templates/parallel-planner/setup.sh +1 -0
  32. package/dist/templates/parallel-planner-with-review/main.mts +81 -19
  33. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  34. package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
  35. package/dist/templates/sequential-reviewer/main.mts +59 -6
  36. package/dist/templates/sequential-reviewer/setup.sh +1 -0
  37. package/dist/templates/shared/setup.sh +1 -0
  38. package/dist/templates/simple-loop/main.mts +58 -5
  39. package/dist/templates/simple-loop/setup.sh +1 -0
  40. package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
  41. package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
  42. package/dist/workflow.d.ts +472 -0
  43. package/dist/workflow.js +4345 -0
  44. package/dist/workflow.js.map +1 -0
  45. package/package.json +11 -15
  46. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  47. package/dist/chunk-ACD46ZM4.js +0 -136
  48. package/dist/chunk-ACD46ZM4.js.map +0 -1
  49. package/dist/chunk-FDYOTN55.js.map +0 -1
  50. package/dist/chunk-KMGNFXKN.js +0 -38
  51. package/dist/chunk-KMGNFXKN.js.map +0 -1
  52. package/dist/chunk-SOJTAJTF.js +0 -78
  53. package/dist/chunk-SOJTAJTF.js.map +0 -1
  54. package/dist/chunk-WAXEMZUV.js.map +0 -1
  55. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  56. package/dist/sandboxes/no-sandbox.js +0 -4
  57. package/dist/sandboxes/no-sandbox.js.map +0 -1
  58. package/dist/sandboxes/vercel.d.ts +0 -104
  59. package/dist/sandboxes/vercel.js +0 -166
  60. package/dist/sandboxes/vercel.js.map +0 -1
  61. package/dist/templates/blank/main.mts +0 -13
  62. package/dist/templates/blank/prompt.md +0 -12
  63. package/dist/templates/blank/template.json +0 -4
@@ -0,0 +1,1049 @@
1
+ import { createWorkBrief, parseRepositoryPolicy, processHumanReviewDecision, runTriage, parseWorkBrief } from '../chunk-NQRFVKCU.js';
2
+ import { createHmac, timingSafeEqual, createHash } from 'crypto';
3
+
4
+ var asBytes = (value) => typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
5
+ var verifyGitHubWebhookSignature = (input) => {
6
+ const expected = createHmac("sha256", asBytes(input.secret)).update(asBytes(input.body)).digest();
7
+ const signature = input.signature ?? "";
8
+ const prefix = "sha256=";
9
+ const encoded = signature.startsWith(prefix) ? signature.slice(prefix.length) : "";
10
+ const isHexDigest = /^[0-9a-f]{64}$/i.test(encoded);
11
+ const candidate = isHexDigest ? Buffer.from(encoded, "hex") : Buffer.alloc(expected.length);
12
+ return timingSafeEqual(expected, candidate) && isHexDigest;
13
+ };
14
+
15
+ // src/integrations/github/store.ts
16
+ var InMemoryGitHubStore = class {
17
+ deliveries = /* @__PURE__ */ new Map();
18
+ trackedPullRequests = /* @__PURE__ */ new Map();
19
+ async recordDeliveryIfAbsent(delivery) {
20
+ const existing = this.deliveries.get(delivery.deliveryId);
21
+ if (existing !== void 0) {
22
+ return { delivery: existing, inserted: false };
23
+ }
24
+ this.deliveries.set(delivery.deliveryId, delivery);
25
+ return { delivery, inserted: true };
26
+ }
27
+ async getDelivery(deliveryId) {
28
+ return this.deliveries.get(deliveryId);
29
+ }
30
+ async updateDelivery(delivery) {
31
+ const existing = this.deliveries.get(delivery.deliveryId);
32
+ if (existing === void 0) {
33
+ throw new Error(`GitHub delivery ${delivery.deliveryId} does not exist`);
34
+ }
35
+ this.deliveries.set(delivery.deliveryId, delivery);
36
+ }
37
+ async findTrackedPullRequest(repository, pullRequestNumber2) {
38
+ return this.trackedPullRequests.get(
39
+ `${repository.toLowerCase()}\0${pullRequestNumber2}`
40
+ );
41
+ }
42
+ async saveTrackedPullRequest(pullRequest) {
43
+ this.trackedPullRequests.set(
44
+ `${pullRequest.repository.toLowerCase()}\0${pullRequest.pullRequestNumber}`,
45
+ pullRequest
46
+ );
47
+ }
48
+ };
49
+
50
+ // src/integrations/github/integration.ts
51
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
52
+ var record = (value) => isRecord(value) ? value : void 0;
53
+ var stringValue = (value, fallback = "") => typeof value === "string" ? value : fallback;
54
+ var requiredStringValue = (value) => {
55
+ const result2 = stringValue(value).trim();
56
+ return result2.length === 0 ? void 0 : result2;
57
+ };
58
+ var numberValue = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
59
+ var header = (headers, name) => {
60
+ const wanted = name.toLowerCase();
61
+ const key = Object.keys(headers).find(
62
+ (candidate) => candidate.toLowerCase() === wanted
63
+ );
64
+ return key === void 0 ? void 0 : headers[key];
65
+ };
66
+ var bodyBytes = (body) => typeof body === "string" ? new TextEncoder().encode(body) : body;
67
+ var bodyText = (body) => typeof body === "string" ? body : new TextDecoder().decode(body);
68
+ var payloadHash = (body) => createHash("sha256").update(bodyBytes(body)).digest("hex");
69
+ var MAX_WEBHOOK_BODY_BYTES = 10 * 1024 * 1024;
70
+ var parseBody = (body) => {
71
+ try {
72
+ return JSON.parse(body);
73
+ } catch {
74
+ return void 0;
75
+ }
76
+ };
77
+ var actor = (value) => {
78
+ const candidate = record(value);
79
+ return {
80
+ login: stringValue(candidate?.login, "unknown"),
81
+ type: stringValue(candidate?.type) || void 0
82
+ };
83
+ };
84
+ var repositoryName = (payload) => requiredStringValue(record(payload.repository)?.full_name);
85
+ var sender = (payload) => actor(payload.sender ?? record(payload.comment)?.user ?? void 0);
86
+ var labels = (value) => {
87
+ if (!Array.isArray(value)) return [];
88
+ return value.map(
89
+ (entry) => typeof entry === "string" ? entry : stringValue(record(entry)?.name)
90
+ ).filter((entry) => entry.length > 0);
91
+ };
92
+ var state = (value) => value === "closed" ? "closed" : "open";
93
+ var allowedEventNames = /* @__PURE__ */ new Set([
94
+ "issues",
95
+ "issue_comment",
96
+ "pull_request",
97
+ "pull_request_review",
98
+ "pull_request_review_comment",
99
+ "check_run",
100
+ "check_suite"
101
+ ]);
102
+ var supportedActions = {
103
+ issues: ["opened", "edited", "reopened", "closed"],
104
+ issue_comment: ["created", "edited"],
105
+ pull_request: [
106
+ "opened",
107
+ "edited",
108
+ "reopened",
109
+ "closed",
110
+ "synchronize",
111
+ "ready_for_review",
112
+ "converted_to_draft"
113
+ ],
114
+ pull_request_review: ["submitted", "edited", "dismissed"],
115
+ pull_request_review_comment: ["created", "edited", "deleted"],
116
+ check_run: ["created", "rerequested", "completed"],
117
+ check_suite: ["completed", "requested", "rerequested"]
118
+ };
119
+ var ignored = (input) => ({ disposition: "ignored", ...input });
120
+ var issueSnapshot = (payload) => {
121
+ const value = record(payload.issue);
122
+ const number = numberValue(value?.number);
123
+ if (value === void 0 || number === void 0) return void 0;
124
+ return {
125
+ number,
126
+ title: stringValue(value.title, "Untitled issue"),
127
+ body: stringValue(value.body),
128
+ state: state(value.state),
129
+ updatedAt: stringValue(value.updated_at),
130
+ htmlUrl: requiredStringValue(value.html_url),
131
+ authorLogin: requiredStringValue(record(value.user)?.login),
132
+ labels: labels(value.labels),
133
+ pullRequestNumber: numberValue(record(value.pull_request)?.number)
134
+ };
135
+ };
136
+ var commentSnapshot = (payload) => {
137
+ const value = record(payload.comment);
138
+ const id = requiredStringValue(value?.id);
139
+ if (value === void 0 || id === void 0) return void 0;
140
+ return {
141
+ id,
142
+ body: stringValue(value.body),
143
+ updatedAt: stringValue(value.updated_at),
144
+ htmlUrl: requiredStringValue(value.html_url),
145
+ authorLogin: requiredStringValue(record(value.user)?.login)
146
+ };
147
+ };
148
+ var reviewState = (value) => {
149
+ const normalized = requiredStringValue(value)?.toLowerCase().replaceAll("_", "-");
150
+ switch (normalized) {
151
+ case "approved":
152
+ case "changes-requested":
153
+ case "commented":
154
+ case "dismissed":
155
+ case "pending":
156
+ return normalized;
157
+ default:
158
+ return void 0;
159
+ }
160
+ };
161
+ var reviewSnapshot = (payload) => {
162
+ const value = record(payload.review);
163
+ if (value === void 0) return void 0;
164
+ const rawId = value.id;
165
+ const id = typeof rawId === "number" && Number.isInteger(rawId) && rawId > 0 ? String(rawId) : requiredStringValue(rawId);
166
+ const state2 = reviewState(value.state);
167
+ const pullRequest = record(payload.pull_request);
168
+ const headSha = requiredStringValue(value.commit_id) ?? requiredStringValue(record(pullRequest?.head)?.sha);
169
+ const submittedAt = requiredStringValue(value.submitted_at) ?? requiredStringValue(value.updated_at) ?? requiredStringValue(value.created_at);
170
+ if (id === void 0 || state2 === void 0 || headSha === void 0 || submittedAt === void 0) {
171
+ return void 0;
172
+ }
173
+ return {
174
+ id,
175
+ state: state2,
176
+ headSha,
177
+ submittedAt,
178
+ authorLogin: requiredStringValue(record(value.user)?.login)
179
+ };
180
+ };
181
+ var reviewDecisionFor = (review) => {
182
+ switch (review.state) {
183
+ case "approved":
184
+ return { decision: "approved" };
185
+ case "changes-requested":
186
+ return { decision: "changes-requested" };
187
+ case "dismissed":
188
+ return { decision: "rejected", reason: "GitHub review was dismissed" };
189
+ case "commented":
190
+ case "pending":
191
+ return void 0;
192
+ }
193
+ };
194
+ var pullRequestNumber = (payload) => {
195
+ const direct = numberValue(record(payload.pull_request)?.number);
196
+ if (direct !== void 0) return direct;
197
+ const issue = numberValue(record(payload.issue)?.number);
198
+ if (issue !== void 0 && record(payload.issue)?.pull_request !== void 0) {
199
+ return issue;
200
+ }
201
+ const check = record(payload.check_run) ?? record(payload.check_suite);
202
+ const pullRequests = check?.pull_requests;
203
+ if (Array.isArray(pullRequests)) {
204
+ return numberValue(record(pullRequests[0])?.number);
205
+ }
206
+ return void 0;
207
+ };
208
+ var pullRequestHead = (payload, fallback) => requiredStringValue(record(record(payload.pull_request)?.head)?.sha) ?? requiredStringValue(record(payload.check_run)?.head_sha) ?? requiredStringValue(record(payload.check_suite)?.head_sha) ?? fallback;
209
+ var issueKind = (issue) => {
210
+ const normalized = new Set(issue.labels.map((label) => label.toLowerCase()));
211
+ if (normalized.has("planning-spec") || normalized.has("planning")) {
212
+ return "planning-spec";
213
+ }
214
+ if (normalized.has("pr-repair") || normalized.has("repair")) {
215
+ return "pr-repair";
216
+ }
217
+ return "executable-issue";
218
+ };
219
+ var sameIssueContent = (brief, issue) => (brief.problem === `${issue.title}
220
+
221
+ ${issue.body || "(empty issue body)"}` || brief.problem === issue.title) && brief.source.originalBody === (issue.body || "(empty issue body)");
222
+ var createGitHubIssueBrief = (input) => {
223
+ const issue = input.event;
224
+ return createWorkBrief({
225
+ id: `${issue.repository}:${issue.kind}:${issue.issueNumber}`,
226
+ revision: input.revision,
227
+ identity: {
228
+ repository: issue.repository,
229
+ itemId: String(issue.issueNumber),
230
+ kind: input.itemKind
231
+ },
232
+ source: {
233
+ provider: "github",
234
+ repository: issue.repository,
235
+ itemId: String(issue.issueNumber),
236
+ url: issue.reply?.htmlUrl ?? issue.trackedPullRequest?.brief.source.url,
237
+ originalBody: issue.body || "(empty issue body)",
238
+ author: issue.reply?.authorLogin
239
+ },
240
+ problem: issue.title + "\n\n" + (issue.body || "(empty issue body)"),
241
+ evidence: ["Submitted through authenticated GitHub intake."],
242
+ acceptanceCriteria: input.defaults?.acceptanceCriteria ?? [],
243
+ exclusions: input.defaults?.exclusions ?? [],
244
+ risk: input.defaults?.risk ?? "medium",
245
+ verification: {
246
+ checks: input.policy.checks.map((check) => check.command),
247
+ artifacts: input.defaults?.verificationArtifacts ?? []
248
+ },
249
+ unresolvedQuestions: input.defaults?.unresolvedQuestions ?? [
250
+ "A maintainer must confirm acceptance criteria and execution authorization."
251
+ ],
252
+ authorization: { status: "pending" },
253
+ base: input.base,
254
+ policyRevision: input.policy.revision,
255
+ skillRevision: input.policy.worker.skillRevision,
256
+ createdAt: issue.observedAt
257
+ });
258
+ };
259
+ var eventName = (value) => allowedEventNames.has(value) ? value : void 0;
260
+ var GitHubIntegration = class {
261
+ options;
262
+ now;
263
+ constructor(options) {
264
+ this.options = {
265
+ ...options,
266
+ policy: parseRepositoryPolicy(options.policy)
267
+ };
268
+ this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
269
+ }
270
+ async receiveWebhook(request) {
271
+ const deliveryId = header(request.headers, "x-github-delivery");
272
+ const eventHeader = header(request.headers, "x-github-event");
273
+ if (deliveryId === void 0 || eventHeader === void 0) {
274
+ return {
275
+ status: "rejected",
276
+ deliveryId: deliveryId ?? "unknown",
277
+ reason: "missing-delivery-or-event-header"
278
+ };
279
+ }
280
+ if (bodyBytes(request.body).byteLength > MAX_WEBHOOK_BODY_BYTES) {
281
+ return { status: "rejected", deliveryId, reason: "body-too-large" };
282
+ }
283
+ const body = bodyText(request.body);
284
+ if (!verifyGitHubWebhookSignature({
285
+ body: request.body,
286
+ signature: header(request.headers, "x-hub-signature-256"),
287
+ secret: this.options.webhookSecret
288
+ })) {
289
+ return { status: "rejected", deliveryId, reason: "invalid-signature" };
290
+ }
291
+ return this.processEnvelope({
292
+ eventName: eventHeader,
293
+ deliveryId,
294
+ payload: parseBody(body),
295
+ receivedAt: this.now(),
296
+ body
297
+ });
298
+ }
299
+ async normalize(envelope) {
300
+ const payload = record(envelope.payload);
301
+ const name = eventName(envelope.eventName);
302
+ const repository = payload ? repositoryName(payload) ?? "unknown" : "unknown";
303
+ const source = payload ? sender(payload) : { login: "unknown" };
304
+ if (name === void 0) {
305
+ return ignored({
306
+ reason: "unsupported-event",
307
+ eventName: envelope.eventName,
308
+ deliveryId: envelope.deliveryId,
309
+ repository,
310
+ sender: source
311
+ });
312
+ }
313
+ const action = stringValue(payload?.action);
314
+ if (!supportedActions[name].includes(action)) {
315
+ return ignored({
316
+ reason: "unsupported-action",
317
+ eventName: name,
318
+ action,
319
+ deliveryId: envelope.deliveryId,
320
+ repository,
321
+ sender: source
322
+ });
323
+ }
324
+ if (payload === void 0) {
325
+ return ignored({
326
+ reason: "unsupported-event",
327
+ eventName: name,
328
+ action,
329
+ deliveryId: envelope.deliveryId,
330
+ repository,
331
+ sender: source
332
+ });
333
+ }
334
+ if (name === "issues" || name === "issue_comment") {
335
+ const issue = issueSnapshot(payload);
336
+ if (issue === void 0) {
337
+ return ignored({
338
+ reason: "unsupported-event",
339
+ eventName: name,
340
+ action,
341
+ deliveryId: envelope.deliveryId,
342
+ repository,
343
+ sender: source
344
+ });
345
+ }
346
+ const issueIsPullRequest = issue.pullRequestNumber !== void 0 || record(record(payload.issue)?.pull_request) !== void 0;
347
+ if (issueIsPullRequest) {
348
+ return this.normalizeTrackedPullRequest(
349
+ envelope,
350
+ payload,
351
+ issue.pullRequestNumber ?? issue.number,
352
+ name === "issue_comment" ? commentSnapshot(payload) : void 0
353
+ );
354
+ }
355
+ const kind = issueKind(issue);
356
+ const draft = {
357
+ kind: name === "issue_comment" ? "issue-replied" : action === "opened" || action === "reopened" ? "issue-created" : "issue-edited",
358
+ eventName: name,
359
+ action,
360
+ deliveryId: envelope.deliveryId,
361
+ repository,
362
+ sender: source,
363
+ issueNumber: issue.number,
364
+ title: issue.title,
365
+ body: issue.body,
366
+ labels: issue.labels,
367
+ relevantRevision: issue.updatedAt || envelope.receivedAt,
368
+ observedAt: issue.updatedAt || envelope.receivedAt,
369
+ sourceState: issue.state,
370
+ reply: name === "issue_comment" ? commentSnapshot(payload) : void 0
371
+ };
372
+ const briefResolution = await this.briefForIssue(draft, issue, kind);
373
+ if ("ignoredReason" in briefResolution) {
374
+ return ignored({
375
+ reason: briefResolution.ignoredReason,
376
+ eventName: envelope.eventName,
377
+ action,
378
+ deliveryId: envelope.deliveryId,
379
+ repository,
380
+ sender: source
381
+ });
382
+ }
383
+ const brief = briefResolution.brief;
384
+ return {
385
+ disposition: "accepted",
386
+ event: {
387
+ ...draft,
388
+ workflowEvent: {
389
+ deliveryId: envelope.deliveryId,
390
+ brief,
391
+ policy: this.options.policy,
392
+ phase: "triage",
393
+ relevantRevision: draft.relevantRevision,
394
+ observedAt: draft.observedAt,
395
+ sourceState: draft.sourceState,
396
+ payload: envelope.payload
397
+ }
398
+ }
399
+ };
400
+ }
401
+ const number = pullRequestNumber(payload);
402
+ if (number === void 0) {
403
+ return ignored({
404
+ reason: "unrelated-pull-request",
405
+ eventName: name,
406
+ action,
407
+ deliveryId: envelope.deliveryId,
408
+ repository,
409
+ sender: source
410
+ });
411
+ }
412
+ const review = name === "pull_request_review" ? reviewSnapshot(payload) : void 0;
413
+ if (name === "pull_request_review" && review === void 0) {
414
+ return ignored({
415
+ reason: "unsupported-event",
416
+ eventName: name,
417
+ action,
418
+ deliveryId: envelope.deliveryId,
419
+ repository,
420
+ sender: source
421
+ });
422
+ }
423
+ return this.normalizeTrackedPullRequest(
424
+ envelope,
425
+ payload,
426
+ number,
427
+ void 0,
428
+ review
429
+ );
430
+ }
431
+ async reconcile(input) {
432
+ const hasIssue = input.issueNumber !== void 0;
433
+ const hasPullRequest = input.pullRequestNumber !== void 0;
434
+ if (hasIssue === hasPullRequest) {
435
+ throw new Error(
436
+ "Reconciliation requires exactly one issue or pull request"
437
+ );
438
+ }
439
+ if (hasIssue) {
440
+ const issue = await input.transport.fetchIssue({
441
+ repository: input.repository,
442
+ issueNumber: input.issueNumber
443
+ });
444
+ if (issue === void 0) {
445
+ return {
446
+ status: "not-found",
447
+ deliveryId: `reconcile:issue:${input.repository}:${input.issueNumber}`
448
+ };
449
+ }
450
+ const payload = this.issuePayload(issue, input.repository);
451
+ return this.toReconciliationResult(
452
+ await this.processEnvelope(
453
+ {
454
+ eventName: "issues",
455
+ deliveryId: `reconcile:issue:${input.repository}:${issue.number}:${issue.updatedAt}`,
456
+ payload,
457
+ receivedAt: this.now()
458
+ },
459
+ true
460
+ )
461
+ );
462
+ }
463
+ const pullRequest = await input.transport.fetchPullRequest({
464
+ repository: input.repository,
465
+ pullRequestNumber: input.pullRequestNumber
466
+ });
467
+ if (pullRequest === void 0) {
468
+ return {
469
+ status: "not-found",
470
+ deliveryId: `reconcile:pull-request:${input.repository}:${input.pullRequestNumber}`
471
+ };
472
+ }
473
+ return this.toReconciliationResult(
474
+ await this.processEnvelope(
475
+ {
476
+ eventName: "pull_request",
477
+ deliveryId: `reconcile:pull-request:${input.repository}:${pullRequest.number}:${pullRequest.headSha}`,
478
+ payload: this.pullRequestPayload(pullRequest, input.repository),
479
+ receivedAt: this.now()
480
+ },
481
+ true
482
+ )
483
+ );
484
+ }
485
+ async processEnvelope(envelope, trustedReconciliation = false) {
486
+ const payload = record(envelope.payload);
487
+ const repository = payload ? repositoryName(payload) ?? "unknown" : "unknown";
488
+ const source = payload ? sender(payload) : { login: "unknown" };
489
+ const body = envelope.body ?? JSON.stringify(envelope.payload);
490
+ const delivery = {
491
+ deliveryId: envelope.deliveryId,
492
+ eventName: envelope.eventName,
493
+ repository,
494
+ senderLogin: source.login,
495
+ receivedAt: envelope.receivedAt,
496
+ payloadHash: payloadHash(body),
497
+ payload: envelope.payload,
498
+ status: "received"
499
+ };
500
+ const stored = await this.options.deliveryStore.recordDeliveryIfAbsent(delivery);
501
+ if (!stored.inserted && stored.delivery.status !== "received") {
502
+ return { status: "duplicate", deliveryId: envelope.deliveryId };
503
+ }
504
+ const effectiveEnvelope = stored.inserted ? envelope : {
505
+ ...envelope,
506
+ payload: stored.delivery.payload,
507
+ receivedAt: stored.delivery.receivedAt,
508
+ body: JSON.stringify(stored.delivery.payload)
509
+ };
510
+ const effectiveDelivery = stored.inserted ? delivery : stored.delivery;
511
+ if (!allowedRepository(
512
+ this.options.authorization,
513
+ effectiveDelivery.repository
514
+ )) {
515
+ return this.rejectStored(effectiveDelivery, "disallowed-repository");
516
+ }
517
+ const effectivePayload = record(effectiveEnvelope.payload);
518
+ const effectiveSource = effectivePayload ? sender(effectivePayload) : { login: "unknown" };
519
+ if (!trustedReconciliation && isBot(effectiveSource, this.options.authorization)) {
520
+ return this.ignoreStored(effectiveDelivery, "bot-originated");
521
+ }
522
+ if (!trustedReconciliation) {
523
+ const isReview = effectiveEnvelope.eventName === "pull_request_review";
524
+ const authorized = isReview ? allowedReviewer(this.options.authorization, effectiveSource) : allowedSender(this.options.authorization, effectiveSource);
525
+ if (!authorized) {
526
+ return this.rejectStored(
527
+ effectiveDelivery,
528
+ isReview ? "disallowed-reviewer" : "disallowed-sender"
529
+ );
530
+ }
531
+ }
532
+ const normalized = await this.normalize(effectiveEnvelope);
533
+ if (normalized.disposition === "ignored") {
534
+ return this.ignoreStored(effectiveDelivery, normalized.reason);
535
+ }
536
+ if (normalized.event.review !== void 0) {
537
+ const tracked = normalized.event.trackedPullRequest;
538
+ const handler = this.options.reviewHandler;
539
+ const decision = reviewDecisionFor(normalized.event.review);
540
+ if (handler === void 0) {
541
+ return this.rejectStored(
542
+ effectiveDelivery,
543
+ "review-handler-unconfigured"
544
+ );
545
+ }
546
+ if (tracked === void 0) {
547
+ return this.rejectStored(
548
+ effectiveDelivery,
549
+ "review-missing-tracked-pull-request"
550
+ );
551
+ }
552
+ if (decision === void 0) {
553
+ return this.ignoreStored(effectiveDelivery, "review-without-decision");
554
+ }
555
+ const candidate = {
556
+ base: tracked.brief.base,
557
+ head: {
558
+ branch: tracked.branch,
559
+ sha: normalized.event.review.headSha
560
+ },
561
+ briefHash: tracked.brief.hash
562
+ };
563
+ let reviewResult;
564
+ try {
565
+ reviewResult = await processHumanReviewDecision({
566
+ candidate,
567
+ decision,
568
+ pullRequestNumber: tracked.pullRequestNumber,
569
+ readCurrent: () => handler.readCurrent({
570
+ candidate,
571
+ trackedPullRequest: tracked
572
+ }),
573
+ requestRepair: handler.requestRepair
574
+ });
575
+ } catch (error) {
576
+ return this.rejectStored(
577
+ effectiveDelivery,
578
+ `review-handler-failed: ${error instanceof Error ? error.message : String(error)}`
579
+ );
580
+ }
581
+ const status = reviewResult.outcome === "blocked" ? "ignored" : "accepted";
582
+ await this.options.deliveryStore.updateDelivery({
583
+ ...effectiveDelivery,
584
+ status,
585
+ eventKind: normalized.event.kind,
586
+ jobId: tracked.jobId,
587
+ reason: reviewResult.reason
588
+ });
589
+ return {
590
+ status,
591
+ deliveryId: envelope.deliveryId,
592
+ event: normalized.event,
593
+ review: reviewResult,
594
+ reason: reviewResult.reason
595
+ };
596
+ }
597
+ const ingest = await this.options.coordinator.ingest(
598
+ normalized.event.workflowEvent
599
+ );
600
+ await this.options.deliveryStore.updateDelivery({
601
+ ...effectiveDelivery,
602
+ status: ingest.disposition === "ignored" ? "ignored" : "accepted",
603
+ eventKind: normalized.event.kind,
604
+ jobId: ingest.job?.id,
605
+ reason: ingest.reason
606
+ });
607
+ return {
608
+ status: ingest.disposition === "duplicate" ? "duplicate" : ingest.disposition === "ignored" ? "ignored" : "accepted",
609
+ deliveryId: envelope.deliveryId,
610
+ event: normalized.event,
611
+ ingest,
612
+ reason: ingest.reason
613
+ };
614
+ }
615
+ async rejectStored(delivery, reason) {
616
+ await this.options.deliveryStore.updateDelivery({
617
+ ...delivery,
618
+ status: "rejected",
619
+ reason
620
+ });
621
+ return { status: "rejected", deliveryId: delivery.deliveryId, reason };
622
+ }
623
+ async ignoreStored(delivery, reason) {
624
+ await this.options.deliveryStore.updateDelivery({
625
+ ...delivery,
626
+ status: "ignored",
627
+ reason
628
+ });
629
+ return { status: "ignored", deliveryId: delivery.deliveryId, reason };
630
+ }
631
+ async briefForIssue(draft, issue, kind) {
632
+ const identity = {
633
+ repository: draft.repository,
634
+ itemId: String(issue.number),
635
+ kind
636
+ };
637
+ const current = await this.options.coordinator.getCurrentJob(identity);
638
+ if (this.options.triage === void 0 && current !== void 0 && current.control === "active" && sameIssueContent(current.brief, issue)) {
639
+ return { brief: current.brief };
640
+ }
641
+ if (this.options.triage !== void 0) {
642
+ const source = {
643
+ provider: "github",
644
+ repository: draft.repository,
645
+ itemId: String(issue.number),
646
+ title: issue.title,
647
+ body: issue.body,
648
+ author: issue.authorLogin ?? draft.sender.login,
649
+ url: issue.htmlUrl,
650
+ updatedAt: issue.updatedAt || draft.observedAt,
651
+ kind,
652
+ labels: issue.labels
653
+ };
654
+ const triage = await runTriage({
655
+ source,
656
+ policy: this.options.policy,
657
+ base: this.options.base,
658
+ store: this.options.triage.store,
659
+ investigator: this.options.triage.investigator,
660
+ clarificationReply: draft.kind === "issue-replied" && draft.reply !== void 0 ? {
661
+ id: draft.reply.id,
662
+ body: draft.reply.body,
663
+ author: draft.reply.authorLogin,
664
+ updatedAt: draft.reply.updatedAt
665
+ } : void 0,
666
+ now: this.now
667
+ });
668
+ if (triage.record.sourceConflict !== void 0) {
669
+ return { ignoredReason: "triage-source-conflict" };
670
+ }
671
+ if (triage.brief !== void 0) {
672
+ if (triage.brief.authorization.status === "approved") {
673
+ return {
674
+ brief: createWorkBrief({
675
+ ...triage.brief,
676
+ authorization: { status: "pending" }
677
+ })
678
+ };
679
+ }
680
+ return { brief: triage.brief };
681
+ }
682
+ return { ignoredReason: "triage-incomplete" };
683
+ }
684
+ const event = draft;
685
+ const revision = current === void 0 ? 1 : current.brief.revision + 1;
686
+ const factory = this.options.briefFactory ?? createGitHubIssueBrief;
687
+ const candidate = factory({
688
+ event,
689
+ itemKind: kind,
690
+ policy: this.options.policy,
691
+ base: this.options.base,
692
+ authorization: { status: "pending" },
693
+ revision,
694
+ defaults: this.options.briefDefaults
695
+ });
696
+ const brief = parseWorkBrief(candidate);
697
+ if (brief.authorization.status !== "pending") {
698
+ throw new Error(
699
+ "GitHub intake cannot authorize work from webhook content"
700
+ );
701
+ }
702
+ return { brief };
703
+ }
704
+ async normalizeTrackedPullRequest(envelope, payload, pullRequestNumberValue, reply, review) {
705
+ const repository = repositoryName(payload) ?? "unknown";
706
+ const source = sender(payload);
707
+ const trackingStore = this.options.trackingStore;
708
+ const tracked = trackingStore ? await trackingStore.findTrackedPullRequest(
709
+ repository,
710
+ pullRequestNumberValue
711
+ ) : void 0;
712
+ if (tracked === void 0) {
713
+ return ignored({
714
+ reason: "unrelated-pull-request",
715
+ eventName: envelope.eventName,
716
+ action: stringValue(payload.action),
717
+ deliveryId: envelope.deliveryId,
718
+ repository,
719
+ sender: source
720
+ });
721
+ }
722
+ const pullRequest = record(payload.pull_request);
723
+ const observedAt = requiredStringValue(pullRequest?.updated_at) ?? requiredStringValue(record(payload.issue)?.updated_at) ?? reply?.updatedAt ?? envelope.receivedAt;
724
+ const headSha = pullRequestHead(payload, tracked.headSha);
725
+ const sourceState = state(pullRequest?.state);
726
+ const event = {
727
+ kind: "tracked-pr-updated",
728
+ eventName: eventName(envelope.eventName) ?? "pull_request",
729
+ action: stringValue(payload.action),
730
+ deliveryId: envelope.deliveryId,
731
+ repository,
732
+ sender: source,
733
+ issueNumber: Number(tracked.itemId) || pullRequestNumberValue,
734
+ pullRequestNumber: pullRequestNumberValue,
735
+ title: stringValue(pullRequest?.title, tracked.brief.problem),
736
+ body: stringValue(pullRequest?.body, tracked.brief.source.originalBody),
737
+ labels: labels(pullRequest?.labels),
738
+ relevantRevision: headSha,
739
+ observedAt,
740
+ sourceState,
741
+ reply,
742
+ review,
743
+ trackedPullRequest: tracked
744
+ };
745
+ return {
746
+ disposition: "accepted",
747
+ event: {
748
+ ...event,
749
+ workflowEvent: {
750
+ deliveryId: envelope.deliveryId,
751
+ brief: tracked.brief,
752
+ policy: tracked.policy,
753
+ phase: "checking",
754
+ relevantRevision: headSha,
755
+ observedAt,
756
+ sourceState,
757
+ payload: envelope.payload
758
+ }
759
+ }
760
+ };
761
+ }
762
+ issuePayload(issue, repository) {
763
+ return {
764
+ action: issue.state === "closed" ? "closed" : "edited",
765
+ issue: {
766
+ number: issue.number,
767
+ title: issue.title,
768
+ body: issue.body,
769
+ state: issue.state,
770
+ updated_at: issue.updatedAt,
771
+ html_url: issue.htmlUrl,
772
+ user: issue.authorLogin ? { login: issue.authorLogin } : void 0,
773
+ labels: issue.labels.map((name) => ({ name })),
774
+ pull_request: issue.pullRequestNumber === void 0 ? void 0 : { number: issue.pullRequestNumber }
775
+ },
776
+ repository: { full_name: repository },
777
+ sender: {
778
+ login: this.options.authorization.allowedSenders[0] ?? "shipyard"
779
+ }
780
+ };
781
+ }
782
+ pullRequestPayload(pullRequest, repository) {
783
+ return {
784
+ action: "synchronize",
785
+ pull_request: {
786
+ number: pullRequest.number,
787
+ title: pullRequest.title,
788
+ body: pullRequest.body,
789
+ state: pullRequest.state,
790
+ updated_at: pullRequest.updatedAt,
791
+ html_url: pullRequest.htmlUrl,
792
+ user: pullRequest.authorLogin ? { login: pullRequest.authorLogin } : void 0,
793
+ head: { ref: pullRequest.branch, sha: pullRequest.headSha },
794
+ base: { ref: pullRequest.baseBranch }
795
+ },
796
+ repository: { full_name: repository },
797
+ sender: {
798
+ login: this.options.authorization.allowedSenders[0] ?? "shipyard"
799
+ }
800
+ };
801
+ }
802
+ toReconciliationResult(receipt) {
803
+ return {
804
+ status: receipt.status === "rejected" ? "ignored" : receipt.status,
805
+ deliveryId: receipt.deliveryId,
806
+ reason: receipt.reason,
807
+ event: receipt.event,
808
+ ingest: receipt.ingest
809
+ };
810
+ }
811
+ };
812
+ var includesCaseInsensitive = (candidates, value) => candidates.some(
813
+ (candidate) => candidate.toLowerCase() === value.toLowerCase()
814
+ );
815
+ var allowedRepository = (policy, repository) => includesCaseInsensitive(policy.allowedRepositories, repository);
816
+ var allowedSender = (policy, source) => includesCaseInsensitive(policy.allowedSenders, source.login);
817
+ var allowedReviewer = (policy, source) => includesCaseInsensitive(policy.allowedReviewers, source.login);
818
+ var isBot = (source, policy) => source.type === "Bot" || includesCaseInsensitive(policy.botLogins ?? [], source.login);
819
+ var markerText = (marker) => `<!-- shipyard:${marker} -->`;
820
+ var markerPart = (value) => encodeURIComponent(String(value));
821
+ var markerHash = (value) => createHash("sha256").update(value).digest("hex").slice(0, 16);
822
+ var result = (marker, execution) => ({
823
+ marker,
824
+ remote: execution.externalRef,
825
+ disposition: execution.disposition,
826
+ effect: execution.effect
827
+ });
828
+ var GitHubPublication = class {
829
+ options;
830
+ constructor(options) {
831
+ this.options = options;
832
+ }
833
+ async publishComment(input) {
834
+ const marker = `comment:${markerPart(input.jobId)}:${markerPart(input.key ?? "default")}`;
835
+ const body = `${markerText(marker)}
836
+ ${input.body}`;
837
+ const execution = await this.options.coordinator.publishEffect({
838
+ jobId: input.jobId,
839
+ lease: input.lease,
840
+ itemId: String(input.issueNumber),
841
+ kind: "github-comment",
842
+ marker,
843
+ payload: {
844
+ repository: input.lease.repository,
845
+ issueNumber: input.issueNumber
846
+ },
847
+ reconcile: () => this.options.transport.findCommentByMarker({
848
+ repository: input.lease.repository,
849
+ issueNumber: input.issueNumber,
850
+ marker: markerText(marker)
851
+ }),
852
+ publish: () => this.options.transport.createComment({
853
+ repository: input.lease.repository,
854
+ issueNumber: input.issueNumber,
855
+ body
856
+ })
857
+ });
858
+ return result(marker, execution);
859
+ }
860
+ async publishBrief(input) {
861
+ const marker = `brief:${markerPart(input.jobId)}:${markerPart(input.brief.revision)}`;
862
+ const body = [
863
+ markerText(marker),
864
+ "## Shipyard executable brief",
865
+ `- Revision: ${input.brief.revision}`,
866
+ `- Brief hash: \`${input.brief.hash}\``,
867
+ `- Risk: ${input.brief.risk}`,
868
+ "",
869
+ input.brief.problem,
870
+ "",
871
+ "### Acceptance criteria",
872
+ ...input.brief.acceptanceCriteria.map((criterion) => `- ${criterion}`),
873
+ "",
874
+ "### Unresolved questions",
875
+ ...input.brief.unresolvedQuestions.map((question) => `- ${question}`)
876
+ ].join("\n");
877
+ const execution = await this.options.coordinator.publishEffect({
878
+ jobId: input.jobId,
879
+ lease: input.lease,
880
+ itemId: String(input.issueNumber),
881
+ kind: "github-brief",
882
+ marker,
883
+ payload: {
884
+ repository: input.lease.repository,
885
+ issueNumber: input.issueNumber
886
+ },
887
+ reconcile: () => this.options.transport.findCommentByMarker({
888
+ repository: input.lease.repository,
889
+ issueNumber: input.issueNumber,
890
+ marker: markerText(marker)
891
+ }),
892
+ publish: () => this.options.transport.createComment({
893
+ repository: input.lease.repository,
894
+ issueNumber: input.issueNumber,
895
+ body
896
+ })
897
+ });
898
+ return result(marker, execution);
899
+ }
900
+ async publishBranch(input) {
901
+ const marker = `branch:${markerPart(input.jobId)}:${markerPart(input.branch)}`;
902
+ const execution = await this.options.coordinator.publishEffect({
903
+ jobId: input.jobId,
904
+ lease: input.lease,
905
+ branch: input.branch,
906
+ headSha: input.headSha,
907
+ kind: "github-branch",
908
+ marker,
909
+ payload: { repository: input.lease.repository, branch: input.branch },
910
+ reconcile: () => this.options.transport.findBranchByName({
911
+ repository: input.lease.repository,
912
+ branch: input.branch
913
+ }),
914
+ publish: () => this.options.transport.createBranch({
915
+ repository: input.lease.repository,
916
+ branch: input.branch,
917
+ headSha: input.headSha,
918
+ marker: markerText(marker)
919
+ })
920
+ });
921
+ return result(marker, execution);
922
+ }
923
+ async publishPullRequest(input) {
924
+ const marker = `pull-request:${markerPart(input.jobId)}:${markerPart(input.branch)}`;
925
+ const execution = await this.options.coordinator.publishEffect({
926
+ jobId: input.jobId,
927
+ lease: input.lease,
928
+ branch: input.branch,
929
+ headSha: input.headSha,
930
+ kind: "github-pull-request",
931
+ marker,
932
+ payload: { repository: input.lease.repository, branch: input.branch },
933
+ reconcile: () => this.options.transport.findPullRequestByMarker({
934
+ repository: input.lease.repository,
935
+ marker: markerText(marker)
936
+ }),
937
+ publish: () => this.options.transport.createPullRequest({
938
+ repository: input.lease.repository,
939
+ title: input.title,
940
+ body: `${markerText(marker)}
941
+ ${input.body}`,
942
+ branch: input.branch,
943
+ baseBranch: input.baseBranch,
944
+ draft: input.draft ?? true,
945
+ marker: markerText(marker)
946
+ })
947
+ });
948
+ const publication = result(marker, execution);
949
+ if (publication.remote !== void 0 && publication.remote.branch === input.branch && publication.remote.baseBranch === input.baseBranch && publication.remote.headSha === input.headSha) {
950
+ const job = await this.options.coordinator.getJob(input.jobId);
951
+ if (job !== void 0) {
952
+ await this.options.trackingStore.saveTrackedPullRequest({
953
+ repository: input.lease.repository,
954
+ pullRequestNumber: publication.remote.number,
955
+ jobId: input.jobId,
956
+ itemId: job.key.itemId,
957
+ branch: input.branch,
958
+ headSha: input.headSha,
959
+ marker,
960
+ brief: job.brief,
961
+ policy: job.policy,
962
+ createdAt: this.options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString()
963
+ });
964
+ }
965
+ }
966
+ return publication;
967
+ }
968
+ async publishCheck(input) {
969
+ const marker = `check:${markerPart(input.jobId)}:${markerPart(input.key ?? input.name)}:${markerPart(input.headSha)}`;
970
+ const execution = await this.options.coordinator.publishEffect({
971
+ jobId: input.jobId,
972
+ lease: input.lease,
973
+ branch: input.branch,
974
+ headSha: input.headSha,
975
+ kind: "github-check",
976
+ marker,
977
+ payload: { repository: input.lease.repository, name: input.name },
978
+ reconcile: () => this.options.transport.findCheckByMarker({
979
+ repository: input.lease.repository,
980
+ marker: markerText(marker),
981
+ headSha: input.headSha
982
+ }),
983
+ publish: () => this.options.transport.createCheck({
984
+ repository: input.lease.repository,
985
+ name: input.name,
986
+ headSha: input.headSha,
987
+ marker: markerText(marker),
988
+ status: input.status,
989
+ conclusion: input.conclusion,
990
+ summary: input.summary
991
+ })
992
+ });
993
+ return result(marker, execution);
994
+ }
995
+ async publishRepairIssue(input) {
996
+ const marker = `repair-issue:${markerPart(input.jobId)}:${markerHash(input.title)}`;
997
+ const execution = await this.options.coordinator.publishEffect({
998
+ jobId: input.jobId,
999
+ lease: input.lease,
1000
+ kind: "github-repair-issue",
1001
+ marker,
1002
+ payload: { repository: input.lease.repository },
1003
+ reconcile: () => this.options.transport.findIssueByMarker({
1004
+ repository: input.lease.repository,
1005
+ marker: markerText(marker)
1006
+ }),
1007
+ publish: () => this.options.transport.createRepairIssue({
1008
+ repository: input.lease.repository,
1009
+ title: input.title,
1010
+ body: `${markerText(marker)}
1011
+ ${input.body}`,
1012
+ marker: markerText(marker),
1013
+ labels: input.labels ?? ["shipyard:pr-repair"]
1014
+ })
1015
+ });
1016
+ return result(marker, execution);
1017
+ }
1018
+ async publishRepairLink(input) {
1019
+ const marker = `repair-link:${markerPart(input.jobId)}:${markerHash(input.repairIssueUrl)}`;
1020
+ const body = `${markerText(marker)}
1021
+ Linked repair issue: ${input.repairIssueUrl}`;
1022
+ const execution = await this.options.coordinator.publishEffect({
1023
+ jobId: input.jobId,
1024
+ lease: input.lease,
1025
+ itemId: String(input.issueNumber),
1026
+ kind: "github-repair-link",
1027
+ marker,
1028
+ payload: {
1029
+ repository: input.lease.repository,
1030
+ issueNumber: input.issueNumber
1031
+ },
1032
+ reconcile: () => this.options.transport.findCommentByMarker({
1033
+ repository: input.lease.repository,
1034
+ issueNumber: input.issueNumber,
1035
+ marker: markerText(marker)
1036
+ }),
1037
+ publish: () => this.options.transport.createComment({
1038
+ repository: input.lease.repository,
1039
+ issueNumber: input.issueNumber,
1040
+ body
1041
+ })
1042
+ });
1043
+ return result(marker, execution);
1044
+ }
1045
+ };
1046
+
1047
+ export { GitHubIntegration, GitHubPublication, InMemoryGitHubStore, createGitHubIssueBrief, reviewDecisionFor, verifyGitHubWebhookSignature };
1048
+ //# sourceMappingURL=github.js.map
1049
+ //# sourceMappingURL=github.js.map