@allurereport/plugin-agent 3.11.0 → 3.12.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.
package/dist/plugin.js CHANGED
@@ -783,6 +783,60 @@ const renderModelingSummary = (modeling) => {
783
783
  : "None");
784
784
  return lines.join("\n");
785
785
  };
786
+ const cloneHumanReportStatus = (status) => ({
787
+ ...status,
788
+ reports: status.reports.map((report) => ({ ...report })),
789
+ ...(status.errors ? { errors: status.errors.map((error) => ({ ...error })) } : {}),
790
+ });
791
+ const resolveHumanReportStatus = async (provider) => {
792
+ if (!provider) {
793
+ return undefined;
794
+ }
795
+ const status = typeof provider === "function" ? await provider() : provider;
796
+ return status ? cloneHumanReportStatus(status) : undefined;
797
+ };
798
+ const renderHumanReportSection = (humanReport) => {
799
+ if (!humanReport) {
800
+ return undefined;
801
+ }
802
+ const lines = [
803
+ "## Human Report",
804
+ "",
805
+ `- Status: ${humanReport.status}`,
806
+ `- Mode: ${humanReport.mode}`,
807
+ `- Result Count: ${humanReport.result_count ?? "unknown"}`,
808
+ `- Threshold: ${humanReport.threshold}`,
809
+ ];
810
+ if (humanReport.path) {
811
+ lines.push(`- Path: [${escapeInlineMarkdown(humanReport.path)}](${normalizeMarkdownPath(humanReport.path)})`);
812
+ }
813
+ if (humanReport.reason) {
814
+ lines.push(`- Reason: ${escapeInlineMarkdown(humanReport.reason)}`);
815
+ }
816
+ if (humanReport.error) {
817
+ lines.push(`- Error: ${escapeInlineMarkdown(humanReport.error)}`);
818
+ }
819
+ if (humanReport.reports.length > 1) {
820
+ lines.push("");
821
+ lines.push("### Reports");
822
+ lines.push("");
823
+ lines.push(humanReport.reports
824
+ .map((report) => `- ${escapeInlineMarkdown(report.plugin_id)}: [${escapeInlineMarkdown(report.path)}](${normalizeMarkdownPath(report.path)})`)
825
+ .join("\n"));
826
+ }
827
+ if (humanReport.errors?.length) {
828
+ lines.push("");
829
+ lines.push("### Report Errors");
830
+ lines.push("");
831
+ lines.push(humanReport.errors
832
+ .map((error) => {
833
+ const prefix = error.plugin_id ? `${error.plugin_id}: ` : "";
834
+ return `- ${escapeInlineMarkdown(`${prefix}${error.message}`)}`;
835
+ })
836
+ .join("\n"));
837
+ }
838
+ return lines.join("\n");
839
+ };
786
840
  const renderSelectorSummary = (title, selectors) => {
787
841
  if (!hasSelector(selectors) && selectors.testCount === undefined) {
788
842
  return `- ${title}: None`;
@@ -1282,7 +1336,7 @@ const renderTestFile = (params) => {
1282
1336
  return `${lines.join("\n").trimEnd()}\n`;
1283
1337
  };
1284
1338
  const renderIndex = (params) => {
1285
- const { context, command, generatedAt, phase, stats, durationSummary, environmentSummary, modelingSummary, expectations, tests, globalArtifacts, globalErrors, globalExitCode, qualityGateResults, findings, } = params;
1339
+ const { context, command, generatedAt, phase, stats, durationSummary, environmentSummary, modelingSummary, expectations, tests, globalArtifacts, globalErrors, globalExitCode, qualityGateResults, findings, humanReport, } = params;
1286
1340
  const stdoutArtifact = globalArtifacts.find((artifact) => artifact.displayName === "stdout.txt");
1287
1341
  const stderrArtifact = globalArtifacts.find((artifact) => artifact.displayName === "stderr.txt");
1288
1342
  const remainingGlobalArtifacts = globalArtifacts.filter((artifact) => artifact.displayName !== "stdout.txt" && artifact.displayName !== "stderr.txt");
@@ -1343,6 +1397,11 @@ const renderIndex = (params) => {
1343
1397
  : "None");
1344
1398
  lines.push("");
1345
1399
  lines.push(renderModelingSummary(modelingSummary));
1400
+ const humanReportSection = renderHumanReportSection(humanReport);
1401
+ if (humanReportSection) {
1402
+ lines.push("");
1403
+ lines.push(humanReportSection);
1404
+ }
1346
1405
  if (expectations) {
1347
1406
  lines.push("");
1348
1407
  lines.push("## Expected Scope");
@@ -2593,7 +2652,7 @@ const appendJsonlLine = async (path, item) => {
2593
2652
  await appendFile(path, `${JSON.stringify(item)}\n`, "utf-8");
2594
2653
  };
2595
2654
  const toRunManifest = (params) => {
2596
- const { context, command, agentContext, generatedAt, phase, expectations, snapshot } = params;
2655
+ const { context, command, agentContext, generatedAt, phase, expectations, snapshot, humanReport } = params;
2597
2656
  const stdoutArtifact = snapshot.globalArtifacts.find((artifact) => artifact.displayName === "stdout.txt");
2598
2657
  const stderrArtifact = snapshot.globalArtifacts.find((artifact) => artifact.displayName === "stderr.txt");
2599
2658
  const originalExitCode = snapshot.globalExitCode?.original ?? null;
@@ -2637,11 +2696,13 @@ const toRunManifest = (params) => {
2637
2696
  findings_manifest: "manifest/findings.jsonl",
2638
2697
  test_events_manifest: "manifest/test-events.jsonl",
2639
2698
  expected_manifest: expectations?.relativePath ?? null,
2699
+ human_report_manifest: humanReport ? "manifest/human-report.json" : null,
2640
2700
  process_logs: {
2641
2701
  stdout: stdoutArtifact?.relativePath ?? null,
2642
2702
  stderr: stderrArtifact?.relativePath ?? null,
2643
2703
  },
2644
2704
  },
2705
+ human_report: humanReport ?? null,
2645
2706
  expectations_present: Boolean(expectations),
2646
2707
  expectations: expectations ? toExpectationModel(expectations) : null,
2647
2708
  expectation_result: expectationResult,
@@ -2659,6 +2720,7 @@ const writeSnapshotFiles = async (params) => {
2659
2720
  const { outputDir, context, command, generatedAt, expectations } = runtime;
2660
2721
  const nextTestPaths = new Set(snapshot.entries.map((entry) => entry.filePath));
2661
2722
  const nextAssetDirs = new Set(snapshot.entries.map((entry) => join(outputDir, entry.relativeAssetDir)));
2723
+ const humanReport = await resolveHumanReportStatus(runtime.humanReport);
2662
2724
  for (const stalePath of runtime.currentTestPaths) {
2663
2725
  if (!nextTestPaths.has(stalePath)) {
2664
2726
  await rm(stalePath, { force: true });
@@ -2687,7 +2749,11 @@ const writeSnapshotFiles = async (params) => {
2687
2749
  phase,
2688
2750
  expectations,
2689
2751
  snapshot,
2752
+ humanReport,
2690
2753
  })),
2754
+ ...(humanReport
2755
+ ? [writeJson(join(outputDir, "manifest", "human-report.json"), humanReport)]
2756
+ : [rm(join(outputDir, "manifest", "human-report.json"), { force: true })]),
2691
2757
  writeJsonlSnapshot(join(outputDir, "manifest", "tests.jsonl"), snapshot.entries.map(toTestsManifestLine)),
2692
2758
  writeJsonlSnapshot(join(outputDir, "manifest", "findings.jsonl"), snapshot.combinedAllFindings.map(toFindingManifestLine)),
2693
2759
  writeTextAtomic(join(outputDir, "index.md"), renderIndex({
@@ -2706,6 +2772,7 @@ const writeSnapshotFiles = async (params) => {
2706
2772
  globalExitCode: snapshot.globalExitCode,
2707
2773
  qualityGateResults: snapshot.qualityGateResults,
2708
2774
  findings: snapshot.combinedAllFindings,
2775
+ humanReport,
2709
2776
  })),
2710
2777
  writeTextAtomic(join(outputDir, "AGENTS.md"), renderAgentsGuide()),
2711
2778
  ]);
@@ -2958,6 +3025,7 @@ const createRuntimeState = async (params) => {
2958
3025
  taskId: options.taskId,
2959
3026
  conversationId: options.conversationId,
2960
3027
  },
3028
+ humanReport: options.humanReport,
2961
3029
  createFinding,
2962
3030
  expectations: expectationLoadResult.expectations,
2963
3031
  expectationLoadFindings: expectationLoadResult.findings,
package/dist/query.d.ts CHANGED
@@ -151,6 +151,7 @@ export declare const buildAgentQueryPayload: (output: AgentOutputBundle, view: A
151
151
  completeness: "complete" | "partial";
152
152
  };
153
153
  } | null;
154
+ human_report: import("./model.js").AgentHumanReportStatus | null;
154
155
  check_summary: {
155
156
  total: number;
156
157
  countsBySeverity: Record<AgentFindingSeverity, number>;
@@ -163,6 +164,7 @@ export declare const buildAgentQueryPayload: (output: AgentOutputBundle, view: A
163
164
  findings_manifest: string | null;
164
165
  test_events_manifest: string | null;
165
166
  expected_manifest: string | null;
167
+ human_report_manifest: string | null;
166
168
  process_logs: {
167
169
  stdout: string | null;
168
170
  stderr: string | null;
package/dist/query.js CHANGED
@@ -101,6 +101,7 @@ const buildAgentQuerySummaryPayload = (output) => ({
101
101
  },
102
102
  summary: output.run.summary,
103
103
  modeling: output.run.modeling ?? null,
104
+ human_report: output.humanReport ?? output.run.human_report ?? null,
104
105
  check_summary: output.run.check_summary,
105
106
  paths: {
106
107
  index_md: resolveAgentOutputPath(output, output.run.paths.index_md),
@@ -109,6 +110,7 @@ const buildAgentQuerySummaryPayload = (output) => ({
109
110
  findings_manifest: resolveAgentOutputPath(output, output.run.paths.findings_manifest),
110
111
  test_events_manifest: resolveAgentOutputPath(output, output.run.paths.test_events_manifest),
111
112
  expected_manifest: resolveAgentOutputPath(output, output.run.paths.expected_manifest),
113
+ human_report_manifest: resolveAgentOutputPath(output, output.run.paths.human_report_manifest),
112
114
  process_logs: {
113
115
  stdout: resolveAgentOutputPath(output, output.run.paths.process_logs.stdout),
114
116
  stderr: resolveAgentOutputPath(output, output.run.paths.process_logs.stderr),
package/dist/state.d.ts CHANGED
@@ -1,15 +1,56 @@
1
- export type AgentLatestState = {
2
- schema: "allure-agent-latest/v1";
1
+ export { ALLURE_AGENT_STATE_DIR_ENV, resolveAgentStateDir } from "./utils.js";
2
+ export type AgentRunState = {
3
+ schema: "allure-agent-run/v1";
4
+ runId: string;
3
5
  cwd: string;
4
6
  outputDir: string;
7
+ managedOutput: boolean;
5
8
  expectationsPath?: string;
6
9
  command: string;
7
- startedAt: string;
8
- finishedAt?: string;
10
+ startedAt: number;
11
+ finishedAt?: number;
9
12
  status: "running" | "finished";
10
13
  exitCode?: number | null;
14
+ pid?: number;
11
15
  };
12
- export declare const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
13
- export declare const resolveAgentStateDir: (cwd: string) => string;
14
- export declare const writeLatestAgentState: (value: Omit<AgentLatestState, "schema">) => Promise<AgentLatestState>;
16
+ export type AgentLatestState = AgentRunState;
17
+ export type AgentStateCleanupResult = {
18
+ deleted: AgentRunState[];
19
+ failed: {
20
+ state: AgentRunState;
21
+ error: unknown;
22
+ }[];
23
+ retained: AgentRunState[];
24
+ };
25
+ export type AgentOrphanOutputCleanupResult = {
26
+ deleted: string[];
27
+ failed: {
28
+ outputDir: string;
29
+ error: unknown;
30
+ }[];
31
+ retained: string[];
32
+ };
33
+ export type AgentStaleStateCleanupResult = AgentStateCleanupResult & {
34
+ checked: number;
35
+ orphaned: AgentOrphanOutputCleanupResult;
36
+ skipped: {
37
+ cwd: string;
38
+ statePath: string;
39
+ reason: "locked";
40
+ }[];
41
+ };
42
+ export declare const writeAgentRunState: (value: Omit<AgentRunState, "schema">) => Promise<AgentRunState>;
43
+ export declare const writeLatestAgentState: (value: Omit<AgentRunState, "schema">) => Promise<AgentRunState>;
44
+ export declare const readAgentRunStates: (cwd: string) => Promise<AgentRunState[]>;
15
45
  export declare const readLatestAgentState: (cwd: string) => Promise<AgentLatestState | undefined>;
46
+ export declare const cleanupAgentRunState: (params: {
47
+ cwd: string;
48
+ currentRunId?: string;
49
+ keepManagedRuns?: number;
50
+ }) => Promise<AgentStateCleanupResult>;
51
+ export declare const cleanupStaleAgentRunStates: (params: {
52
+ cwd: string;
53
+ currentRunId?: string;
54
+ staleOutputTtlMs?: number;
55
+ now?: number;
56
+ }) => Promise<AgentStaleStateCleanupResult>;
package/dist/state.js CHANGED
@@ -1,83 +1,277 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
5
- const AGENT_STATE_SCHEMA = "allure-agent-latest/v1";
6
- export const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
7
- const isFileNotFoundError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
8
- const projectHash = (cwd) => createHash("sha256").update(cwd).digest("hex").slice(0, 16);
9
- export const resolveAgentStateDir = (cwd) => {
10
- const configuredDir = process.env[ALLURE_AGENT_STATE_DIR_ENV]?.trim();
11
- if (configuredDir) {
12
- return resolve(configuredDir);
13
- }
14
- return join(tmpdir(), `allure-agent-state-${projectHash(resolve(cwd))}`);
15
- };
16
- const projectStatePath = (cwd) => join(resolveAgentStateDir(cwd), "latest.json");
17
- const writeJsonAtomic = async (filePath, value) => {
18
- await mkdir(dirname(filePath), { recursive: true });
19
- const tempPath = `${filePath}.${process.pid}.tmp`;
20
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
21
- await rename(tempPath, filePath);
22
- };
23
- const isAgentLatestState = (value) => {
1
+ import { appendFile, readFile, rm } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { isFileNotFoundError, listAgentManagedTempOutputDirs, listAgentStatePaths, pathExists, projectStatePath, readPathMtimeMs, tryWithAgentStateLock, withAgentStateLock, writeJsonlAtomic, } from "./utils.js";
4
+ export { ALLURE_AGENT_STATE_DIR_ENV, resolveAgentStateDir } from "./utils.js";
5
+ const AGENT_RUN_STATE_SCHEMA = "allure-agent-run/v1";
6
+ const AGENT_STALE_OUTPUT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
7
+ const isAgentRunState = (value) => {
24
8
  if (typeof value !== "object" || value === null) {
25
9
  return false;
26
10
  }
27
11
  const candidate = value;
28
- return (candidate.schema === AGENT_STATE_SCHEMA &&
12
+ return (candidate.schema === AGENT_RUN_STATE_SCHEMA &&
13
+ typeof candidate.runId === "string" &&
29
14
  typeof candidate.cwd === "string" &&
30
15
  typeof candidate.outputDir === "string" &&
16
+ typeof candidate.managedOutput === "boolean" &&
31
17
  typeof candidate.command === "string" &&
32
- typeof candidate.startedAt === "string" &&
18
+ typeof candidate.startedAt === "number" &&
19
+ Number.isSafeInteger(candidate.startedAt) &&
33
20
  (candidate.expectationsPath === undefined || typeof candidate.expectationsPath === "string") &&
34
- (candidate.finishedAt === undefined || typeof candidate.finishedAt === "string") &&
21
+ (candidate.finishedAt === undefined ||
22
+ (typeof candidate.finishedAt === "number" && Number.isSafeInteger(candidate.finishedAt))) &&
35
23
  (candidate.status === "running" || candidate.status === "finished") &&
36
- (candidate.exitCode === undefined || typeof candidate.exitCode === "number" || candidate.exitCode === null));
24
+ (candidate.exitCode === undefined || typeof candidate.exitCode === "number" || candidate.exitCode === null) &&
25
+ (candidate.pid === undefined || (typeof candidate.pid === "number" && Number.isSafeInteger(candidate.pid))));
37
26
  };
38
- export const writeLatestAgentState = async (value) => {
39
- const normalizedState = {
40
- schema: AGENT_STATE_SCHEMA,
41
- cwd: resolve(value.cwd),
42
- outputDir: resolve(value.outputDir),
43
- expectationsPath: value.expectationsPath ? resolve(value.expectationsPath) : undefined,
44
- command: value.command,
45
- startedAt: value.startedAt,
46
- finishedAt: value.finishedAt,
47
- status: value.status,
48
- exitCode: value.exitCode,
49
- };
50
- await writeJsonAtomic(projectStatePath(normalizedState.cwd), normalizedState);
51
- return normalizedState;
52
- };
53
- export const readLatestAgentState = async (cwd) => {
54
- const normalizedCwd = resolve(cwd);
55
- const statePath = projectStatePath(normalizedCwd);
27
+ const normalizeAgentRunState = (value) => ({
28
+ schema: AGENT_RUN_STATE_SCHEMA,
29
+ runId: value.runId,
30
+ cwd: resolve(value.cwd),
31
+ outputDir: resolve(value.outputDir),
32
+ managedOutput: value.managedOutput,
33
+ expectationsPath: value.expectationsPath ? resolve(value.expectationsPath) : undefined,
34
+ command: value.command,
35
+ startedAt: value.startedAt,
36
+ finishedAt: value.finishedAt,
37
+ status: value.status,
38
+ exitCode: value.exitCode,
39
+ pid: value.pid,
40
+ });
41
+ const readAgentRunStateFile = async (statePath, cwd) => {
42
+ const normalizedCwd = cwd === undefined ? undefined : resolve(cwd);
56
43
  let raw;
57
44
  try {
58
45
  raw = await readFile(statePath, "utf-8");
59
46
  }
60
47
  catch (error) {
61
48
  if (isFileNotFoundError(error)) {
62
- return undefined;
49
+ return [];
63
50
  }
64
51
  throw error;
65
52
  }
66
- const parsed = JSON.parse(raw);
67
- if (!isAgentLatestState(parsed)) {
68
- throw new Error(`Invalid latest agent state in ${statePath}`);
53
+ return raw
54
+ .split("\n")
55
+ .map((line) => line.trim())
56
+ .filter(Boolean)
57
+ .flatMap((line) => {
58
+ try {
59
+ const parsed = JSON.parse(line);
60
+ if (!isAgentRunState(parsed)) {
61
+ return [];
62
+ }
63
+ return normalizedCwd === undefined || parsed.cwd === normalizedCwd ? [parsed] : [];
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ });
69
+ };
70
+ const readAgentRunStateLines = async (cwd) => {
71
+ const normalizedCwd = resolve(cwd);
72
+ return readAgentRunStateFile(projectStatePath(normalizedCwd), normalizedCwd);
73
+ };
74
+ const foldAgentRunStates = (states) => {
75
+ const order = [];
76
+ const latestByRunId = new Map();
77
+ for (const state of states) {
78
+ if (!latestByRunId.has(state.runId)) {
79
+ order.push(state.runId);
80
+ }
81
+ latestByRunId.set(state.runId, state);
69
82
  }
70
- if (parsed.cwd !== normalizedCwd) {
71
- return undefined;
83
+ return order
84
+ .map((runId) => latestByRunId.get(runId))
85
+ .filter((state) => state !== undefined)
86
+ .sort((a, b) => a.startedAt - b.startedAt || (a.finishedAt ?? a.startedAt) - (b.finishedAt ?? b.startedAt));
87
+ };
88
+ const getAgentRunStateAgeTimestamp = (state) => state.finishedAt ?? state.startedAt;
89
+ const isManagedOutputStale = (state, now, staleOutputTtlMs) => state.managedOutput && now - getAgentRunStateAgeTimestamp(state) >= staleOutputTtlMs;
90
+ const isAgentOutputDirectory = async (outputDir) => (await pathExists(join(outputDir, "manifest", "run.json"))) || (await pathExists(join(outputDir, "index.md")));
91
+ const cleanupStaleAgentRunState = async (params) => {
92
+ const states = foldAgentRunStates(await readAgentRunStateLines(params.cwd));
93
+ const retained = [];
94
+ const deleted = [];
95
+ const failed = [];
96
+ for (const state of states) {
97
+ if (!(await pathExists(state.outputDir))) {
98
+ continue;
99
+ }
100
+ if (!isManagedOutputStale(state, params.now, params.staleOutputTtlMs)) {
101
+ retained.push(state);
102
+ continue;
103
+ }
104
+ try {
105
+ await rm(state.outputDir, { recursive: true, force: true });
106
+ deleted.push(state);
107
+ }
108
+ catch (error) {
109
+ retained.push(state);
110
+ failed.push({ state, error });
111
+ }
72
112
  }
73
- try {
74
- await stat(parsed.outputDir);
113
+ await writeJsonlAtomic(projectStatePath(params.cwd), retained);
114
+ return {
115
+ deleted,
116
+ failed,
117
+ retained,
118
+ };
119
+ };
120
+ const cleanupStaleOrphanAgentOutputs = async (params) => {
121
+ const outputDirs = await listAgentManagedTempOutputDirs();
122
+ const deleted = [];
123
+ const failed = [];
124
+ const retained = [];
125
+ for (const outputDir of outputDirs) {
126
+ const normalizedOutputDir = resolve(outputDir);
127
+ if (params.referencedOutputDirs.has(normalizedOutputDir)) {
128
+ retained.push(normalizedOutputDir);
129
+ continue;
130
+ }
131
+ let mtimeMs;
132
+ try {
133
+ mtimeMs = await readPathMtimeMs(normalizedOutputDir);
134
+ }
135
+ catch (error) {
136
+ if (!isFileNotFoundError(error)) {
137
+ failed.push({ outputDir: normalizedOutputDir, error });
138
+ }
139
+ continue;
140
+ }
141
+ if (params.now - mtimeMs < params.staleOutputTtlMs) {
142
+ retained.push(normalizedOutputDir);
143
+ continue;
144
+ }
145
+ if (!(await isAgentOutputDirectory(normalizedOutputDir))) {
146
+ retained.push(normalizedOutputDir);
147
+ continue;
148
+ }
149
+ try {
150
+ await rm(normalizedOutputDir, { recursive: true, force: true });
151
+ deleted.push(normalizedOutputDir);
152
+ }
153
+ catch (error) {
154
+ failed.push({ outputDir: normalizedOutputDir, error });
155
+ }
75
156
  }
76
- catch (error) {
77
- if (isFileNotFoundError(error)) {
78
- return undefined;
157
+ return {
158
+ deleted,
159
+ failed,
160
+ retained,
161
+ };
162
+ };
163
+ export const writeAgentRunState = async (value) => {
164
+ const normalizedState = normalizeAgentRunState(value);
165
+ await withAgentStateLock(normalizedState.cwd, async () => {
166
+ await appendFile(projectStatePath(normalizedState.cwd), `${JSON.stringify(normalizedState)}\n`, "utf-8");
167
+ });
168
+ return normalizedState;
169
+ };
170
+ export const writeLatestAgentState = writeAgentRunState;
171
+ export const readAgentRunStates = async (cwd) => foldAgentRunStates(await readAgentRunStateLines(cwd));
172
+ export const readLatestAgentState = async (cwd) => {
173
+ const states = await readAgentRunStates(cwd);
174
+ for (let i = states.length - 1; i >= 0; i -= 1) {
175
+ const state = states[i];
176
+ if (await pathExists(state.outputDir)) {
177
+ return state;
79
178
  }
80
- throw error;
81
179
  }
82
- return parsed;
180
+ return undefined;
181
+ };
182
+ export const cleanupAgentRunState = async (params) => withAgentStateLock(params.cwd, async () => {
183
+ const keepManagedRuns = Math.max(0, params.keepManagedRuns ?? 1);
184
+ const states = foldAgentRunStates(await readAgentRunStateLines(params.cwd));
185
+ const existing = [];
186
+ for (const state of states) {
187
+ if (await pathExists(state.outputDir)) {
188
+ existing.push(state);
189
+ }
190
+ }
191
+ const retainedManagedRunIds = new Set(existing
192
+ .filter((state) => state.managedOutput && state.status === "finished")
193
+ .sort((a, b) => (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt) || b.startedAt - a.startedAt)
194
+ .slice(0, keepManagedRuns)
195
+ .map((state) => state.runId));
196
+ if (params.currentRunId) {
197
+ retainedManagedRunIds.add(params.currentRunId);
198
+ }
199
+ const deleted = [];
200
+ const failed = [];
201
+ for (const state of existing) {
202
+ if (!state.managedOutput || state.status !== "finished" || retainedManagedRunIds.has(state.runId)) {
203
+ continue;
204
+ }
205
+ try {
206
+ await rm(state.outputDir, { recursive: true, force: true });
207
+ deleted.push(state);
208
+ }
209
+ catch (error) {
210
+ failed.push({ state, error });
211
+ }
212
+ }
213
+ const deletedRunIds = new Set(deleted.map((state) => state.runId));
214
+ const retained = existing.filter((state) => !deletedRunIds.has(state.runId));
215
+ await writeJsonlAtomic(projectStatePath(params.cwd), retained);
216
+ return {
217
+ deleted,
218
+ failed,
219
+ retained,
220
+ };
221
+ });
222
+ export const cleanupStaleAgentRunStates = async (params) => {
223
+ const currentCwd = resolve(params.cwd);
224
+ const currentStatePath = projectStatePath(currentCwd);
225
+ const statePaths = await listAgentStatePaths(currentCwd);
226
+ const staleOutputTtlMs = Math.max(0, params.staleOutputTtlMs ?? AGENT_STALE_OUTPUT_TTL_MS);
227
+ const now = params.now ?? Date.now();
228
+ const staleCwds = new Map();
229
+ const referencedOutputDirs = new Set();
230
+ for (const statePath of statePaths) {
231
+ const states = foldAgentRunStates(await readAgentRunStateFile(statePath));
232
+ for (const state of states) {
233
+ referencedOutputDirs.add(resolve(state.outputDir));
234
+ if (statePath === currentStatePath) {
235
+ continue;
236
+ }
237
+ if (state.cwd === currentCwd || state.runId === params.currentRunId) {
238
+ continue;
239
+ }
240
+ if (!(await pathExists(state.outputDir)) || isManagedOutputStale(state, now, staleOutputTtlMs)) {
241
+ staleCwds.set(state.cwd, statePath);
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ const deleted = [];
247
+ const failed = [];
248
+ const retained = [];
249
+ const skipped = [];
250
+ for (const [cwd, statePath] of staleCwds) {
251
+ const lockResult = await tryWithAgentStateLock(cwd, () => cleanupStaleAgentRunState({
252
+ cwd,
253
+ now,
254
+ staleOutputTtlMs,
255
+ }));
256
+ if (!lockResult.acquired) {
257
+ skipped.push({ cwd, statePath, reason: "locked" });
258
+ continue;
259
+ }
260
+ deleted.push(...lockResult.result.deleted);
261
+ failed.push(...lockResult.result.failed);
262
+ retained.push(...lockResult.result.retained);
263
+ }
264
+ const orphaned = await cleanupStaleOrphanAgentOutputs({
265
+ referencedOutputDirs,
266
+ now,
267
+ staleOutputTtlMs,
268
+ });
269
+ return {
270
+ checked: statePaths.length,
271
+ deleted,
272
+ failed,
273
+ orphaned,
274
+ retained,
275
+ skipped,
276
+ };
83
277
  };
@@ -0,0 +1,17 @@
1
+ export declare const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
2
+ export declare const AGENT_MANAGED_OUTPUT_DIR_PREFIX = "allure-agent-";
3
+ export declare const isFileNotFoundError: (error: unknown) => error is NodeJS.ErrnoException;
4
+ export declare const resolveAgentStateDir: (_cwd?: string) => string;
5
+ export declare const projectStatePath: (cwd: string) => string;
6
+ export declare const listAgentStatePaths: (cwd?: string) => Promise<string[]>;
7
+ export declare const listAgentManagedTempOutputDirs: () => Promise<string[]>;
8
+ export declare const writeJsonlAtomic: (filePath: string, values: readonly unknown[]) => Promise<void>;
9
+ export declare const withAgentStateLock: <T>(cwd: string, operation: () => Promise<T>) => Promise<T>;
10
+ export declare const tryWithAgentStateLock: <T>(cwd: string, operation: () => Promise<T>) => Promise<{
11
+ acquired: true;
12
+ result: T;
13
+ } | {
14
+ acquired: false;
15
+ }>;
16
+ export declare const pathExists: (path: string) => Promise<boolean>;
17
+ export declare const readPathMtimeMs: (path: string) => Promise<number>;