@arnilo/prism-coding-agent 0.0.96 → 0.1.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 (81) hide show
  1. package/CHANGELOG.md +139 -3
  2. package/README.md +48 -19
  3. package/dist/ask-user-decision.d.ts +160 -0
  4. package/dist/ask-user-decision.js +495 -0
  5. package/dist/atomic-write.d.ts +3 -0
  6. package/dist/atomic-write.js +24 -0
  7. package/dist/checks.js +5 -0
  8. package/dist/coding-checkpoint.js +6 -15
  9. package/dist/delete.d.ts +29 -0
  10. package/dist/delete.js +119 -0
  11. package/dist/edit-diff.js +1 -4
  12. package/dist/edit.d.ts +5 -1
  13. package/dist/edit.js +20 -9
  14. package/dist/effects.d.ts +33 -0
  15. package/dist/effects.js +89 -0
  16. package/dist/execution-policy.d.ts +8 -3
  17. package/dist/execution-policy.js +5 -2
  18. package/dist/file-mutation-queue.js +1 -2
  19. package/dist/forge/github.d.ts +2 -0
  20. package/dist/forge/github.js +554 -0
  21. package/dist/forge/index.d.ts +3 -0
  22. package/dist/forge/index.js +3 -0
  23. package/dist/forge/types.d.ts +150 -0
  24. package/dist/forge/types.js +19 -0
  25. package/dist/git-aware-repository.d.ts +25 -0
  26. package/dist/git-aware-repository.js +268 -0
  27. package/dist/git-exec.js +1 -1
  28. package/dist/git-tools.d.ts +4 -1
  29. package/dist/git-tools.js +15 -7
  30. package/dist/git.d.ts +3 -3
  31. package/dist/git.js +14 -14
  32. package/dist/glob-match.d.ts +6 -0
  33. package/dist/glob-match.js +81 -0
  34. package/dist/glob.d.ts +14 -0
  35. package/dist/glob.js +147 -0
  36. package/dist/goal-verify.d.ts +66 -0
  37. package/dist/goal-verify.js +280 -0
  38. package/dist/index.d.ts +63 -30
  39. package/dist/index.js +40 -16
  40. package/dist/language/client.d.ts +44 -0
  41. package/dist/language/client.js +290 -0
  42. package/dist/language/framing.d.ts +23 -0
  43. package/dist/language/framing.js +112 -0
  44. package/dist/language/index.d.ts +4 -0
  45. package/dist/language/index.js +4 -0
  46. package/dist/language/intelligence.d.ts +10 -0
  47. package/dist/language/intelligence.js +526 -0
  48. package/dist/language/types.d.ts +106 -0
  49. package/dist/language/types.js +21 -0
  50. package/dist/lifecycle.d.ts +75 -0
  51. package/dist/lifecycle.js +102 -0
  52. package/dist/limits.d.ts +41 -0
  53. package/dist/limits.js +41 -0
  54. package/dist/list.js +6 -10
  55. package/dist/move.d.ts +24 -0
  56. package/dist/move.js +150 -0
  57. package/dist/mutation-path.d.ts +7 -0
  58. package/dist/mutation-path.js +51 -0
  59. package/dist/output-accumulator.d.ts +8 -0
  60. package/dist/output-accumulator.js +45 -1
  61. package/dist/path-utils.js +1 -1
  62. package/dist/process/index.d.ts +3 -0
  63. package/dist/process/index.js +3 -0
  64. package/dist/process/sessions.d.ts +2 -0
  65. package/dist/process/sessions.js +592 -0
  66. package/dist/process/types.d.ts +146 -0
  67. package/dist/process/types.js +19 -0
  68. package/dist/read-path-set.d.ts +14 -0
  69. package/dist/read-path-set.js +26 -0
  70. package/dist/read.d.ts +3 -0
  71. package/dist/read.js +11 -17
  72. package/dist/repository.d.ts +54 -3
  73. package/dist/repository.js +144 -38
  74. package/dist/search.d.ts +1 -1
  75. package/dist/search.js +91 -27
  76. package/dist/shell.d.ts +3 -0
  77. package/dist/shell.js +23 -8
  78. package/dist/truncate.js +1 -1
  79. package/dist/write.d.ts +5 -1
  80. package/dist/write.js +19 -6
  81. package/package.json +6 -4
@@ -0,0 +1,554 @@
1
+ import { assertExecutionAllowed } from "@arnilo/prism";
2
+ import { sha256Hex } from "../artifacts.js";
3
+ import { createBoundGitRunner } from "../git-exec.js";
4
+ import { HARD_MAX_GIT_REF_BYTES } from "../limits.js";
5
+ import { ForgeError, resolveForgeLimits } from "./types.js";
6
+ const API_BASE = "https://api.github.com";
7
+ const MUTATION_TTL_MS = 5 * 60_000;
8
+ const MAX_PAGE_SIZE = 100;
9
+ function canonical(value) {
10
+ if (value === null || typeof value === "string" || typeof value === "boolean")
11
+ return value;
12
+ if (typeof value === "number" && Number.isFinite(value))
13
+ return value;
14
+ if (Array.isArray(value))
15
+ return value.map(canonical);
16
+ if (value && typeof value === "object") {
17
+ const out = {};
18
+ for (const key of Object.keys(value).sort())
19
+ out[key] = canonical(value[key]);
20
+ return out;
21
+ }
22
+ return null;
23
+ }
24
+ function hashJson(value) {
25
+ return sha256Hex(Buffer.from(JSON.stringify(canonical(value))));
26
+ }
27
+ function validateRef(ref, label) {
28
+ if (typeof ref !== "string" || ref.length === 0)
29
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", `${label} is required`);
30
+ if (ref.includes("\0") || ref.includes("\n") || ref.includes("\r") || ref.startsWith("-")) {
31
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", `${label} must not start with '-' or contain NUL/newlines`);
32
+ }
33
+ if (Buffer.byteLength(ref, "utf8") > HARD_MAX_GIT_REF_BYTES) {
34
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", `${label} exceeds ${HARD_MAX_GIT_REF_BYTES} byte limit`);
35
+ }
36
+ return ref;
37
+ }
38
+ function validateNumber(value, label) {
39
+ if (!Number.isSafeInteger(value) || value < 1)
40
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", `${label} must be a positive integer`);
41
+ return value;
42
+ }
43
+ function truncate(text, maxBytes) {
44
+ return Buffer.byteLength(text, "utf8") <= maxBytes ? text : Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
45
+ }
46
+ /** Bounded single HTTP request against the GitHub REST API with rate-limit backoff. */
47
+ async function requestJson(token, method, path, limits, options = {}, fetchImpl = globalThis.fetch) {
48
+ const deadline = Date.now() + limits.requestTimeoutMs;
49
+ let attempt = 0;
50
+ for (;;) {
51
+ attempt += 1;
52
+ const remaining = deadline - Date.now();
53
+ if (remaining <= 0)
54
+ throw new ForgeError("ERR_PRISM_FORGE_RATE_LIMIT", "GitHub API request deadline exceeded");
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), remaining);
57
+ const onAbort = () => controller.abort();
58
+ options.signal?.addEventListener("abort", onAbort, { once: true });
59
+ try {
60
+ const response = await fetchImpl(`${API_BASE}${path}`, {
61
+ method,
62
+ headers: {
63
+ Authorization: `Bearer ${token}`,
64
+ Accept: "application/vnd.github+json",
65
+ "X-GitHub-Api-Version": "2022-11-28",
66
+ ...(options.body === undefined ? {} : { "Content-Type": "application/json" }),
67
+ },
68
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
69
+ signal: controller.signal,
70
+ redirect: "manual",
71
+ });
72
+ const text = await readBounded(response, limits.payloadBytes);
73
+ const json = parseJson(text);
74
+ const rateLimited = response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0";
75
+ if (rateLimited || response.status === 429) {
76
+ const waitMs = backoffMs(response.headers.get("retry-after"), attempt, remaining);
77
+ if (waitMs >= remaining)
78
+ throw new ForgeError("ERR_PRISM_FORGE_RATE_LIMIT", "GitHub rate limit not cleared before deadline");
79
+ await sleep(waitMs, controller.signal);
80
+ continue;
81
+ }
82
+ if (response.status === 401)
83
+ throw new ForgeError("ERR_PRISM_FORGE_AUTH", "GitHub API authentication failed");
84
+ if (response.status === 403)
85
+ throw new ForgeError("ERR_PRISM_FORGE_AUTH", "GitHub API authorization failed");
86
+ return { status: response.status, headers: response.headers, json };
87
+ }
88
+ catch (error) {
89
+ if (error instanceof ForgeError)
90
+ throw error;
91
+ if (options.signal?.aborted)
92
+ throw error;
93
+ if (error instanceof DOMException && error.name === "AbortError") {
94
+ throw new ForgeError("ERR_PRISM_FORGE_API", "GitHub API request timed out");
95
+ }
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ throw new ForgeError("ERR_PRISM_FORGE_API", `GitHub API request failed: ${message}`);
98
+ }
99
+ finally {
100
+ clearTimeout(timer);
101
+ options.signal?.removeEventListener("abort", onAbort);
102
+ }
103
+ }
104
+ }
105
+ async function readBounded(response, maxBytes) {
106
+ const declared = Number(response.headers.get("content-length") ?? "0");
107
+ if (Number.isFinite(declared) && declared > maxBytes)
108
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", `GitHub response exceeds ${maxBytes} byte limit`);
109
+ if (!response.body)
110
+ return "";
111
+ const reader = response.body.getReader();
112
+ const chunks = [];
113
+ let total = 0;
114
+ for (;;) {
115
+ const { done, value } = await reader.read();
116
+ if (done)
117
+ break;
118
+ total += value.byteLength;
119
+ if (total > maxBytes) {
120
+ await reader.cancel().catch(() => undefined);
121
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", `GitHub response exceeds ${maxBytes} byte limit`);
122
+ }
123
+ chunks.push(Buffer.from(value));
124
+ }
125
+ return Buffer.concat(chunks).toString("utf8");
126
+ }
127
+ function parseJson(text) {
128
+ if (text.length === 0)
129
+ return undefined;
130
+ try {
131
+ return JSON.parse(text);
132
+ }
133
+ catch {
134
+ throw new ForgeError("ERR_PRISM_FORGE_API", "GitHub API returned malformed JSON");
135
+ }
136
+ }
137
+ function backoffMs(retryAfter, attempt, remaining) {
138
+ if (retryAfter !== null) {
139
+ const seconds = Number(retryAfter);
140
+ if (Number.isFinite(seconds) && seconds >= 0)
141
+ return Math.min(seconds * 1000, remaining);
142
+ }
143
+ return Math.min(500 * 2 ** Math.min(attempt - 1, 5), remaining);
144
+ }
145
+ function sleep(ms, signal) {
146
+ return new Promise((resolve, reject) => {
147
+ const timer = setTimeout(resolve, ms);
148
+ signal.addEventListener("abort", () => {
149
+ clearTimeout(timer);
150
+ reject(new DOMException("aborted", "AbortError"));
151
+ }, { once: true });
152
+ });
153
+ }
154
+ function errorForStatus(status, json, fallback) {
155
+ if (status >= 200 && status < 300)
156
+ return undefined;
157
+ const message = json && typeof json === "object" && "message" in json && typeof json.message === "string" ? json.message : fallback;
158
+ if (status === 404)
159
+ return new ForgeError("ERR_PRISM_FORGE_API", message);
160
+ if (status === 422)
161
+ return new ForgeError("ERR_PRISM_FORGE_STALE", message);
162
+ if (status >= 500)
163
+ return new ForgeError("ERR_PRISM_FORGE_API", message);
164
+ return new ForgeError("ERR_PRISM_FORGE_API", message);
165
+ }
166
+ function asRecord(json, label) {
167
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
168
+ throw new ForgeError("ERR_PRISM_FORGE_API", `${label}: unexpected GitHub response shape`);
169
+ }
170
+ return json;
171
+ }
172
+ function stringField(record, key, fallback = "") {
173
+ const value = record[key];
174
+ return typeof value === "string" ? value : fallback;
175
+ }
176
+ function toForgePullRequest(record) {
177
+ return {
178
+ number: Number(record.number),
179
+ state: record.state === "closed" ? "closed" : "open",
180
+ merged: record.merged === true,
181
+ head: stringField(record, "head", ""),
182
+ base: stringField(record, "base", ""),
183
+ title: stringField(record, "title"),
184
+ body: stringField(record, "body"),
185
+ url: stringField(record, "html_url"),
186
+ };
187
+ }
188
+ function toForgeCheck(record) {
189
+ const conclusion = record.conclusion === null || record.conclusion === undefined ? undefined : String(record.conclusion);
190
+ return {
191
+ name: stringField(record, "name", "check"),
192
+ status: ["queued", "in_progress", "completed"].includes(record.status)
193
+ ? record.status
194
+ : "queued",
195
+ ...(conclusion === undefined ? {} : { conclusion }),
196
+ ...(typeof record.details_url === "string" ? { detailsUrl: record.details_url } : {}),
197
+ };
198
+ }
199
+ export function createGitHubForge(options) {
200
+ const repository = options.repository;
201
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
202
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "repository must be 'owner/repo'");
203
+ }
204
+ const limits = resolveForgeLimits(options.limits);
205
+ const identity = options.identity;
206
+ const ownership = options.ownership;
207
+ if (identity && ownership && identity.tenantId !== ownership.tenantId) {
208
+ throw new ForgeError("ERR_PRISM_FORGE_OWNERSHIP", "identity and ownership tenants must match");
209
+ }
210
+ const policy = options.policy;
211
+ const store = options.effectStore;
212
+ const fetchImpl = options.fetch ?? globalThis.fetch;
213
+ let runnerPromise;
214
+ function runner() {
215
+ runnerPromise ??= "exec" in options.git ? Promise.resolve(options.git) : createBoundGitRunner(options.git);
216
+ return runnerPromise;
217
+ }
218
+ function mutationContext() {
219
+ if (!identity || !ownership || !options.sessionId || !options.runId) {
220
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "identity/ownership/sessionId/runId are required for forge mutations");
221
+ }
222
+ if (identity.tenantId !== ownership.tenantId) {
223
+ throw new ForgeError("ERR_PRISM_FORGE_OWNERSHIP", "identity and ownership tenants must match");
224
+ }
225
+ return { identity, ownership, sessionId: options.sessionId, runId: options.runId, store };
226
+ }
227
+ async function gate(action) {
228
+ if (!policy)
229
+ return;
230
+ // Denials propagate as the core ExecutionDeniedError so hosts can distinguish
231
+ // policy refusal from forge failures; no request is attempted.
232
+ await assertExecutionAllowed(policy, {
233
+ kind: "forge",
234
+ operation: action.operation,
235
+ command: action.command,
236
+ paths: action.paths,
237
+ risk: "high",
238
+ metadata: { repository, ...action.metadata },
239
+ });
240
+ }
241
+ async function resolveToken() {
242
+ const credential = await options.credentials.resolver.resolve({
243
+ name: options.credentials.name,
244
+ provider: "github",
245
+ metadata: { repository },
246
+ });
247
+ if (!credential || typeof credential.value !== "string" || credential.value.length === 0) {
248
+ throw new ForgeError("ERR_PRISM_FORGE_AUTH", "credential resolver returned no token");
249
+ }
250
+ return credential.value;
251
+ }
252
+ async function runMutation(operation, args, execute) {
253
+ const ctx = mutationContext();
254
+ const toolName = `forge.${operation}`;
255
+ const toolCallId = `forge:${operation}`;
256
+ const argumentsHash = hashJson(args);
257
+ const key = `prism:forge:v1:${hashJson({ tenant: ctx.ownership.tenantId, session: ctx.sessionId, run: ctx.runId, toolName, argumentsHash })}`;
258
+ const base = {
259
+ identity: ctx.identity,
260
+ ownership: ctx.ownership,
261
+ key,
262
+ sessionId: ctx.sessionId,
263
+ runId: ctx.runId,
264
+ toolCallId,
265
+ toolName,
266
+ argumentsHash,
267
+ };
268
+ const { outcome, record } = await ctx.store.begin({ ...base, claimTtlMs: MUTATION_TTL_MS });
269
+ if (outcome === "existing") {
270
+ if (record.status === "completed" && record.result)
271
+ return record.result.value;
272
+ if (record.status === "failed_terminal" || record.status === "failed_retryable") {
273
+ throw new ForgeError("ERR_PRISM_FORGE_API", `forge mutation previously failed (${record.failure?.code ?? "unknown"})`);
274
+ }
275
+ throw new ForgeError("ERR_PRISM_FORGE_API", "forge mutation outcome requires reconciliation; verify via reconcileHandoff");
276
+ }
277
+ if (!record.claimToken) {
278
+ throw new ForgeError("ERR_PRISM_FORGE_API", "forge mutation claim is not usable; retry or reconcile");
279
+ }
280
+ const transition = { ...base, claimToken: record.claimToken, expectedVersion: record.version };
281
+ const dispatched = await ctx.store.markDispatched(transition);
282
+ const current = { ...transition, expectedVersion: dispatched.version };
283
+ try {
284
+ const result = await execute(await resolveToken());
285
+ await ctx.store.complete({
286
+ ...current,
287
+ result: { toolCallId, name: toolName, content: [{ type: "text", text: JSON.stringify(result) }], value: result },
288
+ });
289
+ return result;
290
+ }
291
+ catch (error) {
292
+ const code = error instanceof ForgeError ? error.code : "ERR_PRISM_FORGE_API";
293
+ try {
294
+ await ctx.store.fail({ ...current, status: "failed_terminal", failure: { code } });
295
+ }
296
+ catch {
297
+ // The mutation may or may not have landed; recovery goes through reconcileHandoff.
298
+ }
299
+ throw error;
300
+ }
301
+ }
302
+ async function get(path, signal) {
303
+ const token = await resolveToken();
304
+ const response = await requestJson(token, "GET", path, limits, { signal }, fetchImpl);
305
+ return { status: response.status, json: response.json };
306
+ }
307
+ /** Fetch every page of a GET list up to the page cap; returns items from each page. */
308
+ async function getPages(path, signal, collect) {
309
+ const items = [];
310
+ for (let page = 1; page <= limits.pagesPerOperation; page += 1) {
311
+ const { status, json } = await get(`${path}${path.includes("?") ? "&" : "?"}per_page=${MAX_PAGE_SIZE}&page=${page}`, signal);
312
+ const error = errorForStatus(status, json, "GitHub list request failed");
313
+ if (error)
314
+ throw error;
315
+ const pageItems = collect(json);
316
+ if (pageItems.length === 0)
317
+ break;
318
+ items.push(...pageItems);
319
+ }
320
+ return items;
321
+ }
322
+ return {
323
+ async issueContext(input) {
324
+ const number = validateNumber(input.number, "issue number");
325
+ const { status, json } = await get(`/repos/${repository}/issues/${number}`);
326
+ const error = errorForStatus(status, json, "issue fetch failed");
327
+ if (error)
328
+ throw error;
329
+ const record = asRecord(json, "issue");
330
+ const labels = Array.isArray(record.labels)
331
+ ? record.labels.map((label) => stringField(asRecord(label, "label"), "name")).filter(Boolean)
332
+ : [];
333
+ return {
334
+ number,
335
+ title: stringField(record, "title"),
336
+ state: record.state === "closed" ? "closed" : "open",
337
+ body: stringField(record, "body"),
338
+ labels,
339
+ author: typeof record.user === "object" && record.user !== null
340
+ ? stringField(record.user, "login", "unknown")
341
+ : "unknown",
342
+ updatedAt: stringField(record, "updated_at"),
343
+ url: stringField(record, "html_url"),
344
+ };
345
+ },
346
+ async push(input) {
347
+ await gate({ operation: "push", command: `git push origin ${input.refspec ?? "HEAD"}` });
348
+ return runMutation("push", { refspec: input.refspec ?? null }, async (token) => {
349
+ const bound = await runner();
350
+ let ref = input.refspec;
351
+ if (!ref) {
352
+ const head = await bound.exec({ args: ["rev-parse", "--abbrev-ref", "HEAD"], cwd: options.cwd, maxOutputBytes: 4096 });
353
+ if (head.exitCode !== 0)
354
+ throw new ForgeError("ERR_PRISM_FORGE_API", "git rev-parse failed: no checked-out branch to push");
355
+ ref = head.stdout.toString("utf8").trim();
356
+ }
357
+ validateRef(ref, "refspec");
358
+ const remoteRef = ref.startsWith("refs/") ? ref : `refs/heads/${ref}`;
359
+ const push = await bound.exec({
360
+ args: ["push", "origin", ref],
361
+ cwd: options.cwd,
362
+ env: {
363
+ GIT_CONFIG_COUNT: "1",
364
+ GIT_CONFIG_KEY_0: "http.extraHeader",
365
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`,
366
+ },
367
+ timeoutMs: limits.requestTimeoutMs,
368
+ maxOutputBytes: 256 * 1024,
369
+ });
370
+ if (push.exitCode !== 0) {
371
+ throw new ForgeError("ERR_PRISM_FORGE_API", `git push failed (exit ${push.exitCode})`);
372
+ }
373
+ return { remoteRef };
374
+ });
375
+ },
376
+ async createPullRequest(input) {
377
+ const head = validateRef(input.head, "head");
378
+ const base = validateRef(input.base, "base");
379
+ if (typeof input.title !== "string" || input.title.length === 0)
380
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "title is required");
381
+ if (typeof input.body !== "string")
382
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "body is required");
383
+ await gate({ operation: "create_pull_request", metadata: { head, base } });
384
+ return runMutation("createPullRequest", { head, base, title: input.title, body: input.body }, async (token) => {
385
+ const { status, json } = await requestJson(token, "POST", `/repos/${repository}/pulls`, limits, {
386
+ body: { head, base, title: input.title, body: input.body },
387
+ }, fetchImpl);
388
+ if (status === 422 &&
389
+ typeof json === "object" &&
390
+ json !== null &&
391
+ "message" in json &&
392
+ String(json.message).includes("already exists")) {
393
+ // Idempotent: an open PR for this head/base already exists — return it.
394
+ const existing = await get(`/repos/${repository}/pulls?head=${repository.split("/")[0]}:${head}&state=open`);
395
+ if (existing.status === 200 && Array.isArray(existing.json) && existing.json.length > 0) {
396
+ return toForgePullRequest(asRecord(existing.json[0], "existing pull request"));
397
+ }
398
+ }
399
+ const error = errorForStatus(status, json, "pull request creation failed");
400
+ if (error)
401
+ throw error;
402
+ return toForgePullRequest(asRecord(json, "pull request"));
403
+ });
404
+ },
405
+ async updatePullRequest(input) {
406
+ const number = validateNumber(input.number, "pull request number");
407
+ if (input.state !== undefined && input.state !== "open" && input.state !== "closed") {
408
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "state must be 'open' or 'closed'");
409
+ }
410
+ await gate({ operation: "update_pull_request", metadata: { number } });
411
+ const body = {};
412
+ if (input.title !== undefined)
413
+ body.title = input.title;
414
+ if (input.body !== undefined)
415
+ body.body = input.body;
416
+ if (input.state !== undefined)
417
+ body.state = input.state;
418
+ if (Object.keys(body).length === 0)
419
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "nothing to update");
420
+ return runMutation("updatePullRequest", { number, ...body }, async (token) => {
421
+ const { status, json } = await requestJson(token, "PATCH", `/repos/${repository}/pulls/${number}`, limits, { body }, fetchImpl);
422
+ const error = errorForStatus(status, json, "pull request update failed");
423
+ if (error)
424
+ throw error;
425
+ return toForgePullRequest(asRecord(json, "pull request"));
426
+ });
427
+ },
428
+ async createReviewComment(input) {
429
+ const number = validateNumber(input.number, "pull request number");
430
+ const path = validateRef(input.path, "path");
431
+ const line = validateNumber(input.line, "line");
432
+ if (typeof input.body !== "string" || input.body.length === 0)
433
+ throw new ForgeError("ERR_PRISM_FORGE_LIMIT", "comment body is required");
434
+ await gate({ operation: "create_review_comment", metadata: { number, path, line } });
435
+ return runMutation("createReviewComment", { number, path, line, body: input.body }, async (token) => {
436
+ const { status, json } = await requestJson(token, "POST", `/repos/${repository}/pulls/${number}/comments`, limits, {
437
+ body: { body: input.body, path, line },
438
+ }, fetchImpl);
439
+ const error = errorForStatus(status, json, "review comment creation failed");
440
+ if (error)
441
+ throw error;
442
+ const record = asRecord(json, "review comment");
443
+ return { id: Number(record.id) };
444
+ });
445
+ },
446
+ async checks(input) {
447
+ const ref = validateRef(input.ref, "ref");
448
+ const checkRuns = await getPages(`/repos/${repository}/commits/${encodeURIComponent(ref)}/check-runs`, undefined, (json) => {
449
+ const record = asRecord(json, "check-runs");
450
+ return Array.isArray(record.check_runs) ? record.check_runs : [];
451
+ });
452
+ const statuses = await getPages(`/repos/${repository}/commits/${encodeURIComponent(ref)}/status`, undefined, (json) => {
453
+ const record = asRecord(json, "statuses");
454
+ return Array.isArray(record.statuses) ? record.statuses : [];
455
+ });
456
+ const out = [];
457
+ const seen = new Set();
458
+ for (const item of checkRuns) {
459
+ const check = toForgeCheck(asRecord(item, "check run"));
460
+ if (seen.has(check.name))
461
+ continue;
462
+ seen.add(check.name);
463
+ out.push(check);
464
+ }
465
+ for (const item of statuses) {
466
+ const record = asRecord(item, "commit status");
467
+ const state = stringField(record, "state");
468
+ const conclusion = state === "pending" ? undefined : state === "success" ? "success" : "failure";
469
+ const name = stringField(record, "context", "status");
470
+ if (seen.has(name))
471
+ continue;
472
+ seen.add(name);
473
+ out.push({ name, status: state === "pending" ? "in_progress" : "completed", ...(conclusion ? { conclusion } : {}) });
474
+ }
475
+ return out;
476
+ },
477
+ async reconcileHandoff(input) {
478
+ const base = validateRef(input.base, "base");
479
+ const head = validateRef(input.head, "head");
480
+ const owner = repository.split("/")[0];
481
+ const comparePath = `/repos/${repository}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
482
+ const compare = await get(comparePath);
483
+ if (compare.status === 404) {
484
+ return {
485
+ base,
486
+ head,
487
+ pushed: false,
488
+ aheadBy: 0,
489
+ behindBy: 0,
490
+ alreadyUpToDate: false,
491
+ alreadyMerged: false,
492
+ checks: [],
493
+ commits: [],
494
+ changedPaths: [],
495
+ diffstat: "",
496
+ warnings: ["head ref not found on remote"],
497
+ };
498
+ }
499
+ const error = errorForStatus(compare.status, compare.json, "compare failed");
500
+ if (error)
501
+ throw error;
502
+ const warnings = [];
503
+ const record = asRecord(compare.json, "compare");
504
+ const aheadBy = Number(record.ahead_by ?? 0);
505
+ const behindBy = Number(record.behind_by ?? 0);
506
+ const commits = Array.isArray(record.commits)
507
+ ? record.commits.slice(0, limits.pagesPerOperation * MAX_PAGE_SIZE).map((c) => {
508
+ const commitRecord = asRecord(c, "commit");
509
+ const inner = asRecord(commitRecord.commit ?? {}, "commit detail");
510
+ return { sha: stringField(commitRecord, "sha"), subject: stringField(inner, "message", "").split("\n")[0] };
511
+ })
512
+ : [];
513
+ const files = Array.isArray(record.files) ? record.files : [];
514
+ const changedPaths = files.map((f) => stringField(asRecord(f, "file"), "filename")).filter(Boolean);
515
+ const diffstat = truncate(files
516
+ .map((f) => {
517
+ const file = asRecord(f, "file");
518
+ return `${stringField(file, "filename")} +${Number(file.additions ?? 0)}/-${Number(file.deletions ?? 0)}`;
519
+ })
520
+ .join("\n"), limits.payloadBytes);
521
+ const prResult = await get(`/repos/${repository}/pulls?head=${owner}:${head}&base=${base}&state=all`);
522
+ let pullRequest;
523
+ if (prResult.status === 200 && Array.isArray(prResult.json) && prResult.json.length > 0) {
524
+ pullRequest = toForgePullRequest(asRecord(prResult.json[0], "pull request"));
525
+ }
526
+ const alreadyMerged = pullRequest?.merged === true;
527
+ if (alreadyMerged)
528
+ warnings.push("pull request already merged; no new push or PR needed");
529
+ if (aheadBy === 0 && behindBy === 0)
530
+ warnings.push("head is up to date with base");
531
+ const checkRuns = await getPages(`/repos/${repository}/commits/${encodeURIComponent(head)}/check-runs`, undefined, (json) => {
532
+ const page = asRecord(json, "check-runs");
533
+ return Array.isArray(page.check_runs) ? page.check_runs : [];
534
+ });
535
+ const checks = checkRuns.map((c) => toForgeCheck(asRecord(c, "check run")));
536
+ return {
537
+ base,
538
+ head,
539
+ pushed: aheadBy > 0 || behindBy > 0,
540
+ aheadBy,
541
+ behindBy,
542
+ alreadyUpToDate: aheadBy === 0 && behindBy === 0,
543
+ alreadyMerged,
544
+ pullRequest,
545
+ checks,
546
+ commits,
547
+ changedPaths,
548
+ diffstat,
549
+ warnings,
550
+ };
551
+ },
552
+ };
553
+ }
554
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1,3 @@
1
+ export type { CreateGitHubForgeOptions, ForgeCheck, ForgeCredential, ForgeCredentialResolver, ForgeCredentialResolverSource, ForgeErrorCode, ForgeHandoffReport, ForgeIssueContext, ForgeLimits, ForgeOperations, ForgePullRequest, ResolvedForgeLimits, } from "./types.js";
2
+ export { ForgeError, resolveForgeLimits } from "./types.js";
3
+ export { createGitHubForge } from "./github.js";
@@ -0,0 +1,3 @@
1
+ export { ForgeError, resolveForgeLimits } from "./types.js";
2
+ export { createGitHubForge } from "./github.js";
3
+ //# sourceMappingURL=index.js.map