@elench/testkit 0.1.89 → 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 +16 -7
- 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 +3 -7
- package/lib/cli/assistant/session.mjs +3 -1
- package/lib/cli/assistant/state.mjs +4 -2
- package/lib/cli/assistant/tool-registry.mjs +17 -17
- package/lib/cli/commands/assistant.mjs +44 -34
- package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
- package/lib/cli/entrypoint.mjs +12 -4
- 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/assistant/content.mjs +0 -60
- package/lib/cli/tui/assistant-app.mjs +0 -82
- package/lib/cli/tui/assistant-render.mjs +0 -99
package/README.md
CHANGED
|
@@ -75,13 +75,22 @@ npx @elench/testkit assistant --message "/logs api"
|
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
85
94
|
`.testkit/results/`.
|
|
86
95
|
|
|
87
96
|
`testkit discover` also maintains a small durable per-test history index at
|
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({
|
|
@@ -55,13 +55,9 @@ function buildSelectionSummary(snapshot) {
|
|
|
55
55
|
function buildFocusPreview(productDir, snapshot) {
|
|
56
56
|
if (!productDir || !snapshot) return [];
|
|
57
57
|
try {
|
|
58
|
-
const content =
|
|
58
|
+
const content = readContextContent({
|
|
59
59
|
productDir,
|
|
60
|
-
|
|
61
|
-
getSnapshot() {
|
|
62
|
-
return snapshot;
|
|
63
|
-
},
|
|
64
|
-
},
|
|
60
|
+
snapshot,
|
|
65
61
|
mode: "detail",
|
|
66
62
|
logTail: 8,
|
|
67
63
|
});
|
|
@@ -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));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../viewer.mjs";
|
|
2
2
|
import { createInspectState } from "../tui/inspect-state.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { buildContextSelection } from "../context-resources.mjs";
|
|
4
4
|
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
5
5
|
import { executeAssistantTool } from "./tool-registry.mjs";
|
|
6
6
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
@@ -10,6 +10,7 @@ export function createAssistantState({
|
|
|
10
10
|
provider = "auto",
|
|
11
11
|
dataSource = "artifact",
|
|
12
12
|
configs = [],
|
|
13
|
+
env = process.env,
|
|
13
14
|
} = {}) {
|
|
14
15
|
const inspectState = createInspectState({ dataSource });
|
|
15
16
|
|
|
@@ -160,6 +161,7 @@ export function createAssistantState({
|
|
|
160
161
|
transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
|
|
161
162
|
userMessage: trimmed,
|
|
162
163
|
provider: providerName,
|
|
164
|
+
env,
|
|
163
165
|
configs,
|
|
164
166
|
onStatus(status) {
|
|
165
167
|
activeStatus = status;
|
|
@@ -184,7 +186,7 @@ export function createAssistantState({
|
|
|
184
186
|
|
|
185
187
|
getSnapshot() {
|
|
186
188
|
return {
|
|
187
|
-
context:
|
|
189
|
+
context: buildContextSelection(inspectState.getSnapshot()),
|
|
188
190
|
messages: [...messages],
|
|
189
191
|
composer,
|
|
190
192
|
notice,
|
|
@@ -5,9 +5,9 @@ import { resolveRequestedFiles } from "../args.mjs";
|
|
|
5
5
|
import { buildDiscoveryReportLines } from "../presentation/discovery-reporter.mjs";
|
|
6
6
|
import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "
|
|
8
|
+
formatContextToolText,
|
|
9
|
+
readContextContent,
|
|
10
|
+
} from "../context-resources.mjs";
|
|
11
11
|
import { createAssistantRunReporter } from "./tool-run-reporter.mjs";
|
|
12
12
|
import { buildRunRequest } from "../command-helpers.mjs";
|
|
13
13
|
import * as runner from "../../runner/index.mjs";
|
|
@@ -131,16 +131,16 @@ function focusServiceTool(args, context) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function inspectFocusTool(_args, context) {
|
|
134
|
-
const content =
|
|
134
|
+
const content = readContextContent({
|
|
135
135
|
productDir: context.productDir,
|
|
136
|
-
|
|
136
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
137
137
|
mode: "detail",
|
|
138
138
|
logTail: 12,
|
|
139
139
|
});
|
|
140
140
|
return {
|
|
141
141
|
ok: true,
|
|
142
142
|
title: content.title,
|
|
143
|
-
text:
|
|
143
|
+
text: formatContextToolText(content.title, content.lines),
|
|
144
144
|
data: {
|
|
145
145
|
title: content.title,
|
|
146
146
|
lines: content.lines,
|
|
@@ -157,16 +157,16 @@ function readLogsTool(args, context) {
|
|
|
157
157
|
throw new Error(`Unknown service "${args.service}"`);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
const content =
|
|
160
|
+
const content = readContextContent({
|
|
161
161
|
productDir: context.productDir,
|
|
162
|
-
|
|
162
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
163
163
|
mode: "logs",
|
|
164
164
|
logTail: args.logTail == null ? 12 : Number(args.logTail),
|
|
165
165
|
});
|
|
166
166
|
return {
|
|
167
167
|
ok: true,
|
|
168
168
|
title: content.title,
|
|
169
|
-
text:
|
|
169
|
+
text: formatContextToolText(content.title, content.lines),
|
|
170
170
|
data: {
|
|
171
171
|
title: content.title,
|
|
172
172
|
lines: content.lines,
|
|
@@ -183,16 +183,16 @@ function readArtifactsTool(args, context) {
|
|
|
183
183
|
const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
|
|
184
184
|
context.inspectState.revealFile(subject.service.name, subject.file.path);
|
|
185
185
|
}
|
|
186
|
-
const content =
|
|
186
|
+
const content = readContextContent({
|
|
187
187
|
productDir: context.productDir,
|
|
188
|
-
|
|
188
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
189
189
|
mode: "artifacts",
|
|
190
190
|
logTail: 12,
|
|
191
191
|
});
|
|
192
192
|
return {
|
|
193
193
|
ok: true,
|
|
194
194
|
title: content.title,
|
|
195
|
-
text:
|
|
195
|
+
text: formatContextToolText(content.title, content.lines),
|
|
196
196
|
data: {
|
|
197
197
|
title: content.title,
|
|
198
198
|
lines: content.lines,
|
|
@@ -209,16 +209,16 @@ function readSetupTool(args, context) {
|
|
|
209
209
|
throw new Error(`Unknown service "${args.service}"`);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
const content =
|
|
212
|
+
const content = readContextContent({
|
|
213
213
|
productDir: context.productDir,
|
|
214
|
-
|
|
214
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
215
215
|
mode: "setup",
|
|
216
216
|
logTail: 12,
|
|
217
217
|
});
|
|
218
218
|
return {
|
|
219
219
|
ok: true,
|
|
220
220
|
title: content.title,
|
|
221
|
-
text:
|
|
221
|
+
text: formatContextToolText(content.title, content.lines),
|
|
222
222
|
data: {
|
|
223
223
|
title: content.title,
|
|
224
224
|
lines: content.lines,
|
|
@@ -255,7 +255,7 @@ function showStatusTool(_args, context) {
|
|
|
255
255
|
return {
|
|
256
256
|
ok: true,
|
|
257
257
|
title: "Status",
|
|
258
|
-
text:
|
|
258
|
+
text: formatContextToolText("Status", lines),
|
|
259
259
|
data: { lines },
|
|
260
260
|
};
|
|
261
261
|
}
|
|
@@ -272,7 +272,7 @@ async function runDoctorTool(args, context) {
|
|
|
272
272
|
return {
|
|
273
273
|
ok: result.ok,
|
|
274
274
|
title: "Doctor",
|
|
275
|
-
text:
|
|
275
|
+
text: formatContextToolText("Doctor", lines),
|
|
276
276
|
data: result,
|
|
277
277
|
};
|
|
278
278
|
}
|