@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-host/production-host.js +20239 -10556
- package/dist/analyst-gate-cycles-read.js +387 -0
- package/dist/archivist-record-entry.js +22 -1
- package/dist/audit-escalation.js +6 -0
- package/dist/auditor-soul.js +74 -0
- package/dist/collector-config.js +86 -0
- package/dist/collector-evidence.js +316 -0
- package/dist/collector-github.js +527 -0
- package/dist/collector-ledger.js +853 -0
- package/dist/collector-tool-schemas.js +33 -0
- package/dist/compliance-transport.js +104 -52
- package/dist/diarist-mechanical.js +411 -0
- package/dist/diarist-ticket-resolution.js +177 -0
- package/dist/diarist.js +311 -0
- package/dist/doctor-auditor.js +25 -0
- package/dist/doctor-contracts.js +2 -0
- package/dist/doctor-evidence.js +142 -0
- package/dist/gatekeeper-role.js +104 -81
- package/dist/host-transition-prior-native.js +78 -0
- package/dist/institutional-resolution.js +1 -95
- package/dist/judge-auditor.js +26 -0
- package/dist/ledger-session-read.js +227 -0
- package/dist/merger-git-state.js +78 -0
- package/dist/navigator-attendance.js +15 -7
- package/dist/navigator-public-session.js +170 -0
- package/dist/navigator-session-contracts.js +36 -40
- package/dist/notary-source-run.js +122 -0
- package/dist/package-contracts/auditor-output.js +66 -0
- package/dist/package-contracts/evidence-child-output.js +34 -0
- package/dist/package-contracts/judge-output.js +2 -0
- package/dist/package-contracts/terminating-tools.js +25 -2
- package/dist/package-resources/method-skill.js +254 -0
- package/dist/packaged-role-registry.js +36 -0
- package/dist/pi/durable-principal.js +61 -0
- package/dist/pi/in-process-session.js +25 -6
- package/dist/pi/known-failure.js +52 -0
- package/dist/pi/role-turn-host.js +430 -0
- package/dist/public-cli/auto-resume.js +414 -0
- package/dist/public-cli/cli-errors.js +8 -0
- package/dist/public-cli/cli-io.js +1 -0
- package/dist/public-cli/command-renderer.js +5 -0
- package/dist/public-cli/doctor-run.js +84 -0
- package/dist/public-cli/inspector-run.js +136 -0
- package/dist/public-cli/instruction-seat-run.js +256 -0
- package/dist/public-cli/invocation.js +2225 -0
- package/dist/public-cli/judge-run.js +118 -0
- package/dist/public-cli/load-production-acp-host.js +40 -0
- package/dist/public-cli/main.js +1140 -652
- package/dist/public-cli/notary-run.js +161 -0
- package/dist/public-cli/option-definitions.js +1192 -0
- package/dist/public-cli/post-admission.js +667 -0
- package/dist/public-cli/public-run-credentials.js +49 -0
- package/dist/public-cli/registry.js +4 -1
- package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
- package/dist/public-cli/run-lifecycle.js +1344 -0
- package/dist/public-cli/seat-ticket-binding.js +113 -0
- package/dist/public-cli/settlement.js +3441 -0
- package/dist/public-cli/terminal.js +135 -0
- package/dist/public-cli/turn-request.js +25 -0
- package/dist/public-role-summons.js +301 -0
- package/dist/receipt-delivery-policy.js +10 -0
- package/dist/reviewer-child-executor.js +80 -8
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/run-terminal-artifacts.js +195 -0
- package/dist/run-ticket-number.js +40 -0
- package/dist/session-assistant-usage.js +107 -0
- package/dist/shape-unreadable-failure.js +34 -0
- package/dist/submission-errors.js +3 -0
- package/dist/submission-ledger.js +388 -0
- package/dist/ticket-provenance-contracts.js +115 -0
- package/dist/ticket-provenance.js +340 -0
- package/extensions/role-runtime.ts +7 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -1
- package/souls/doctor-auditor.md +1 -0
- package/souls/evidence-child.md +1 -0
- package/src/acp-host/production-host.ts +3 -0
- package/src/acp-host/role-envelope.ts +2 -2
- package/src/acp-host/role-turn-host.ts +9 -1
- package/src/analyst-gate-cycles-read.ts +37 -2
- package/src/archivist-record-entry.ts +45 -1
- package/src/audit-escalation.ts +17 -0
- package/src/auditor-role.ts +22 -0
- package/src/auditor-soul.ts +42 -0
- package/src/compliance-transport.ts +177 -66
- package/src/doctor-auditor.ts +10 -14
- package/src/doctor-contracts.ts +1 -0
- package/src/doctor-role.ts +2 -2
- package/src/evidence-child-role.ts +22 -0
- package/src/gatekeeper-pass-envelope.ts +109 -0
- package/src/gatekeeper-role.ts +145 -104
- package/src/host-contracts.ts +9 -1
- package/src/institutional-resolution.ts +8 -163
- package/src/judge-auditor.ts +10 -15
- package/src/judge-role.ts +8 -0
- package/src/navigator-attendance.ts +25 -9
- package/src/navigator-public-session.ts +217 -0
- package/src/navigator-session-contracts.ts +61 -54
- package/src/notary-source-run.ts +3 -1
- package/src/package-contracts/auditor-output.ts +82 -0
- package/src/package-contracts/evidence-child-output.ts +51 -0
- package/src/package-contracts/judge-output.ts +1 -0
- package/src/package-contracts/reviewer-output.ts +2 -2
- package/src/package-contracts/terminating-tools.ts +31 -1
- package/src/packaged-role-registry.ts +43 -0
- package/src/pi/adapter.ts +2 -1
- package/src/pi/in-process-session.ts +38 -12
- package/src/pi/role-turn-host.ts +34 -0
- package/src/public-cli/cli.ts +14 -6
- package/src/public-cli/instruction-seat-run.ts +289 -50
- package/src/public-cli/invocation.ts +66 -25
- package/src/public-cli/option-definitions.ts +61 -0
- package/src/public-cli/post-admission.ts +7 -1
- package/src/public-cli/registry.ts +4 -1
- package/src/public-cli/run-lifecycle.ts +22 -3
- package/src/public-cli/settlement.ts +120 -5
- package/src/public-cli/terminal.ts +2 -0
- package/src/public-role-summons.ts +431 -0
- package/src/receipt-delivery-policy.ts +10 -0
- package/src/reviewer-agent.ts +1 -1
- package/src/reviewer-child-executor.ts +114 -12
- package/src/reviewer-execution-ledger.ts +3 -2
- package/src/role-runtime.ts +158 -17
- package/src/session-assistant-usage.ts +119 -0
- package/src/session-opening-materials.ts +1 -1
- package/src/shape-unreadable-failure.ts +49 -0
- package/src/submission-errors.ts +3 -0
- package/dist/evidence-child-executor.js +0 -829
- package/src/evidence-child-executor.ts +0 -1140
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function requireString(value, label) {
|
|
7
|
+
if (typeof value !== "string") {
|
|
8
|
+
throw new Error(`GitHub payload missing string ${label}`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function requireNumber(value, label) {
|
|
13
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
14
|
+
throw new Error(`GitHub payload missing number ${label}`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function optionalString(value) {
|
|
19
|
+
return typeof value === "string" ? value : null;
|
|
20
|
+
}
|
|
21
|
+
function parseLinkNext(linkHeader) {
|
|
22
|
+
if (linkHeader === undefined || linkHeader.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
for (const part of linkHeader.split(",")) {
|
|
25
|
+
const match = part.trim().match(/^<([^>]+)>\s*;\s*rel="next"$/i);
|
|
26
|
+
if (match?.[1])
|
|
27
|
+
return match[1];
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function parseJson(text, label) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`GitHub ${label} returned malformed JSON`, { cause: error });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let commentFailureEvidence = 0;
|
|
40
|
+
function commentFailureCause(error) {
|
|
41
|
+
return {
|
|
42
|
+
name: error instanceof Error ? error.name : typeof error,
|
|
43
|
+
message: error instanceof Error ? error.message : String(error),
|
|
44
|
+
evidenceId: `github-comment-failure-${++commentFailureEvidence}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Authenticated /user requires a login; throws when absent. */
|
|
48
|
+
function requireUserLogin(raw) {
|
|
49
|
+
if (!isRecord(raw) || typeof raw["login"] !== "string") {
|
|
50
|
+
throw new Error("GitHub payload missing user.login");
|
|
51
|
+
}
|
|
52
|
+
return raw["login"];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Surface authors may be tombstoned only as literal JSON null (`user: null`).
|
|
56
|
+
* Preserve that case with null login; every other shape fails closed.
|
|
57
|
+
*/
|
|
58
|
+
function optionalUserLogin(raw) {
|
|
59
|
+
if (raw === null)
|
|
60
|
+
return null;
|
|
61
|
+
if (!isRecord(raw) || typeof raw["login"] !== "string") {
|
|
62
|
+
throw new Error("GitHub payload missing user.login");
|
|
63
|
+
}
|
|
64
|
+
return raw["login"];
|
|
65
|
+
}
|
|
66
|
+
function machineIdentity(raw) {
|
|
67
|
+
const user = raw["user"];
|
|
68
|
+
if (!isRecord(user) || typeof user["type"] !== "string" || typeof user["id"] !== "number") {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const app = raw["performed_via_github_app"];
|
|
72
|
+
const appId = isRecord(app) && typeof app["id"] === "number" ? app["id"] : undefined;
|
|
73
|
+
return {
|
|
74
|
+
userType: user["type"],
|
|
75
|
+
userId: user["id"],
|
|
76
|
+
...(appId === undefined ? {} : { appId }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function normalizePullRequest(raw) {
|
|
80
|
+
if (!isRecord(raw))
|
|
81
|
+
throw new Error("GitHub pull request payload must be an object");
|
|
82
|
+
const head = raw["head"];
|
|
83
|
+
if (!isRecord(head) || typeof head["sha"] !== "string" || head["sha"].length === 0) {
|
|
84
|
+
throw new Error("GitHub pull request payload missing head.sha");
|
|
85
|
+
}
|
|
86
|
+
const number = requireNumber(raw["number"], "number");
|
|
87
|
+
const state = requireString(raw["state"], "state").toUpperCase();
|
|
88
|
+
const htmlUrl = typeof raw["html_url"] === "string"
|
|
89
|
+
? raw["html_url"]
|
|
90
|
+
: `https://github.com/unknown/unknown/pull/${number}`;
|
|
91
|
+
return {
|
|
92
|
+
number,
|
|
93
|
+
state: state === "OPEN" || state === "open" ? "OPEN" : state,
|
|
94
|
+
headOid: head["sha"],
|
|
95
|
+
...(typeof raw["updated_at"] === "string" ? { updatedAt: raw["updated_at"] } : {}),
|
|
96
|
+
url: htmlUrl,
|
|
97
|
+
raw,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function normalizePullRequestReaction(raw) {
|
|
101
|
+
if (!isRecord(raw))
|
|
102
|
+
throw new Error("GitHub reaction payload must be an object");
|
|
103
|
+
return {
|
|
104
|
+
id: requireNumber(raw["id"], "reaction.id"),
|
|
105
|
+
userLogin: optionalUserLogin(raw["user"]),
|
|
106
|
+
machineIdentity: machineIdentity(raw),
|
|
107
|
+
content: requireString(raw["content"], "reaction.content"),
|
|
108
|
+
createdAt: requireString(raw["created_at"], "reaction.created_at"),
|
|
109
|
+
raw,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function normalizeReview(raw) {
|
|
113
|
+
if (!isRecord(raw))
|
|
114
|
+
throw new Error("GitHub review payload must be an object");
|
|
115
|
+
return {
|
|
116
|
+
id: requireNumber(raw["id"], "review.id"),
|
|
117
|
+
...(typeof raw["node_id"] === "string" ? { nodeId: raw["node_id"] } : {}),
|
|
118
|
+
userLogin: optionalUserLogin(raw["user"]),
|
|
119
|
+
machineIdentity: machineIdentity(raw),
|
|
120
|
+
state: requireString(raw["state"], "review.state").toUpperCase(),
|
|
121
|
+
body: typeof raw["body"] === "string" ? raw["body"] : "",
|
|
122
|
+
commitId: optionalString(raw["commit_id"]),
|
|
123
|
+
submittedAt: optionalString(raw["submitted_at"]),
|
|
124
|
+
htmlUrl: typeof raw["html_url"] === "string" ? raw["html_url"] : "",
|
|
125
|
+
raw,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function normalizeIssueComment(raw) {
|
|
129
|
+
if (!isRecord(raw))
|
|
130
|
+
throw new Error("GitHub issue comment payload must be an object");
|
|
131
|
+
return {
|
|
132
|
+
id: requireNumber(raw["id"], "comment.id"),
|
|
133
|
+
userLogin: optionalUserLogin(raw["user"]),
|
|
134
|
+
machineIdentity: machineIdentity(raw),
|
|
135
|
+
body: typeof raw["body"] === "string" ? raw["body"] : "",
|
|
136
|
+
createdAt: requireString(raw["created_at"], "comment.created_at"),
|
|
137
|
+
updatedAt: requireString(raw["updated_at"], "comment.updated_at"),
|
|
138
|
+
htmlUrl: typeof raw["html_url"] === "string" ? raw["html_url"] : "",
|
|
139
|
+
raw,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function normalizeReviewComment(raw) {
|
|
143
|
+
if (!isRecord(raw))
|
|
144
|
+
throw new Error("GitHub review comment payload must be an object");
|
|
145
|
+
return {
|
|
146
|
+
id: requireNumber(raw["id"], "review_comment.id"),
|
|
147
|
+
pullRequestReviewId: typeof raw["pull_request_review_id"] === "number"
|
|
148
|
+
? raw["pull_request_review_id"]
|
|
149
|
+
: null,
|
|
150
|
+
userLogin: optionalUserLogin(raw["user"]),
|
|
151
|
+
machineIdentity: machineIdentity(raw),
|
|
152
|
+
body: typeof raw["body"] === "string" ? raw["body"] : "",
|
|
153
|
+
path: requireString(raw["path"], "review_comment.path"),
|
|
154
|
+
line: typeof raw["line"] === "number" ? raw["line"] : null,
|
|
155
|
+
originalLine: typeof raw["original_line"] === "number" ? raw["original_line"] : null,
|
|
156
|
+
side: optionalString(raw["side"]),
|
|
157
|
+
position: typeof raw["position"] === "number" ? raw["position"] : null,
|
|
158
|
+
originalPosition: typeof raw["original_position"] === "number"
|
|
159
|
+
? raw["original_position"]
|
|
160
|
+
: null,
|
|
161
|
+
commitId: optionalString(raw["commit_id"]),
|
|
162
|
+
originalCommitId: optionalString(raw["original_commit_id"]),
|
|
163
|
+
createdAt: requireString(raw["created_at"], "review_comment.created_at"),
|
|
164
|
+
updatedAt: requireString(raw["updated_at"], "review_comment.updated_at"),
|
|
165
|
+
htmlUrl: typeof raw["html_url"] === "string" ? raw["html_url"] : "",
|
|
166
|
+
raw,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function createGhApiRunner(options = {}) {
|
|
170
|
+
const spawnImpl = options.spawnImpl ?? spawn;
|
|
171
|
+
return async (args, runOptions = {}) => {
|
|
172
|
+
return await new Promise((resolve, reject) => {
|
|
173
|
+
const signal = runOptions.signal;
|
|
174
|
+
if (signal?.aborted) {
|
|
175
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const child = spawnImpl("gh", args, {
|
|
179
|
+
env: options.env ?? process.env,
|
|
180
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
181
|
+
});
|
|
182
|
+
let stdout = "";
|
|
183
|
+
let stderr = "";
|
|
184
|
+
let settled = false;
|
|
185
|
+
const settle = (fn) => {
|
|
186
|
+
if (settled)
|
|
187
|
+
return;
|
|
188
|
+
settled = true;
|
|
189
|
+
if (signal !== undefined) {
|
|
190
|
+
signal.removeEventListener("abort", onAbort);
|
|
191
|
+
}
|
|
192
|
+
fn();
|
|
193
|
+
};
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
try {
|
|
196
|
+
child.kill("SIGTERM");
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
settle(() => reject(error));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
settle(() => {
|
|
203
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
if (signal !== undefined) {
|
|
207
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
208
|
+
}
|
|
209
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
210
|
+
stdout += chunk;
|
|
211
|
+
});
|
|
212
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
213
|
+
stderr += chunk;
|
|
214
|
+
});
|
|
215
|
+
child.on("error", (error) => {
|
|
216
|
+
settle(() => reject(error));
|
|
217
|
+
});
|
|
218
|
+
// Attach before write/end so early child exit cannot raise uncaught EPIPE.
|
|
219
|
+
child.stdin.on("error", (error) => {
|
|
220
|
+
settle(() => {
|
|
221
|
+
if (signal?.aborted) {
|
|
222
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
226
|
+
reject(Object.assign(err, { ambiguousGhFailure: true }));
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
if (runOptions.stdin !== undefined) {
|
|
230
|
+
child.stdin.write(runOptions.stdin);
|
|
231
|
+
}
|
|
232
|
+
child.stdin.end();
|
|
233
|
+
child.on("close", (code, signal) => {
|
|
234
|
+
settle(() => {
|
|
235
|
+
// gh api --include prints: HTTP/ headers blank-line body
|
|
236
|
+
const match = stdout.match(/^HTTP\/[\d.]+\s+(\d+)[^\n]*\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)$/);
|
|
237
|
+
if (match) {
|
|
238
|
+
const status = Number(match[1]);
|
|
239
|
+
const headerText = match[2] ?? "";
|
|
240
|
+
const bodyText = match[3] ?? "";
|
|
241
|
+
const headers = {};
|
|
242
|
+
for (const line of headerText.split(/\r?\n/)) {
|
|
243
|
+
const idx = line.indexOf(":");
|
|
244
|
+
if (idx === -1)
|
|
245
|
+
continue;
|
|
246
|
+
const name = line.slice(0, idx).trim().toLowerCase();
|
|
247
|
+
const value = line.slice(idx + 1).trim();
|
|
248
|
+
headers[name] = value;
|
|
249
|
+
}
|
|
250
|
+
resolve({ status, headers, bodyText });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (code === 0) {
|
|
254
|
+
resolve({ status: 200, headers: {}, bodyText: stdout });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Ambiguous: process failed without parseable HTTP response
|
|
258
|
+
const failure = new Error(`gh api failed without a parseable HTTP response (code=${String(code)}): ${stderr || stdout}`, { cause: { code, signal, stderr, stdout } });
|
|
259
|
+
reject(Object.assign(failure, { ambiguousGhFailure: true, stderr, stdout, code, signal }));
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function isAmbiguousGhFailure(error) {
|
|
266
|
+
return (typeof error === "object" &&
|
|
267
|
+
error !== null &&
|
|
268
|
+
error.ambiguousGhFailure === true);
|
|
269
|
+
}
|
|
270
|
+
/** gh binary missing or otherwise unable to launch — ticket-authorized soft unavailable. */
|
|
271
|
+
function isGhProcessStartFailure(error) {
|
|
272
|
+
if (typeof error !== "object" || error === null)
|
|
273
|
+
return false;
|
|
274
|
+
const code = error.code;
|
|
275
|
+
if (code === "ENOENT")
|
|
276
|
+
return true;
|
|
277
|
+
const syscall = error.syscall;
|
|
278
|
+
return typeof syscall === "string" && (syscall === "spawn" || syscall.startsWith("spawn "));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Sole shared issue-body fetch + projection over createGhApiRunner.
|
|
282
|
+
* Soft and hard callers only map this result; they do not duplicate argv/parse.
|
|
283
|
+
*/
|
|
284
|
+
export async function projectGhIssueBody(runner, input) {
|
|
285
|
+
const path = `repos/${input.owner}/${input.repo}/issues/${input.ticketNumber}`;
|
|
286
|
+
let response;
|
|
287
|
+
try {
|
|
288
|
+
response = await runner(["api", "--hostname", "github.com", "--include", "-X", "GET", path], input.signal === undefined ? {} : { signal: input.signal });
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
// Transport ambiguity / gh never started → unavailable. Other failures keep true cause as throw.
|
|
292
|
+
if (isAmbiguousGhFailure(error) || isGhProcessStartFailure(error)) {
|
|
293
|
+
return { status: "unavailable", reason: "transport", cause: error };
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
if (response.status < 200 || response.status >= 300) {
|
|
298
|
+
return {
|
|
299
|
+
status: "unavailable",
|
|
300
|
+
reason: "http-non-2xx",
|
|
301
|
+
httpStatus: response.status,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = JSON.parse(response.bodyText);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
return { status: "invalid", reason: "not-json", cause: error };
|
|
310
|
+
}
|
|
311
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
312
|
+
return { status: "invalid", reason: "not-object" };
|
|
313
|
+
}
|
|
314
|
+
// Issues endpoint also returns PRs. Own-key presence of pull_request → not an issue face.
|
|
315
|
+
if (Object.hasOwn(parsed, "pull_request")) {
|
|
316
|
+
return { status: "unavailable", reason: "pull-request" };
|
|
317
|
+
}
|
|
318
|
+
// Match former gh --jq `(.body // "")`: null/missing body projects to empty string.
|
|
319
|
+
const bodyRaw = parsed.body;
|
|
320
|
+
if (bodyRaw !== undefined && bodyRaw !== null && typeof bodyRaw !== "string") {
|
|
321
|
+
return { status: "invalid", reason: "body-invalid" };
|
|
322
|
+
}
|
|
323
|
+
const body = typeof bodyRaw === "string" ? bodyRaw : "";
|
|
324
|
+
return { status: "available", body };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Production issue-fetch capability owned by the shared gh execution seam.
|
|
328
|
+
* Reuses projectGhIssueBody. Softens only ticket-authorized unavailable results
|
|
329
|
+
* (tracker unreachable / issue not found / gh cannot start / PR marker);
|
|
330
|
+
* invalid payload shapes propagate with true cause.
|
|
331
|
+
*/
|
|
332
|
+
export function createGhIssueSoftFetcher(runner = createGhApiRunner()) {
|
|
333
|
+
return async (input) => {
|
|
334
|
+
const projected = await projectGhIssueBody(runner, input);
|
|
335
|
+
if (projected.status === "available") {
|
|
336
|
+
return Object.freeze({ body: projected.body });
|
|
337
|
+
}
|
|
338
|
+
if (projected.status === "unavailable") {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
// invalid — keep true cause (historical soft-fetcher contract).
|
|
342
|
+
if (projected.reason === "not-json") {
|
|
343
|
+
throw new Error("GitHub issue payload is not JSON", {
|
|
344
|
+
cause: projected.cause,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (projected.reason === "not-object") {
|
|
348
|
+
throw new Error("GitHub issue payload must be a JSON object");
|
|
349
|
+
}
|
|
350
|
+
throw new Error("GitHub issue payload body must be string or null");
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export function createGhCollectorGitHubTransport(runner = createGhApiRunner()) {
|
|
354
|
+
const hostnameArgs = ["api", "--hostname", "github.com", "--include"];
|
|
355
|
+
async function apiGet(path, signal) {
|
|
356
|
+
return await runner([...hostnameArgs, "-X", "GET", path], signal === undefined ? {} : { signal });
|
|
357
|
+
}
|
|
358
|
+
async function paginate(path, mapItem, options = {}) {
|
|
359
|
+
const { signal, retainPage } = options;
|
|
360
|
+
const items = [];
|
|
361
|
+
const pages = [];
|
|
362
|
+
let nextPath = path;
|
|
363
|
+
let page = 1;
|
|
364
|
+
const seen = new Set();
|
|
365
|
+
while (nextPath !== undefined) {
|
|
366
|
+
if (signal?.aborted) {
|
|
367
|
+
throw signal.reason ?? new Error("aborted");
|
|
368
|
+
}
|
|
369
|
+
if (seen.has(nextPath)) {
|
|
370
|
+
throw new Error(`GitHub pagination repeated page: ${nextPath}`);
|
|
371
|
+
}
|
|
372
|
+
seen.add(nextPath);
|
|
373
|
+
const response = await apiGet(nextPath, signal);
|
|
374
|
+
const diagnostics = {
|
|
375
|
+
path: nextPath,
|
|
376
|
+
page,
|
|
377
|
+
status: response.status,
|
|
378
|
+
itemCount: 0,
|
|
379
|
+
...(response.headers["link"] === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: { linkHeader: response.headers["link"] }),
|
|
382
|
+
};
|
|
383
|
+
if (response.status === 429) {
|
|
384
|
+
throw Object.assign(new Error(`GitHub API rate limited on ${nextPath} (HTTP 429)`), { githubStatus: 429, page: diagnostics });
|
|
385
|
+
}
|
|
386
|
+
if (response.status < 200 || response.status >= 300) {
|
|
387
|
+
throw Object.assign(new Error(`GitHub API ${nextPath} failed with HTTP ${response.status}`), { githubStatus: response.status, page: diagnostics });
|
|
388
|
+
}
|
|
389
|
+
const parsed = parseJson(response.bodyText, nextPath);
|
|
390
|
+
if (!Array.isArray(parsed)) {
|
|
391
|
+
throw new Error(`GitHub API ${nextPath} did not return a JSON array`);
|
|
392
|
+
}
|
|
393
|
+
diagnostics.itemCount = parsed.length;
|
|
394
|
+
pages.push(diagnostics);
|
|
395
|
+
const pageItems = parsed.map((entry) => mapItem(entry));
|
|
396
|
+
// Observation budget hook: charge before aggregate append / next-page fetch.
|
|
397
|
+
retainPage?.(pageItems);
|
|
398
|
+
for (const item of pageItems)
|
|
399
|
+
items.push(item);
|
|
400
|
+
const nextUrl = parseLinkNext(response.headers["link"]);
|
|
401
|
+
if (nextUrl === undefined) {
|
|
402
|
+
nextPath = undefined;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// Accept absolute or path-style next links; normalize to API path + query.
|
|
406
|
+
if (nextUrl.startsWith("/")) {
|
|
407
|
+
nextPath = nextUrl;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const url = new URL(nextUrl);
|
|
411
|
+
if (url.hostname !== "api.github.com" && url.hostname !== "github.com") {
|
|
412
|
+
throw new Error(`unexpected pagination host ${url.hostname}`);
|
|
413
|
+
}
|
|
414
|
+
nextPath = `${url.pathname}${url.search}`;
|
|
415
|
+
if (nextPath.startsWith("/api/v3/")) {
|
|
416
|
+
nextPath = nextPath.slice("/api/v3".length);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
page += 1;
|
|
421
|
+
}
|
|
422
|
+
return { items, pages };
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
async getAuthenticatedUser(options = {}) {
|
|
426
|
+
const response = await apiGet("/user", options.signal);
|
|
427
|
+
if (response.status < 200 || response.status >= 300) {
|
|
428
|
+
throw new Error(`GitHub /user failed with HTTP ${response.status}`, { cause: { endpoint: "/user", status: response.status, headers: response.headers, body: response.bodyText } });
|
|
429
|
+
}
|
|
430
|
+
const raw = parseJson(response.bodyText, "/user");
|
|
431
|
+
return { login: requireUserLogin(raw).toLowerCase(), raw };
|
|
432
|
+
},
|
|
433
|
+
async getPullRequest(input) {
|
|
434
|
+
const path = `/repos/${input.owner}/${input.repo}/pulls/${input.prNumber}`;
|
|
435
|
+
const response = await apiGet(path, input.signal);
|
|
436
|
+
if (response.status < 200 || response.status >= 300) {
|
|
437
|
+
throw new Error(`GitHub ${path} failed with HTTP ${response.status}`, { cause: { endpoint: path, status: response.status, headers: response.headers, body: response.bodyText } });
|
|
438
|
+
}
|
|
439
|
+
return normalizePullRequest(parseJson(response.bodyText, path));
|
|
440
|
+
},
|
|
441
|
+
async listPullRequestReviews(input) {
|
|
442
|
+
const path = `/repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/reviews?per_page=100`;
|
|
443
|
+
return await paginate(path, normalizeReview, {
|
|
444
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
445
|
+
...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
async listPullRequestReactions(input) {
|
|
449
|
+
const path = `/repos/${input.owner}/${input.repo}/issues/${input.prNumber}/reactions?per_page=100`;
|
|
450
|
+
return await paginate(path, normalizePullRequestReaction, {
|
|
451
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
452
|
+
...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
async listIssueComments(input) {
|
|
456
|
+
const path = `/repos/${input.owner}/${input.repo}/issues/${input.prNumber}/comments?per_page=100`;
|
|
457
|
+
return await paginate(path, normalizeIssueComment, {
|
|
458
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
459
|
+
...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
async listReviewComments(input) {
|
|
463
|
+
const path = `/repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/comments?per_page=100`;
|
|
464
|
+
return await paginate(path, normalizeReviewComment, {
|
|
465
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
466
|
+
...(input.retainPage === undefined ? {} : { retainPage: input.retainPage }),
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
async createIssueComment(input) {
|
|
470
|
+
const path = `/repos/${input.owner}/${input.repo}/issues/${input.prNumber}/comments`;
|
|
471
|
+
try {
|
|
472
|
+
const response = await runner([...hostnameArgs, "-X", "POST", path, "--input", "-"], input.signal === undefined
|
|
473
|
+
? { stdin: JSON.stringify({ body: input.body }) }
|
|
474
|
+
: { stdin: JSON.stringify({ body: input.body }), signal: input.signal });
|
|
475
|
+
if (response.status >= 200 && response.status < 300) {
|
|
476
|
+
// Contract: README.md#Collector — a successful write with unreadable response is recoverable only as an observable ambiguous result.
|
|
477
|
+
// 2xx means the comment may already exist; parse/normalize failure is
|
|
478
|
+
// ambiguous_loss so ledger marker recovery can resolve without repost.
|
|
479
|
+
try {
|
|
480
|
+
return {
|
|
481
|
+
kind: "success",
|
|
482
|
+
comment: normalizeIssueComment(parseJson(response.bodyText, path)),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
const cause = commentFailureCause(error);
|
|
487
|
+
return { kind: "ambiguous_loss", diagnostics: cause.message, cause };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
kind: "rejected",
|
|
492
|
+
status: response.status,
|
|
493
|
+
diagnostics: `HTTP ${response.status}: ${response.bodyText.slice(0, 500)}`,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
// Signal state is authoritative cancel; rethrow exact caught value.
|
|
498
|
+
if (input.signal?.aborted) {
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
// Non-aborted transport tag only (after signal).
|
|
502
|
+
if (isRecord(error) && error["ambiguousGhFailure"] === true) {
|
|
503
|
+
const cause = commentFailureCause(error);
|
|
504
|
+
return { kind: "ambiguous_loss", diagnostics: cause.message, cause };
|
|
505
|
+
}
|
|
506
|
+
// Non-signal AbortError belt only (after tag).
|
|
507
|
+
if (isRecord(error) && error["name"] === "AbortError") {
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
export function buildCollectorRequestMarker(input) {
|
|
516
|
+
const prefix = input.manifestDigest.slice(0, 12);
|
|
517
|
+
const requestMarkerId = createHash("sha256").update(input.requestId).digest("hex");
|
|
518
|
+
return `<!-- ak-collector:v1 manifest=${prefix} request=${requestMarkerId} head=${input.headOid} -->`;
|
|
519
|
+
}
|
|
520
|
+
export function buildCollectorRequestBody(input) {
|
|
521
|
+
const marker = buildCollectorRequestMarker(input);
|
|
522
|
+
// Preserve configured body byte-for-byte; append marker on its own line.
|
|
523
|
+
const body = input.configuredBody.endsWith("\n")
|
|
524
|
+
? `${input.configuredBody}${marker}\n`
|
|
525
|
+
: `${input.configuredBody}\n${marker}\n`;
|
|
526
|
+
return { body, marker };
|
|
527
|
+
}
|