@elench/testkit 0.1.88 → 0.1.90
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 +19 -8
- package/lib/cli/agents/index.mjs +48 -9
- package/lib/cli/agents/providers/claude.mjs +3 -3
- package/lib/cli/agents/providers/codex.mjs +3 -3
- package/lib/cli/assistant/bootstrap.mjs +248 -0
- package/lib/cli/assistant/interactive.mjs +52 -0
- package/lib/cli/assistant/prompt-builder.mjs +9 -9
- package/lib/cli/assistant/session.mjs +4 -1
- package/lib/cli/assistant/slash-commands.mjs +24 -10
- package/lib/cli/assistant/state.mjs +15 -13
- package/lib/cli/assistant/tool-registry.mjs +116 -37
- package/lib/cli/commands/assistant.mjs +44 -41
- package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
- package/lib/cli/entrypoint.mjs +12 -5
- package/lib/cli/presentation/tree-reporter.mjs +0 -101
- package/lib/cli/tui/inspect-app.mjs +7 -88
- package/lib/cli/tui/inspect-state.mjs +0 -117
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/agents/investigate.mjs +0 -75
- package/lib/cli/agents/investigation-context.mjs +0 -102
- package/lib/cli/agents/investigation-interpreter.mjs +0 -320
- package/lib/cli/agents/investigation-log.mjs +0 -37
- package/lib/cli/agents/prompt-builder.mjs +0 -25
- package/lib/cli/tui/assistant-app.mjs +0 -131
package/README.md
CHANGED
|
@@ -65,8 +65,8 @@ npx @elench/testkit cleanup
|
|
|
65
65
|
|
|
66
66
|
# Inspect the latest run artifact through the assistant
|
|
67
67
|
npx @elench/testkit assistant --message '/inspect "__testkit__/health/health.int.testkit.ts"'
|
|
68
|
-
npx @elench/testkit assistant --
|
|
69
|
-
npx @elench/testkit assistant --
|
|
68
|
+
npx @elench/testkit assistant --message '/artifacts "__testkit__/health/health.int.testkit.ts"'
|
|
69
|
+
npx @elench/testkit assistant --message "/logs api"
|
|
70
70
|
|
|
71
71
|
# Automatic regression intelligence
|
|
72
72
|
# Configure testkit.regressions.json and testkit classifies new vs known regressions automatically during runs
|
|
@@ -75,12 +75,23 @@ npx @elench/testkit assistant --pane logs --message '/inspect "__testkit__/healt
|
|
|
75
75
|
npx @elench/testkit db snapshot capture --service api --output scripts/testkit/schema-baseline.sql
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
`testkit` is now assistant-first in an interactive TTY
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
`testkit` is now assistant-first in an interactive TTY, but the interactive
|
|
79
|
+
assistant is provider-native. `testkit` boots a real Codex CLI or Claude CLI
|
|
80
|
+
session, writes local context files under `.testkit/assistant/`, places a
|
|
81
|
+
repo-local `testkit` wrapper on the provider `PATH`, and then hands the
|
|
82
|
+
terminal to the provider. That means the cursor, transcript, approvals, and
|
|
83
|
+
conversation UX come from the provider itself, while `testkit` contributes
|
|
84
|
+
repo context, durable run artifacts, focused detail/log/artifact/setup text
|
|
85
|
+
files, and a stable local command surface.
|
|
86
|
+
|
|
87
|
+
The non-interactive `assistant --message ...` mode remains available for one
|
|
88
|
+
hosted turn at a time. It is useful in scripts and tests, but it is not the
|
|
89
|
+
primary interactive UX.
|
|
90
|
+
|
|
91
|
+
Batch `run` output stays intentionally short: one line per completed file, a
|
|
92
|
+
concise failure block, and a final summary. Service logs, captured runtime
|
|
93
|
+
output, emitted artifacts, and assistant-visible run state are persisted under
|
|
94
|
+
`.testkit/results/`.
|
|
84
95
|
|
|
85
96
|
`testkit discover` also maintains a small durable per-test history index at
|
|
86
97
|
`.testkit/history/tests.json`. The index tracks first/last seen timestamps,
|
package/lib/cli/agents/index.mjs
CHANGED
|
@@ -20,7 +20,22 @@ export function resolvePreferredProvider(preferred = null, env = process.env) {
|
|
|
20
20
|
throw new Error("Neither codex nor claude was found on PATH");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function resolveProviderBinary(provider, env = process.env) {
|
|
24
|
+
const override = env?.[provider === "codex" ? "TESTKIT_CODEX_BIN" : "TESTKIT_CLAUDE_BIN"];
|
|
25
|
+
if (override) return override;
|
|
26
|
+
return provider;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export function isProviderInstalled(provider, env = process.env) {
|
|
30
|
+
const override = env?.[provider === "codex" ? "TESTKIT_CODEX_BIN" : "TESTKIT_CLAUDE_BIN"];
|
|
31
|
+
if (override) {
|
|
32
|
+
try {
|
|
33
|
+
fs.accessSync(override, fs.constants.X_OK);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
24
39
|
const pathValue = env.PATH || "";
|
|
25
40
|
const candidates = pathValue.split(path.delimiter).filter(Boolean);
|
|
26
41
|
const fileNames =
|
|
@@ -41,24 +56,48 @@ export function isProviderInstalled(provider, env = process.env) {
|
|
|
41
56
|
return false;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
export function startAgentSession({
|
|
45
|
-
|
|
59
|
+
export function startAgentSession({
|
|
60
|
+
provider = "auto",
|
|
61
|
+
cwd,
|
|
62
|
+
prompt,
|
|
63
|
+
onEvent,
|
|
64
|
+
purpose = "assistant",
|
|
65
|
+
env = process.env,
|
|
66
|
+
} = {}) {
|
|
67
|
+
const resolvedProvider = resolvePreferredProvider(provider, env);
|
|
68
|
+
const command = resolveProviderBinary(resolvedProvider, env);
|
|
46
69
|
if (resolvedProvider === "claude") {
|
|
47
|
-
return startClaudeHostedSession({ cwd, prompt, onEvent, purpose });
|
|
70
|
+
return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose });
|
|
48
71
|
}
|
|
49
|
-
return startCodexHostedSession({ cwd, prompt, onEvent, purpose });
|
|
72
|
+
return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose });
|
|
50
73
|
}
|
|
51
74
|
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
export function startInteractiveProviderSession({
|
|
76
|
+
provider = "auto",
|
|
77
|
+
cwd,
|
|
78
|
+
prompt,
|
|
79
|
+
env = process.env,
|
|
80
|
+
} = {}) {
|
|
81
|
+
const resolvedProvider = resolvePreferredProvider(provider, env);
|
|
82
|
+
const command = resolveProviderBinary(resolvedProvider, env);
|
|
83
|
+
const args = buildInteractiveProviderArgs(resolvedProvider, prompt);
|
|
56
84
|
const child = spawn(command, args, {
|
|
57
85
|
cwd,
|
|
86
|
+
env,
|
|
58
87
|
stdio: "inherit",
|
|
59
88
|
});
|
|
60
89
|
return new Promise((resolve, reject) => {
|
|
61
90
|
child.on("error", reject);
|
|
62
|
-
child.on("close", (code) =>
|
|
91
|
+
child.on("close", (code) =>
|
|
92
|
+
resolve({
|
|
93
|
+
provider: resolvedProvider,
|
|
94
|
+
exitCode: code ?? 0,
|
|
95
|
+
})
|
|
96
|
+
);
|
|
63
97
|
});
|
|
64
98
|
}
|
|
99
|
+
|
|
100
|
+
export function buildInteractiveProviderArgs(provider, prompt = null) {
|
|
101
|
+
if (!prompt) return [];
|
|
102
|
+
return [prompt];
|
|
103
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
extractTextFragments,
|
|
8
8
|
} from "./shared.mjs";
|
|
9
9
|
|
|
10
|
-
export function startClaudeHostedSession({ cwd, prompt, onEvent, purpose = "
|
|
10
|
+
export function startClaudeHostedSession({ command = "claude", cwd, prompt, onEvent, purpose = "assistant" } = {}) {
|
|
11
11
|
const args = [
|
|
12
12
|
"-p",
|
|
13
13
|
"--output-format",
|
|
@@ -15,13 +15,13 @@ export function startClaudeHostedSession({ cwd, prompt, onEvent, purpose = "inve
|
|
|
15
15
|
"--include-partial-messages",
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
if (purpose === "
|
|
18
|
+
if (purpose === "assistant") {
|
|
19
19
|
args.push("--permission-mode", "plan");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
args.push(prompt);
|
|
23
23
|
|
|
24
|
-
const child = execa(
|
|
24
|
+
const child = execa(command, args, {
|
|
25
25
|
cwd,
|
|
26
26
|
stdout: "pipe",
|
|
27
27
|
stderr: "pipe",
|
|
@@ -11,18 +11,18 @@ import {
|
|
|
11
11
|
readTextFileIfPresent,
|
|
12
12
|
} from "./shared.mjs";
|
|
13
13
|
|
|
14
|
-
export function startCodexHostedSession({ cwd, prompt, onEvent, purpose = "
|
|
14
|
+
export function startCodexHostedSession({ command = "codex", cwd, prompt, onEvent, purpose = "assistant" } = {}) {
|
|
15
15
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-codex-"));
|
|
16
16
|
const outputFile = path.join(tempDir, "final-message.txt");
|
|
17
17
|
const args = ["exec", "--json", "-o", outputFile];
|
|
18
18
|
|
|
19
|
-
if (purpose === "
|
|
19
|
+
if (purpose === "assistant") {
|
|
20
20
|
args.push("-s", "read-only");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
args.push(prompt);
|
|
24
24
|
|
|
25
|
-
const child = execa(
|
|
25
|
+
const child = execa(command, args, {
|
|
26
26
|
cwd,
|
|
27
27
|
stdout: "pipe",
|
|
28
28
|
stderr: "pipe",
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { readContextContent } from "../context-resources.mjs";
|
|
5
|
+
|
|
6
|
+
export function prepareAssistantBootstrap({
|
|
7
|
+
productDir,
|
|
8
|
+
inspectState,
|
|
9
|
+
initialPrompt = null,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const contextDir = path.join(productDir, ".testkit", "assistant");
|
|
12
|
+
const binDir = path.join(contextDir, "bin");
|
|
13
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
14
|
+
|
|
15
|
+
const snapshot = inspectState?.getSnapshot?.() || {};
|
|
16
|
+
const commandLogPath = path.join(contextDir, "commands.jsonl");
|
|
17
|
+
const contextPath = path.join(contextDir, "context.md");
|
|
18
|
+
const summaryPath = path.join(contextDir, "latest-run-summary.json");
|
|
19
|
+
const selectionPath = path.join(contextDir, "current-selection.json");
|
|
20
|
+
const commandsPath = path.join(contextDir, "commands.md");
|
|
21
|
+
const focusedDetailPath = path.join(contextDir, "focused-detail.txt");
|
|
22
|
+
const focusedLogsPath = path.join(contextDir, "focused-logs.txt");
|
|
23
|
+
const focusedArtifactsPath = path.join(contextDir, "focused-artifacts.txt");
|
|
24
|
+
const focusedSetupPath = path.join(contextDir, "focused-setup.txt");
|
|
25
|
+
const wrapperPath = path.join(binDir, "testkit");
|
|
26
|
+
|
|
27
|
+
const detailContent = readContextContent({ productDir, snapshot, mode: "detail", logTail: 12 });
|
|
28
|
+
const logsContent = readContextContent({ productDir, snapshot, mode: "logs", logTail: 12 });
|
|
29
|
+
const artifactsContent = readContextContent({ productDir, snapshot, mode: "artifacts", logTail: 12 });
|
|
30
|
+
const setupContent = readContextContent({ productDir, snapshot, mode: "setup", logTail: 12 });
|
|
31
|
+
|
|
32
|
+
writeJson(summaryPath, {
|
|
33
|
+
summaryRows: snapshot.summaryData?.rows || [],
|
|
34
|
+
phase: snapshot.phase || null,
|
|
35
|
+
artifactPath: path.join(productDir, ".testkit", "results", "latest.json"),
|
|
36
|
+
});
|
|
37
|
+
writeJson(selectionPath, serializeSelection(snapshot.selectedEntry || null));
|
|
38
|
+
fs.writeFileSync(commandsPath, buildCommandsMarkdown(), "utf8");
|
|
39
|
+
fs.writeFileSync(focusedDetailPath, `${detailContent.lines.join("\n")}\n`, "utf8");
|
|
40
|
+
fs.writeFileSync(focusedLogsPath, `${logsContent.lines.join("\n")}\n`, "utf8");
|
|
41
|
+
fs.writeFileSync(focusedArtifactsPath, `${artifactsContent.lines.join("\n")}\n`, "utf8");
|
|
42
|
+
fs.writeFileSync(focusedSetupPath, `${setupContent.lines.join("\n")}\n`, "utf8");
|
|
43
|
+
fs.writeFileSync(contextPath, buildContextMarkdown(productDir, snapshot, {
|
|
44
|
+
contextPath,
|
|
45
|
+
summaryPath,
|
|
46
|
+
selectionPath,
|
|
47
|
+
commandsPath,
|
|
48
|
+
commandLogPath,
|
|
49
|
+
focusedDetailPath,
|
|
50
|
+
focusedLogsPath,
|
|
51
|
+
focusedArtifactsPath,
|
|
52
|
+
focusedSetupPath,
|
|
53
|
+
}), "utf8");
|
|
54
|
+
fs.writeFileSync(wrapperPath, buildWrapperScript({
|
|
55
|
+
cliPath: resolveCliPath(),
|
|
56
|
+
commandLogPath,
|
|
57
|
+
}), { encoding: "utf8", mode: 0o755 });
|
|
58
|
+
fs.chmodSync(wrapperPath, 0o755);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
contextDir,
|
|
62
|
+
contextPath,
|
|
63
|
+
summaryPath,
|
|
64
|
+
selectionPath,
|
|
65
|
+
commandsPath,
|
|
66
|
+
commandLogPath,
|
|
67
|
+
focusedDetailPath,
|
|
68
|
+
focusedLogsPath,
|
|
69
|
+
focusedArtifactsPath,
|
|
70
|
+
focusedSetupPath,
|
|
71
|
+
binDir,
|
|
72
|
+
env: {
|
|
73
|
+
TESTKIT_ASSISTANT_CONTEXT_DIR: contextDir,
|
|
74
|
+
TESTKIT_ASSISTANT_CONTEXT_PATH: contextPath,
|
|
75
|
+
TESTKIT_ASSISTANT_COMMAND_LOG: commandLogPath,
|
|
76
|
+
TESTKIT_NO_ASSISTANT_DEFAULT: "1",
|
|
77
|
+
},
|
|
78
|
+
prompt: buildStartupPrompt({ initialPrompt }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveCliPath() {
|
|
83
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin", "testkit.mjs");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildWrapperScript({ cliPath, commandLogPath } = {}) {
|
|
87
|
+
return `#!/usr/bin/env node
|
|
88
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
89
|
+
import { dirname } from "path";
|
|
90
|
+
import { spawnSync } from "child_process";
|
|
91
|
+
|
|
92
|
+
const CLI_PATH = ${JSON.stringify(cliPath)};
|
|
93
|
+
const COMMAND_LOG = ${JSON.stringify(commandLogPath)};
|
|
94
|
+
const argv = process.argv.slice(2);
|
|
95
|
+
const commandId = \`cmd-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
|
|
96
|
+
|
|
97
|
+
function log(event) {
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(dirname(COMMAND_LOG), { recursive: true });
|
|
100
|
+
appendFileSync(COMMAND_LOG, JSON.stringify({
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
...event,
|
|
103
|
+
}) + "\\n");
|
|
104
|
+
} catch {
|
|
105
|
+
// Ignore assistant command log failures.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log({
|
|
110
|
+
type: "command_start",
|
|
111
|
+
command: "testkit",
|
|
112
|
+
commandId,
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
argv,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = spawnSync(process.execPath, [CLI_PATH, ...argv], {
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
env: {
|
|
120
|
+
...process.env,
|
|
121
|
+
TESTKIT_NO_ASSISTANT_DEFAULT: "1",
|
|
122
|
+
TESTKIT_ASSISTANT_COMMAND_ID: commandId,
|
|
123
|
+
TESTKIT_ASSISTANT_COMMAND_LOG: COMMAND_LOG,
|
|
124
|
+
},
|
|
125
|
+
stdio: "inherit",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
log({
|
|
129
|
+
type: "command_exit",
|
|
130
|
+
command: "testkit",
|
|
131
|
+
commandId,
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
argv,
|
|
134
|
+
code: result.status ?? null,
|
|
135
|
+
signal: result.signal ?? null,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (result.error) {
|
|
139
|
+
throw result.error;
|
|
140
|
+
}
|
|
141
|
+
process.exit(result.status ?? 0);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildStartupPrompt({ initialPrompt } = {}) {
|
|
146
|
+
if (initialPrompt) {
|
|
147
|
+
return [
|
|
148
|
+
"You are operating inside a testkit-managed repository.",
|
|
149
|
+
"Read .testkit/assistant/context.md before taking action.",
|
|
150
|
+
"Use the local `testkit` command on PATH for runs, discovery, status, and doctor checks.",
|
|
151
|
+
`User request: ${String(initialPrompt).trim()}`,
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [
|
|
156
|
+
"You are operating inside a testkit-managed repository.",
|
|
157
|
+
"Read .testkit/assistant/context.md before taking action.",
|
|
158
|
+
"Use the local `testkit` command on PATH for runs, discovery, status, and doctor checks.",
|
|
159
|
+
"Respond with a brief readiness note and wait for the user's next request.",
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildContextMarkdown(productDir, snapshot, paths) {
|
|
164
|
+
const rows = snapshot.summaryData?.rows || [];
|
|
165
|
+
const selection = snapshot.selectedEntry;
|
|
166
|
+
const lines = [
|
|
167
|
+
"# Testkit Assistant Context",
|
|
168
|
+
"",
|
|
169
|
+
`- Product directory: ${productDir}`,
|
|
170
|
+
`- Context file: ${paths.contextPath}`,
|
|
171
|
+
`- Run summary JSON: ${paths.summaryPath}`,
|
|
172
|
+
`- Current selection JSON: ${paths.selectionPath}`,
|
|
173
|
+
`- Command reference: ${paths.commandsPath}`,
|
|
174
|
+
`- Command log: ${paths.commandLogPath}`,
|
|
175
|
+
`- Focused detail: ${paths.focusedDetailPath}`,
|
|
176
|
+
`- Focused logs: ${paths.focusedLogsPath}`,
|
|
177
|
+
`- Focused artifacts: ${paths.focusedArtifactsPath}`,
|
|
178
|
+
`- Focused setup: ${paths.focusedSetupPath}`,
|
|
179
|
+
"",
|
|
180
|
+
"## Latest run summary",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
if (rows.length === 0) {
|
|
184
|
+
lines.push("- No persisted run artifact is currently loaded.");
|
|
185
|
+
} else {
|
|
186
|
+
for (const [label, value] of rows) {
|
|
187
|
+
lines.push(`- ${label}: ${value}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
lines.push("", "## Current selection");
|
|
192
|
+
if (!selection) {
|
|
193
|
+
lines.push("- No focused file or service.");
|
|
194
|
+
} else {
|
|
195
|
+
lines.push(`- Kind: ${selection.kind}`);
|
|
196
|
+
if (selection.serviceName) lines.push(`- Service: ${selection.serviceName}`);
|
|
197
|
+
if (selection.type) lines.push(`- Type: ${selection.type}`);
|
|
198
|
+
if (selection.suiteName) lines.push(`- Suite: ${selection.suiteName}`);
|
|
199
|
+
if (selection.filePath) lines.push(`- File: ${selection.filePath}`);
|
|
200
|
+
if (selection.status) lines.push(`- Status: ${selection.status}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
lines.push(
|
|
204
|
+
"",
|
|
205
|
+
"## Guidance",
|
|
206
|
+
"- Use `testkit run --dir . --type <type>` to run suites.",
|
|
207
|
+
"- Use `testkit status --dir .` to inspect local state.",
|
|
208
|
+
"- Use `testkit discover --dir .` to inspect managed test coverage.",
|
|
209
|
+
"- The `testkit` command on PATH is already wired to this local package.",
|
|
210
|
+
"- Reuse the focused context text files under .testkit/assistant/ before rereading artifacts manually.",
|
|
211
|
+
"- Avoid launching nested `testkit assistant` sessions from inside the provider.",
|
|
212
|
+
""
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildCommandsMarkdown() {
|
|
219
|
+
return [
|
|
220
|
+
"# Testkit Commands",
|
|
221
|
+
"",
|
|
222
|
+
"- `testkit run --dir . --type int`",
|
|
223
|
+
"- `testkit run --dir . --type e2e`",
|
|
224
|
+
"- `testkit run --dir . --file path/to/file.testkit.ts`",
|
|
225
|
+
"- `testkit discover --dir .`",
|
|
226
|
+
"- `testkit status --dir .`",
|
|
227
|
+
"- `testkit doctor --dir .`",
|
|
228
|
+
"- `testkit destroy --dir .`",
|
|
229
|
+
"",
|
|
230
|
+
].join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function serializeSelection(entry) {
|
|
234
|
+
if (!entry) return null;
|
|
235
|
+
return {
|
|
236
|
+
kind: entry.kind || null,
|
|
237
|
+
serviceName: entry.serviceName || null,
|
|
238
|
+
type: entry.type || null,
|
|
239
|
+
suiteName: entry.suiteName || null,
|
|
240
|
+
filePath: entry.filePath || null,
|
|
241
|
+
status: entry.status || null,
|
|
242
|
+
label: entry.label || null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function writeJson(filePath, value) {
|
|
247
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
|
|
248
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { createInspectState } from "../tui/inspect-state.mjs";
|
|
3
|
+
import { loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
4
|
+
import { startInteractiveProviderSession } from "../agents/index.mjs";
|
|
5
|
+
import { prepareAssistantBootstrap } from "./bootstrap.mjs";
|
|
6
|
+
|
|
7
|
+
export async function runInteractiveAssistant({
|
|
8
|
+
productDir,
|
|
9
|
+
provider = "auto",
|
|
10
|
+
file = null,
|
|
11
|
+
service = null,
|
|
12
|
+
prompt = null,
|
|
13
|
+
env = process.env,
|
|
14
|
+
} = {}) {
|
|
15
|
+
const inspectState = createInspectState({ dataSource: "artifact" });
|
|
16
|
+
try {
|
|
17
|
+
inspectState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
|
|
18
|
+
} catch {
|
|
19
|
+
// No persisted artifact yet.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (file) {
|
|
23
|
+
try {
|
|
24
|
+
const artifact = loadLatestRunArtifact(productDir);
|
|
25
|
+
const subject = resolveFileSubject(artifact, file, service || null);
|
|
26
|
+
inspectState.revealFile(subject.service.name, subject.file.path);
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignore unresolved focus.
|
|
29
|
+
}
|
|
30
|
+
} else if (service) {
|
|
31
|
+
inspectState.revealService(service);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bootstrap = prepareAssistantBootstrap({
|
|
35
|
+
productDir,
|
|
36
|
+
inspectState,
|
|
37
|
+
initialPrompt: prompt,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const sessionEnv = {
|
|
41
|
+
...env,
|
|
42
|
+
...bootstrap.env,
|
|
43
|
+
PATH: [bootstrap.binDir, env.PATH || ""].filter(Boolean).join(path.delimiter),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return startInteractiveProviderSession({
|
|
47
|
+
provider,
|
|
48
|
+
cwd: productDir,
|
|
49
|
+
prompt: bootstrap.prompt,
|
|
50
|
+
env: sessionEnv,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readContextContent } from "../context-resources.mjs";
|
|
2
2
|
import { buildAssistantResponseContract } from "./protocol.mjs";
|
|
3
3
|
|
|
4
4
|
export function buildAssistantPrompt({
|
|
@@ -9,7 +9,7 @@ export function buildAssistantPrompt({
|
|
|
9
9
|
userMessage,
|
|
10
10
|
} = {}) {
|
|
11
11
|
const selectionSummary = buildSelectionSummary(snapshot);
|
|
12
|
-
const
|
|
12
|
+
const focusPreview = buildFocusPreview(productDir, snapshot);
|
|
13
13
|
const summaryRows = snapshot?.summaryData?.rows || [];
|
|
14
14
|
|
|
15
15
|
return [
|
|
@@ -24,8 +24,8 @@ export function buildAssistantPrompt({
|
|
|
24
24
|
"Current selection:",
|
|
25
25
|
selectionSummary,
|
|
26
26
|
"",
|
|
27
|
-
"Current
|
|
28
|
-
...(
|
|
27
|
+
"Current focus preview:",
|
|
28
|
+
...(focusPreview.length > 0 ? focusPreview : ["(empty)"]),
|
|
29
29
|
"",
|
|
30
30
|
"Recent conversation:",
|
|
31
31
|
...formatTranscript(transcript),
|
|
@@ -52,18 +52,18 @@ function buildSelectionSummary(snapshot) {
|
|
|
52
52
|
return entry.label || "Unknown selection.";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function
|
|
55
|
+
function buildFocusPreview(productDir, snapshot) {
|
|
56
56
|
if (!productDir || !snapshot) return [];
|
|
57
57
|
try {
|
|
58
|
-
const
|
|
58
|
+
const content = readContextContent({
|
|
59
59
|
productDir,
|
|
60
60
|
snapshot,
|
|
61
|
-
|
|
61
|
+
mode: "detail",
|
|
62
62
|
logTail: 8,
|
|
63
63
|
});
|
|
64
|
-
return (
|
|
64
|
+
return (content.lines || []).slice(0, 24).map((line) => `- ${line}`);
|
|
65
65
|
} catch (error) {
|
|
66
|
-
return [`(
|
|
66
|
+
return [`(focus unavailable: ${error instanceof Error ? error.message : String(error)})`];
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -9,6 +9,7 @@ export async function runAssistantConversationTurn({
|
|
|
9
9
|
transcript,
|
|
10
10
|
userMessage,
|
|
11
11
|
provider = "auto",
|
|
12
|
+
env = process.env,
|
|
12
13
|
configs,
|
|
13
14
|
onStatus,
|
|
14
15
|
onToolEvent,
|
|
@@ -34,13 +35,14 @@ export async function runAssistantConversationTurn({
|
|
|
34
35
|
userMessage,
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
onStatus?.(`Thinking with ${resolvePreferredProvider(provider)}...`);
|
|
38
|
+
onStatus?.(`Thinking with ${resolvePreferredProvider(provider, env)}...`);
|
|
38
39
|
const events = [];
|
|
39
40
|
const session = startAgentSession({
|
|
40
41
|
provider,
|
|
41
42
|
cwd: productDir,
|
|
42
43
|
prompt,
|
|
43
44
|
purpose: "assistant",
|
|
45
|
+
env,
|
|
44
46
|
onEvent(event) {
|
|
45
47
|
events.push(event);
|
|
46
48
|
if (event.type === "status" || event.type === "tool") onStatus?.(formatProviderEvent(event));
|
|
@@ -60,6 +62,7 @@ export async function runAssistantConversationTurn({
|
|
|
60
62
|
role: "tool",
|
|
61
63
|
text: toolText,
|
|
62
64
|
toolName: envelope.tool,
|
|
65
|
+
title: toolResult.title || envelope.tool,
|
|
63
66
|
data: toolResult.data || null,
|
|
64
67
|
});
|
|
65
68
|
currentTranscript.push({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const RUN_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
2
|
-
const PANES = new Set(["detail", "artifacts", "logs", "setup"]);
|
|
3
2
|
const PROVIDERS = new Set(["auto", "claude", "codex"]);
|
|
4
3
|
|
|
5
4
|
export function parseSlashCommand(input) {
|
|
@@ -20,14 +19,6 @@ export function parseSlashCommand(input) {
|
|
|
20
19
|
return { type: "provider", provider };
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
if (command === "pane") {
|
|
24
|
-
const pane = tokens[0] || "detail";
|
|
25
|
-
if (!PANES.has(pane)) {
|
|
26
|
-
throw new Error(`/pane expects one of: ${[...PANES].join(", ")}`);
|
|
27
|
-
}
|
|
28
|
-
return { type: "pane", pane };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
22
|
if (command === "file" || command === "focus") {
|
|
32
23
|
if (!tokens[0]) throw new Error(`/${command} expects a file path`);
|
|
33
24
|
return { type: "file", file: tokens.join(" ") };
|
|
@@ -45,6 +36,27 @@ export function parseSlashCommand(input) {
|
|
|
45
36
|
};
|
|
46
37
|
}
|
|
47
38
|
|
|
39
|
+
if (command === "logs") {
|
|
40
|
+
return {
|
|
41
|
+
type: "logs",
|
|
42
|
+
service: tokens[0] || null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (command === "artifacts") {
|
|
47
|
+
return {
|
|
48
|
+
type: "artifacts",
|
|
49
|
+
file: tokens[0] || null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === "setup") {
|
|
54
|
+
return {
|
|
55
|
+
type: "setup",
|
|
56
|
+
service: tokens[0] || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
if (command === "status") return { type: "status" };
|
|
49
61
|
if (command === "discover") return { type: "discover" };
|
|
50
62
|
if (command === "doctor") return { type: "doctor" };
|
|
@@ -64,8 +76,10 @@ export function formatSlashHelpLines() {
|
|
|
64
76
|
"/run [type] [--service name] [--file path] [--suite selector]",
|
|
65
77
|
"/file <path>",
|
|
66
78
|
"/service <name>",
|
|
67
|
-
"/pane <detail|artifacts|logs|setup>",
|
|
68
79
|
"/inspect [path]",
|
|
80
|
+
"/logs [service]",
|
|
81
|
+
"/artifacts [path]",
|
|
82
|
+
"/setup [service]",
|
|
69
83
|
"/discover",
|
|
70
84
|
"/status",
|
|
71
85
|
"/doctor",
|