@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 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. The assistant is a
79
- single chat surface with a bottom composer. It keeps the current run artifact,
80
- selected file, logs, setup operations, and emitted artifacts as hidden context
81
- and pulls them into the conversation inline when needed. Batch `run` output
82
- stays intentionally short: one line per completed file, a concise failure
83
- block, and a final summary. Service logs, captured runtime output, emitted
84
- artifacts, and assistant-visible run state are persisted under
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
@@ -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 { readAssistantContent } from "./content.mjs";
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 = readAssistantContent({
58
+ const content = readContextContent({
59
59
  productDir,
60
- inspectState: {
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 { buildAssistantContext } from "./content.mjs";
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: buildAssistantContext(inspectState.getSnapshot()),
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
- formatAssistantToolText,
9
- readAssistantContent,
10
- } from "./content.mjs";
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 = readAssistantContent({
134
+ const content = readContextContent({
135
135
  productDir: context.productDir,
136
- inspectState: context.inspectState,
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: formatAssistantToolText(content.title, content.lines),
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 = readAssistantContent({
160
+ const content = readContextContent({
161
161
  productDir: context.productDir,
162
- inspectState: context.inspectState,
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: formatAssistantToolText(content.title, content.lines),
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 = readAssistantContent({
186
+ const content = readContextContent({
187
187
  productDir: context.productDir,
188
- inspectState: context.inspectState,
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: formatAssistantToolText(content.title, content.lines),
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 = readAssistantContent({
212
+ const content = readContextContent({
213
213
  productDir: context.productDir,
214
- inspectState: context.inspectState,
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: formatAssistantToolText(content.title, content.lines),
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: formatAssistantToolText("Status", lines),
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: formatAssistantToolText("Doctor", lines),
275
+ text: formatContextToolText("Doctor", lines),
276
276
  data: result,
277
277
  };
278
278
  }