agent-gauntlet 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +25 -23
  2. package/dist/index.js +9226 -0
  3. package/dist/index.js.map +65 -0
  4. package/dist/scripts/status.js +280 -0
  5. package/dist/scripts/status.js.map +10 -0
  6. package/package.json +22 -8
  7. package/src/built-in-reviews/code-quality.md +0 -25
  8. package/src/built-in-reviews/index.ts +0 -28
  9. package/src/bun-plugins.d.ts +0 -4
  10. package/src/cli-adapters/claude.ts +0 -327
  11. package/src/cli-adapters/codex.ts +0 -290
  12. package/src/cli-adapters/cursor.ts +0 -128
  13. package/src/cli-adapters/gemini.ts +0 -510
  14. package/src/cli-adapters/github-copilot.ts +0 -141
  15. package/src/cli-adapters/index.ts +0 -250
  16. package/src/cli-adapters/thinking-budget.ts +0 -23
  17. package/src/commands/check.ts +0 -311
  18. package/src/commands/ci/index.ts +0 -15
  19. package/src/commands/ci/init.ts +0 -96
  20. package/src/commands/ci/list-jobs.ts +0 -90
  21. package/src/commands/clean.ts +0 -54
  22. package/src/commands/detect.ts +0 -173
  23. package/src/commands/health.ts +0 -169
  24. package/src/commands/help.ts +0 -34
  25. package/src/commands/index.ts +0 -13
  26. package/src/commands/init.ts +0 -1878
  27. package/src/commands/list.ts +0 -33
  28. package/src/commands/review.ts +0 -311
  29. package/src/commands/run.ts +0 -29
  30. package/src/commands/shared.ts +0 -267
  31. package/src/commands/stop-hook.ts +0 -567
  32. package/src/commands/validate.ts +0 -20
  33. package/src/commands/wait-ci.ts +0 -518
  34. package/src/config/ci-loader.ts +0 -33
  35. package/src/config/ci-schema.ts +0 -28
  36. package/src/config/global.ts +0 -87
  37. package/src/config/loader.ts +0 -301
  38. package/src/config/schema.ts +0 -165
  39. package/src/config/stop-hook-config.ts +0 -130
  40. package/src/config/types.ts +0 -65
  41. package/src/config/validator.ts +0 -592
  42. package/src/core/change-detector.ts +0 -137
  43. package/src/core/diff-stats.ts +0 -442
  44. package/src/core/entry-point.ts +0 -190
  45. package/src/core/job.ts +0 -96
  46. package/src/core/run-executor.ts +0 -621
  47. package/src/core/runner.ts +0 -290
  48. package/src/gates/check.ts +0 -118
  49. package/src/gates/resolve-check-command.ts +0 -21
  50. package/src/gates/result.ts +0 -54
  51. package/src/gates/review.ts +0 -1333
  52. package/src/hooks/adapters/claude-stop-hook.ts +0 -99
  53. package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
  54. package/src/hooks/adapters/types.ts +0 -94
  55. package/src/hooks/stop-hook-handler.ts +0 -748
  56. package/src/index.ts +0 -47
  57. package/src/output/app-logger.ts +0 -214
  58. package/src/output/console-log.ts +0 -168
  59. package/src/output/console.ts +0 -359
  60. package/src/output/logger.ts +0 -126
  61. package/src/output/sinks/console-sink.ts +0 -59
  62. package/src/output/sinks/file-sink.ts +0 -110
  63. package/src/scripts/status.ts +0 -433
  64. package/src/templates/workflow.yml +0 -79
  65. package/src/types/gauntlet-status.ts +0 -79
  66. package/src/utils/debug-log.ts +0 -392
  67. package/src/utils/diff-parser.ts +0 -103
  68. package/src/utils/execution-state.ts +0 -472
  69. package/src/utils/log-parser.ts +0 -696
  70. package/src/utils/sanitizer.ts +0 -3
  71. package/src/utils/session-ref.ts +0 -91
@@ -1,472 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { getDebugLogger } from "./debug-log.js";
5
-
6
- const EXECUTION_STATE_FILENAME = ".execution_state";
7
- const SESSION_REF_FILENAME = ".session_ref";
8
-
9
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
10
- // Use loose equality to check both null and undefined in one comparison
11
- if (value == null) return false;
12
- if (Array.isArray(value)) return false;
13
- return typeof value === "object";
14
- }
15
-
16
- function extractUnhealthyAdapters(
17
- rawData: Record<string, unknown> | null,
18
- ): Record<string, UnhealthyAdapter> | undefined {
19
- const adapters = rawData?.unhealthy_adapters;
20
- if (!isPlainRecord(adapters)) {
21
- return undefined;
22
- }
23
- return adapters as Record<string, UnhealthyAdapter>;
24
- }
25
-
26
- export interface UnhealthyAdapter {
27
- marked_at: string;
28
- reason: string;
29
- }
30
-
31
- export interface ExecutionState {
32
- last_run_completed_at: string;
33
- branch: string;
34
- commit: string;
35
- working_tree_ref?: string;
36
- unhealthy_adapters?: Record<string, UnhealthyAdapter>;
37
- }
38
-
39
- /**
40
- * Read the execution state from the log directory.
41
- * Returns null if the state file or directory doesn't exist.
42
- */
43
- function isValidStateData(data: unknown): data is Record<string, unknown> & {
44
- last_run_completed_at: string;
45
- branch: string;
46
- commit: string;
47
- } {
48
- if (typeof data !== "object" || data === null) return false;
49
- const record = data as Record<string, unknown>;
50
- return (
51
- typeof record.last_run_completed_at === "string" &&
52
- typeof record.branch === "string" &&
53
- typeof record.commit === "string"
54
- );
55
- }
56
-
57
- export async function readExecutionState(
58
- logDir: string,
59
- ): Promise<ExecutionState | null> {
60
- try {
61
- const statePath = path.join(logDir, EXECUTION_STATE_FILENAME);
62
- const content = await fs.readFile(statePath, "utf-8");
63
- const data = JSON.parse(content) as unknown;
64
-
65
- if (!isValidStateData(data)) return null;
66
-
67
- const state: ExecutionState = {
68
- last_run_completed_at: data.last_run_completed_at,
69
- branch: data.branch,
70
- commit: data.commit,
71
- };
72
-
73
- if (typeof data.working_tree_ref === "string") {
74
- state.working_tree_ref = data.working_tree_ref;
75
- }
76
-
77
- if (
78
- data.unhealthy_adapters &&
79
- typeof data.unhealthy_adapters === "object"
80
- ) {
81
- state.unhealthy_adapters = data.unhealthy_adapters as Record<
82
- string,
83
- UnhealthyAdapter
84
- >;
85
- }
86
-
87
- return state;
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- /**
94
- * Create a stash SHA that captures the current working tree state.
95
- * Uses `git stash create --include-untracked` which creates a stash commit
96
- * without modifying the working tree.
97
- * Returns the stash SHA, or HEAD SHA if working tree is clean.
98
- */
99
- export async function createWorkingTreeRef(): Promise<string> {
100
- return new Promise((resolve, reject) => {
101
- const child = spawn("git", ["stash", "create", "--include-untracked"], {
102
- stdio: ["ignore", "pipe", "pipe"],
103
- });
104
-
105
- let stdout = "";
106
- child.stdout.on("data", (data: Buffer) => {
107
- stdout += data.toString();
108
- });
109
-
110
- child.on("close", async (code) => {
111
- if (code === 0) {
112
- const sha = stdout.trim();
113
- if (sha) {
114
- // Stash created with working tree changes
115
- resolve(sha);
116
- } else {
117
- // Clean working tree - use HEAD instead
118
- try {
119
- const headSha = await getCurrentCommit();
120
- resolve(headSha);
121
- } catch (err) {
122
- reject(err);
123
- }
124
- }
125
- } else {
126
- // Try to fall back to HEAD
127
- try {
128
- const headSha = await getCurrentCommit();
129
- resolve(headSha);
130
- } catch {
131
- reject(new Error(`git stash create failed with code ${code}`));
132
- }
133
- }
134
- });
135
-
136
- child.on("error", reject);
137
- });
138
- }
139
-
140
- /**
141
- * Write the execution state to the log directory.
142
- * Records the current branch, commit SHA, working tree ref, and timestamp.
143
- * Also cleans up legacy .session_ref file if it exists.
144
- */
145
- export async function writeExecutionState(logDir: string): Promise<void> {
146
- const statePath = path.join(logDir, EXECUTION_STATE_FILENAME);
147
- const [branch, commit, workingTreeRef, rawState] = await Promise.all([
148
- getCurrentBranch(),
149
- getCurrentCommit(),
150
- createWorkingTreeRef(),
151
- readRawState(statePath),
152
- ]);
153
- const existingUnhealthy = extractUnhealthyAdapters(rawState);
154
-
155
- const state: ExecutionState = {
156
- last_run_completed_at: new Date().toISOString(),
157
- branch,
158
- commit,
159
- working_tree_ref: workingTreeRef,
160
- };
161
-
162
- // Preserve unhealthy_adapters from existing state
163
- if (existingUnhealthy) {
164
- state.unhealthy_adapters = existingUnhealthy;
165
- }
166
-
167
- // Log changed fields (skip last_run_completed_at since every log line is timestamped)
168
- const changes: Record<string, string> = {};
169
- const oldState = rawState as Record<string, unknown> | null;
170
- if (oldState) {
171
- if (oldState.branch !== branch) changes.branch = branch;
172
- if (oldState.commit !== commit) changes.commit = commit;
173
- if (oldState.working_tree_ref !== workingTreeRef)
174
- changes.working_tree_ref = workingTreeRef;
175
- } else {
176
- // First write - log all fields
177
- changes.branch = branch;
178
- changes.commit = commit;
179
- changes.working_tree_ref = workingTreeRef;
180
- }
181
- await getDebugLogger()?.logStateWrite(changes);
182
-
183
- // Ensure the log directory exists
184
- await fs.mkdir(logDir, { recursive: true });
185
- await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
186
-
187
- // Clean up legacy .session_ref file if it exists
188
- try {
189
- const sessionRefPath = path.join(logDir, SESSION_REF_FILENAME);
190
- await fs.rm(sessionRefPath, { force: true });
191
- } catch {
192
- // Ignore errors
193
- }
194
- }
195
-
196
- /**
197
- * Get the current git branch name.
198
- */
199
- export async function getCurrentBranch(): Promise<string> {
200
- return new Promise((resolve, reject) => {
201
- const child = spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
202
- stdio: ["ignore", "pipe", "pipe"],
203
- });
204
-
205
- let stdout = "";
206
- child.stdout.on("data", (data: Buffer) => {
207
- stdout += data.toString();
208
- });
209
-
210
- child.on("close", (code) => {
211
- if (code === 0) {
212
- resolve(stdout.trim());
213
- } else {
214
- reject(new Error(`git rev-parse failed with code ${code}`));
215
- }
216
- });
217
-
218
- child.on("error", reject);
219
- });
220
- }
221
-
222
- /**
223
- * Get the current HEAD commit SHA.
224
- */
225
- export async function getCurrentCommit(): Promise<string> {
226
- return new Promise((resolve, reject) => {
227
- const child = spawn("git", ["rev-parse", "HEAD"], {
228
- stdio: ["ignore", "pipe", "pipe"],
229
- });
230
-
231
- let stdout = "";
232
- child.stdout.on("data", (data: Buffer) => {
233
- stdout += data.toString();
234
- });
235
-
236
- child.on("close", (code) => {
237
- if (code === 0) {
238
- resolve(stdout.trim());
239
- } else {
240
- reject(new Error(`git rev-parse failed with code ${code}`));
241
- }
242
- });
243
-
244
- child.on("error", reject);
245
- });
246
- }
247
-
248
- /**
249
- * Check if a commit is an ancestor of a branch (i.e., the commit has been merged).
250
- * Uses `git merge-base --is-ancestor`.
251
- * Returns true if commit is reachable from branch.
252
- */
253
- export async function isCommitInBranch(
254
- commit: string,
255
- branch: string,
256
- ): Promise<boolean> {
257
- return new Promise((resolve) => {
258
- const child = spawn(
259
- "git",
260
- ["merge-base", "--is-ancestor", commit, branch],
261
- { stdio: ["ignore", "pipe", "pipe"] },
262
- );
263
-
264
- child.on("close", (code) => {
265
- // Exit 0 = is ancestor (merged), exit 1 = not ancestor
266
- resolve(code === 0);
267
- });
268
-
269
- child.on("error", () => {
270
- resolve(false);
271
- });
272
- });
273
- }
274
-
275
- /**
276
- * Get the execution state filename (for use in clean operations).
277
- */
278
- export function getExecutionStateFilename(): string {
279
- return EXECUTION_STATE_FILENAME;
280
- }
281
-
282
- /**
283
- * Check if a git object (commit, tree, blob, etc.) exists in the repository.
284
- * Uses `git cat-file -t <sha>` to check object type.
285
- */
286
- export async function gitObjectExists(sha: string): Promise<boolean> {
287
- return new Promise((resolve) => {
288
- const child = spawn("git", ["cat-file", "-t", sha], {
289
- stdio: ["ignore", "pipe", "pipe"],
290
- });
291
-
292
- child.on("close", (code) => {
293
- resolve(code === 0);
294
- });
295
-
296
- child.on("error", () => {
297
- resolve(false);
298
- });
299
- });
300
- }
301
-
302
- /**
303
- * When a commit has been merged, check if working_tree_ref still scopes valid changes.
304
- */
305
- async function resolveFixBaseForMergedCommit(
306
- working_tree_ref: string | undefined,
307
- ): Promise<{ fixBase: string | null; warning?: string }> {
308
- if (!working_tree_ref) {
309
- return { fixBase: null };
310
- }
311
- const refExists = await gitObjectExists(working_tree_ref);
312
- if (!refExists) {
313
- return { fixBase: null };
314
- }
315
- return {
316
- fixBase: working_tree_ref,
317
- warning:
318
- "Commit merged into base branch, using working tree ref for diff scope",
319
- };
320
- }
321
-
322
- /**
323
- * Resolve the fixBase for change detection based on execution state.
324
- * Used for post-clean runs to scope diffs to changes since the last passing run.
325
- *
326
- * Returns:
327
- * - working_tree_ref if valid (not gc'd) and commit not merged
328
- * - commit as fallback if working_tree_ref is gc'd
329
- * - null if state is stale (commit merged) or all refs are invalid
330
- */
331
- export async function resolveFixBase(
332
- executionState: ExecutionState,
333
- baseBranch: string,
334
- ): Promise<{ fixBase: string | null; warning?: string }> {
335
- const { commit, working_tree_ref } = executionState;
336
-
337
- // Check if commit has been merged into base branch (state is stale)
338
- const commitMerged = await isCommitInBranch(commit, baseBranch);
339
- if (commitMerged) {
340
- return resolveFixBaseForMergedCommit(working_tree_ref);
341
- }
342
-
343
- // Check if working_tree_ref exists
344
- if (working_tree_ref) {
345
- const refExists = await gitObjectExists(working_tree_ref);
346
- if (refExists) {
347
- // Use working tree ref for precise diff
348
- return { fixBase: working_tree_ref };
349
- }
350
- }
351
-
352
- // working_tree_ref doesn't exist or was gc'd, try commit as fallback
353
- const commitExists = await gitObjectExists(commit);
354
- if (commitExists) {
355
- return {
356
- fixBase: commit,
357
- warning: "Session stash was garbage collected, using commit as fallback",
358
- };
359
- }
360
-
361
- // Everything is gone, fall back to base branch
362
- return { fixBase: null };
363
- }
364
-
365
- const COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
366
-
367
- /**
368
- * Check if an unhealthy adapter entry is still within the cooldown period.
369
- * Returns true if marked_at is less than 1 hour ago.
370
- * Invalid or missing timestamps default to "expired" (returns false).
371
- */
372
- export function isAdapterCoolingDown(entry: UnhealthyAdapter): boolean {
373
- const markedAt = new Date(entry.marked_at).getTime();
374
- if (Number.isNaN(markedAt)) return false;
375
- return Date.now() - markedAt < COOLDOWN_MS;
376
- }
377
-
378
- /**
379
- * Get the unhealthy adapters map from execution state.
380
- * Returns an empty object if no unhealthy adapters are recorded.
381
- */
382
- export async function getUnhealthyAdapters(
383
- logDir: string,
384
- ): Promise<Record<string, UnhealthyAdapter>> {
385
- const statePath = path.join(logDir, EXECUTION_STATE_FILENAME);
386
- const rawState = await readRawState(statePath);
387
- return extractUnhealthyAdapters(rawState) ?? {};
388
- }
389
-
390
- /**
391
- * Read raw state data from the state file.
392
- * Returns null if the file doesn't exist or is invalid.
393
- */
394
- async function readRawState(
395
- statePath: string,
396
- ): Promise<Record<string, unknown> | null> {
397
- try {
398
- const content = await fs.readFile(statePath, "utf-8");
399
- return JSON.parse(content) as Record<string, unknown>;
400
- } catch {
401
- return null;
402
- }
403
- }
404
-
405
- /**
406
- * Mark an adapter as unhealthy in the execution state.
407
- * Reads the current state, upserts the entry, and writes back.
408
- */
409
- export async function markAdapterUnhealthy(
410
- logDir: string,
411
- adapterName: string,
412
- reason: string,
413
- ): Promise<void> {
414
- await getDebugLogger()?.logAdapterHealthChange(adapterName, false, reason);
415
-
416
- const statePath = path.join(logDir, EXECUTION_STATE_FILENAME);
417
- const rawData = (await readRawState(statePath)) ?? {};
418
-
419
- const adapters =
420
- (rawData.unhealthy_adapters as Record<string, UnhealthyAdapter>) ?? {};
421
- adapters[adapterName] = {
422
- marked_at: new Date().toISOString(),
423
- reason,
424
- };
425
- rawData.unhealthy_adapters = adapters;
426
-
427
- await fs.mkdir(logDir, { recursive: true });
428
- await fs.writeFile(statePath, JSON.stringify(rawData, null, 2), "utf-8");
429
- }
430
-
431
- /**
432
- * Mark an adapter as healthy by removing it from the unhealthy list.
433
- * Reads the current state, removes the entry, and writes back.
434
- */
435
- export async function markAdapterHealthy(
436
- logDir: string,
437
- adapterName: string,
438
- ): Promise<void> {
439
- await getDebugLogger()?.logAdapterHealthChange(adapterName, true);
440
-
441
- const statePath = path.join(logDir, EXECUTION_STATE_FILENAME);
442
- const rawData = await readRawState(statePath);
443
- if (!rawData) return;
444
-
445
- const adapters = rawData.unhealthy_adapters as
446
- | Record<string, UnhealthyAdapter>
447
- | undefined;
448
- if (!adapters || !(adapterName in adapters)) return;
449
-
450
- delete adapters[adapterName];
451
- if (Object.keys(adapters).length === 0) {
452
- delete rawData.unhealthy_adapters;
453
- } else {
454
- rawData.unhealthy_adapters = adapters;
455
- }
456
-
457
- await fs.writeFile(statePath, JSON.stringify(rawData, null, 2), "utf-8");
458
- }
459
-
460
- /**
461
- * Delete the execution state file.
462
- * Used when auto-clean resets state due to context change.
463
- */
464
- export async function deleteExecutionState(logDir: string): Promise<void> {
465
- try {
466
- await getDebugLogger()?.logStateDelete();
467
- const statePath = path.join(logDir, EXECUTION_STATE_FILENAME);
468
- await fs.rm(statePath, { force: true });
469
- } catch {
470
- // Ignore errors
471
- }
472
- }