@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/README.md +36 -1
- package/dist/capabilities.d.ts +31 -3
- package/dist/capabilities.js +97 -4
- package/dist/guidance.d.ts +3 -3
- package/dist/guidance.js +34 -8
- package/dist/harness.d.ts +4 -0
- package/dist/harness.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/invalid-output.js +1 -1
- package/dist/model.d.ts +25 -0
- package/dist/plugin.js +70 -2
- package/dist/query.d.ts +2 -0
- package/dist/query.js +2 -0
- package/dist/state.d.ts +48 -7
- package/dist/state.js +252 -58
- package/dist/utils.d.ts +17 -0
- package/dist/utils.js +171 -0
- package/package.json +4 -4
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
|
|
2
|
-
|
|
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:
|
|
8
|
-
finishedAt?:
|
|
10
|
+
startedAt: number;
|
|
11
|
+
finishedAt?: number;
|
|
9
12
|
status: "running" | "finished";
|
|
10
13
|
exitCode?: number | null;
|
|
14
|
+
pid?: number;
|
|
11
15
|
};
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
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 ===
|
|
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 === "
|
|
18
|
+
typeof candidate.startedAt === "number" &&
|
|
19
|
+
Number.isSafeInteger(candidate.startedAt) &&
|
|
33
20
|
(candidate.expectationsPath === undefined || typeof candidate.expectationsPath === "string") &&
|
|
34
|
-
(candidate.finishedAt === undefined ||
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
49
|
+
return [];
|
|
63
50
|
}
|
|
64
51
|
throw error;
|
|
65
52
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
};
|
package/dist/utils.d.ts
ADDED
|
@@ -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>;
|