@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 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 --pane artifacts --message '/inspect "__testkit__/health/health.int.testkit.ts"'
69
- npx @elench/testkit assistant --pane logs --message '/inspect "__testkit__/health/health.int.testkit.ts"'
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. The assistant keeps a
79
- chat transcript and a live workbench around the current run artifact, selected
80
- file, logs, setup operations, and emitted artifacts. Batch `run` output stays
81
- intentionally short: one line per completed file, a concise failure block, and
82
- a final summary. Service logs, captured runtime output, emitted artifacts, and
83
- assistant-visible run state are persisted under `.testkit/results/`.
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,
@@ -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({ provider = "auto", cwd, prompt, onEvent, purpose = "investigate" } = {}) {
45
- const resolvedProvider = resolvePreferredProvider(provider);
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 startInteractiveAgentHandoff({ provider = "auto", cwd, prompt } = {}) {
53
- const resolvedProvider = resolvePreferredProvider(provider);
54
- const command = resolvedProvider;
55
- const args = prompt ? [prompt] : [];
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) => resolve({ provider: resolvedProvider, exitCode: code ?? 0 }));
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 = "investigate" } = {}) {
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 === "investigate" || purpose === "assistant") {
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("claude", args, {
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 = "investigate" } = {}) {
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 === "investigate" || purpose === "assistant") {
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("codex", args, {
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 { buildInspectPaneContent } from "../tui/detail-pane.mjs";
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 panePreview = buildPanePreview(productDir, snapshot);
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 pane preview:",
28
- ...(panePreview.length > 0 ? panePreview : ["(empty)"]),
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 buildPanePreview(productDir, snapshot) {
55
+ function buildFocusPreview(productDir, snapshot) {
56
56
  if (!productDir || !snapshot) return [];
57
57
  try {
58
- const pane = buildInspectPaneContent({
58
+ const content = readContextContent({
59
59
  productDir,
60
60
  snapshot,
61
- paneMode: snapshot.paneMode || "detail",
61
+ mode: "detail",
62
62
  logTail: 8,
63
63
  });
64
- return (pane.lines || []).slice(0, 24).map((line) => `- ${line}`);
64
+ return (content.lines || []).slice(0, 24).map((line) => `- ${line}`);
65
65
  } catch (error) {
66
- return [`(pane unavailable: ${error instanceof Error ? error.message : String(error)})`];
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",