@elench/testkit 0.1.89 → 0.1.91

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.
Files changed (31) hide show
  1. package/README.md +14 -7
  2. package/lib/cli/agents/index.mjs +27 -19
  3. package/lib/cli/agents/providers/claude.mjs +3 -3
  4. package/lib/cli/agents/providers/codex.mjs +3 -3
  5. package/lib/cli/assistant/app.mjs +210 -0
  6. package/lib/cli/assistant/context-pack.mjs +191 -0
  7. package/lib/cli/assistant/interactive.mjs +53 -0
  8. package/lib/cli/assistant/prompt-builder.mjs +7 -9
  9. package/lib/cli/assistant/session.mjs +6 -1
  10. package/lib/cli/assistant/state.mjs +134 -46
  11. package/lib/cli/assistant/tool-registry.mjs +220 -230
  12. package/lib/cli/commands/assistant.mjs +50 -34
  13. package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
  14. package/lib/cli/entrypoint.mjs +12 -4
  15. package/lib/cli/presentation/tree-reporter.mjs +0 -101
  16. package/lib/cli/tui/inspect-app.mjs +7 -88
  17. package/lib/cli/tui/inspect-state.mjs +0 -117
  18. package/node_modules/@elench/next-analysis/package.json +1 -1
  19. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  20. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  21. package/node_modules/@elench/ts-analysis/package.json +1 -1
  22. package/package.json +5 -5
  23. package/lib/cli/agents/investigate.mjs +0 -75
  24. package/lib/cli/agents/investigation-context.mjs +0 -102
  25. package/lib/cli/agents/investigation-interpreter.mjs +0 -320
  26. package/lib/cli/agents/investigation-log.mjs +0 -37
  27. package/lib/cli/agents/prompt-builder.mjs +0 -25
  28. package/lib/cli/assistant/content.mjs +0 -60
  29. package/lib/cli/assistant/tool-run-reporter.mjs +0 -80
  30. package/lib/cli/tui/assistant-app.mjs +0 -82
  31. package/lib/cli/tui/assistant-render.mjs +0 -99
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -82,10 +82,10 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@babel/code-frame": "^7.29.0",
85
- "@elench/next-analysis": "0.1.89",
86
- "@elench/testkit-bridge": "0.1.89",
87
- "@elench/testkit-protocol": "0.1.89",
88
- "@elench/ts-analysis": "0.1.89",
85
+ "@elench/next-analysis": "0.1.91",
86
+ "@elench/testkit-bridge": "0.1.91",
87
+ "@elench/testkit-protocol": "0.1.91",
88
+ "@elench/ts-analysis": "0.1.91",
89
89
  "@oclif/core": "^4.10.6",
90
90
  "esbuild": "^0.25.11",
91
91
  "execa": "^9.5.0",
@@ -1,75 +0,0 @@
1
- import { loadInvestigationContext } from "./investigation-context.mjs";
2
- import { buildInvestigationPrompt } from "./prompt-builder.mjs";
3
- import { startAgentSession, startInteractiveAgentHandoff } from "./index.mjs";
4
-
5
- export function createInvestigationRequest({
6
- productDir,
7
- serviceName,
8
- filePath,
9
- provider = "auto",
10
- userMessage,
11
- } = {}) {
12
- const context = loadInvestigationContext({
13
- productDir,
14
- serviceName,
15
- filePath,
16
- });
17
- const prompt = buildInvestigationPrompt({ context, userMessage });
18
- return {
19
- provider,
20
- context,
21
- prompt,
22
- };
23
- }
24
-
25
- export function startHostedInvestigation({
26
- productDir,
27
- serviceName,
28
- filePath,
29
- provider = "auto",
30
- userMessage,
31
- onEvent,
32
- } = {}) {
33
- const request = createInvestigationRequest({
34
- productDir,
35
- serviceName,
36
- filePath,
37
- provider,
38
- userMessage,
39
- });
40
- const session = startAgentSession({
41
- provider: request.provider,
42
- cwd: productDir,
43
- prompt: request.prompt,
44
- onEvent,
45
- });
46
- return {
47
- ...session,
48
- request,
49
- };
50
- }
51
-
52
- export async function runInteractiveInvestigation({
53
- productDir,
54
- serviceName,
55
- filePath,
56
- provider = "auto",
57
- userMessage,
58
- } = {}) {
59
- const request = createInvestigationRequest({
60
- productDir,
61
- serviceName,
62
- filePath,
63
- provider,
64
- userMessage,
65
- });
66
- const result = await startInteractiveAgentHandoff({
67
- provider: request.provider,
68
- cwd: productDir,
69
- prompt: request.prompt,
70
- });
71
- return {
72
- ...result,
73
- request,
74
- };
75
- }
@@ -1,102 +0,0 @@
1
- import path from "path";
2
- import {
3
- collectArtifactEntries,
4
- formatArtifactPreview,
5
- formatFileDetail,
6
- getServiceLogRefs,
7
- getSetupOperationsForService,
8
- loadCurrentRunArtifact,
9
- resolveFileSubject,
10
- } from "../viewer.mjs";
11
- import { readLogTail } from "../../runner/logs.mjs";
12
-
13
- export function loadInvestigationContext({
14
- productDir,
15
- serviceName,
16
- filePath,
17
- logTail = 20,
18
- failureLimit = 5,
19
- previewLength = 6,
20
- } = {}) {
21
- if (!productDir) throw new Error("productDir is required");
22
- if (!serviceName) throw new Error("serviceName is required");
23
- if (!filePath) throw new Error("filePath is required");
24
-
25
- const runArtifact = loadCurrentRunArtifact(productDir);
26
- const subject = resolveFileSubject(runArtifact, filePath, serviceName);
27
- const detailLines = formatFileDetail(productDir, runArtifact, subject, {
28
- logTail,
29
- failureLimit,
30
- previewLength,
31
- });
32
- const artifacts = collectArtifactEntries(productDir, runArtifact, subject.file.path, subject.service.name).map((entry) => ({
33
- name: entry.artifactRef.name,
34
- kind: entry.artifactRef.kind || null,
35
- summary: entry.artifactRef.summary || null,
36
- path: entry.artifactRef.path,
37
- previewLines: formatArtifactPreview(entry.payload, previewLength),
38
- }));
39
- const setupOperations = getSetupOperationsForService(runArtifact, subject.service.name).slice(0, 8).map((operation) => ({
40
- stage: operation.stage,
41
- status: operation.status,
42
- summary: operation.summary || null,
43
- durationMs: operation.durationMs ?? null,
44
- logPath: operation.logRef?.path || null,
45
- error: operation.error || null,
46
- }));
47
- const backendLogs = getServiceLogRefs(runArtifact, subject.service.name).map((logRef) => ({
48
- runtimeLabel: logRef.runtimeLabel,
49
- path: logRef.path,
50
- lines: readLogTail(path.join(productDir, logRef.path), logTail),
51
- }));
52
-
53
- return {
54
- productDir,
55
- runArtifact,
56
- subject,
57
- detailLines,
58
- summary: {
59
- service: subject.service.name,
60
- suite: `${subject.suite.type}:${subject.suite.name}`,
61
- file: subject.file.path,
62
- status: subject.file.status,
63
- durationMs: subject.file.durationMs || 0,
64
- error: subject.file.error || null,
65
- failureCount: Array.isArray(subject.file.failureDetails) ? subject.file.failureDetails.length : 0,
66
- },
67
- setupOperations,
68
- artifacts,
69
- backendLogs,
70
- };
71
- }
72
-
73
- export function formatInvestigationTranscriptContext(context) {
74
- if (!context) return "";
75
- const artifactSummary = context.artifacts.length === 0
76
- ? "none"
77
- : context.artifacts
78
- .map((entry) => `${entry.name}${entry.kind ? ` [${entry.kind}]` : ""}: ${entry.path}`)
79
- .join("\n");
80
- const logSummary = context.backendLogs.length === 0
81
- ? "none"
82
- : context.backendLogs.map((entry) => `${entry.runtimeLabel}: ${entry.path}`).join("\n");
83
-
84
- return [
85
- `Service: ${context.summary.service}`,
86
- `Suite: ${context.summary.suite}`,
87
- `File: ${context.summary.file}`,
88
- `Status: ${context.summary.status}`,
89
- context.summary.error ? `Error: ${context.summary.error}` : null,
90
- "",
91
- "Artifacts:",
92
- artifactSummary,
93
- "",
94
- "Backend Logs:",
95
- logSummary,
96
- "",
97
- "Detailed Failure View:",
98
- ...context.detailLines,
99
- ]
100
- .filter(Boolean)
101
- .join("\n");
102
- }
@@ -1,320 +0,0 @@
1
- const INSPECT_COMMANDS = new Set([
2
- "cat",
3
- "find",
4
- "grep",
5
- "head",
6
- "ls",
7
- "open",
8
- "read",
9
- "rg",
10
- "sed",
11
- "tail",
12
- ]);
13
-
14
- const EDIT_COMMANDS = new Set([
15
- "apply_patch",
16
- "cp",
17
- "ed",
18
- "mv",
19
- "perl",
20
- "python",
21
- "python3",
22
- "ruby",
23
- "tee",
24
- "write",
25
- ]);
26
-
27
- const RERUN_COMMANDS = new Set([
28
- "npm",
29
- "npx",
30
- "pnpm",
31
- "pytest",
32
- "testkit",
33
- "vitest",
34
- "yarn",
35
- ]);
36
-
37
- const PHASE_LABELS = {
38
- planning: "Planning investigation",
39
- inspecting: "Inspecting relevant files",
40
- editing: "Editing candidate fix",
41
- rerunning: "Rerunning validation",
42
- summarizing: "Summarizing outcome",
43
- };
44
-
45
- export function createInvestigationInterpreter() {
46
- let phase = "planning";
47
- let timeline = [];
48
- let activeStepId = null;
49
- let nextId = 1;
50
- let inspectedCount = 0;
51
- let rerunCount = 0;
52
- const editedFiles = new Set();
53
-
54
- pushTimeline({
55
- kind: "phase",
56
- phase,
57
- label: PHASE_LABELS[phase],
58
- });
59
-
60
- return {
61
- consumeProviderEvent(event) {
62
- if (!event || !event.type) return getSnapshot();
63
-
64
- if (event.type === "start") {
65
- setPhase("planning");
66
- activateStep("planning", "Preparing investigation", "active");
67
- return getSnapshot();
68
- }
69
-
70
- if (event.type === "tool") {
71
- handleToolEvent(event);
72
- return getSnapshot();
73
- }
74
-
75
- if (event.type === "status") {
76
- handleStatusEvent(event);
77
- return getSnapshot();
78
- }
79
-
80
- if (event.type === "error") {
81
- failActiveStep();
82
- pushTimeline({
83
- kind: "notice",
84
- severity: "error",
85
- message: event.message || "Agent error",
86
- });
87
- return getSnapshot();
88
- }
89
-
90
- if (event.type === "final") {
91
- finalizeWithText(event.text || "");
92
- return getSnapshot();
93
- }
94
-
95
- if (event.type === "exit" && Number(event.code) !== 0) {
96
- failActiveStep();
97
- pushTimeline({
98
- kind: "notice",
99
- severity: "error",
100
- message: `Agent exited with code ${event.code}`,
101
- });
102
- return getSnapshot();
103
- }
104
-
105
- return getSnapshot();
106
- },
107
-
108
- getSnapshot,
109
- };
110
-
111
- function getSnapshot() {
112
- return {
113
- phase,
114
- activeStep: activeStepId ? findTimeline(activeStepId) : null,
115
- timeline: timeline.map((entry) => ({
116
- ...entry,
117
- files: Array.isArray(entry.files) ? [...entry.files] : entry.files,
118
- })),
119
- summary: {
120
- inspectedCount,
121
- rerunCount,
122
- editedFiles: [...editedFiles].sort(),
123
- },
124
- };
125
- }
126
-
127
- function handleToolEvent(event) {
128
- const commandText = String(event.name || "").trim();
129
- const detailText = String(event.detail || "").trim();
130
- const command = firstToken(commandText);
131
-
132
- if (RERUN_COMMANDS.has(command) || /(?:^|\s)(testkit|vitest|pytest|npm|pnpm|yarn)(?:\s|$)/i.test(commandText)) {
133
- setPhase("rerunning");
134
- rerunCount += 1;
135
- activateStep("rerunning", "Rerunning validation", "active", compactDetail(commandText, detailText));
136
- pushTimeline({
137
- kind: "rerun",
138
- summary: compactDetail(commandText, detailText) || commandText,
139
- command: commandText || detailText || null,
140
- });
141
- return;
142
- }
143
-
144
- if (EDIT_COMMANDS.has(command) || /apply_patch|write|edit/i.test(commandText)) {
145
- setPhase("editing");
146
- const files = extractFileHints(`${commandText} ${detailText}`);
147
- for (const file of files) editedFiles.add(file);
148
- activateStep(
149
- "editing",
150
- "Editing candidate fix",
151
- "active",
152
- files.length > 0 ? `${files.length} file${files.length === 1 ? "" : "s"} touched` : compactDetail(commandText, detailText)
153
- );
154
- if (files.length > 0) {
155
- upsertTimeline(
156
- "latest-edit",
157
- {
158
- kind: "edit",
159
- summary: `Edited ${files.length} file${files.length === 1 ? "" : "s"}`,
160
- files,
161
- },
162
- true
163
- );
164
- }
165
- return;
166
- }
167
-
168
- if (INSPECT_COMMANDS.has(command) || /git|diff|read|search|inspect|open/i.test(commandText)) {
169
- setPhase("inspecting");
170
- inspectedCount += 1;
171
- activateStep(
172
- "inspecting",
173
- "Inspecting relevant files",
174
- "active",
175
- `${inspectedCount} check${inspectedCount === 1 ? "" : "s"}`
176
- );
177
- return;
178
- }
179
-
180
- setPhase("inspecting");
181
- activateStep("inspecting", "Inspecting relevant files", "active", compactDetail(commandText, detailText));
182
- }
183
-
184
- function handleStatusEvent(event) {
185
- const message = String(event.message || "").trim();
186
- if (!message) return;
187
- if (/error|failed|exception/i.test(message)) {
188
- pushTimeline({
189
- kind: "notice",
190
- severity: "warning",
191
- message,
192
- });
193
- return;
194
- }
195
- if (/plan|thinking|analy/i.test(message) && phase === "planning") {
196
- activateStep("planning", "Planning investigation", "active", "Gathering context");
197
- }
198
- }
199
-
200
- function finalizeWithText(text) {
201
- setPhase("summarizing");
202
- completeActiveStep();
203
- const summary = summarizeFinalText(text);
204
- if (summary) {
205
- pushTimeline({
206
- kind: "result",
207
- summary,
208
- finalText: text,
209
- });
210
- if (/root cause|likely|caused by|problem/i.test(summary)) {
211
- pushTimeline({
212
- kind: "finding",
213
- severity: "info",
214
- summary,
215
- confidence: inferConfidence(summary),
216
- });
217
- }
218
- }
219
- }
220
-
221
- function setPhase(nextPhase) {
222
- if (!nextPhase || nextPhase === phase) return;
223
- completeActiveStep();
224
- phase = nextPhase;
225
- pushTimeline({
226
- kind: "phase",
227
- phase,
228
- label: PHASE_LABELS[phase] || nextPhase,
229
- });
230
- }
231
-
232
- function activateStep(key, label, status, detail = null) {
233
- const stepId = `step:${key}`;
234
- upsertTimeline(
235
- stepId,
236
- {
237
- kind: "step",
238
- label,
239
- status,
240
- detail,
241
- },
242
- false
243
- );
244
- activeStepId = stepId;
245
- }
246
-
247
- function completeActiveStep() {
248
- if (!activeStepId) return;
249
- const entry = findTimeline(activeStepId);
250
- if (entry && entry.status === "active") entry.status = "done";
251
- activeStepId = null;
252
- }
253
-
254
- function failActiveStep() {
255
- if (!activeStepId) return;
256
- const entry = findTimeline(activeStepId);
257
- if (entry) entry.status = "failed";
258
- activeStepId = null;
259
- }
260
-
261
- function findTimeline(id) {
262
- return timeline.find((entry) => entry.id === id) || null;
263
- }
264
-
265
- function pushTimeline(entry) {
266
- timeline.push({
267
- id: `timeline:${nextId += 1}`,
268
- at: Date.now(),
269
- ...entry,
270
- });
271
- }
272
-
273
- function upsertTimeline(id, entry, replaceFiles = false) {
274
- const existing = findTimeline(id);
275
- if (existing) {
276
- Object.assign(existing, entry);
277
- if (replaceFiles && Array.isArray(entry.files)) existing.files = [...entry.files];
278
- return existing;
279
- }
280
- const nextEntry = {
281
- id,
282
- at: Date.now(),
283
- ...entry,
284
- };
285
- timeline.push(nextEntry);
286
- return nextEntry;
287
- }
288
- }
289
-
290
- function firstToken(commandText) {
291
- const normalized = String(commandText || "").trim();
292
- if (!normalized) return "";
293
- return normalized.split(/\s+/)[0].toLowerCase();
294
- }
295
-
296
- function compactDetail(commandText, detailText) {
297
- if (commandText && detailText && detailText !== commandText) {
298
- return `${commandText} (${detailText})`;
299
- }
300
- return commandText || detailText || null;
301
- }
302
-
303
- function summarizeFinalText(text) {
304
- const normalized = String(text || "").trim();
305
- if (!normalized) return "";
306
- const [firstParagraph] = normalized.split(/\n\s*\n/);
307
- const [firstSentence] = firstParagraph.split(/(?<=[.!?])\s+/);
308
- return firstSentence.trim();
309
- }
310
-
311
- function inferConfidence(summary) {
312
- if (/definitely|confirmed|root cause/i.test(summary)) return "high";
313
- if (/likely|probably|appears/i.test(summary)) return "medium";
314
- return "low";
315
- }
316
-
317
- function extractFileHints(text) {
318
- const matches = String(text || "").match(/[A-Za-z0-9_./-]+\.(?:mjs|js|cjs|ts|tsx|jsx|json|md)/g) || [];
319
- return [...new Set(matches)];
320
- }
@@ -1,37 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- export function writeInvestigationLog({ productDir, selectedFailure, agentSession } = {}) {
5
- if (!productDir || !selectedFailure || !agentSession) return null;
6
-
7
- try {
8
- const rootDir = path.join(productDir, ".testkit", "agent-sessions");
9
- fs.mkdirSync(rootDir, { recursive: true });
10
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
11
- const fileName = `${stamp}-${sanitize(selectedFailure.serviceName)}-${sanitize(path.basename(selectedFailure.filePath))}.json`;
12
- const filePath = path.join(rootDir, fileName);
13
- const payload = {
14
- serviceName: selectedFailure.serviceName,
15
- filePath: selectedFailure.filePath,
16
- provider: agentSession.provider,
17
- status: agentSession.status,
18
- startedAt: agentSession.startedAt || null,
19
- endedAt: agentSession.endedAt || null,
20
- finalText: agentSession.finalText || "",
21
- error: agentSession.error || null,
22
- exitCode: agentSession.exitCode ?? null,
23
- transcriptEntries: agentSession.transcriptEntries || [],
24
- timeline: agentSession.timeline || [],
25
- summary: agentSession.summary || null,
26
- activePhase: agentSession.activePhase || null,
27
- };
28
- fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
29
- return filePath;
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- function sanitize(value) {
36
- return String(value || "unknown").replace(/[^A-Za-z0-9._-]+/g, "-");
37
- }
@@ -1,25 +0,0 @@
1
- import { formatInvestigationTranscriptContext } from "./investigation-context.mjs";
2
-
3
- const DEFAULT_INVESTIGATION_MESSAGE =
4
- "Investigate the selected testkit failure. Explain the most likely root cause, cite concrete evidence from the provided context, and suggest the next debugging steps or code changes.";
5
-
6
- export function defaultInvestigationMessage() {
7
- return DEFAULT_INVESTIGATION_MESSAGE;
8
- }
9
-
10
- export function buildInvestigationPrompt({ context, userMessage } = {}) {
11
- const message = userMessage || DEFAULT_INVESTIGATION_MESSAGE;
12
- const transcriptContext = formatInvestigationTranscriptContext(context);
13
-
14
- return [
15
- "You are investigating a failed testkit run.",
16
- "Use the provided run context as the primary evidence.",
17
- "If the evidence is incomplete, say what additional command or file you would inspect next.",
18
- "Keep the response actionable and focused on root cause, evidence, and next steps.",
19
- "",
20
- `User request: ${message}`,
21
- "",
22
- "Run context:",
23
- transcriptContext,
24
- ].join("\n");
25
- }
@@ -1,60 +0,0 @@
1
- import { buildInspectPaneContent } from "../tui/detail-pane.mjs";
2
-
3
- export function readAssistantContent({
4
- productDir,
5
- inspectState,
6
- mode = "detail",
7
- logTail = 12,
8
- } = {}) {
9
- const snapshot = inspectState.getSnapshot();
10
- const content = buildInspectPaneContent({
11
- productDir,
12
- snapshot,
13
- paneMode: normalizeMode(mode),
14
- logTail,
15
- });
16
- return {
17
- mode: normalizeMode(mode),
18
- title: content.title,
19
- lines: content.lines || [],
20
- data: content.data ?? null,
21
- selection: snapshot.selectedEntry || null,
22
- summaryRows: snapshot.summaryData?.rows || [],
23
- };
24
- }
25
-
26
- export function buildAssistantContext(snapshot) {
27
- const selectedEntry = snapshot?.selectedEntry || null;
28
- const summaryRows = snapshot?.summaryData?.rows || [];
29
- return {
30
- selection: selectedEntry
31
- ? {
32
- kind: selectedEntry.kind,
33
- label: selectedEntry.label || selectedEntry.filePath || selectedEntry.serviceName || "selection",
34
- serviceName: selectedEntry.serviceName || null,
35
- type: selectedEntry.type || null,
36
- suiteName: selectedEntry.suiteName || null,
37
- filePath: selectedEntry.filePath || null,
38
- status: selectedEntry.status || null,
39
- }
40
- : null,
41
- summaryRows,
42
- phase: snapshot?.phase || null,
43
- hasArtifact: Boolean(snapshot?.runArtifact),
44
- };
45
- }
46
-
47
- export function formatAssistantToolText(title, lines) {
48
- const normalizedTitle = String(title || "").trim();
49
- const normalizedLines = Array.isArray(lines) ? lines.filter((line) => String(line).length > 0) : [];
50
- if (normalizedTitle && normalizedLines.length > 0) {
51
- return [normalizedTitle, ...normalizedLines].join("\n");
52
- }
53
- if (normalizedTitle) return normalizedTitle;
54
- return normalizedLines.join("\n");
55
- }
56
-
57
- function normalizeMode(mode) {
58
- if (mode === "logs" || mode === "artifacts" || mode === "setup") return mode;
59
- return "detail";
60
- }