@stackbilt/aegis-core 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 (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
package/src/github.ts ADDED
@@ -0,0 +1,670 @@
1
+ // GitHub REST API v3 — fetch-based, no external dependencies
2
+ // Follows the same pattern as groq.ts (pure fetch, no imports)
3
+
4
+ const GITHUB_API = 'https://api.github.com';
5
+ const RETRY_STATUSES = new Set([500, 502, 503]);
6
+ const MAX_RETRIES = 2;
7
+ const RETRY_BASE_MS = 500;
8
+
9
+ // Local directory name → GitHub repo name (when they differ)
10
+ // BizOps may list projects with names that don't match GitHub repos —
11
+ // add aliases here to prevent 404 loops in goal execution.
12
+ const REPO_ALIASES: Record<string, string> = {
13
+ 'my-project': 'aegis',
14
+ 'bizops-copilot': 'example-app',
15
+ // Add your repo aliases here,
16
+ // 'docs': 'docs',
17
+ };
18
+
19
+ // Known org name variants that should resolve to the canonical GitHub org.
20
+ // LLMs in goal loops often guess shortened org names instead of the full canonical org.
21
+ const ORG_ALIASES: Record<string, string> = {
22
+ 'exampleorg': 'ExampleOrg',
23
+ 'Exampleorg': 'ExampleOrg',
24
+ 'exampleorg-dev': 'ExampleOrg',
25
+ };
26
+
27
+ /** Normalize a repo slug: resolve aliases and ensure org/repo format. */
28
+ export function resolveRepoName(repo: string, defaultOrg = 'ExampleOrg'): string {
29
+ const parts = repo.split('/');
30
+ const rawOrg = parts.length > 1 ? parts[0] : defaultOrg;
31
+ const org = ORG_ALIASES[rawOrg] ?? rawOrg;
32
+ const raw = parts.length > 1 ? parts[1] : parts[0];
33
+ const name = raw.replace(/\.git$/, '');
34
+ const resolved = REPO_ALIASES[name] ?? name;
35
+ return `${org}/${resolved}`;
36
+ }
37
+
38
+ export class GitHubApiError extends Error {
39
+ constructor(
40
+ public status: number,
41
+ public method: string,
42
+ public path: string,
43
+ public body: string,
44
+ ) {
45
+ super(`GitHub API ${status} (${method} ${path}): ${body}`);
46
+ }
47
+ }
48
+
49
+ function ghHeaders(token: string): HeadersInit {
50
+ return {
51
+ Authorization: `Bearer ${token}`,
52
+ Accept: 'application/vnd.github+json',
53
+ 'X-GitHub-Api-Version': '2022-11-28',
54
+ 'User-Agent': 'aegis-worker/1.0',
55
+ };
56
+ }
57
+
58
+ async function ghFetch(token: string, path: string, init?: RequestInit): Promise<Response> {
59
+ const method = init?.method ?? 'GET';
60
+
61
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
62
+ const res = await fetch(`${GITHUB_API}${path}`, {
63
+ ...init,
64
+ headers: { ...ghHeaders(token), ...(init?.headers ?? {}) },
65
+ });
66
+
67
+ if (res.ok) return res;
68
+
69
+ const errText = await res.text();
70
+
71
+ // Rate limit: primary (403 + x-ratelimit-remaining: 0)
72
+ if (res.status === 403) {
73
+ const remaining = res.headers.get('x-ratelimit-remaining');
74
+ if (remaining === '0') {
75
+ const resetEpoch = parseInt(res.headers.get('x-ratelimit-reset') ?? '0', 10);
76
+ const waitSec = Math.max(0, resetEpoch - Math.floor(Date.now() / 1000));
77
+ throw new GitHubApiError(403, method, path, `Rate limited — resets in ${waitSec}s`);
78
+ }
79
+ }
80
+
81
+ // Rate limit: secondary (429 + retry-after)
82
+ if (res.status === 429) {
83
+ const retryAfter = parseInt(res.headers.get('retry-after') ?? '60', 10);
84
+ if (attempt < MAX_RETRIES && retryAfter <= 10) {
85
+ console.warn(`[github] 429 on ${method} ${path}, retry-after ${retryAfter}s (attempt ${attempt + 1})`);
86
+ await new Promise(r => setTimeout(r, retryAfter * 1000));
87
+ continue;
88
+ }
89
+ throw new GitHubApiError(429, method, path, `Secondary rate limit — retry after ${retryAfter}s`);
90
+ }
91
+
92
+ // Transient server errors — retry with exponential backoff
93
+ if (RETRY_STATUSES.has(res.status) && attempt < MAX_RETRIES) {
94
+ const delayMs = RETRY_BASE_MS * Math.pow(2, attempt);
95
+ console.warn(`[github] ${res.status} on ${method} ${path}, retrying in ${delayMs}ms (attempt ${attempt + 1})`);
96
+ await new Promise(r => setTimeout(r, delayMs));
97
+ continue;
98
+ }
99
+
100
+ throw new GitHubApiError(res.status, method, path, errText);
101
+ }
102
+
103
+ // Should not reach here, but satisfy TypeScript
104
+ throw new GitHubApiError(500, method, path, 'Exhausted retries');
105
+ }
106
+
107
+ // ─── Read operations ──────────────────────────────────────────────────────────
108
+
109
+ // Returns flat list of all file paths in the repo (blob entries only)
110
+ export async function getRepoTree(token: string, repo: string, branch = 'main'): Promise<string[]> {
111
+ const res = await ghFetch(token, `/repos/${repo}/git/trees/${branch}?recursive=1`);
112
+ const data = (await res.json()) as { tree: { type: string; path: string }[]; truncated: boolean };
113
+ if (data.truncated) {
114
+ console.warn('[github] repo tree truncated — large repo');
115
+ }
116
+ return data.tree.filter((e) => e.type === 'blob').map((e) => e.path);
117
+ }
118
+
119
+ // Returns decoded UTF-8 content of a single file
120
+ export async function getFileContent(
121
+ token: string,
122
+ repo: string,
123
+ path: string,
124
+ ref?: string,
125
+ ): Promise<string> {
126
+ const query = ref ? `?ref=${encodeURIComponent(ref)}` : '';
127
+ // Encode each segment individually — do NOT encode slashes or GitHub treats the whole path as one filename
128
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
129
+ const res = await ghFetch(token, `/repos/${repo}/contents/${encodedPath}${query}`);
130
+ const data = (await res.json()) as { content: string; encoding: string };
131
+ if (data.encoding !== 'base64') throw new Error(`Unexpected encoding: ${data.encoding}`);
132
+ // atob is available in Workers runtime
133
+ return atob(data.content.replace(/\n/g, ''));
134
+ }
135
+
136
+ export interface Commit {
137
+ sha: string;
138
+ message: string;
139
+ author: string;
140
+ date: string;
141
+ }
142
+
143
+ export async function getRecentCommits(
144
+ token: string,
145
+ repo: string,
146
+ limit = 10,
147
+ ): Promise<Commit[]> {
148
+ const res = await ghFetch(token, `/repos/${repo}/commits?per_page=${Math.min(limit, 100)}`);
149
+ const data = (await res.json()) as {
150
+ sha: string;
151
+ commit: { message: string; author: { name: string; date: string } };
152
+ }[];
153
+ return data.map((c) => ({
154
+ sha: c.sha,
155
+ message: c.commit.message.split('\n')[0], // first line only
156
+ author: c.commit.author.name,
157
+ date: c.commit.author.date,
158
+ }));
159
+ }
160
+
161
+ export interface Issue {
162
+ number: number;
163
+ title: string;
164
+ state: string;
165
+ url: string;
166
+ labels: string[];
167
+ body: string;
168
+ created_at: string;
169
+ assignee: string | null;
170
+ }
171
+
172
+ export async function listIssues(
173
+ token: string,
174
+ repo: string,
175
+ state: 'open' | 'closed' | 'all' = 'open',
176
+ labels?: string[],
177
+ ): Promise<Issue[]> {
178
+ let url = `/repos/${repo}/issues?state=${state}&per_page=30`;
179
+ if (labels?.length) url += `&labels=${labels.join(',')}`;
180
+ const res = await ghFetch(token, url);
181
+ const data = (await res.json()) as {
182
+ number: number;
183
+ title: string;
184
+ state: string;
185
+ html_url: string;
186
+ body: string | null;
187
+ labels: { name: string }[];
188
+ created_at: string;
189
+ pull_request?: unknown;
190
+ assignee?: { login: string } | null;
191
+ }[];
192
+ // Filter out PRs (they appear in the issues endpoint)
193
+ return data
194
+ .filter((i) => !i.pull_request)
195
+ .map((i) => ({
196
+ number: i.number,
197
+ title: i.title,
198
+ state: i.state,
199
+ url: i.html_url,
200
+ labels: i.labels.map((l) => l.name),
201
+ body: i.body ?? '',
202
+ created_at: i.created_at,
203
+ assignee: i.assignee?.login ?? null,
204
+ }));
205
+ }
206
+
207
+ export interface PullRequest {
208
+ number: number;
209
+ title: string;
210
+ state: string;
211
+ merged: boolean;
212
+ url: string;
213
+ head: string;
214
+ base: string;
215
+ created_at: string;
216
+ }
217
+
218
+ export async function listPullRequests(
219
+ token: string,
220
+ repo: string,
221
+ state: 'open' | 'closed' | 'all' = 'open',
222
+ ): Promise<PullRequest[]> {
223
+ const res = await ghFetch(token, `/repos/${repo}/pulls?state=${state}&per_page=30`);
224
+ const data = (await res.json()) as {
225
+ number: number;
226
+ title: string;
227
+ state: string;
228
+ merged_at: string | null;
229
+ html_url: string;
230
+ head: { ref: string };
231
+ base: { ref: string };
232
+ created_at: string;
233
+ }[];
234
+ return data.map((p) => ({
235
+ number: p.number,
236
+ title: p.title,
237
+ state: p.state,
238
+ merged: p.merged_at !== null,
239
+ url: p.html_url,
240
+ head: p.head.ref,
241
+ base: p.base.ref,
242
+ created_at: p.created_at,
243
+ }));
244
+ }
245
+
246
+ export interface OrgRepo {
247
+ name: string;
248
+ description: string | null;
249
+ visibility: string;
250
+ language: string | null;
251
+ default_branch: string;
252
+ updated_at: string;
253
+ url: string;
254
+ }
255
+
256
+ export async function listOrgRepos(
257
+ token: string,
258
+ org: string,
259
+ type: 'all' | 'public' | 'private' = 'all',
260
+ ): Promise<OrgRepo[]> {
261
+ const res = await ghFetch(token, `/orgs/${org}/repos?type=${type}&sort=updated&per_page=30`);
262
+ const data = (await res.json()) as {
263
+ name: string;
264
+ description: string | null;
265
+ visibility: string;
266
+ language: string | null;
267
+ default_branch: string;
268
+ updated_at: string;
269
+ html_url: string;
270
+ }[];
271
+ return data.map((r) => ({
272
+ name: r.name,
273
+ description: r.description,
274
+ visibility: r.visibility,
275
+ language: r.language,
276
+ default_branch: r.default_branch,
277
+ updated_at: r.updated_at,
278
+ url: r.html_url,
279
+ }));
280
+ }
281
+
282
+ // ─── Write operations ─────────────────────────────────────────────────────────
283
+
284
+ export async function createIssue(
285
+ token: string,
286
+ repo: string,
287
+ title: string,
288
+ body: string,
289
+ labels?: string[],
290
+ ): Promise<{ number: number; url: string }> {
291
+ const res = await ghFetch(token, `/repos/${repo}/issues`, {
292
+ method: 'POST',
293
+ headers: { 'Content-Type': 'application/json' },
294
+ body: JSON.stringify({ title, body, labels: labels ?? [] }),
295
+ });
296
+ const data = (await res.json()) as { number: number; html_url: string };
297
+ return { number: data.number, url: data.html_url };
298
+ }
299
+
300
+ export async function createBranch(
301
+ token: string,
302
+ repo: string,
303
+ branchName: string,
304
+ fromSha: string,
305
+ ): Promise<void> {
306
+ try {
307
+ await ghFetch(token, `/repos/${repo}/git/refs`, {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha: fromSha }),
311
+ });
312
+ } catch (err) {
313
+ if (err instanceof GitHubApiError && err.status === 422) {
314
+ throw new Error(`Branch "${branchName}" already exists in ${repo}`);
315
+ }
316
+ throw err;
317
+ }
318
+ }
319
+
320
+ // Returns the blob SHA of a file — required by the Contents API PUT for optimistic concurrency
321
+ export async function getFileSha(
322
+ token: string,
323
+ repo: string,
324
+ path: string,
325
+ branch?: string,
326
+ ): Promise<string> {
327
+ const query = branch ? `?ref=${encodeURIComponent(branch)}` : '';
328
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
329
+ const res = await ghFetch(token, `/repos/${repo}/contents/${encodedPath}${query}`);
330
+ const data = (await res.json()) as { sha: string };
331
+ return data.sha;
332
+ }
333
+
334
+ // Updates a single file on an existing branch (single-file PR scope)
335
+ export async function updateFile(
336
+ token: string,
337
+ repo: string,
338
+ path: string,
339
+ content: string, // UTF-8 string — encoded internally
340
+ message: string,
341
+ sha: string, // current blob SHA from getFileSha()
342
+ branch: string,
343
+ ): Promise<void> {
344
+ // btoa is available in Workers runtime
345
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
346
+ await ghFetch(token, `/repos/${repo}/contents/${encodedPath}`, {
347
+ method: 'PUT',
348
+ headers: { 'Content-Type': 'application/json' },
349
+ body: JSON.stringify({ message, content: btoa(content), sha, branch }),
350
+ });
351
+ }
352
+
353
+ export async function createPullRequest(
354
+ token: string,
355
+ repo: string,
356
+ title: string,
357
+ body: string,
358
+ head: string,
359
+ base = 'main',
360
+ ): Promise<{ number: number; url: string }> {
361
+ const res = await ghFetch(token, `/repos/${repo}/pulls`, {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({ title, body, head, base, maintainer_can_modify: true }),
365
+ });
366
+ const data = (await res.json()) as { number: number; html_url: string };
367
+ return { number: data.number, url: data.html_url };
368
+ }
369
+
370
+ // ─── Issue lifecycle ──────────────────────────────────────────────────────────
371
+
372
+ export interface SearchIssueResult {
373
+ number: number;
374
+ title: string;
375
+ state: string;
376
+ html_url: string;
377
+ }
378
+
379
+ /** Search issues in a repo using the GitHub Search API. repo is 'owner/name'. */
380
+ export async function searchIssues(
381
+ token: string,
382
+ repo: string,
383
+ query: string,
384
+ ): Promise<SearchIssueResult[]> {
385
+ const q = encodeURIComponent(`repo:${repo} is:issue ${query}`);
386
+ const res = await ghFetch(token, `/search/issues?q=${q}&per_page=10`);
387
+ const data = (await res.json()) as {
388
+ items: Array<{
389
+ number: number;
390
+ title: string;
391
+ state: string;
392
+ html_url: string;
393
+ }>;
394
+ };
395
+ return data.items.map((i) => ({
396
+ number: i.number,
397
+ title: i.title,
398
+ state: i.state,
399
+ html_url: i.html_url,
400
+ }));
401
+ }
402
+
403
+ /** Close an issue with an optional comment. */
404
+ export async function closeIssue(
405
+ token: string,
406
+ repo: string,
407
+ issueNumber: number,
408
+ comment?: string,
409
+ ): Promise<void> {
410
+ // Post comment first if provided
411
+ if (comment) {
412
+ await commentOnIssue(token, repo, issueNumber, comment);
413
+ }
414
+ await ghFetch(token, `/repos/${repo}/issues/${issueNumber}`, {
415
+ method: 'PATCH',
416
+ headers: { 'Content-Type': 'application/json' },
417
+ body: JSON.stringify({ state: 'closed' }),
418
+ });
419
+ }
420
+
421
+ /** Add a comment to an issue. */
422
+ export async function commentOnIssue(
423
+ token: string,
424
+ repo: string,
425
+ issueNumber: number,
426
+ body: string,
427
+ ): Promise<void> {
428
+ await ghFetch(token, `/repos/${repo}/issues/${issueNumber}/comments`, {
429
+ method: 'POST',
430
+ headers: { 'Content-Type': 'application/json' },
431
+ body: JSON.stringify({ body }),
432
+ });
433
+ }
434
+
435
+ /** Add labels to an existing issue. */
436
+ export async function addLabelsToIssue(
437
+ token: string,
438
+ repo: string,
439
+ issueNumber: number,
440
+ labels: string[],
441
+ ): Promise<void> {
442
+ await ghFetch(token, `/repos/${repo}/issues/${issueNumber}/labels`, {
443
+ method: 'POST',
444
+ headers: { 'Content-Type': 'application/json' },
445
+ body: JSON.stringify({ labels }),
446
+ });
447
+ }
448
+
449
+ // ─── Git Data API (multi-file commits) (#36) ─────────────────
450
+
451
+ export async function createBlob(
452
+ token: string,
453
+ repo: string,
454
+ content: string,
455
+ encoding: 'utf-8' | 'base64' = 'utf-8',
456
+ ): Promise<string> {
457
+ const res = await ghFetch(token, `/repos/${repo}/git/blobs`, {
458
+ method: 'POST',
459
+ headers: { 'Content-Type': 'application/json' },
460
+ body: JSON.stringify({ content, encoding }),
461
+ });
462
+ const data = (await res.json()) as { sha: string };
463
+ return data.sha;
464
+ }
465
+
466
+ export async function getBaseTreeSha(
467
+ token: string,
468
+ repo: string,
469
+ commitSha: string,
470
+ ): Promise<string> {
471
+ const res = await ghFetch(token, `/repos/${repo}/git/commits/${commitSha}`);
472
+ const data = (await res.json()) as { tree: { sha: string } };
473
+ return data.tree.sha;
474
+ }
475
+
476
+ export async function createTree(
477
+ token: string,
478
+ repo: string,
479
+ baseTreeSha: string,
480
+ files: Array<{ path: string; blobSha: string }>,
481
+ ): Promise<string> {
482
+ const tree = files.map(f => ({
483
+ path: f.path,
484
+ mode: '100644' as const,
485
+ type: 'blob' as const,
486
+ sha: f.blobSha,
487
+ }));
488
+ const res = await ghFetch(token, `/repos/${repo}/git/trees`, {
489
+ method: 'POST',
490
+ headers: { 'Content-Type': 'application/json' },
491
+ body: JSON.stringify({ base_tree: baseTreeSha, tree }),
492
+ });
493
+ const data = (await res.json()) as { sha: string };
494
+ return data.sha;
495
+ }
496
+
497
+ export async function createGitCommit(
498
+ token: string,
499
+ repo: string,
500
+ message: string,
501
+ treeSha: string,
502
+ parentSha: string,
503
+ ): Promise<string> {
504
+ const res = await ghFetch(token, `/repos/${repo}/git/commits`, {
505
+ method: 'POST',
506
+ headers: { 'Content-Type': 'application/json' },
507
+ body: JSON.stringify({ message, tree: treeSha, parents: [parentSha] }),
508
+ });
509
+ const data = (await res.json()) as { sha: string };
510
+ return data.sha;
511
+ }
512
+
513
+ export async function updateRef(
514
+ token: string,
515
+ repo: string,
516
+ ref: string,
517
+ commitSha: string,
518
+ ): Promise<void> {
519
+ await ghFetch(token, `/repos/${repo}/git/refs/${ref}`, {
520
+ method: 'PATCH',
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: JSON.stringify({ sha: commitSha, force: false }),
523
+ });
524
+ }
525
+
526
+ // ─── CI Status (Actions + Status API) ────────────────────────
527
+
528
+ export interface WorkflowRun {
529
+ id: number;
530
+ name: string;
531
+ status: string;
532
+ conclusion: string | null;
533
+ branch: string;
534
+ event: string;
535
+ url: string;
536
+ created_at: string;
537
+ }
538
+
539
+ export async function listWorkflowRuns(
540
+ token: string,
541
+ repo: string,
542
+ branch?: string,
543
+ limit = 5,
544
+ ): Promise<WorkflowRun[]> {
545
+ const params = new URLSearchParams({ per_page: String(Math.min(limit, 20)) });
546
+ if (branch) params.set('branch', branch);
547
+ const res = await ghFetch(token, `/repos/${repo}/actions/runs?${params}`);
548
+ const data = (await res.json()) as {
549
+ workflow_runs: {
550
+ id: number;
551
+ name: string;
552
+ status: string;
553
+ conclusion: string | null;
554
+ head_branch: string;
555
+ event: string;
556
+ html_url: string;
557
+ created_at: string;
558
+ }[];
559
+ };
560
+ return data.workflow_runs.map((r) => ({
561
+ id: r.id,
562
+ name: r.name,
563
+ status: r.status,
564
+ conclusion: r.conclusion,
565
+ branch: r.head_branch,
566
+ event: r.event,
567
+ url: r.html_url,
568
+ created_at: r.created_at,
569
+ }));
570
+ }
571
+
572
+ export interface CombinedStatus {
573
+ state: string;
574
+ total: number;
575
+ statuses: { context: string; state: string; description: string }[];
576
+ }
577
+
578
+ export async function getCombinedStatus(
579
+ token: string,
580
+ repo: string,
581
+ ref: string,
582
+ ): Promise<CombinedStatus> {
583
+ const res = await ghFetch(token, `/repos/${repo}/commits/${encodeURIComponent(ref)}/status`);
584
+ const data = (await res.json()) as {
585
+ state: string;
586
+ total_count: number;
587
+ statuses: { context: string; state: string; description: string }[];
588
+ };
589
+ return {
590
+ state: data.state,
591
+ total: data.total_count,
592
+ statuses: data.statuses.map((s) => ({
593
+ context: s.context,
594
+ state: s.state,
595
+ description: s.description,
596
+ })),
597
+ };
598
+ }
599
+
600
+ // ─── PR Stats ────────────────────────────────────────────────
601
+
602
+ export interface PullRequestStats {
603
+ number: number;
604
+ state: string;
605
+ mergeable: boolean | null;
606
+ additions: number;
607
+ deletions: number;
608
+ changed_files: number;
609
+ head: string;
610
+ base: string;
611
+ }
612
+
613
+ export async function getPullRequestStats(
614
+ token: string,
615
+ repo: string,
616
+ pullNumber: number,
617
+ ): Promise<PullRequestStats> {
618
+ const res = await ghFetch(token, `/repos/${repo}/pulls/${pullNumber}`);
619
+ const data = (await res.json()) as {
620
+ number: number;
621
+ state: string;
622
+ mergeable: boolean | null;
623
+ additions: number;
624
+ deletions: number;
625
+ changed_files: number;
626
+ head: { ref: string };
627
+ base: { ref: string };
628
+ };
629
+ return {
630
+ number: data.number,
631
+ state: data.state,
632
+ mergeable: data.mergeable,
633
+ additions: data.additions,
634
+ deletions: data.deletions,
635
+ changed_files: data.changed_files,
636
+ head: data.head.ref,
637
+ base: data.base.ref,
638
+ };
639
+ }
640
+
641
+ // ─── PR Changed Files ────────────────────────────────────────
642
+
643
+ export async function getPullRequestFiles(
644
+ token: string,
645
+ repo: string,
646
+ pullNumber: number,
647
+ ): Promise<string[]> {
648
+ const res = await ghFetch(token, `/repos/${repo}/pulls/${pullNumber}/files?per_page=100`);
649
+ const data = (await res.json()) as { filename: string }[];
650
+ return data.map(f => f.filename);
651
+ }
652
+
653
+ // ─── PR Merge ────────────────────────────────────────────────
654
+
655
+ export async function mergePullRequest(
656
+ token: string,
657
+ repo: string,
658
+ pullNumber: number,
659
+ method: 'merge' | 'squash' | 'rebase' = 'squash',
660
+ commitMessage?: string,
661
+ ): Promise<{ sha: string; merged: boolean; message: string }> {
662
+ const body: Record<string, unknown> = { merge_method: method };
663
+ if (commitMessage) body.commit_message = commitMessage;
664
+ const res = await ghFetch(token, `/repos/${repo}/pulls/${pullNumber}/merge`, {
665
+ method: 'PUT',
666
+ headers: { 'Content-Type': 'application/json' },
667
+ body: JSON.stringify(body),
668
+ });
669
+ return (await res.json()) as { sha: string; merged: boolean; message: string };
670
+ }