ai-spend-agent 0.8.1 → 0.9.1
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/githubAcceptedOutcome.d.ts +47 -0
- package/dist/githubAcceptedOutcome.js +435 -0
- package/dist/guidedExperience.d.ts +71 -0
- package/dist/guidedExperience.js +435 -0
- package/dist/guidedPrompt.d.ts +122 -0
- package/dist/guidedPrompt.js +249 -0
- package/dist/improveExperience.d.ts +146 -0
- package/dist/improveExperience.js +350 -0
- package/dist/improveFlow.d.ts +203 -0
- package/dist/improveFlow.js +637 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +3218 -141
- package/dist/projectAccountabilityState.d.ts +94 -0
- package/dist/projectAccountabilityState.js +839 -0
- package/dist/statuslineInstaller.d.ts +7 -0
- package/dist/statuslineInstaller.js +24 -0
- package/dist/statuslineRuntime.d.ts +19 -1
- package/dist/statuslineRuntime.js +250 -57
- package/dist/tokenVerificationState.d.ts +34 -0
- package/dist/tokenVerificationState.js +478 -0
- package/package.json +3 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type AcceptedOutcomeV0 } from "@agent-finops/core";
|
|
2
|
+
export type GitHubAcceptedOutcomeRequest = {
|
|
3
|
+
/** Absolute repository working tree used by `gh`; never persisted or returned. */
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
/** When omitted, `gh pr view` uses its exact current-branch PR association. */
|
|
6
|
+
pullRequestNumber?: number;
|
|
7
|
+
/** Optional human-provided business meaning, explicitly labeled user-declared. */
|
|
8
|
+
businessDescription?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export type GitHubAcceptedOutcomeErrorCode = "invalid_request" | "invalid_business_description" | "local_repository_unavailable" | "repository_mismatch" | "gh_unavailable" | "timeout" | "command_failed" | "response_too_large" | "malformed_response" | "pull_request_not_merged" | "merge_commit_missing" | "head_commit_missing" | "checks_missing" | "checks_pending" | "checks_failed";
|
|
12
|
+
export type GitHubAcceptedOutcomeResult = {
|
|
13
|
+
status: "ok";
|
|
14
|
+
selection: "explicit_pr" | "current_branch";
|
|
15
|
+
outcome: AcceptedOutcomeV0;
|
|
16
|
+
} | {
|
|
17
|
+
status: "error";
|
|
18
|
+
code: GitHubAcceptedOutcomeErrorCode;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
export type GitHubExecFileError = Error & {
|
|
22
|
+
code?: string | number | null;
|
|
23
|
+
killed?: boolean;
|
|
24
|
+
signal?: NodeJS.Signals | null;
|
|
25
|
+
};
|
|
26
|
+
export type GitHubExecFile = (file: string, args: readonly string[], options: {
|
|
27
|
+
cwd: string;
|
|
28
|
+
encoding: "utf8";
|
|
29
|
+
timeout: number;
|
|
30
|
+
maxBuffer: number;
|
|
31
|
+
windowsHide: true;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
}, callback: (error: GitHubExecFileError | null, stdout: string, stderr: string) => void) => void;
|
|
34
|
+
export type GitHubAcceptedOutcomeDependencies = {
|
|
35
|
+
execFile?: GitHubExecFile;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Explicit, opt-in GitHub evidence fetch. Nothing calls this adapter from the
|
|
39
|
+
* default receipt or improve flow; the caller must deliberately invoke it.
|
|
40
|
+
*
|
|
41
|
+
* The adapter is deliberately conservative: it accepts only a merged PR with
|
|
42
|
+
* exact head and merge commit OIDs and a non-empty rollup in which every
|
|
43
|
+
* observed check reports SUCCESS. Native repository, commit, check, URL and
|
|
44
|
+
* branch values are reduced to one-way project-economics references.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fetchGitHubAcceptedOutcomeV0(request: GitHubAcceptedOutcomeRequest, dependencies?: GitHubAcceptedOutcomeDependencies): Promise<GitHubAcceptedOutcomeResult>;
|
|
47
|
+
//# sourceMappingURL=githubAcceptedOutcome.d.ts.map
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { execFile as nodeExecFile } from "node:child_process";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { createAcceptedOutcomeV0, createProjectEconomicsReference } from "@agent-finops/core";
|
|
4
|
+
const GH_FIELDS = [
|
|
5
|
+
"number",
|
|
6
|
+
"state",
|
|
7
|
+
"mergedAt",
|
|
8
|
+
"mergeCommit",
|
|
9
|
+
"url",
|
|
10
|
+
"headRefOid",
|
|
11
|
+
"statusCheckRollup"
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
14
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
15
|
+
const MAX_TIMEOUT_MS = 20_000;
|
|
16
|
+
const MAX_STDOUT_BYTES = 256 * 1_024;
|
|
17
|
+
/**
|
|
18
|
+
* Explicit, opt-in GitHub evidence fetch. Nothing calls this adapter from the
|
|
19
|
+
* default receipt or improve flow; the caller must deliberately invoke it.
|
|
20
|
+
*
|
|
21
|
+
* The adapter is deliberately conservative: it accepts only a merged PR with
|
|
22
|
+
* exact head and merge commit OIDs and a non-empty rollup in which every
|
|
23
|
+
* observed check reports SUCCESS. Native repository, commit, check, URL and
|
|
24
|
+
* branch values are reduced to one-way project-economics references.
|
|
25
|
+
*/
|
|
26
|
+
export async function fetchGitHubAcceptedOutcomeV0(request, dependencies = {}) {
|
|
27
|
+
const validatedRequest = validateRequest(request);
|
|
28
|
+
if (validatedRequest.status === "error")
|
|
29
|
+
return validatedRequest;
|
|
30
|
+
const executor = dependencies.execFile ?? defaultExecFile;
|
|
31
|
+
const localRepository = await resolveLocalGitHubRepository(executor, validatedRequest.cwd, validatedRequest.timeoutMs);
|
|
32
|
+
if (localRepository.status === "error")
|
|
33
|
+
return localRepository;
|
|
34
|
+
const selection = request.pullRequestNumber === undefined
|
|
35
|
+
? "current_branch"
|
|
36
|
+
: "explicit_pr";
|
|
37
|
+
const args = request.pullRequestNumber === undefined
|
|
38
|
+
? [
|
|
39
|
+
"pr",
|
|
40
|
+
"view",
|
|
41
|
+
"--repo",
|
|
42
|
+
localRepository.selector,
|
|
43
|
+
"--json",
|
|
44
|
+
GH_FIELDS.join(",")
|
|
45
|
+
]
|
|
46
|
+
: [
|
|
47
|
+
"pr",
|
|
48
|
+
"view",
|
|
49
|
+
String(request.pullRequestNumber),
|
|
50
|
+
"--repo",
|
|
51
|
+
localRepository.selector,
|
|
52
|
+
"--json",
|
|
53
|
+
GH_FIELDS.join(",")
|
|
54
|
+
];
|
|
55
|
+
const command = await runGh(executor, args, validatedRequest.cwd, validatedRequest.timeoutMs);
|
|
56
|
+
if (command.status === "error")
|
|
57
|
+
return command;
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = JSON.parse(command.stdout);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return error("malformed_response", "GitHub returned malformed PR evidence.");
|
|
64
|
+
}
|
|
65
|
+
const parsed = parsePullRequest(raw, request.pullRequestNumber, localRepository.identity);
|
|
66
|
+
if (parsed.status === "error")
|
|
67
|
+
return parsed;
|
|
68
|
+
let outcome;
|
|
69
|
+
try {
|
|
70
|
+
const repositoryRef = createProjectEconomicsReference("github.repository", parsed.repositoryIdentity);
|
|
71
|
+
const workUnitRef = createProjectEconomicsReference("github.pull_request", `${parsed.repositoryIdentity}\u0000${parsed.number}\u0000${parsed.headOid}`);
|
|
72
|
+
const commitRef = createProjectEconomicsReference("github.merge_commit", `${parsed.repositoryIdentity}\u0000${parsed.mergeOid}`);
|
|
73
|
+
const evidenceRefs = [...new Set(parsed.checkIdentities.map((identity) => createProjectEconomicsReference("github.status_check", `${parsed.repositoryIdentity}\u0000${parsed.number}\u0000${identity}`)))];
|
|
74
|
+
outcome = createAcceptedOutcomeV0({
|
|
75
|
+
kind: "aibill.accepted_outcome",
|
|
76
|
+
schemaVersion: "0.1.0",
|
|
77
|
+
platform: "github",
|
|
78
|
+
outcomeType: "pull_request",
|
|
79
|
+
repositoryRef,
|
|
80
|
+
workUnitRef,
|
|
81
|
+
state: "merged",
|
|
82
|
+
stateEvidence: "verified",
|
|
83
|
+
acceptedAt: parsed.mergedAt,
|
|
84
|
+
commit: { commitRef, evidence: "verified" },
|
|
85
|
+
checks: {
|
|
86
|
+
status: "passed",
|
|
87
|
+
// `gh pr view` exposes the observed rollup. It does not prove which
|
|
88
|
+
// checks branch protection required, so this evidence is observed.
|
|
89
|
+
evidence: "observed",
|
|
90
|
+
evidenceRefs
|
|
91
|
+
},
|
|
92
|
+
...(request.businessDescription === undefined ? {} : {
|
|
93
|
+
businessDescription: {
|
|
94
|
+
value: request.businessDescription.trim(),
|
|
95
|
+
evidence: "user_declared"
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return error("malformed_response", "GitHub returned invalid PR evidence.");
|
|
102
|
+
}
|
|
103
|
+
return { status: "ok", selection, outcome };
|
|
104
|
+
}
|
|
105
|
+
async function resolveLocalGitHubRepository(executor, cwd, timeout) {
|
|
106
|
+
const rootResult = await runGit(executor, ["rev-parse", "--show-toplevel"], cwd, timeout);
|
|
107
|
+
if (rootResult.status === "error")
|
|
108
|
+
return rootResult;
|
|
109
|
+
const repositoryRoot = singleLineOutput(rootResult.stdout, 4_096);
|
|
110
|
+
if (!repositoryRoot || !isAbsolute(repositoryRoot) ||
|
|
111
|
+
resolve(repositoryRoot) !== cwd) {
|
|
112
|
+
return error("local_repository_unavailable", "The selected project root is not an exact local Git repository root.");
|
|
113
|
+
}
|
|
114
|
+
const remoteResult = await runGit(executor, ["remote", "get-url", "origin"], repositoryRoot, timeout);
|
|
115
|
+
if (remoteResult.status === "error")
|
|
116
|
+
return remoteResult;
|
|
117
|
+
const remote = singleLineOutput(remoteResult.stdout, 4_096);
|
|
118
|
+
const repository = remote ? normalizeGitHubRemote(remote) : null;
|
|
119
|
+
if (!repository) {
|
|
120
|
+
return error("local_repository_unavailable", "The local Git origin is not an unambiguous GitHub repository.");
|
|
121
|
+
}
|
|
122
|
+
return { status: "ok", ...repository };
|
|
123
|
+
}
|
|
124
|
+
function validateRequest(request) {
|
|
125
|
+
if (typeof request.projectRoot !== "string" ||
|
|
126
|
+
!isAbsolute(request.projectRoot) || request.projectRoot.includes("\0")) {
|
|
127
|
+
return error("invalid_request", "A valid absolute project root is required.");
|
|
128
|
+
}
|
|
129
|
+
if (request.pullRequestNumber !== undefined &&
|
|
130
|
+
(!Number.isSafeInteger(request.pullRequestNumber) ||
|
|
131
|
+
request.pullRequestNumber < 1 || request.pullRequestNumber > 1_000_000_000)) {
|
|
132
|
+
return error("invalid_request", "A valid pull-request number is required.");
|
|
133
|
+
}
|
|
134
|
+
if (request.businessDescription !== undefined) {
|
|
135
|
+
const description = request.businessDescription.trim();
|
|
136
|
+
if (description.length < 1 || description.length > 240 ||
|
|
137
|
+
/[\u0000-\u001f\u007f-\u009f]/u.test(description) ||
|
|
138
|
+
hasUnpairedSurrogate(description) || looksCredentialLike(description)) {
|
|
139
|
+
return error("invalid_business_description", "The business description must be bounded, control-free, and contain no credentials.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const requestedTimeout = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
143
|
+
if (!Number.isFinite(requestedTimeout)) {
|
|
144
|
+
return error("invalid_request", "A finite GitHub timeout is required.");
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
status: "ok",
|
|
148
|
+
cwd: resolve(request.projectRoot),
|
|
149
|
+
timeoutMs: Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.round(requestedTimeout)))
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function runGit(executor, args, cwd, timeout) {
|
|
153
|
+
return new Promise((resolveResult) => {
|
|
154
|
+
try {
|
|
155
|
+
executor("git", args, {
|
|
156
|
+
cwd,
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
timeout,
|
|
159
|
+
maxBuffer: MAX_STDOUT_BYTES,
|
|
160
|
+
windowsHide: true
|
|
161
|
+
}, (commandError, stdout) => {
|
|
162
|
+
if (commandError) {
|
|
163
|
+
resolveResult(error("local_repository_unavailable", "The local Git repository identity could not be resolved."));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (Buffer.byteLength(stdout, "utf8") > MAX_STDOUT_BYTES) {
|
|
167
|
+
resolveResult(error("local_repository_unavailable", "The local Git repository identity could not be resolved."));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolveResult({ status: "ok", stdout });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
resolveResult(error("local_repository_unavailable", "The local Git repository identity could not be resolved."));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function runGh(executor, args, cwd, timeout) {
|
|
179
|
+
return new Promise((resolveResult) => {
|
|
180
|
+
try {
|
|
181
|
+
executor("gh", args, {
|
|
182
|
+
cwd,
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
timeout,
|
|
185
|
+
maxBuffer: MAX_STDOUT_BYTES,
|
|
186
|
+
windowsHide: true,
|
|
187
|
+
// `GH_REPO` is a process-wide selector. Never let it redirect an
|
|
188
|
+
// outcome fetch away from the repository derived from this local root.
|
|
189
|
+
env: environmentWithoutGhRepo()
|
|
190
|
+
}, (commandError, stdout) => {
|
|
191
|
+
if (commandError) {
|
|
192
|
+
if (commandError.code === "ENOENT") {
|
|
193
|
+
resolveResult(error("gh_unavailable", "GitHub CLI is not available."));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (commandError.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
197
|
+
resolveResult(error("response_too_large", "GitHub PR evidence exceeded the safe limit."));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (commandError.code === "ETIMEDOUT" || commandError.killed ||
|
|
201
|
+
commandError.signal === "SIGTERM") {
|
|
202
|
+
resolveResult(error("timeout", "GitHub PR evidence timed out."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
resolveResult(error("command_failed", "GitHub could not verify the pull request."));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (Buffer.byteLength(stdout, "utf8") > MAX_STDOUT_BYTES) {
|
|
209
|
+
resolveResult(error("response_too_large", "GitHub PR evidence exceeded the safe limit."));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
resolveResult({ status: "ok", stdout });
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
resolveResult(error("command_failed", "GitHub could not verify the pull request."));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function environmentWithoutGhRepo() {
|
|
221
|
+
const environment = { ...process.env };
|
|
222
|
+
delete environment.GH_REPO;
|
|
223
|
+
return environment;
|
|
224
|
+
}
|
|
225
|
+
function singleLineOutput(value, maxLength) {
|
|
226
|
+
if (Buffer.byteLength(value, "utf8") > maxLength)
|
|
227
|
+
return null;
|
|
228
|
+
const normalized = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
229
|
+
const line = normalized.endsWith("\r") ? normalized.slice(0, -1) : normalized;
|
|
230
|
+
return line.length >= 1 && line.length <= maxLength &&
|
|
231
|
+
!/[\u0000-\u001f\u007f-\u009f]/u.test(line) && !hasUnpairedSurrogate(line)
|
|
232
|
+
? line
|
|
233
|
+
: null;
|
|
234
|
+
}
|
|
235
|
+
function normalizeGitHubRemote(value) {
|
|
236
|
+
const scpLike = /^git@([A-Za-z0-9.-]+):([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/u
|
|
237
|
+
.exec(value.replace(/\.git$/iu, ""));
|
|
238
|
+
if (scpLike) {
|
|
239
|
+
return normalizedRepositoryParts(scpLike[1], scpLike[2], scpLike[3]);
|
|
240
|
+
}
|
|
241
|
+
let remote;
|
|
242
|
+
try {
|
|
243
|
+
remote = new URL(value);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (!new Set(["https:", "ssh:", "git:"]).has(remote.protocol) ||
|
|
249
|
+
remote.password || remote.search || remote.hash || remote.port ||
|
|
250
|
+
(remote.username && !(remote.protocol === "ssh:" && remote.username === "git")) ||
|
|
251
|
+
!/^[A-Za-z0-9.-]+$/u.test(remote.hostname))
|
|
252
|
+
return null;
|
|
253
|
+
const segments = remote.pathname.split("/").filter(Boolean);
|
|
254
|
+
if (segments.length !== 2)
|
|
255
|
+
return null;
|
|
256
|
+
const repository = segments[1].replace(/\.git$/iu, "");
|
|
257
|
+
return normalizedRepositoryParts(remote.hostname, segments[0], repository);
|
|
258
|
+
}
|
|
259
|
+
function normalizedRepositoryParts(hostname, owner, repository) {
|
|
260
|
+
if (!hostname || !owner || !repository ||
|
|
261
|
+
!/^[A-Za-z0-9.-]+$/u.test(hostname) ||
|
|
262
|
+
!/^[A-Za-z0-9_.-]+$/u.test(owner) ||
|
|
263
|
+
!/^[A-Za-z0-9_.-]+$/u.test(repository))
|
|
264
|
+
return null;
|
|
265
|
+
const identity = `${hostname.toLowerCase()}/${owner.toLowerCase()}/${repository.toLowerCase()}`;
|
|
266
|
+
return { identity, selector: identity };
|
|
267
|
+
}
|
|
268
|
+
function parsePullRequest(value, requestedNumber, expectedRepositoryIdentity) {
|
|
269
|
+
const object = asObject(value);
|
|
270
|
+
if (!object || !Number.isSafeInteger(object.number) ||
|
|
271
|
+
object.number < 1 ||
|
|
272
|
+
(requestedNumber !== undefined && object.number !== requestedNumber)) {
|
|
273
|
+
return error("malformed_response", "GitHub returned invalid PR evidence.");
|
|
274
|
+
}
|
|
275
|
+
const number = object.number;
|
|
276
|
+
if (object.state !== "MERGED") {
|
|
277
|
+
return error("pull_request_not_merged", "The pull request is not merged.");
|
|
278
|
+
}
|
|
279
|
+
const mergedAt = normalizeTimestamp(object.mergedAt);
|
|
280
|
+
if (!mergedAt) {
|
|
281
|
+
return error("malformed_response", "GitHub returned invalid merge-time evidence.");
|
|
282
|
+
}
|
|
283
|
+
const mergeCommit = asObject(object.mergeCommit);
|
|
284
|
+
const mergeOid = normalizeOid(mergeCommit?.oid);
|
|
285
|
+
if (!mergeOid) {
|
|
286
|
+
return error("merge_commit_missing", "The merged pull request has no exact merge commit evidence.");
|
|
287
|
+
}
|
|
288
|
+
const headOid = normalizeOid(object.headRefOid);
|
|
289
|
+
if (!headOid) {
|
|
290
|
+
return error("head_commit_missing", "The merged pull request has no exact head commit evidence.");
|
|
291
|
+
}
|
|
292
|
+
const repositoryIdentity = normalizeRepositoryIdentity(object.url, number);
|
|
293
|
+
if (!repositoryIdentity) {
|
|
294
|
+
return error("malformed_response", "GitHub returned invalid repository evidence.");
|
|
295
|
+
}
|
|
296
|
+
if (repositoryIdentity !== expectedRepositoryIdentity) {
|
|
297
|
+
return error("repository_mismatch", "The pull request does not belong to the selected local Git repository.");
|
|
298
|
+
}
|
|
299
|
+
if (!Array.isArray(object.statusCheckRollup)) {
|
|
300
|
+
return error("malformed_response", "GitHub returned invalid check evidence.");
|
|
301
|
+
}
|
|
302
|
+
if (object.statusCheckRollup.length === 0) {
|
|
303
|
+
return error("checks_missing", "No GitHub status-check evidence was available.");
|
|
304
|
+
}
|
|
305
|
+
const checks = parseChecks(object.statusCheckRollup);
|
|
306
|
+
if (checks.status === "malformed") {
|
|
307
|
+
return error("malformed_response", "GitHub returned invalid check evidence.");
|
|
308
|
+
}
|
|
309
|
+
if (checks.status === "failed") {
|
|
310
|
+
return error("checks_failed", "At least one GitHub status check failed.");
|
|
311
|
+
}
|
|
312
|
+
if (checks.status === "pending") {
|
|
313
|
+
return error("checks_pending", "At least one GitHub status check is pending.");
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
status: "ok",
|
|
317
|
+
number,
|
|
318
|
+
mergedAt,
|
|
319
|
+
repositoryIdentity,
|
|
320
|
+
headOid,
|
|
321
|
+
mergeOid,
|
|
322
|
+
checkIdentities: checks.identities
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function parseChecks(value) {
|
|
326
|
+
const identities = [];
|
|
327
|
+
let pending = false;
|
|
328
|
+
let failed = false;
|
|
329
|
+
for (const item of value) {
|
|
330
|
+
const check = asObject(item);
|
|
331
|
+
if (!check)
|
|
332
|
+
return { status: "malformed" };
|
|
333
|
+
const name = boundedText(check.name ?? check.context, 512);
|
|
334
|
+
if (!name)
|
|
335
|
+
return { status: "malformed" };
|
|
336
|
+
if ("conclusion" in check || "status" in check) {
|
|
337
|
+
const status = boundedText(check.status, 32)?.toUpperCase();
|
|
338
|
+
const conclusion = boundedText(check.conclusion, 32)?.toUpperCase();
|
|
339
|
+
if (status !== "COMPLETED" || !conclusion)
|
|
340
|
+
pending = true;
|
|
341
|
+
else if (conclusion !== "SUCCESS")
|
|
342
|
+
failed = true;
|
|
343
|
+
identities.push(`check_run\u0000${name}\u0000${status ?? "missing"}\u0000${conclusion ?? "missing"}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if ("state" in check) {
|
|
347
|
+
const state = boundedText(check.state, 32)?.toUpperCase();
|
|
348
|
+
if (!state)
|
|
349
|
+
return { status: "malformed" };
|
|
350
|
+
if (state === "PENDING" || state === "EXPECTED")
|
|
351
|
+
pending = true;
|
|
352
|
+
else if (state !== "SUCCESS")
|
|
353
|
+
failed = true;
|
|
354
|
+
identities.push(`status_context\u0000${name}\u0000${state}`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
return { status: "malformed" };
|
|
358
|
+
}
|
|
359
|
+
if (failed)
|
|
360
|
+
return { status: "failed" };
|
|
361
|
+
if (pending)
|
|
362
|
+
return { status: "pending" };
|
|
363
|
+
return { status: "passed", identities };
|
|
364
|
+
}
|
|
365
|
+
function normalizeRepositoryIdentity(value, number) {
|
|
366
|
+
if (typeof value !== "string" || value.length > 4_096)
|
|
367
|
+
return null;
|
|
368
|
+
let url;
|
|
369
|
+
try {
|
|
370
|
+
url = new URL(value);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
if (url.protocol !== "https:" || url.username || url.password ||
|
|
376
|
+
url.search || url.hash || !/^[a-z0-9.-]+$/i.test(url.hostname))
|
|
377
|
+
return null;
|
|
378
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
379
|
+
if (segments.length !== 4 || segments[2] !== "pull" ||
|
|
380
|
+
segments[3] !== String(number) ||
|
|
381
|
+
!/^[A-Za-z0-9_.-]+$/.test(segments[0] ?? "") ||
|
|
382
|
+
!/^[A-Za-z0-9_.-]+$/.test(segments[1] ?? ""))
|
|
383
|
+
return null;
|
|
384
|
+
return `${url.hostname.toLowerCase()}/${segments[0].toLowerCase()}/${segments[1]
|
|
385
|
+
.replace(/\.git$/i, "").toLowerCase()}`;
|
|
386
|
+
}
|
|
387
|
+
function normalizeTimestamp(value) {
|
|
388
|
+
if (typeof value !== "string" || value.length > 64)
|
|
389
|
+
return null;
|
|
390
|
+
const milliseconds = Date.parse(value);
|
|
391
|
+
return Number.isFinite(milliseconds) ? new Date(milliseconds).toISOString() : null;
|
|
392
|
+
}
|
|
393
|
+
function normalizeOid(value) {
|
|
394
|
+
return typeof value === "string" && /^[a-f0-9]{40,64}$/i.test(value)
|
|
395
|
+
? value.toLowerCase()
|
|
396
|
+
: null;
|
|
397
|
+
}
|
|
398
|
+
function boundedText(value, maxLength) {
|
|
399
|
+
return typeof value === "string" && value.length >= 1 && value.length <= maxLength &&
|
|
400
|
+
!/[\u0000-\u001f\u007f-\u009f]/u.test(value) && !hasUnpairedSurrogate(value)
|
|
401
|
+
? value
|
|
402
|
+
: null;
|
|
403
|
+
}
|
|
404
|
+
function asObject(value) {
|
|
405
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
406
|
+
? value
|
|
407
|
+
: null;
|
|
408
|
+
}
|
|
409
|
+
function looksCredentialLike(value) {
|
|
410
|
+
return /(?:github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9]+|npm_[A-Za-z0-9]+|sk-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]+)/i
|
|
411
|
+
.test(value);
|
|
412
|
+
}
|
|
413
|
+
function hasUnpairedSurrogate(value) {
|
|
414
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
415
|
+
const code = value.charCodeAt(index);
|
|
416
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
417
|
+
const next = value.charCodeAt(index + 1);
|
|
418
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
419
|
+
return true;
|
|
420
|
+
index += 1;
|
|
421
|
+
}
|
|
422
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
function error(code, message) {
|
|
428
|
+
return { status: "error", code, message };
|
|
429
|
+
}
|
|
430
|
+
const defaultExecFile = (file, args, options, callback) => {
|
|
431
|
+
nodeExecFile(file, [...args], options, (commandError, stdout, stderr) => {
|
|
432
|
+
callback(commandError, stdout, stderr);
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
//# sourceMappingURL=githubAcceptedOutcome.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ActionVerificationProjectionV0, type AgentEconomicsReceiptV0, type ContextHealthResult, type SessionVitalsV0, type SpendSummary, type TokenReductionExperimentV0, type WasteFindingV0 } from "@agent-finops/core";
|
|
2
|
+
/**
|
|
3
|
+
* One compact first-run experience assembled from existing canonical facts.
|
|
4
|
+
*
|
|
5
|
+
* This adapter never scans transcripts, evaluates an experiment, or changes
|
|
6
|
+
* state. The CLI entrypoint owns those operations. Keeping this file pure
|
|
7
|
+
* makes the same words deterministic in interactive and captured output.
|
|
8
|
+
*/
|
|
9
|
+
export type GuidedExperienceInput = {
|
|
10
|
+
receipt?: Pick<AgentEconomicsReceiptV0, "demoOnly" | "window" | "lines"> | null;
|
|
11
|
+
sessionVitals?: SessionVitalsV0 | null;
|
|
12
|
+
summary?: Pick<SpendSummary, "totalUsd" | "byProject"> | null;
|
|
13
|
+
contextHealth?: ContextHealthResult | null;
|
|
14
|
+
wasteFinding?: WasteFindingV0 | null;
|
|
15
|
+
/** The preferred locally persisted experiment, when the caller has it. */
|
|
16
|
+
preferredExperiment?: TokenReductionExperimentV0 | null;
|
|
17
|
+
/** A canonical read projection for callers that intentionally do not load the full experiment. */
|
|
18
|
+
projection?: ActionVerificationProjectionV0 | null;
|
|
19
|
+
/** Coverage of the qualitative/session reader that produced drivers and findings. */
|
|
20
|
+
qualitativeCoverage: "complete" | "partial" | "unknown";
|
|
21
|
+
/** Financial project aggregation may be complete even when qualitative indexing is partial. */
|
|
22
|
+
financialDriverComplete?: boolean;
|
|
23
|
+
/** True only when both the input and output streams are suitable for a prompt. */
|
|
24
|
+
interactive: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type GuidedExperienceModel = {
|
|
27
|
+
schemaVersion: 0;
|
|
28
|
+
usage: {
|
|
29
|
+
headline: string;
|
|
30
|
+
detail: string;
|
|
31
|
+
source: "completed_sessions" | "receipt" | "latest_turn" | "not_available";
|
|
32
|
+
};
|
|
33
|
+
mainDriver: {
|
|
34
|
+
heading: "MAIN DRIVER" | "TOP OBSERVED PROJECT";
|
|
35
|
+
headline: string;
|
|
36
|
+
detail: string;
|
|
37
|
+
source: "completed_session_tokens" | "tracked_cost_value" | "not_available";
|
|
38
|
+
};
|
|
39
|
+
insight: {
|
|
40
|
+
heading: "WHY IS IT HIGH?" | "WHAT STANDS OUT" | "WHAT STANDS OUT IN INDEXED EVIDENCE";
|
|
41
|
+
headline: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
};
|
|
44
|
+
safeTest: {
|
|
45
|
+
available: boolean;
|
|
46
|
+
headline: string;
|
|
47
|
+
detail: string;
|
|
48
|
+
};
|
|
49
|
+
progress: {
|
|
50
|
+
headline: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
} | null;
|
|
53
|
+
result: {
|
|
54
|
+
headline: string;
|
|
55
|
+
detail: string;
|
|
56
|
+
direction: "fewer" | "more" | "unchanged";
|
|
57
|
+
} | null;
|
|
58
|
+
interaction: {
|
|
59
|
+
mode: "interactive" | "read_only";
|
|
60
|
+
startPrompt: {
|
|
61
|
+
key: "enter";
|
|
62
|
+
label: string;
|
|
63
|
+
intent: "set_up_test" | "start_test";
|
|
64
|
+
} | null;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Build one launch card without performing I/O or mutating local state. */
|
|
68
|
+
export declare function buildGuidedExperience(input: GuidedExperienceInput): GuidedExperienceModel;
|
|
69
|
+
/** Render the model with stable headings and no terminal-control sequences. */
|
|
70
|
+
export declare function renderGuidedExperience(model: GuidedExperienceModel): string;
|
|
71
|
+
//# sourceMappingURL=guidedExperience.d.ts.map
|