@akagilnc/pi-workflow-roles 0.1.1751

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 (192) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +104 -0
  3. package/README.zh-CN.md +133 -0
  4. package/THIRD_PARTY_NOTICES.md +60 -0
  5. package/dist/activation-ledger-git.js +68 -0
  6. package/dist/activation-ledger-session.js +120 -0
  7. package/dist/activation-ledger-topology.js +239 -0
  8. package/dist/activation-reconciliation.js +61 -0
  9. package/dist/audit-escalation.js +108 -0
  10. package/dist/auditor-dossier-tool.js +35 -0
  11. package/dist/canonical-json.js +78 -0
  12. package/dist/compliance-transport.js +77 -0
  13. package/dist/doctor-contracts.js +172 -0
  14. package/dist/dossier-resolution.js +103 -0
  15. package/dist/evidence-child-executor.js +661 -0
  16. package/dist/exact-utf8.js +12 -0
  17. package/dist/git-object-id.js +7 -0
  18. package/dist/in-process-session.js +50 -0
  19. package/dist/merger-contracts.js +76 -0
  20. package/dist/navigator-attendance.js +995 -0
  21. package/dist/navigator-invocation-identity.js +220 -0
  22. package/dist/open-tool-schema.js +39 -0
  23. package/dist/package-contracts/collector-output.js +50 -0
  24. package/dist/package-contracts/fixer-output.js +72 -0
  25. package/dist/package-contracts/fixer-packet.js +77 -0
  26. package/dist/package-contracts/judge-output.js +17 -0
  27. package/dist/package-contracts/reviewer-output.js +82 -0
  28. package/dist/package-contracts/terminating-tools.js +173 -0
  29. package/dist/package-contracts/worker-output.js +13 -0
  30. package/dist/package-owned-tool-idle.js +104 -0
  31. package/dist/packaged-role-registry.js +34 -0
  32. package/dist/public-cli/main.js +23867 -0
  33. package/dist/public-command-renderer.js +20 -0
  34. package/dist/reviewer-agent.js +93 -0
  35. package/dist/reviewer-child-executor.js +23 -0
  36. package/dist/reviewer-construction.js +95 -0
  37. package/dist/reviewer-dispatch.js +77 -0
  38. package/dist/reviewer-execution-ledger.js +160 -0
  39. package/dist/reviewer-failure-diagnostic.js +17 -0
  40. package/dist/reviewer-git-snapshot.js +38 -0
  41. package/dist/reviewer-pinned-git.js +146 -0
  42. package/dist/reviewer-preflight-error.js +15 -0
  43. package/dist/reviewer-prompt-identity.js +10 -0
  44. package/dist/reviewer-scope-prompt.js +21 -0
  45. package/dist/reviewer-workspace.js +151 -0
  46. package/dist/sha256.js +5 -0
  47. package/dist/sitian-record-entry.js +33 -0
  48. package/dist/stderr-jsonl.js +26 -0
  49. package/dist/stream-idle-guard.js +75 -0
  50. package/dist/tool-execution-observation.js +141 -0
  51. package/dist/uuidv7.js +21 -0
  52. package/dist/work-subject-identity.js +53 -0
  53. package/extensions/role-runtime.ts +303 -0
  54. package/package.json +69 -0
  55. package/packets/fixer-prerequisites.json +6 -0
  56. package/packets/fixer-repair.md +5 -0
  57. package/packets/judge-apply.md +77 -0
  58. package/packets/judge-authority.md +64 -0
  59. package/packets/judge-plan.md +55 -0
  60. package/packets/judge-review.md +49 -0
  61. package/packets/judge-submission.md +34 -0
  62. package/resources/methods/code-review/SKILL.md +92 -0
  63. package/resources/methods/code-review/agents/openai.yaml +3 -0
  64. package/resources/methods/code-review/provenance.json +26 -0
  65. package/resources/methods/diagnosing-bugs/SKILL.md +134 -0
  66. package/resources/methods/diagnosing-bugs/agents/openai.yaml +3 -0
  67. package/resources/methods/diagnosing-bugs/provenance.json +31 -0
  68. package/resources/methods/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  69. package/resources/methods/resolving-merge-conflicts/SKILL.md +14 -0
  70. package/resources/methods/resolving-merge-conflicts/agents/openai.yaml +3 -0
  71. package/resources/methods/resolving-merge-conflicts/provenance.json +26 -0
  72. package/resources/methods/tdd/SKILL.md +38 -0
  73. package/resources/methods/tdd/agents/openai.yaml +3 -0
  74. package/resources/methods/tdd/mocking.md +59 -0
  75. package/resources/methods/tdd/provenance.json +36 -0
  76. package/resources/methods/tdd/tests.md +77 -0
  77. package/resources/navigator-route-playbook.md +32 -0
  78. package/schemas/tool-execution-observation.schema.json +107 -0
  79. package/scripts/build-package.mjs +65 -0
  80. package/scripts/generate-tool-execution-observation-schema.ts +7 -0
  81. package/souls/coder.md +10 -0
  82. package/souls/collector.md +11 -0
  83. package/souls/doctor-auditor.md +23 -0
  84. package/souls/doctor.md +8 -0
  85. package/souls/fixer-auditor.md +33 -0
  86. package/souls/fixer.md +13 -0
  87. package/souls/judge-auditor.md +33 -0
  88. package/souls/judge.md +74 -0
  89. package/souls/merger.md +5 -0
  90. package/souls/navigator.md +5 -0
  91. package/souls/reviewer-auditor.md +25 -0
  92. package/souls/reviewer.md +11 -0
  93. package/src/activation-ledger-git.ts +96 -0
  94. package/src/activation-ledger-session.ts +188 -0
  95. package/src/activation-ledger-topology.ts +301 -0
  96. package/src/activation-ledger.ts +240 -0
  97. package/src/activation-reconciliation.ts +163 -0
  98. package/src/activation-trace.ts +38 -0
  99. package/src/audit-escalation.ts +177 -0
  100. package/src/auditor-dossier-tool.ts +48 -0
  101. package/src/auditor-soul.ts +28 -0
  102. package/src/canonical-json.ts +74 -0
  103. package/src/canonical-skill-binding.ts +107 -0
  104. package/src/collector-config.ts +89 -0
  105. package/src/collector-evidence.ts +461 -0
  106. package/src/collector-github.ts +656 -0
  107. package/src/collector-identity.ts +161 -0
  108. package/src/collector-ledger.ts +827 -0
  109. package/src/collector-receipt.ts +87 -0
  110. package/src/collector-role.ts +592 -0
  111. package/src/collector-tool-schemas.ts +19 -0
  112. package/src/compliance-transport.ts +130 -0
  113. package/src/doctor-auditor.ts +53 -0
  114. package/src/doctor-contracts.ts +166 -0
  115. package/src/doctor-evidence.ts +47 -0
  116. package/src/doctor-role.ts +18 -0
  117. package/src/dossier-resolution.ts +137 -0
  118. package/src/evidence-child-executor.ts +775 -0
  119. package/src/exact-utf8.ts +9 -0
  120. package/src/factory-board.ts +1822 -0
  121. package/src/git-object-id.ts +11 -0
  122. package/src/human-format.ts +65 -0
  123. package/src/in-process-session.ts +78 -0
  124. package/src/judge-auditor.ts +55 -0
  125. package/src/judge-recording-anti-forge.ts +53 -0
  126. package/src/judge-role.ts +160 -0
  127. package/src/merger-contracts.ts +71 -0
  128. package/src/merger-git-state.ts +76 -0
  129. package/src/merger-role.ts +60 -0
  130. package/src/navigator-attendance.ts +1254 -0
  131. package/src/navigator-invocation-identity.ts +446 -0
  132. package/src/open-tool-schema.ts +46 -0
  133. package/src/package-contracts/collector-output.ts +109 -0
  134. package/src/package-contracts/fixer-output.ts +81 -0
  135. package/src/package-contracts/fixer-packet.ts +93 -0
  136. package/src/package-contracts/judge-output.ts +39 -0
  137. package/src/package-contracts/reviewer-output.ts +115 -0
  138. package/src/package-contracts/terminating-tools.ts +259 -0
  139. package/src/package-contracts/worker-output.ts +36 -0
  140. package/src/package-owned-tool-idle.ts +134 -0
  141. package/src/package-resources/method-skill-binding.ts +87 -0
  142. package/src/package-resources/method-skill.ts +358 -0
  143. package/src/packaged-role-registry.ts +36 -0
  144. package/src/public-cli/cli-errors.ts +11 -0
  145. package/src/public-cli/cli-io.ts +4 -0
  146. package/src/public-cli/cli.ts +912 -0
  147. package/src/public-cli/coder-run.ts +575 -0
  148. package/src/public-cli/collector-run.ts +375 -0
  149. package/src/public-cli/command-renderer.ts +8 -0
  150. package/src/public-cli/config.ts +346 -0
  151. package/src/public-cli/doctor-run.ts +355 -0
  152. package/src/public-cli/explicit-internal.ts +274 -0
  153. package/src/public-cli/fixer-run.ts +587 -0
  154. package/src/public-cli/host-pi-runtime.ts +112 -0
  155. package/src/public-cli/invocation.ts +1958 -0
  156. package/src/public-cli/judge-run.ts +507 -0
  157. package/src/public-cli/main.ts +15 -0
  158. package/src/public-cli/merger-run.ts +681 -0
  159. package/src/public-cli/public-run-credentials.ts +71 -0
  160. package/src/public-cli/registry.ts +153 -0
  161. package/src/public-cli/reviewer-run.ts +561 -0
  162. package/src/public-cli/run-lifecycle.ts +884 -0
  163. package/src/public-cli/settlement.ts +3765 -0
  164. package/src/public-cli/terminal.ts +325 -0
  165. package/src/public-command-renderer.ts +43 -0
  166. package/src/reviewer-agent.ts +94 -0
  167. package/src/reviewer-auditor.ts +53 -0
  168. package/src/reviewer-child-executor.ts +31 -0
  169. package/src/reviewer-construction.ts +137 -0
  170. package/src/reviewer-dispatch.ts +94 -0
  171. package/src/reviewer-execution-ledger.ts +206 -0
  172. package/src/reviewer-failure-diagnostic.ts +18 -0
  173. package/src/reviewer-git-snapshot.ts +53 -0
  174. package/src/reviewer-pinned-git.ts +144 -0
  175. package/src/reviewer-preflight-error.ts +14 -0
  176. package/src/reviewer-prompt-identity.ts +17 -0
  177. package/src/reviewer-role.ts +193 -0
  178. package/src/reviewer-scope-prompt.ts +24 -0
  179. package/src/reviewer-settlement.ts +63 -0
  180. package/src/reviewer-workspace.ts +111 -0
  181. package/src/role-runtime.ts +884 -0
  182. package/src/sha256.ts +6 -0
  183. package/src/sitian-record-entry.ts +57 -0
  184. package/src/stderr-jsonl.ts +28 -0
  185. package/src/stream-idle-guard.ts +98 -0
  186. package/src/ticket-snapshot.ts +662 -0
  187. package/src/ticket-trajectory.ts +1000 -0
  188. package/src/tool-execution-observation.ts +168 -0
  189. package/src/uuidv7.ts +1 -0
  190. package/src/work-subject-identity.ts +94 -0
  191. package/src/worker-role.ts +434 -0
  192. package/src/worker-submission-gates.ts +225 -0
@@ -0,0 +1,656 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+
4
+ export type GitHubPullRequest = {
5
+ number: number;
6
+ state: string;
7
+ headOid: string;
8
+ updatedAt?: string;
9
+ url: string;
10
+ raw: unknown;
11
+ };
12
+
13
+ export type GitHubUser = {
14
+ login: string;
15
+ raw: unknown;
16
+ };
17
+
18
+ export type GitHubMachineIdentity = {
19
+ userType: string;
20
+ userId: number;
21
+ appId?: number;
22
+ };
23
+
24
+ export type GitHubPullRequestReaction = {
25
+ id: number;
26
+ userLogin: string | null;
27
+ machineIdentity?: GitHubMachineIdentity | null;
28
+ content: string;
29
+ createdAt: string;
30
+ raw: unknown;
31
+ };
32
+
33
+ export type GitHubReview = {
34
+ id: number;
35
+ nodeId?: string;
36
+ /** Null when GitHub tombstones the author (`user: null`). */
37
+ userLogin: string | null;
38
+ machineIdentity?: GitHubMachineIdentity | null;
39
+ state: string;
40
+ body: string;
41
+ commitId: string | null;
42
+ submittedAt: string | null;
43
+ htmlUrl: string;
44
+ raw: unknown;
45
+ };
46
+
47
+ export type GitHubIssueComment = {
48
+ id: number;
49
+ /** Null when GitHub tombstones the author (`user: null`). */
50
+ userLogin: string | null;
51
+ machineIdentity?: GitHubMachineIdentity | null;
52
+ body: string;
53
+ createdAt: string;
54
+ updatedAt: string;
55
+ htmlUrl: string;
56
+ raw: unknown;
57
+ };
58
+
59
+ export type GitHubReviewComment = {
60
+ id: number;
61
+ pullRequestReviewId: number | null;
62
+ /** Null when GitHub tombstones the author (`user: null`). */
63
+ userLogin: string | null;
64
+ machineIdentity?: GitHubMachineIdentity | null;
65
+ body: string;
66
+ path: string;
67
+ line: number | null;
68
+ originalLine: number | null;
69
+ side: string | null;
70
+ position: number | null;
71
+ originalPosition: number | null;
72
+ commitId: string | null;
73
+ originalCommitId: string | null;
74
+ createdAt: string;
75
+ updatedAt: string;
76
+ htmlUrl: string;
77
+ raw: unknown;
78
+ };
79
+
80
+ export type GitHubPageDiagnostics = {
81
+ path: string;
82
+ page: number;
83
+ status: number;
84
+ itemCount: number;
85
+ linkHeader?: string;
86
+ };
87
+
88
+ export type GitHubCreateCommentResult =
89
+ | {
90
+ kind: "success";
91
+ comment: GitHubIssueComment;
92
+ }
93
+ | {
94
+ kind: "ambiguous_loss";
95
+ diagnostics: string;
96
+ cause?: { name: string; message: string; evidenceId: string };
97
+ }
98
+ | {
99
+ kind: "rejected";
100
+ status?: number;
101
+ diagnostics: string;
102
+ };
103
+
104
+ export type CollectorGitHubTransport = {
105
+ getAuthenticatedUser(options?: {
106
+ signal?: AbortSignal;
107
+ }): Promise<GitHubUser>;
108
+ getPullRequest(input: {
109
+ owner: string;
110
+ repo: string;
111
+ prNumber: number;
112
+ signal?: AbortSignal;
113
+ }): Promise<GitHubPullRequest>;
114
+ listPullRequestReviews(input: {
115
+ owner: string;
116
+ repo: string;
117
+ prNumber: number;
118
+ signal?: AbortSignal;
119
+ /** Charge observation budget before aggregate append / next-page fetch. */
120
+ retainPage?: (items: GitHubReview[]) => void;
121
+ }): Promise<{ items: GitHubReview[]; pages: GitHubPageDiagnostics[] }>;
122
+ listPullRequestReactions?(input: {
123
+ owner: string;
124
+ repo: string;
125
+ prNumber: number;
126
+ signal?: AbortSignal;
127
+ retainPage?: (items: GitHubPullRequestReaction[]) => void;
128
+ }): Promise<{ items: GitHubPullRequestReaction[]; pages: GitHubPageDiagnostics[] }>;
129
+ listIssueComments(input: {
130
+ owner: string;
131
+ repo: string;
132
+ prNumber: number;
133
+ signal?: AbortSignal;
134
+ retainPage?: (items: GitHubIssueComment[]) => void;
135
+ }): Promise<{ items: GitHubIssueComment[]; pages: GitHubPageDiagnostics[] }>;
136
+ listReviewComments(input: {
137
+ owner: string;
138
+ repo: string;
139
+ prNumber: number;
140
+ signal?: AbortSignal;
141
+ retainPage?: (items: GitHubReviewComment[]) => void;
142
+ }): Promise<{ items: GitHubReviewComment[]; pages: GitHubPageDiagnostics[] }>;
143
+ createIssueComment(input: {
144
+ owner: string;
145
+ repo: string;
146
+ prNumber: number;
147
+ body: string;
148
+ signal?: AbortSignal;
149
+ }): Promise<GitHubCreateCommentResult>;
150
+ };
151
+
152
+ export type GhApiResponse = {
153
+ status: number;
154
+ headers: Record<string, string>;
155
+ bodyText: string;
156
+ };
157
+
158
+ export type GhApiRunner = (
159
+ args: string[],
160
+ options?: { stdin?: string; signal?: AbortSignal },
161
+ ) => Promise<GhApiResponse>;
162
+
163
+ function isRecord(value: unknown): value is Record<string, unknown> {
164
+ return typeof value === "object" && value !== null && !Array.isArray(value);
165
+ }
166
+
167
+ function requireString(value: unknown, label: string): string {
168
+ if (typeof value !== "string") {
169
+ throw new Error(`GitHub payload missing string ${label}`);
170
+ }
171
+ return value;
172
+ }
173
+
174
+ function requireNumber(value: unknown, label: string): number {
175
+ if (typeof value !== "number" || !Number.isFinite(value)) {
176
+ throw new Error(`GitHub payload missing number ${label}`);
177
+ }
178
+ return value;
179
+ }
180
+
181
+ function optionalString(value: unknown): string | null {
182
+ return typeof value === "string" ? value : null;
183
+ }
184
+
185
+ function parseLinkNext(linkHeader: string | undefined): string | undefined {
186
+ if (linkHeader === undefined || linkHeader.length === 0) return undefined;
187
+ for (const part of linkHeader.split(",")) {
188
+ const match = part.trim().match(/^<([^>]+)>\s*;\s*rel="next"$/i);
189
+ if (match?.[1]) return match[1];
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ function parseJson(text: string, label: string): unknown {
195
+ try {
196
+ return JSON.parse(text);
197
+ } catch (error) {
198
+ throw new Error(`GitHub ${label} returned malformed JSON`, { cause: error });
199
+ }
200
+ }
201
+
202
+ let commentFailureEvidence = 0;
203
+ function commentFailureCause(error: unknown) {
204
+ return {
205
+ name: error instanceof Error ? error.name : typeof error,
206
+ message: error instanceof Error ? error.message : String(error),
207
+ evidenceId: `github-comment-failure-${++commentFailureEvidence}`,
208
+ };
209
+ }
210
+
211
+ /** Authenticated /user requires a login; throws when absent. */
212
+ function requireUserLogin(raw: unknown): string {
213
+ if (!isRecord(raw) || typeof raw["login"] !== "string") {
214
+ throw new Error("GitHub payload missing user.login");
215
+ }
216
+ return raw["login"];
217
+ }
218
+
219
+ /**
220
+ * Surface authors may be tombstoned only as literal JSON null (`user: null`).
221
+ * Preserve that case with null login; every other shape fails closed.
222
+ */
223
+ function optionalUserLogin(raw: unknown): string | null {
224
+ if (raw === null) return null;
225
+ if (!isRecord(raw) || typeof raw["login"] !== "string") {
226
+ throw new Error("GitHub payload missing user.login");
227
+ }
228
+ return raw["login"];
229
+ }
230
+
231
+ function machineIdentity(raw: Record<string, unknown>): GitHubMachineIdentity | null {
232
+ const user = raw["user"];
233
+ if (!isRecord(user) || typeof user["type"] !== "string" || typeof user["id"] !== "number") {
234
+ return null;
235
+ }
236
+ const app = raw["performed_via_github_app"];
237
+ const appId = isRecord(app) && typeof app["id"] === "number" ? app["id"] : undefined;
238
+ return {
239
+ userType: user["type"],
240
+ userId: user["id"],
241
+ ...(appId === undefined ? {} : { appId }),
242
+ };
243
+ }
244
+
245
+ export function normalizePullRequest(raw: unknown): GitHubPullRequest {
246
+ if (!isRecord(raw)) throw new Error("GitHub pull request payload must be an object");
247
+ const head = raw["head"];
248
+ if (!isRecord(head) || typeof head["sha"] !== "string" || head["sha"].length === 0) {
249
+ throw new Error("GitHub pull request payload missing head.sha");
250
+ }
251
+ const number = requireNumber(raw["number"], "number");
252
+ const state = requireString(raw["state"], "state").toUpperCase();
253
+ const htmlUrl = typeof raw["html_url"] === "string"
254
+ ? raw["html_url"]
255
+ : `https://github.com/unknown/unknown/pull/${number}`;
256
+ return {
257
+ number,
258
+ state: state === "OPEN" || state === "open" ? "OPEN" : state,
259
+ headOid: head["sha"],
260
+ ...(typeof raw["updated_at"] === "string" ? { updatedAt: raw["updated_at"] } : {}),
261
+ url: htmlUrl,
262
+ raw,
263
+ };
264
+ }
265
+
266
+ export function normalizePullRequestReaction(raw: unknown): GitHubPullRequestReaction {
267
+ if (!isRecord(raw)) throw new Error("GitHub reaction payload must be an object");
268
+ return {
269
+ id: requireNumber(raw["id"], "reaction.id"),
270
+ userLogin: optionalUserLogin(raw["user"]),
271
+ machineIdentity: machineIdentity(raw),
272
+ content: requireString(raw["content"], "reaction.content"),
273
+ createdAt: requireString(raw["created_at"], "reaction.created_at"),
274
+ raw,
275
+ };
276
+ }
277
+
278
+ export function normalizeReview(raw: unknown): GitHubReview {
279
+ if (!isRecord(raw)) throw new Error("GitHub review payload must be an object");
280
+ return {
281
+ id: requireNumber(raw["id"], "review.id"),
282
+ ...(typeof raw["node_id"] === "string" ? { nodeId: raw["node_id"] } : {}),
283
+ userLogin: optionalUserLogin(raw["user"]),
284
+ machineIdentity: machineIdentity(raw),
285
+ state: requireString(raw["state"], "review.state").toUpperCase(),
286
+ body: typeof raw["body"] === "string" ? raw["body"] : "",
287
+ commitId: optionalString(raw["commit_id"]),
288
+ submittedAt: optionalString(raw["submitted_at"]),
289
+ htmlUrl: typeof raw["html_url"] === "string" ? raw["html_url"] : "",
290
+ raw,
291
+ };
292
+ }
293
+
294
+ export function normalizeIssueComment(raw: unknown): GitHubIssueComment {
295
+ if (!isRecord(raw)) throw new Error("GitHub issue comment payload must be an object");
296
+ return {
297
+ id: requireNumber(raw["id"], "comment.id"),
298
+ userLogin: optionalUserLogin(raw["user"]),
299
+ machineIdentity: machineIdentity(raw),
300
+ body: typeof raw["body"] === "string" ? raw["body"] : "",
301
+ createdAt: requireString(raw["created_at"], "comment.created_at"),
302
+ updatedAt: requireString(raw["updated_at"], "comment.updated_at"),
303
+ htmlUrl: typeof raw["html_url"] === "string" ? raw["html_url"] : "",
304
+ raw,
305
+ };
306
+ }
307
+
308
+ export function normalizeReviewComment(raw: unknown): GitHubReviewComment {
309
+ if (!isRecord(raw)) throw new Error("GitHub review comment payload must be an object");
310
+ return {
311
+ id: requireNumber(raw["id"], "review_comment.id"),
312
+ pullRequestReviewId: typeof raw["pull_request_review_id"] === "number"
313
+ ? raw["pull_request_review_id"]
314
+ : null,
315
+ userLogin: optionalUserLogin(raw["user"]),
316
+ machineIdentity: machineIdentity(raw),
317
+ body: typeof raw["body"] === "string" ? raw["body"] : "",
318
+ path: requireString(raw["path"], "review_comment.path"),
319
+ line: typeof raw["line"] === "number" ? raw["line"] : null,
320
+ originalLine: typeof raw["original_line"] === "number" ? raw["original_line"] : null,
321
+ side: optionalString(raw["side"]),
322
+ position: typeof raw["position"] === "number" ? raw["position"] : null,
323
+ originalPosition: typeof raw["original_position"] === "number"
324
+ ? raw["original_position"]
325
+ : null,
326
+ commitId: optionalString(raw["commit_id"]),
327
+ originalCommitId: optionalString(raw["original_commit_id"]),
328
+ createdAt: requireString(raw["created_at"], "review_comment.created_at"),
329
+ updatedAt: requireString(raw["updated_at"], "review_comment.updated_at"),
330
+ htmlUrl: typeof raw["html_url"] === "string" ? raw["html_url"] : "",
331
+ raw,
332
+ };
333
+ }
334
+
335
+ export function createGhApiRunner(
336
+ options: {
337
+ spawnImpl?: typeof spawn;
338
+ env?: NodeJS.ProcessEnv;
339
+ } = {},
340
+ ): GhApiRunner {
341
+ const spawnImpl = options.spawnImpl ?? spawn;
342
+ return async (args, runOptions = {}) => {
343
+ return await new Promise<GhApiResponse>((resolve, reject) => {
344
+ const signal = runOptions.signal;
345
+ if (signal?.aborted) {
346
+ reject(signal.reason ?? new Error("aborted"));
347
+ return;
348
+ }
349
+ const child = spawnImpl("gh", args, {
350
+ env: options.env ?? process.env,
351
+ stdio: ["pipe", "pipe", "pipe"],
352
+ });
353
+ let stdout = "";
354
+ let stderr = "";
355
+ let settled = false;
356
+ const settle = (fn: () => void) => {
357
+ if (settled) return;
358
+ settled = true;
359
+ if (signal !== undefined) {
360
+ signal.removeEventListener("abort", onAbort);
361
+ }
362
+ fn();
363
+ };
364
+ const onAbort = () => {
365
+ try {
366
+ child.kill("SIGTERM");
367
+ } catch (error) {
368
+ settle(() => reject(error));
369
+ return;
370
+ }
371
+ settle(() => {
372
+ reject(signal?.reason ?? new Error("aborted"));
373
+ });
374
+ };
375
+ if (signal !== undefined) {
376
+ signal.addEventListener("abort", onAbort, { once: true });
377
+ }
378
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
379
+ stdout += chunk;
380
+ });
381
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
382
+ stderr += chunk;
383
+ });
384
+ child.on("error", (error) => {
385
+ settle(() => reject(error));
386
+ });
387
+ // Attach before write/end so early child exit cannot raise uncaught EPIPE.
388
+ child.stdin.on("error", (error) => {
389
+ settle(() => {
390
+ if (signal?.aborted) {
391
+ reject(signal.reason ?? new Error("aborted"));
392
+ return;
393
+ }
394
+ const err = error instanceof Error ? error : new Error(String(error));
395
+ reject(Object.assign(err, { ambiguousGhFailure: true }));
396
+ });
397
+ });
398
+ if (runOptions.stdin !== undefined) {
399
+ child.stdin.write(runOptions.stdin);
400
+ }
401
+ child.stdin.end();
402
+ child.on("close", (code, signal) => {
403
+ settle(() => {
404
+ // gh api --include prints: HTTP/ headers blank-line body
405
+ const match = stdout.match(/^HTTP\/[\d.]+\s+(\d+)[^\n]*\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)$/);
406
+ if (match) {
407
+ const status = Number(match[1]);
408
+ const headerText = match[2] ?? "";
409
+ const bodyText = match[3] ?? "";
410
+ const headers: Record<string, string> = {};
411
+ for (const line of headerText.split(/\r?\n/)) {
412
+ const idx = line.indexOf(":");
413
+ if (idx === -1) continue;
414
+ const name = line.slice(0, idx).trim().toLowerCase();
415
+ const value = line.slice(idx + 1).trim();
416
+ headers[name] = value;
417
+ }
418
+ resolve({ status, headers, bodyText });
419
+ return;
420
+ }
421
+ if (code === 0) {
422
+ resolve({ status: 200, headers: {}, bodyText: stdout });
423
+ return;
424
+ }
425
+ // Ambiguous: process failed without parseable HTTP response
426
+ const failure = new Error(
427
+ `gh api failed without a parseable HTTP response (code=${String(code)}): ${stderr || stdout}`,
428
+ { cause: { code, signal, stderr, stdout } },
429
+ );
430
+ reject(Object.assign(failure, { ambiguousGhFailure: true, stderr, stdout, code, signal }));
431
+ });
432
+ });
433
+ });
434
+ };
435
+ }
436
+
437
+ export function createGhCollectorGitHubTransport(
438
+ runner: GhApiRunner = createGhApiRunner(),
439
+ ): CollectorGitHubTransport {
440
+ const hostnameArgs = ["api", "--hostname", "github.com", "--include"] as const;
441
+
442
+ async function apiGet(
443
+ path: string,
444
+ signal?: AbortSignal,
445
+ ): Promise<GhApiResponse> {
446
+ return await runner(
447
+ [...hostnameArgs, "-X", "GET", path],
448
+ signal === undefined ? {} : { signal },
449
+ );
450
+ }
451
+
452
+ async function paginate<T>(
453
+ path: string,
454
+ mapItem: (raw: unknown) => T,
455
+ options: {
456
+ signal?: AbortSignal;
457
+ retainPage?: (items: T[]) => void;
458
+ } = {},
459
+ ): Promise<{ items: T[]; pages: GitHubPageDiagnostics[] }> {
460
+ const { signal, retainPage } = options;
461
+ const items: T[] = [];
462
+ const pages: GitHubPageDiagnostics[] = [];
463
+ let nextPath: string | undefined = path;
464
+ let page = 1;
465
+ const seen = new Set<string>();
466
+ while (nextPath !== undefined) {
467
+ if (signal?.aborted) {
468
+ throw signal.reason ?? new Error("aborted");
469
+ }
470
+ if (seen.has(nextPath)) {
471
+ throw new Error(`GitHub pagination repeated page: ${nextPath}`);
472
+ }
473
+ seen.add(nextPath);
474
+ const response = await apiGet(nextPath, signal);
475
+ const diagnostics: GitHubPageDiagnostics = {
476
+ path: nextPath,
477
+ page,
478
+ status: response.status,
479
+ itemCount: 0,
480
+ ...(response.headers["link"] === undefined
481
+ ? {}
482
+ : { linkHeader: response.headers["link"] }),
483
+ };
484
+ if (response.status === 429) {
485
+ throw Object.assign(
486
+ new Error(`GitHub API rate limited on ${nextPath} (HTTP 429)`),
487
+ { githubStatus: 429, page: diagnostics },
488
+ );
489
+ }
490
+ if (response.status < 200 || response.status >= 300) {
491
+ throw Object.assign(
492
+ new Error(`GitHub API ${nextPath} failed with HTTP ${response.status}`),
493
+ { githubStatus: response.status, page: diagnostics },
494
+ );
495
+ }
496
+ const parsed = parseJson(response.bodyText, nextPath);
497
+ if (!Array.isArray(parsed)) {
498
+ throw new Error(`GitHub API ${nextPath} did not return a JSON array`);
499
+ }
500
+ diagnostics.itemCount = parsed.length;
501
+ pages.push(diagnostics);
502
+ const pageItems = parsed.map((entry) => mapItem(entry));
503
+ // Observation budget hook: charge before aggregate append / next-page fetch.
504
+ retainPage?.(pageItems);
505
+ for (const item of pageItems) items.push(item);
506
+ const nextUrl = parseLinkNext(response.headers["link"]);
507
+ if (nextUrl === undefined) {
508
+ nextPath = undefined;
509
+ } else {
510
+ // Accept absolute or path-style next links; normalize to API path + query.
511
+ if (nextUrl.startsWith("/")) {
512
+ nextPath = nextUrl;
513
+ } else {
514
+ const url = new URL(nextUrl);
515
+ if (url.hostname !== "api.github.com" && url.hostname !== "github.com") {
516
+ throw new Error(`unexpected pagination host ${url.hostname}`);
517
+ }
518
+ nextPath = `${url.pathname}${url.search}`;
519
+ if (nextPath.startsWith("/api/v3/")) {
520
+ nextPath = nextPath.slice("/api/v3".length);
521
+ }
522
+ }
523
+ }
524
+ page += 1;
525
+ }
526
+ return { items, pages };
527
+ }
528
+
529
+ return {
530
+ async getAuthenticatedUser(options = {}) {
531
+ const response = await apiGet("/user", options.signal);
532
+ if (response.status < 200 || response.status >= 300) {
533
+ throw new Error(`GitHub /user failed with HTTP ${response.status}`, { cause: { endpoint: "/user", status: response.status, headers: response.headers, body: response.bodyText } });
534
+ }
535
+ const raw = parseJson(response.bodyText, "/user");
536
+ return { login: requireUserLogin(raw).toLowerCase(), raw };
537
+ },
538
+
539
+ async getPullRequest(input) {
540
+ const path =
541
+ `/repos/${input.owner}/${input.repo}/pulls/${input.prNumber}`;
542
+ const response = await apiGet(path, input.signal);
543
+ if (response.status < 200 || response.status >= 300) {
544
+ throw new Error(`GitHub ${path} failed with HTTP ${response.status}`, { cause: { endpoint: path, status: response.status, headers: response.headers, body: response.bodyText } });
545
+ }
546
+ return normalizePullRequest(parseJson(response.bodyText, path));
547
+ },
548
+
549
+ async listPullRequestReviews(input) {
550
+ const path =
551
+ `/repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/reviews?per_page=100`;
552
+ return await paginate(path, normalizeReview, {
553
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
554
+ ...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
555
+ });
556
+ },
557
+
558
+ async listPullRequestReactions(input) {
559
+ const path =
560
+ `/repos/${input.owner}/${input.repo}/issues/${input.prNumber}/reactions?per_page=100`;
561
+ return await paginate(path, normalizePullRequestReaction, {
562
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
563
+ ...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
564
+ });
565
+ },
566
+
567
+ async listIssueComments(input) {
568
+ const path =
569
+ `/repos/${input.owner}/${input.repo}/issues/${input.prNumber}/comments?per_page=100`;
570
+ return await paginate(path, normalizeIssueComment, {
571
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
572
+ ...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
573
+ });
574
+ },
575
+
576
+ async listReviewComments(input) {
577
+ const path =
578
+ `/repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/comments?per_page=100`;
579
+ return await paginate(path, normalizeReviewComment, {
580
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
581
+ ...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
582
+ });
583
+ },
584
+
585
+ async createIssueComment(input) {
586
+ const path =
587
+ `/repos/${input.owner}/${input.repo}/issues/${input.prNumber}/comments`;
588
+ try {
589
+ const response = await runner(
590
+ [...hostnameArgs, "-X", "POST", path, "--input", "-"],
591
+ input.signal === undefined
592
+ ? { stdin: JSON.stringify({ body: input.body }) }
593
+ : { stdin: JSON.stringify({ body: input.body }), signal: input.signal },
594
+ );
595
+ if (response.status >= 200 && response.status < 300) {
596
+ // Contract: README.md#Collector — a successful write with unreadable response is recoverable only as an observable ambiguous result.
597
+ // 2xx means the comment may already exist; parse/normalize failure is
598
+ // ambiguous_loss so ledger marker recovery can resolve without repost.
599
+ try {
600
+ return {
601
+ kind: "success",
602
+ comment: normalizeIssueComment(parseJson(response.bodyText, path)),
603
+ };
604
+ } catch (error) {
605
+ const cause = commentFailureCause(error);
606
+ return { kind: "ambiguous_loss", diagnostics: cause.message, cause };
607
+ }
608
+ }
609
+ return {
610
+ kind: "rejected",
611
+ status: response.status,
612
+ diagnostics: `HTTP ${response.status}: ${response.bodyText.slice(0, 500)}`,
613
+ };
614
+ } catch (error) {
615
+ // Signal state is authoritative cancel; rethrow exact caught value.
616
+ if (input.signal?.aborted) {
617
+ throw error;
618
+ }
619
+ // Non-aborted transport tag only (after signal).
620
+ if (isRecord(error) && error["ambiguousGhFailure"] === true) {
621
+ const cause = commentFailureCause(error);
622
+ return { kind: "ambiguous_loss", diagnostics: cause.message, cause };
623
+ }
624
+ // Non-signal AbortError belt only (after tag).
625
+ if (isRecord(error) && error["name"] === "AbortError") {
626
+ throw error;
627
+ }
628
+ throw error;
629
+ }
630
+ },
631
+ };
632
+ }
633
+
634
+ export function buildCollectorRequestMarker(input: {
635
+ manifestDigest: string;
636
+ requestId: string;
637
+ headOid: string;
638
+ }): string {
639
+ const prefix = input.manifestDigest.slice(0, 12);
640
+ const requestMarkerId = createHash("sha256").update(input.requestId).digest("hex");
641
+ return `<!-- ak-collector:v1 manifest=${prefix} request=${requestMarkerId} head=${input.headOid} -->`;
642
+ }
643
+
644
+ export function buildCollectorRequestBody(input: {
645
+ configuredBody: string;
646
+ manifestDigest: string;
647
+ requestId: string;
648
+ headOid: string;
649
+ }): { body: string; marker: string } {
650
+ const marker = buildCollectorRequestMarker(input);
651
+ // Preserve configured body byte-for-byte; append marker on its own line.
652
+ const body = input.configuredBody.endsWith("\n")
653
+ ? `${input.configuredBody}${marker}\n`
654
+ : `${input.configuredBody}\n${marker}\n`;
655
+ return { body, marker };
656
+ }