@mintlify/cli 4.0.1437 → 4.0.1438
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/bin/agent-harness/buildTaskPrompt.js +76 -0
- package/bin/agent-harness/discoverCodeBlocks.js +101 -0
- package/bin/agent-harness/executeCodeBlocks.js +220 -0
- package/bin/agent-harness/filterSelectedPages.js +10 -0
- package/bin/agent-harness/generateTestCode.js +293 -0
- package/bin/agent-harness/index.js +85 -0
- package/bin/agent-harness/loadAgentSdk.js +46 -0
- package/bin/agent-harness/mapWithConcurrency.js +26 -0
- package/bin/agent-harness/runSave.js +47 -0
- package/bin/agent-harness/setupFolders.js +59 -0
- package/bin/agent-harness/tasks/checkTestability.js +226 -0
- package/bin/agent-harness/types.js +35 -0
- package/bin/mintTest.js +1 -1
- package/bin/mintTestUi.js +1 -1
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -3
- package/src/agent-harness/MINT_TEST_SYSTEM_DESIGN.md +58 -0
- package/src/agent-harness/buildTaskPrompt.ts +82 -0
- package/src/agent-harness/discoverCodeBlocks.ts +90 -0
- package/src/agent-harness/executeCodeBlocks.ts +285 -0
- package/src/agent-harness/filterSelectedPages.ts +14 -0
- package/src/agent-harness/generateTestCode.ts +332 -0
- package/src/agent-harness/index.ts +109 -0
- package/src/agent-harness/loadAgentSdk.ts +35 -0
- package/src/agent-harness/mapWithConcurrency.ts +21 -0
- package/src/agent-harness/runSave.ts +38 -0
- package/src/agent-harness/setupFolders.ts +73 -0
- package/src/agent-harness/tasks/checkTestability.ts +247 -0
- package/src/agent-harness/types.ts +152 -0
- package/src/mintTest.tsx +3 -3
- package/src/mintTestTree.ts +1 -1
- package/src/mintTestUi.tsx +5 -5
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildCheckPrompt,
|
|
7
|
+
CHECK_JSON_INSTRUCTION,
|
|
8
|
+
CHECK_SYSTEM_PROMPT,
|
|
9
|
+
CHECK_TOOL_INSTRUCTION,
|
|
10
|
+
} from '../buildTaskPrompt.js';
|
|
11
|
+
import { loadClaudeAgentSdk, loadCodexSdk } from '../loadAgentSdk.js';
|
|
12
|
+
import {
|
|
13
|
+
GeneratedTask,
|
|
14
|
+
HarnessAgent,
|
|
15
|
+
RunCodeTestsOptions,
|
|
16
|
+
TaskUpdate,
|
|
17
|
+
TestCheck,
|
|
18
|
+
testCheckSchema,
|
|
19
|
+
} from '../types.js';
|
|
20
|
+
|
|
21
|
+
const CHECK_FILENAME = 'mint-test-check.json';
|
|
22
|
+
|
|
23
|
+
const CHECK_MODEL = 'claude-haiku-4-5-20251001';
|
|
24
|
+
|
|
25
|
+
async function readValidCheck(directory: string): Promise<TestCheck | null> {
|
|
26
|
+
try {
|
|
27
|
+
const check = testCheckSchema.parse(
|
|
28
|
+
JSON.parse(await fs.readFile(path.join(directory, CHECK_FILENAME), 'utf8'))
|
|
29
|
+
);
|
|
30
|
+
return validateCheck(check).length === 0 ? check : null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validateCheck(check: TestCheck): string[] {
|
|
37
|
+
return !check.testable && !check.reason ? ['Not-testable page needs a reason'] : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseJsonReply(reply: string): unknown {
|
|
41
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(reply);
|
|
42
|
+
const raw = fenced?.[1] ?? reply;
|
|
43
|
+
const start = raw.indexOf('{');
|
|
44
|
+
const end = raw.lastIndexOf('}');
|
|
45
|
+
if (start === -1 || end === -1) throw new Error('Reply contained no JSON object');
|
|
46
|
+
return JSON.parse(raw.slice(start, end + 1));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runTestabilityCheck({
|
|
50
|
+
file,
|
|
51
|
+
content,
|
|
52
|
+
agent,
|
|
53
|
+
outputPath,
|
|
54
|
+
signal,
|
|
55
|
+
}: {
|
|
56
|
+
file: string;
|
|
57
|
+
content: string;
|
|
58
|
+
agent: HarnessAgent;
|
|
59
|
+
outputPath: string;
|
|
60
|
+
signal: AbortSignal;
|
|
61
|
+
}): Promise<{ tokens: number; check: TestCheck }> {
|
|
62
|
+
const prompt = buildCheckPrompt(file, content);
|
|
63
|
+
const workingDirectory = path.dirname(outputPath);
|
|
64
|
+
let tokens = 0;
|
|
65
|
+
let raw: unknown;
|
|
66
|
+
if (agent === 'claude') {
|
|
67
|
+
const reply = await promptClaudeCheck({ prompt, workingDirectory, signal });
|
|
68
|
+
tokens = reply.tokens;
|
|
69
|
+
if (reply.captured === null) throw new Error('agent never called report_testability');
|
|
70
|
+
raw = reply.captured;
|
|
71
|
+
} else {
|
|
72
|
+
const reply = await promptCodex({
|
|
73
|
+
prompt,
|
|
74
|
+
systemPrompt: CHECK_SYSTEM_PROMPT + CHECK_JSON_INSTRUCTION,
|
|
75
|
+
workingDirectory,
|
|
76
|
+
signal,
|
|
77
|
+
});
|
|
78
|
+
tokens = reply.tokens;
|
|
79
|
+
raw = parseJsonReply(reply.result);
|
|
80
|
+
}
|
|
81
|
+
const check = testCheckSchema.parse(raw);
|
|
82
|
+
const errors = validateCheck(check);
|
|
83
|
+
if (errors.length > 0) throw new Error(errors.join('\n'));
|
|
84
|
+
await fs.writeFile(outputPath, `${JSON.stringify(check, null, 2)}\n`);
|
|
85
|
+
return { tokens, check };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const checkFileTestability = async (
|
|
89
|
+
task: GeneratedTask,
|
|
90
|
+
{ onTaskUpdate, signal }: Pick<RunCodeTestsOptions, 'onTaskUpdate' | 'signal'>
|
|
91
|
+
): Promise<GeneratedTask> => {
|
|
92
|
+
const emit = (phase: TaskUpdate['phase'], tokens: number) =>
|
|
93
|
+
onTaskUpdate({
|
|
94
|
+
id: task.id,
|
|
95
|
+
agent: task.agent,
|
|
96
|
+
file: task.file,
|
|
97
|
+
phase,
|
|
98
|
+
tokens,
|
|
99
|
+
});
|
|
100
|
+
let attempts = 0;
|
|
101
|
+
let tokens = 0;
|
|
102
|
+
let lastError = 'run cancelled';
|
|
103
|
+
|
|
104
|
+
let check = await readValidCheck(task.directory);
|
|
105
|
+
if (!check) {
|
|
106
|
+
emit('checking', tokens);
|
|
107
|
+
const content = await fs.readFile(path.join(task.docsRoot, task.file), 'utf8');
|
|
108
|
+
for (let attempt = 1; attempt <= 2 && !signal.aborted && !check; attempt++) {
|
|
109
|
+
attempts = attempt;
|
|
110
|
+
try {
|
|
111
|
+
const reply = await runTestabilityCheck({
|
|
112
|
+
file: task.file,
|
|
113
|
+
content,
|
|
114
|
+
agent: task.agent,
|
|
115
|
+
outputPath: path.join(task.directory, CHECK_FILENAME),
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
tokens += reply.tokens;
|
|
119
|
+
check = reply.check;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!check) {
|
|
125
|
+
if (signal.aborted) {
|
|
126
|
+
emit('cancelled', tokens);
|
|
127
|
+
return { ...task, attempts, tokens, error: 'run cancelled' };
|
|
128
|
+
}
|
|
129
|
+
emit('agent_error', tokens);
|
|
130
|
+
return { ...task, attempts, tokens, error: lastError };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!check.testable) {
|
|
135
|
+
emit('not_testable', tokens);
|
|
136
|
+
return { ...task, attempts, tokens, testable: false };
|
|
137
|
+
}
|
|
138
|
+
emit('checked', tokens);
|
|
139
|
+
return { ...task, attempts, tokens, testable: true };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
async function promptClaudeCheck({
|
|
143
|
+
prompt,
|
|
144
|
+
workingDirectory,
|
|
145
|
+
signal,
|
|
146
|
+
}: {
|
|
147
|
+
prompt: string;
|
|
148
|
+
workingDirectory: string;
|
|
149
|
+
signal: AbortSignal;
|
|
150
|
+
}): Promise<{ tokens: number; captured: unknown }> {
|
|
151
|
+
const { query, tool, createSdkMcpServer } = await loadClaudeAgentSdk();
|
|
152
|
+
const abortController = new AbortController();
|
|
153
|
+
const abort = () => abortController.abort(signal.reason);
|
|
154
|
+
if (signal.aborted) abort();
|
|
155
|
+
else signal.addEventListener('abort', abort, { once: true });
|
|
156
|
+
|
|
157
|
+
let captured: unknown = null;
|
|
158
|
+
const server = createSdkMcpServer({
|
|
159
|
+
name: 'mint-test',
|
|
160
|
+
tools: [
|
|
161
|
+
tool(
|
|
162
|
+
'report_testability',
|
|
163
|
+
'Report whether the documentation page is testable. Call exactly once.',
|
|
164
|
+
{
|
|
165
|
+
version: z.literal(1),
|
|
166
|
+
testable: z.boolean(),
|
|
167
|
+
reason: z.string().optional(),
|
|
168
|
+
},
|
|
169
|
+
async (input) => {
|
|
170
|
+
captured = input;
|
|
171
|
+
return { content: [{ type: 'text', text: 'recorded' }] };
|
|
172
|
+
}
|
|
173
|
+
),
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const session = query({
|
|
178
|
+
prompt,
|
|
179
|
+
options: {
|
|
180
|
+
abortController,
|
|
181
|
+
allowedTools: ['mcp__mint-test__report_testability'],
|
|
182
|
+
cwd: workingDirectory,
|
|
183
|
+
mcpServers: { 'mint-test': server },
|
|
184
|
+
model: CHECK_MODEL,
|
|
185
|
+
permissionMode: 'dontAsk',
|
|
186
|
+
persistSession: false,
|
|
187
|
+
settingSources: [],
|
|
188
|
+
strictMcpConfig: true,
|
|
189
|
+
systemPrompt: CHECK_SYSTEM_PROMPT + CHECK_TOOL_INSTRUCTION,
|
|
190
|
+
tools: [],
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
for await (const message of session) {
|
|
196
|
+
if (message.type !== 'result') continue;
|
|
197
|
+
if (message.subtype !== 'success' || message.is_error) {
|
|
198
|
+
const details = message.subtype === 'success' ? message.result : message.errors.join('; ');
|
|
199
|
+
throw new Error(details || `Claude stopped with ${message.subtype}`);
|
|
200
|
+
}
|
|
201
|
+
const usage = message.usage;
|
|
202
|
+
return {
|
|
203
|
+
tokens:
|
|
204
|
+
usage.input_tokens +
|
|
205
|
+
usage.output_tokens +
|
|
206
|
+
usage.cache_creation_input_tokens +
|
|
207
|
+
usage.cache_read_input_tokens,
|
|
208
|
+
captured,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
throw new Error('Claude finished without returning a result');
|
|
212
|
+
} finally {
|
|
213
|
+
signal.removeEventListener('abort', abort);
|
|
214
|
+
session.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function promptCodex({
|
|
219
|
+
prompt,
|
|
220
|
+
systemPrompt,
|
|
221
|
+
workingDirectory,
|
|
222
|
+
signal,
|
|
223
|
+
}: {
|
|
224
|
+
prompt: string;
|
|
225
|
+
systemPrompt: string;
|
|
226
|
+
workingDirectory: string;
|
|
227
|
+
signal: AbortSignal;
|
|
228
|
+
}): Promise<{ tokens: number; result: string }> {
|
|
229
|
+
const { Codex } = await loadCodexSdk();
|
|
230
|
+
const thread = new Codex().startThread({
|
|
231
|
+
approvalPolicy: 'never',
|
|
232
|
+
networkAccessEnabled: true,
|
|
233
|
+
sandboxMode: 'workspace-write',
|
|
234
|
+
skipGitRepoCheck: true,
|
|
235
|
+
workingDirectory,
|
|
236
|
+
});
|
|
237
|
+
const result = await thread.run(`${systemPrompt}\n\n${prompt}`, { signal });
|
|
238
|
+
const usage = result.usage;
|
|
239
|
+
const tokens = usage
|
|
240
|
+
? usage.input_tokens +
|
|
241
|
+
usage.cached_input_tokens +
|
|
242
|
+
usage.cache_write_input_tokens +
|
|
243
|
+
usage.output_tokens +
|
|
244
|
+
usage.reasoning_output_tokens
|
|
245
|
+
: 0;
|
|
246
|
+
return { tokens, result: result.finalResponse };
|
|
247
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export type HarnessAgent = 'claude' | 'codex';
|
|
4
|
+
|
|
5
|
+
export interface DocsPage {
|
|
6
|
+
file: string;
|
|
7
|
+
blockCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type CommandStatus = 'passed' | 'failed' | 'cancelled';
|
|
11
|
+
|
|
12
|
+
export interface CommandResult {
|
|
13
|
+
id: string;
|
|
14
|
+
phase: 'setup' | 'test';
|
|
15
|
+
status: CommandStatus;
|
|
16
|
+
executable: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
cwd: string;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
exitCode: number | null;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TaskStatus = 'passed' | 'failed' | 'agent_error' | 'cancelled';
|
|
27
|
+
|
|
28
|
+
export interface TaskResult {
|
|
29
|
+
id: string;
|
|
30
|
+
agent: HarnessAgent;
|
|
31
|
+
file: string;
|
|
32
|
+
directory: string;
|
|
33
|
+
attempts: number;
|
|
34
|
+
tokens: number;
|
|
35
|
+
status: TaskStatus;
|
|
36
|
+
error?: string;
|
|
37
|
+
commands: CommandResult[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CodeTestReport {
|
|
41
|
+
version: 1;
|
|
42
|
+
runId: string;
|
|
43
|
+
status: 'passed' | 'completed' | 'failed' | 'cancelled';
|
|
44
|
+
docsRoot: string;
|
|
45
|
+
outputDirectory: string;
|
|
46
|
+
reportPath: string;
|
|
47
|
+
agents: HarnessAgent[];
|
|
48
|
+
selectedFiles: string[];
|
|
49
|
+
startedAt: string;
|
|
50
|
+
completedAt: string;
|
|
51
|
+
durationMs: number;
|
|
52
|
+
tasks: TaskResult[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type TaskPhase =
|
|
56
|
+
| 'checking'
|
|
57
|
+
| 'checked'
|
|
58
|
+
| 'not_testable'
|
|
59
|
+
| 'generating'
|
|
60
|
+
| 'generated'
|
|
61
|
+
| 'agent_error'
|
|
62
|
+
| 'running'
|
|
63
|
+
| 'passed'
|
|
64
|
+
| 'failed'
|
|
65
|
+
| 'cancelled';
|
|
66
|
+
|
|
67
|
+
export interface TaskUpdate {
|
|
68
|
+
id: string;
|
|
69
|
+
agent: HarnessAgent;
|
|
70
|
+
file: string;
|
|
71
|
+
phase: TaskPhase;
|
|
72
|
+
tokens: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RunSave {
|
|
76
|
+
version: 1;
|
|
77
|
+
runId: string;
|
|
78
|
+
agents: HarnessAgent[];
|
|
79
|
+
model: string;
|
|
80
|
+
selectedFiles: string[];
|
|
81
|
+
createdAt: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RunCodeTestsOptions {
|
|
85
|
+
path: string;
|
|
86
|
+
agents: HarnessAgent[];
|
|
87
|
+
model: string;
|
|
88
|
+
selectedFiles: string[];
|
|
89
|
+
outputDirectory: string;
|
|
90
|
+
runId: string | null;
|
|
91
|
+
concurrency: number;
|
|
92
|
+
commandTimeoutMs: number;
|
|
93
|
+
signal: AbortSignal;
|
|
94
|
+
onTaskUpdate: (update: TaskUpdate) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const identifier = z.string().trim().min(1).max(120);
|
|
98
|
+
|
|
99
|
+
const commandSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
id: identifier,
|
|
102
|
+
executable: z.string().trim().min(1).max(512),
|
|
103
|
+
args: z.array(z.string().max(10_000)).max(100),
|
|
104
|
+
cwd: z.string().max(1_024).optional(),
|
|
105
|
+
timeoutMs: z
|
|
106
|
+
.number()
|
|
107
|
+
.int()
|
|
108
|
+
.min(1_000)
|
|
109
|
+
.max(30 * 60 * 1_000)
|
|
110
|
+
.optional(),
|
|
111
|
+
})
|
|
112
|
+
.strict();
|
|
113
|
+
|
|
114
|
+
export const manifestSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
version: z.literal(1),
|
|
117
|
+
generatedFiles: z.array(z.string().min(1).max(1_024)).max(1_000),
|
|
118
|
+
setupCommands: z.array(commandSchema).max(50),
|
|
119
|
+
testCommands: z.array(commandSchema).max(200),
|
|
120
|
+
})
|
|
121
|
+
.strict();
|
|
122
|
+
|
|
123
|
+
export type GeneratedTestManifest = z.infer<typeof manifestSchema>;
|
|
124
|
+
|
|
125
|
+
export const testCheckSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
version: z.literal(1),
|
|
128
|
+
testable: z.boolean(),
|
|
129
|
+
reason: z
|
|
130
|
+
.string()
|
|
131
|
+
.max(2_000)
|
|
132
|
+
.optional()
|
|
133
|
+
.transform((value) => (value?.trim() ? value.trim() : undefined)),
|
|
134
|
+
})
|
|
135
|
+
.strict();
|
|
136
|
+
|
|
137
|
+
export type TestCheck = z.infer<typeof testCheckSchema>;
|
|
138
|
+
|
|
139
|
+
export type ManifestCommand = GeneratedTestManifest['testCommands'][number];
|
|
140
|
+
|
|
141
|
+
export interface GeneratedTask {
|
|
142
|
+
id: string;
|
|
143
|
+
agent: HarnessAgent;
|
|
144
|
+
docsRoot: string;
|
|
145
|
+
file: string;
|
|
146
|
+
directory: string;
|
|
147
|
+
attempts: number;
|
|
148
|
+
tokens: number;
|
|
149
|
+
testable?: boolean;
|
|
150
|
+
manifest?: GeneratedTestManifest;
|
|
151
|
+
error?: string;
|
|
152
|
+
}
|
package/src/mintTest.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
3
5
|
DEFAULT_CONCURRENCY,
|
|
@@ -6,9 +8,7 @@ import {
|
|
|
6
8
|
loadRunSave,
|
|
7
9
|
runCodeTests,
|
|
8
10
|
type RunSave,
|
|
9
|
-
} from '
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
|
|
11
|
+
} from './agent-harness/index.js';
|
|
12
12
|
import { CMD_EXEC_PATH, findDocsRoot, isAI, terminate } from './helpers.js';
|
|
13
13
|
import { getAccessToken } from './keyring.js';
|
|
14
14
|
import { fileInNavPaths, loadDocsNavScope } from './mintTestNav.js';
|
package/src/mintTestTree.ts
CHANGED
package/src/mintTestUi.tsx
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Box, render, Text, useInput } from 'ink';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
3
7
|
DEFAULT_CONCURRENCY,
|
|
@@ -10,11 +14,7 @@ import {
|
|
|
10
14
|
type TaskPhase,
|
|
11
15
|
type TaskResult,
|
|
12
16
|
type TaskUpdate,
|
|
13
|
-
} from '
|
|
14
|
-
import { Box, render, Text, useInput } from 'ink';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
17
|
-
|
|
17
|
+
} from './agent-harness/index.js';
|
|
18
18
|
import { toggleScopeSelection, visibleScopeRows, type ScopeTreeRow } from './mintTestTree.js';
|
|
19
19
|
|
|
20
20
|
type Screen = 'resume' | 'harnesses' | 'scope' | 'running' | 'report' | 'error';
|