@elench/testkit 0.1.100 → 0.1.102

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 (38) hide show
  1. package/README.md +6 -3
  2. package/lib/cli/args.mjs +0 -19
  3. package/lib/cli/assistant/app.mjs +6 -0
  4. package/lib/cli/assistant/command-observer.mjs +75 -44
  5. package/lib/cli/assistant/command-results.mjs +29 -2
  6. package/lib/cli/assistant/context-pack.mjs +21 -1
  7. package/lib/cli/assistant/providers/claude.mjs +42 -7
  8. package/lib/cli/assistant/providers/codex.mjs +87 -9
  9. package/lib/cli/assistant/providers/events.mjs +71 -0
  10. package/lib/cli/assistant/providers/index.mjs +5 -4
  11. package/lib/cli/assistant/providers/shared.mjs +40 -21
  12. package/lib/cli/assistant/session.mjs +46 -8
  13. package/lib/cli/assistant/settings.mjs +29 -6
  14. package/lib/cli/assistant/state.mjs +181 -6
  15. package/lib/cli/assistant/transcript-text.mjs +35 -0
  16. package/lib/cli/assistant/view-model.mjs +11 -0
  17. package/lib/cli/command-flags.mjs +0 -3
  18. package/lib/cli/entrypoint.mjs +0 -2
  19. package/lib/cli/operations/run/operation.mjs +0 -3
  20. package/lib/runner/live-run.mjs +5 -1
  21. package/lib/runner/orchestrator.mjs +26 -26
  22. package/lib/runner/planning.mjs +0 -75
  23. package/lib/runner/provenance.mjs +20 -0
  24. package/lib/runner/reporting.mjs +14 -9
  25. package/lib/runner/run-finalization.mjs +5 -2
  26. package/lib/runner/run-guards.mjs +0 -1
  27. package/lib/runner/scheduler/estimates.mjs +61 -0
  28. package/lib/runner/scheduler/identity.mjs +31 -0
  29. package/lib/runner/scheduler/index.mjs +126 -0
  30. package/lib/runner/scheduler/observations.mjs +27 -0
  31. package/lib/runner/selection.mjs +1 -2
  32. package/lib/runner/worker-loop.mjs +3 -4
  33. package/lib/timing/index.mjs +33 -33
  34. package/node_modules/@elench/next-analysis/package.json +1 -1
  35. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  36. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  37. package/node_modules/@elench/ts-analysis/package.json +1 -1
  38. package/package.json +14 -8
@@ -1,29 +1,50 @@
1
1
  import fs from "fs";
2
2
  import readline from "readline";
3
+ import {
4
+ providerError,
5
+ providerEvent,
6
+ providerSessionEnd,
7
+ providerStatus,
8
+ providerToolStart,
9
+ } from "./events.mjs";
3
10
 
4
- export function createHostedSessionRunner({ provider, child, onEvent, parsePayload, readFinalText, shouldIgnoreStatus } = {}) {
11
+ export function createHostedSessionRunner({
12
+ provider,
13
+ child,
14
+ onEvent,
15
+ onRawLine,
16
+ parsePayload,
17
+ readFinalText,
18
+ shouldIgnoreStatus,
19
+ } = {}) {
5
20
  let cancelled = false;
6
21
  let settled = false;
7
22
  let assistantText = "";
23
+ let finalText = null;
8
24
  let lastErrorMessage = null;
9
25
 
10
26
  const emit = (event) => {
11
- if (event?.type === "delta" || event?.type === "final") {
27
+ if (!event) return;
28
+ if (event.type === "assistant-delta") {
12
29
  assistantText += event.text || "";
13
30
  }
14
- if (event?.type === "error") {
15
- lastErrorMessage = event.message || lastErrorMessage;
31
+ if (event.type === "assistant-final") {
32
+ finalText = event.text || finalText;
33
+ }
34
+ if (event.type === "error") {
35
+ lastErrorMessage = event.text || lastErrorMessage;
16
36
  }
17
37
  if (typeof onEvent === "function" && event) onEvent({ provider, ...event });
18
38
  };
19
39
 
20
- emit({ type: "start" });
40
+ emit(providerEvent("session-start"));
21
41
 
22
42
  const stdoutReader = readline.createInterface({ input: child.stdout });
23
43
  stdoutReader.on("line", (line) => {
44
+ onRawLine?.({ provider, stream: "stdout", line });
24
45
  const parsed = tryParseJson(line);
25
46
  if (parsed == null) {
26
- emit({ type: "status", message: line });
47
+ emit(providerStatus(line));
27
48
  return;
28
49
  }
29
50
  const events = parsePayload ? parsePayload(parsed) : [];
@@ -33,27 +54,31 @@ export function createHostedSessionRunner({ provider, child, onEvent, parsePaylo
33
54
 
34
55
  const stderrReader = readline.createInterface({ input: child.stderr });
35
56
  stderrReader.on("line", (line) => {
57
+ onRawLine?.({ provider, stream: "stderr", line });
36
58
  if (shouldIgnoreStatus?.(line)) return;
37
- emit({ type: "status", message: line });
59
+ emit(providerStatus(line, { stream: "stderr" }));
38
60
  });
39
61
 
40
62
  const completion = (async () => {
41
63
  const result = await child;
42
- const finalText = (readFinalText ? readFinalText(result) : null) || assistantText.trim() || null;
64
+ const fileFinalText = readFinalText ? readFinalText(result) : null;
65
+ const resolvedFinalText = fileFinalText || finalText || assistantText.trim() || null;
43
66
  if ((result.exitCode ?? 0) !== 0) {
44
67
  const message = lastErrorMessage || result.stderr || `${provider} exited with code ${result.exitCode ?? 1}`;
45
- emit({ type: "error", message });
68
+ emit(providerError(message));
46
69
  throw new Error(message);
47
70
  }
48
- if (finalText) emit({ type: "final", text: finalText });
49
- emit({ type: "exit", code: result.exitCode ?? 0 });
71
+ if (resolvedFinalText && resolvedFinalText !== finalText) {
72
+ emit(providerEvent("assistant-final", { text: resolvedFinalText }));
73
+ }
74
+ emit(providerSessionEnd({ exitCode: result.exitCode ?? 0 }));
50
75
  settled = true;
51
76
  return {
52
77
  provider,
53
78
  exitCode: result.exitCode ?? 0,
54
79
  stdout: result.stdout || "",
55
80
  stderr: result.stderr || "",
56
- finalText: finalText || result.stdout || "",
81
+ finalText: resolvedFinalText || result.stdout || "",
57
82
  cancelled,
58
83
  };
59
84
  })();
@@ -84,21 +109,15 @@ export function tryParseJson(line) {
84
109
 
85
110
  export function buildToolEvent(name, detail = null) {
86
111
  if (!name) return null;
87
- return {
88
- type: "tool",
89
- name: String(name),
90
- ...(detail ? { detail: String(detail) } : {}),
91
- };
112
+ return providerToolStart(name, detail ? { detail: String(detail) } : {});
92
113
  }
93
114
 
94
115
  export function buildStatusEvent(message) {
95
- if (!message) return null;
96
- return { type: "status", message: String(message) };
116
+ return providerStatus(message);
97
117
  }
98
118
 
99
119
  export function buildErrorEvent(message) {
100
- if (!message) return null;
101
- return { type: "error", message: String(message) };
120
+ return providerError(message);
102
121
  }
103
122
 
104
123
  export function extractTextFragments(payload, fragments = [], depth = 0) {
@@ -1,3 +1,5 @@
1
+ import fs from "fs";
2
+ import path from "path";
1
3
  import { startProviderSession, resolvePreferredProvider } from "./providers/index.mjs";
2
4
  import { buildAssistantPrompt } from "./prompt-builder.mjs";
3
5
  import { createAssistantCommandObserver } from "./command-observer.mjs";
@@ -14,6 +16,7 @@ export async function runAssistantConversationTurn({
14
16
  commandLog,
15
17
  onStatus,
16
18
  onToolEvent,
19
+ onProviderEvent,
17
20
  onResolvedProvider,
18
21
  onPrompt,
19
22
  } = {}) {
@@ -36,6 +39,12 @@ export async function runAssistantConversationTurn({
36
39
  const runtimeSettings = settings || { provider };
37
40
  const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
38
41
  const providerEnv = commandLog?.providerEnv?.(env) || env;
42
+ const tracePath = shouldTraceProviderEvents(env, providerEnv)
43
+ ? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
44
+ : null;
45
+ const rawTracePath = shouldTraceProviderEvents(env, providerEnv)
46
+ ? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-raw.jsonl")
47
+ : null;
39
48
  onResolvedProvider?.(resolvedProvider);
40
49
  onPrompt?.({
41
50
  prompt,
@@ -44,6 +53,7 @@ export async function runAssistantConversationTurn({
44
53
  effort: runtimeSettings.effort || null,
45
54
  });
46
55
  onStatus?.(`Thinking with ${resolvedProvider}...`);
56
+ onProviderEvent?.({ type: "status", provider: resolvedProvider, text: `Thinking with ${resolvedProvider}...` });
47
57
 
48
58
  observer.start();
49
59
  try {
@@ -56,25 +66,53 @@ export async function runAssistantConversationTurn({
56
66
  prompt,
57
67
  purpose: "assistant",
58
68
  env: providerEnv,
69
+ onRawLine(line) {
70
+ appendProviderTrace(rawTracePath, line);
71
+ },
59
72
  onEvent(event) {
60
- if (event.type === "status" || event.type === "tool") onStatus?.(formatProviderEvent(event));
73
+ appendProviderTrace(tracePath, event);
74
+ onProviderEvent?.(event);
75
+ if (event.type === "status" || event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end") {
76
+ onStatus?.(formatProviderEvent(event));
77
+ }
61
78
  },
62
79
  });
63
- const result = await session.completion;
80
+ await session.completion;
64
81
  observer.scan();
65
82
  commandLog?.refresh?.();
66
- return [{
67
- role: "assistant",
68
- text: result.finalText || "",
69
- }];
83
+ return { provider: resolvedProvider };
70
84
  } finally {
71
85
  observer.stop();
72
86
  }
73
87
  }
74
88
 
75
89
  function formatProviderEvent(event) {
76
- if (event.type === "tool") {
90
+ if (event.type === "tool-start") {
77
91
  return `${event.provider}: ${event.name}${event.detail ? ` (${event.detail})` : ""}`;
78
92
  }
79
- return `${event.provider}: ${event.message}`;
93
+ if (event.type === "tool-update") {
94
+ return `${event.provider}: ${event.name || "tool"}${event.text ? ` (${event.text})` : ""}`;
95
+ }
96
+ if (event.type === "tool-end") {
97
+ return `${event.provider}: ${event.name || "tool"} ${event.status || "done"}`;
98
+ }
99
+ return `${event.provider}: ${event.text || "working"}`;
100
+ }
101
+
102
+ function shouldTraceProviderEvents(...envs) {
103
+ return envs.some((entry) => String(entry?.TESTKIT_PROVIDER_TRACE || "").trim() === "1");
104
+ }
105
+
106
+ function appendProviderTrace(tracePath, event) {
107
+ if (!tracePath || !event) return;
108
+ try {
109
+ fs.mkdirSync(path.dirname(tracePath), { recursive: true });
110
+ fs.appendFileSync(
111
+ tracePath,
112
+ `${JSON.stringify({ timestamp: new Date().toISOString(), ...event })}\n`,
113
+ "utf8"
114
+ );
115
+ } catch {
116
+ // Provider tracing must never interfere with the assistant turn.
117
+ }
80
118
  }
@@ -1,7 +1,8 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { HOSTED_ASSISTANT_PROVIDERS } from "./providers/index.mjs";
3
4
 
4
- export const ASSISTANT_PROVIDERS = ["auto", "codex", "claude"];
5
+ export const ASSISTANT_PROVIDERS = ["auto", ...HOSTED_ASSISTANT_PROVIDERS];
5
6
  export const ASSISTANT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
6
7
 
7
8
  export const DEFAULT_ASSISTANT_SETTINGS = Object.freeze({
@@ -19,7 +20,11 @@ export function loadAssistantSettings(productDir) {
19
20
  const filePath = assistantSettingsPath(productDir);
20
21
  try {
21
22
  const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
22
- return normalizeAssistantSettings(parsed);
23
+ const normalized = normalizeAssistantSettings(parsed);
24
+ if (JSON.stringify(parsed) !== JSON.stringify(normalized)) {
25
+ writeAssistantSettingsFile(filePath, normalized);
26
+ }
27
+ return normalized;
23
28
  } catch {
24
29
  return { ...DEFAULT_ASSISTANT_SETTINGS };
25
30
  }
@@ -27,8 +32,7 @@ export function loadAssistantSettings(productDir) {
27
32
 
28
33
  export function saveAssistantSettings(productDir, settings) {
29
34
  const filePath = assistantSettingsPath(productDir);
30
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
31
- fs.writeFileSync(filePath, `${JSON.stringify(normalizeAssistantSettings(settings), null, 2)}\n`);
35
+ writeAssistantSettingsFile(filePath, normalizeAssistantSettings(settings));
32
36
  }
33
37
 
34
38
  export function resetAssistantSettings(productDir) {
@@ -52,8 +56,9 @@ export function mergeAssistantSettings(...settingsObjects) {
52
56
 
53
57
  export function normalizeAssistantSettings(value = {}) {
54
58
  const provider = normalizeProvider(value.provider);
55
- const effort = normalizeEffort(value.effort);
56
- const model = normalizeOptionalString(value.model);
59
+ const extracted = extractEffortFromModel(value.model);
60
+ const effort = normalizeEffort(value.effort || extracted.effort);
61
+ const model = extracted.model;
57
62
  const providerArgs = Array.isArray(value.providerArgs)
58
63
  ? value.providerArgs.map((entry) => normalizeOptionalString(entry)).filter(Boolean)
59
64
  : [];
@@ -89,6 +94,19 @@ export function normalizeOptionalString(value) {
89
94
  return stringValue || null;
90
95
  }
91
96
 
97
+ export function extractEffortFromModel(value) {
98
+ const model = normalizeOptionalString(value);
99
+ if (!model) return { model: null, effort: null };
100
+ const tokens = model.split(/\s+/);
101
+ if (tokens.length < 2) return { model, effort: null };
102
+ const trailingEffort = tokens.at(-1);
103
+ if (!ASSISTANT_EFFORTS.includes(trailingEffort)) return { model, effort: null };
104
+ return {
105
+ model: tokens.slice(0, -1).join(" ").trim() || null,
106
+ effort: trailingEffort,
107
+ };
108
+ }
109
+
92
110
  function dropNullishSettings(settings) {
93
111
  const result = {};
94
112
  for (const [key, value] of Object.entries(settings)) {
@@ -96,3 +114,8 @@ function dropNullishSettings(settings) {
96
114
  }
97
115
  return result;
98
116
  }
117
+
118
+ function writeAssistantSettingsFile(filePath, settings) {
119
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
120
+ fs.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`);
121
+ }
@@ -99,11 +99,25 @@ export function createAssistantState({
99
99
  }
100
100
 
101
101
  function appendMessage(message) {
102
- messages.push({
102
+ const entry = {
103
103
  id: `msg-${messages.length + 1}`,
104
104
  ...message,
105
- });
105
+ };
106
+ messages.push(entry);
107
+ notify();
108
+ return entry.id;
109
+ }
110
+
111
+ function updateMessage(id, updater) {
112
+ const index = messages.findIndex((message) => message.id === id);
113
+ if (index < 0) return false;
114
+ const next = updater(messages[index]);
115
+ messages[index] = {
116
+ ...messages[index],
117
+ ...next,
118
+ };
106
119
  notify();
120
+ return true;
107
121
  }
108
122
 
109
123
  function setBusy(nextBusy, status = null) {
@@ -354,10 +368,11 @@ export function createAssistantState({
354
368
 
355
369
  try {
356
370
  setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
357
- const emitted = await runAssistantConversationTurn({
371
+ const providerTurn = createProviderTurnState();
372
+ await runAssistantConversationTurn({
358
373
  productDir,
359
374
  runState,
360
- transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
375
+ transcript: buildConversationTranscript(messages),
361
376
  userMessage: trimmed,
362
377
  settings,
363
378
  env,
@@ -379,11 +394,20 @@ export function createAssistantState({
379
394
  });
380
395
  notify();
381
396
  },
397
+ onProviderEvent(event) {
398
+ handleProviderEvent(providerTurn, event, {
399
+ appendMessage,
400
+ updateMessage,
401
+ setStatus(status) {
402
+ activeStatus = status;
403
+ notify();
404
+ },
405
+ });
406
+ },
382
407
  onToolEvent(event) {
383
408
  handleAssistantToolEvent(state, event, appendMessage);
384
409
  },
385
410
  });
386
- for (const message of emitted) appendMessage(message);
387
411
  } catch (error) {
388
412
  appendMessage({
389
413
  role: "system",
@@ -470,7 +494,13 @@ async function executeSlashCommand({
470
494
  }
471
495
  if (slash.type === "model") {
472
496
  state.setModel(slash.model, { custom: slash.custom });
473
- appendMessage({ role: "assistant", text: slash.model ? `Model set to ${slash.model}.` : "Model reset to provider default." });
497
+ const snapshot = state.getSnapshot();
498
+ appendMessage({
499
+ role: "assistant",
500
+ text: slash.model
501
+ ? `Model set to ${snapshot.model}${snapshot.effort ? ` with ${snapshot.effort} effort` : ""}.`
502
+ : "Model reset to provider default.",
503
+ });
474
504
  return;
475
505
  }
476
506
  if (slash.type === "model-list") {
@@ -602,6 +632,145 @@ function handleAssistantToolEvent(state, event, appendMessage) {
602
632
  }
603
633
  }
604
634
 
635
+ function createProviderTurnState() {
636
+ return {
637
+ assistantMessageId: null,
638
+ lastActivityText: null,
639
+ };
640
+ }
641
+
642
+ function handleProviderEvent(turn, event, { appendMessage, updateMessage, setStatus } = {}) {
643
+ if (!event) return;
644
+ if (event.type === "assistant-delta") {
645
+ appendAssistantDelta(turn, event, { appendMessage, updateMessage });
646
+ setStatus?.(`${event.provider || "provider"} responding`);
647
+ return;
648
+ }
649
+ if (event.type === "assistant-final") {
650
+ finalizeAssistantMessage(turn, event, { appendMessage, updateMessage });
651
+ setStatus?.(`${event.provider || "provider"} complete`);
652
+ return;
653
+ }
654
+ if (event.type === "session-start") {
655
+ appendProviderActivity(turn, {
656
+ role: "provider-activity",
657
+ title: formatProviderName(event.provider),
658
+ text: "Session started",
659
+ data: event,
660
+ }, { appendMessage, setStatus });
661
+ return;
662
+ }
663
+ if (event.type === "session-end") {
664
+ appendProviderActivity(turn, {
665
+ role: "provider-activity",
666
+ title: formatProviderName(event.provider),
667
+ text: Number.isInteger(event.exitCode) ? `Session ended with exit code ${event.exitCode}` : "Session ended",
668
+ data: event,
669
+ }, { appendMessage, setStatus });
670
+ return;
671
+ }
672
+ if (event.type === "status") {
673
+ appendProviderActivity(turn, {
674
+ role: "provider-activity",
675
+ title: formatProviderName(event.provider),
676
+ text: event.text || "Working",
677
+ data: event,
678
+ }, { appendMessage, setStatus });
679
+ return;
680
+ }
681
+ if (event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end") {
682
+ appendProviderActivity(turn, {
683
+ role: "provider-tool",
684
+ title: formatProviderToolTitle(event),
685
+ text: formatProviderToolText(event),
686
+ data: event,
687
+ }, { appendMessage, setStatus });
688
+ return;
689
+ }
690
+ if (event.type === "error") {
691
+ appendMessage({
692
+ role: "provider-error",
693
+ title: formatProviderName(event.provider),
694
+ text: event.text || "Provider error",
695
+ data: event,
696
+ });
697
+ setStatus?.(`${event.provider || "provider"} error`);
698
+ }
699
+ }
700
+
701
+ function appendAssistantDelta(turn, event, { appendMessage, updateMessage }) {
702
+ if (!turn.assistantMessageId) {
703
+ turn.assistantMessageId = appendMessage({
704
+ role: "assistant",
705
+ status: "streaming",
706
+ provider: event.provider || null,
707
+ text: "",
708
+ });
709
+ }
710
+ updateMessage(turn.assistantMessageId, (message) => ({
711
+ text: `${message.text || ""}${event.text || ""}`,
712
+ status: "streaming",
713
+ }));
714
+ }
715
+
716
+ function finalizeAssistantMessage(turn, event, { appendMessage, updateMessage }) {
717
+ const finalText = event.text || "";
718
+ if (!turn.assistantMessageId) {
719
+ turn.assistantMessageId = appendMessage({
720
+ role: "assistant",
721
+ provider: event.provider || null,
722
+ text: finalText,
723
+ });
724
+ return;
725
+ }
726
+ updateMessage(turn.assistantMessageId, (message) => ({
727
+ text: finalText || message.text || "",
728
+ status: null,
729
+ }));
730
+ }
731
+
732
+ function appendProviderActivity(turn, message, { appendMessage, setStatus }) {
733
+ const text = String(message.text || "").trim();
734
+ if (!text) return;
735
+ const signature = `${message.role}:${message.title || ""}:${text}`;
736
+ if (turn.lastActivityText === signature) return;
737
+ turn.lastActivityText = signature;
738
+ appendMessage(message);
739
+ setStatus?.(message.title ? `${message.title}: ${text}` : text);
740
+ }
741
+
742
+ function formatProviderName(provider) {
743
+ return provider ? String(provider) : "provider";
744
+ }
745
+
746
+ function formatProviderToolTitle(event) {
747
+ const provider = formatProviderName(event.provider);
748
+ const name = event.name || "tool";
749
+ if (event.type === "tool-end") return `${provider} finished ${name}`;
750
+ if (event.type === "tool-update") return `${provider} updated ${name}`;
751
+ return `${provider} started ${name}`;
752
+ }
753
+
754
+ function formatProviderToolText(event) {
755
+ const lines = [];
756
+ if (event.detail) lines.push(String(event.detail));
757
+ if (event.text) lines.push(String(event.text));
758
+ if (event.input) lines.push(formatProviderData("input", event.input));
759
+ if (event.output) lines.push(formatProviderData("output", event.output));
760
+ if (event.status) lines.push(`status: ${event.status}`);
761
+ return lines.filter(Boolean).join("\n") || event.name || "Provider tool activity";
762
+ }
763
+
764
+ function formatProviderData(label, value) {
765
+ if (value == null) return null;
766
+ if (typeof value === "string") return `${label}: ${value}`;
767
+ try {
768
+ return `${label}: ${JSON.stringify(value)}`;
769
+ } catch {
770
+ return `${label}: ${String(value)}`;
771
+ }
772
+ }
773
+
605
774
  function formatSettings(snapshot) {
606
775
  const rows = [
607
776
  ["Provider", snapshot.provider || "auto"],
@@ -648,6 +817,12 @@ function serializeRunSession(session) {
648
817
  };
649
818
  }
650
819
 
820
+ function buildConversationTranscript(messages) {
821
+ return (messages || [])
822
+ .filter((entry) => !["provider-activity", "provider-tool", "provider-error"].includes(entry.role))
823
+ .map((entry) => ({ role: entry.role, text: entry.text }));
824
+ }
825
+
651
826
  function formatObservedCommandTitle(command) {
652
827
  const kind = command?.kind || "command";
653
828
  return `testkit ${kind}`;
@@ -0,0 +1,35 @@
1
+ import stripAnsi from "strip-ansi";
2
+ import { buildAssistantViewModel } from "./view-model.mjs";
3
+ import { renderMarkdownToAnsi } from "./markdown-block.mjs";
4
+
5
+ export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false } = {}) {
6
+ const view = buildAssistantViewModel(snapshot || {}, { cwd });
7
+ const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || ""]
8
+ .filter(Boolean);
9
+ for (const block of view.blocks || []) {
10
+ lines.push("");
11
+ lines.push(...renderBlockLines(block, { ansi }));
12
+ }
13
+ lines.push("");
14
+ lines.push(view.statusLine);
15
+ const text = `${lines.join("\n").trimEnd()}\n`;
16
+ return ansi ? text : stripAnsi(text);
17
+ }
18
+
19
+ function renderBlockLines(block, { ansi = false } = {}) {
20
+ const marker = block.marker || "";
21
+ const title = block.title ? ` ${block.title}` : "";
22
+ const text = String(block.text || "").trimEnd();
23
+ if (block.format === "markdown") {
24
+ const rendered = text ? renderMarkdownToAnsi(text) : "";
25
+ const normalized = ansi ? rendered : stripAnsi(rendered);
26
+ return prefixLines(`${marker}${title}`, normalized);
27
+ }
28
+ return prefixLines(`${marker}${title}`, text);
29
+ }
30
+
31
+ function prefixLines(prefix, text) {
32
+ const lines = String(text || "").split(/\r?\n/);
33
+ if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) return [prefix];
34
+ return lines.map((line, index) => (index === 0 ? `${prefix}${prefix && line ? " " : ""}${line}` : ` ${line}`));
35
+ }
@@ -65,6 +65,17 @@ export function buildTranscriptBlocks(messages) {
65
65
  exitCode: message.data?.exitCode ?? null,
66
66
  };
67
67
  }
68
+ if (role === "provider-activity" || role === "provider-tool" || role === "provider-error") {
69
+ return {
70
+ id: message.id,
71
+ kind: role,
72
+ format: "plain",
73
+ marker: role === "provider-error" ? "!" : "◌",
74
+ title: message.title || null,
75
+ text: message.text || "",
76
+ status: message.status || null,
77
+ };
78
+ }
68
79
  if (role === "user") {
69
80
  return {
70
81
  id: message.id,
@@ -32,9 +32,6 @@ export const runFlags = {
32
32
  "file-timeout-seconds": Flags.string({
33
33
  description: "Per-file wall-clock timeout in seconds",
34
34
  }),
35
- shard: Flags.string({
36
- description: "Run only shard i of n at suite granularity",
37
- }),
38
35
  seed: Flags.string({
39
36
  description: "Deterministic seed for scenario suites",
40
37
  }),
@@ -23,7 +23,6 @@ export function normalizeCliArgs(argv) {
23
23
  "--file",
24
24
  "--workers",
25
25
  "--file-timeout-seconds",
26
- "--shard",
27
26
  "--seed",
28
27
  "--input",
29
28
  "--output",
@@ -54,7 +53,6 @@ export function normalizeCliArgs(argv) {
54
53
  "-f",
55
54
  "--workers",
56
55
  "--file-timeout-seconds",
57
- "--shard",
58
56
  "--seed",
59
57
  "--write-status",
60
58
  "--allow-partial-status",
@@ -2,7 +2,6 @@ import * as runner from "../../../runner/index.mjs";
2
2
  import { loadManagedConfigs } from "../../../app/configs.mjs";
3
3
  import {
4
4
  parseFileTimeoutOption,
5
- parseShardOption,
6
5
  parseSuiteOption,
7
6
  parseTypeOption,
8
7
  parseWorkersOption,
@@ -25,7 +24,6 @@ export async function buildRunRequest(flags, positionalType = null, cwd = proces
25
24
  flags["file-timeout-seconds"] == null
26
25
  ? null
27
26
  : parseFileTimeoutOption(flags["file-timeout-seconds"]);
28
- const shard = parseShardOption(flags.shard);
29
27
  const typeValues = parseTypeOption(flags.type, positionalType);
30
28
  const suiteSelectors = parseSuiteOption(flags.suite);
31
29
  const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
@@ -44,7 +42,6 @@ export async function buildRunRequest(flags, positionalType = null, cwd = proces
44
42
  fileNames,
45
43
  workers,
46
44
  fileTimeoutSeconds,
47
- shard,
48
45
  scenarioSeed: flags.seed || null,
49
46
  serviceFilter: flags.service || null,
50
47
  writeStatus: flags["write-status"],
@@ -4,12 +4,14 @@ import { writeLiveRunArtifact } from "./artifacts.mjs";
4
4
 
5
5
  export function createLiveSnapshotWriter({
6
6
  productDir,
7
+ runId,
7
8
  configs,
8
9
  trackers,
9
10
  startedAt,
10
11
  execution,
11
12
  workerState,
12
13
  selection,
14
+ provenance = null,
13
15
  metadata,
14
16
  logRegistry,
15
17
  setupRegistry,
@@ -21,6 +23,7 @@ export function createLiveSnapshotWriter({
21
23
  productDir,
22
24
  buildLiveRunArtifact({
23
25
  productDir,
26
+ runId,
24
27
  results: partialResults,
25
28
  startedAt,
26
29
  updatedAt: now,
@@ -31,9 +34,10 @@ export function createLiveSnapshotWriter({
31
34
  typeValues: selection.typeValues,
32
35
  suiteSelectors: selection.suiteSelectors,
33
36
  fileNames: selection.fileNames,
34
- shard: selection.shard,
35
37
  serviceFilter: selection.serviceFilter,
36
38
  scenarioSeed: selection.scenarioSeed,
39
+ planning: selection.planning || null,
40
+ provenance,
37
41
  metadata,
38
42
  summarizeDbBackend,
39
43
  serviceLogs: logRegistry.listServiceLogs(),