@sean.holung/minicode 0.3.4 → 0.3.6

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 (72) hide show
  1. package/README.md +25 -47
  2. package/dist/scripts/run-benchmarks.js +73 -28
  3. package/dist/src/agent/config.js +51 -66
  4. package/dist/src/agent/editable-config.js +50 -58
  5. package/dist/src/agent/home-env.js +74 -0
  6. package/dist/src/benchmark/runner.js +142 -59
  7. package/dist/src/cli/config-slash-command.js +15 -13
  8. package/dist/src/indexer/project-index.js +49 -13
  9. package/dist/src/serve/agent-bridge.js +99 -31
  10. package/dist/src/serve/mcp-server.js +70 -21
  11. package/dist/src/serve/server.js +198 -8
  12. package/dist/src/session/session-preview.js +14 -0
  13. package/dist/src/shared/graph-search.js +80 -0
  14. package/dist/src/shared/graph-selection.js +40 -0
  15. package/dist/src/shared/graph-symbols.js +82 -0
  16. package/dist/src/shared/symbol-resolution.js +33 -0
  17. package/dist/src/tools/find-path.js +15 -6
  18. package/dist/src/tools/find-references.js +7 -2
  19. package/dist/src/tools/get-dependencies.js +8 -3
  20. package/dist/src/tools/read-symbol.js +9 -3
  21. package/dist/src/tools/registry.js +4 -1
  22. package/dist/src/tools/search-code-map.js +18 -3
  23. package/dist/src/web/app.js +646 -87
  24. package/dist/src/web/index.html +68 -6
  25. package/dist/src/web/style.css +208 -1
  26. package/dist/tests/benchmark-harness.test.js +100 -0
  27. package/dist/tests/config-api.test.js +5 -5
  28. package/dist/tests/config-integration.test.js +130 -56
  29. package/dist/tests/config-slash-command.test.js +12 -11
  30. package/dist/tests/config.test.js +12 -4
  31. package/dist/tests/editable-config.test.js +15 -12
  32. package/dist/tests/file-tools.test.js +34 -1
  33. package/dist/tests/find-path.test.js +43 -2
  34. package/dist/tests/find-references.test.js +49 -0
  35. package/dist/tests/get-dependencies.test.js +23 -0
  36. package/dist/tests/graph-onboarding.test.js +10 -1
  37. package/dist/tests/graph-search.test.js +66 -0
  38. package/dist/tests/graph-selection.test.js +58 -0
  39. package/dist/tests/graph-symbols.test.js +45 -0
  40. package/dist/tests/home-env.test.js +56 -0
  41. package/dist/tests/indexer.test.js +6 -0
  42. package/dist/tests/read-symbol.test.js +35 -0
  43. package/dist/tests/request-tracker.test.js +15 -0
  44. package/dist/tests/run-benchmarks.test.js +117 -33
  45. package/dist/tests/search-code-map.test.js +2 -0
  46. package/dist/tests/serve.integration.test.js +338 -9
  47. package/dist/tests/session-preview.test.js +56 -0
  48. package/dist/tests/session-ui.test.js +4 -0
  49. package/dist/tests/settings-ui.test.js +18 -0
  50. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  51. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
  52. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  53. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  54. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  55. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  56. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
  57. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  58. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
  59. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
  60. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
  61. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
  62. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
  63. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  64. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
  65. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  66. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
  67. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
  68. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
  69. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
  70. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
  71. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +1 -1
@@ -1,6 +1,5 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { loadConfigFile, MINICODE_HOME, resolveConfigEnv, } from "./config.js";
1
+ import { MINICODE_HOME, resolveConfigEnv, } from "./config.js";
2
+ import { getHomeEnvPath, loadHomeEnvValues, upsertHomeEnvValues } from "./home-env.js";
4
3
  const REASONING_VALUES = [
5
4
  "xhigh",
6
5
  "high",
@@ -12,7 +11,6 @@ const REASONING_VALUES = [
12
11
  export const EDITABLE_CONFIG_DEFINITIONS = [
13
12
  {
14
13
  key: "modelProvider",
15
- fileKey: "modelProvider",
16
14
  envVar: "MODEL_PROVIDER",
17
15
  type: "enum",
18
16
  values: ["anthropic", "openai-compatible"],
@@ -20,119 +18,102 @@ export const EDITABLE_CONFIG_DEFINITIONS = [
20
18
  },
21
19
  {
22
20
  key: "model",
23
- fileKey: "model",
24
21
  envVar: "MODEL",
25
22
  type: "string",
26
23
  description: "Default model id for new sessions",
27
24
  },
28
25
  {
29
26
  key: "maxSteps",
30
- fileKey: "maxSteps",
31
27
  envVar: "MAX_STEPS",
32
28
  type: "number",
33
29
  description: "Turn call limit before the agent pauses and waits for another prompt",
34
30
  },
35
31
  {
36
32
  key: "maxTokens",
37
- fileKey: "maxTokens",
38
33
  envVar: "MAX_TOKENS",
39
34
  type: "number",
40
35
  description: "Maximum completion tokens per model response",
41
36
  },
42
37
  {
43
38
  key: "maxContextTokens",
44
- fileKey: "maxContextTokens",
45
39
  envVar: "MAX_CONTEXT_TOKENS",
46
40
  type: "number",
47
41
  description: "Estimated prompt-context budget before compaction",
48
42
  },
49
43
  {
50
44
  key: "commandTimeoutMs",
51
- fileKey: "commandTimeout",
52
45
  envVar: "COMMAND_TIMEOUT_MS",
53
46
  type: "number",
54
47
  description: "Shell command timeout in milliseconds",
55
48
  },
56
49
  {
57
50
  key: "maxFileSizeBytes",
58
- fileKey: "maxFileSizeBytes",
59
51
  envVar: "MAX_FILE_SIZE_BYTES",
60
52
  type: "number",
61
53
  description: "Maximum file size allowed for read/edit tools",
62
54
  },
63
55
  {
64
56
  key: "confirmDestructive",
65
- fileKey: "confirmDestructive",
66
57
  envVar: "CONFIRM_DESTRUCTIVE",
67
58
  type: "boolean",
68
59
  description: "Whether destructive commands require confirmation",
69
60
  },
70
61
  {
71
62
  key: "keepRecentMessages",
72
- fileKey: "keepRecentMessages",
73
63
  envVar: "KEEP_RECENT_MESSAGES",
74
64
  type: "number",
75
65
  description: "Recent messages preserved when trimming session history",
76
66
  },
77
67
  {
78
68
  key: "loopDetectionWindow",
79
- fileKey: "loopDetectionWindow",
80
69
  envVar: "LOOP_DETECTION_WINDOW",
81
70
  type: "number",
82
71
  description: "Window size for repeated-tool-call loop detection",
83
72
  },
84
73
  {
85
74
  key: "maxToolOutputChars",
86
- fileKey: "maxToolOutputChars",
87
75
  envVar: "MAX_TOOL_OUTPUT_CHARS",
88
76
  type: "number",
89
77
  description: "Maximum tool output retained before truncation",
90
78
  },
91
79
  {
92
80
  key: "openAiBaseUrl",
93
- fileKey: "openAiBaseUrl",
94
81
  envVar: "OPENAI_BASE_URL",
95
82
  type: "string",
96
83
  description: "Base URL for OpenAI-compatible providers",
97
84
  },
98
85
  {
99
86
  key: "enableFileReadDedup",
100
- fileKey: "enableFileReadDedup",
101
87
  envVar: "ENABLE_FILE_READ_DEDUP",
102
88
  type: "boolean",
103
89
  description: "Deduplicate repeated file reads in prompt context",
104
90
  },
105
91
  {
106
92
  key: "enableAdaptiveKeepRecent",
107
- fileKey: "enableAdaptiveKeepRecent",
108
93
  envVar: "ENABLE_ADAPTIVE_KEEP_RECENT",
109
94
  type: "boolean",
110
95
  description: "Adjust recent-message retention based on context pressure",
111
96
  },
112
97
  {
113
98
  key: "enableToolOutputTruncation",
114
- fileKey: "enableToolOutputTruncation",
115
99
  envVar: "ENABLE_TOOL_OUTPUT_TRUNCATION",
116
100
  type: "boolean",
117
101
  description: "Truncate oversized tool output before storing it in session history",
118
102
  },
119
103
  {
120
104
  key: "compactionThreshold",
121
- fileKey: "compactionThreshold",
122
105
  envVar: "COMPACTION_THRESHOLD",
123
106
  type: "number",
124
107
  description: "Compaction threshold ratio used before a turn starts",
125
108
  },
126
109
  {
127
110
  key: "compactionModel",
128
- fileKey: "compactionModel",
129
111
  envVar: "COMPACTION_MODEL",
130
112
  type: "string",
131
113
  description: "Optional model id used for LLM-based compaction",
132
114
  },
133
115
  {
134
116
  key: "reasoningEffort",
135
- fileKey: "reasoningEffort",
136
117
  envVar: "REASONING_EFFORT",
137
118
  type: "enum",
138
119
  values: REASONING_VALUES,
@@ -140,7 +121,6 @@ export const EDITABLE_CONFIG_DEFINITIONS = [
140
121
  },
141
122
  {
142
123
  key: "enableDynamicPrompt",
143
- fileKey: "enableDynamicPrompt",
144
124
  envVar: "ENABLE_DYNAMIC_PROMPT",
145
125
  type: "boolean",
146
126
  description: "Toggle project-aware dynamic prompt generation",
@@ -194,9 +174,6 @@ function normalizePersistedValue(value) {
194
174
  }
195
175
  return null;
196
176
  }
197
- function isEmptyConfigFile(config) {
198
- return Object.keys(config).length === 0;
199
- }
200
177
  export function listEditableConfigDefinitions() {
201
178
  return EDITABLE_CONFIG_DEFINITIONS;
202
179
  }
@@ -223,10 +200,25 @@ export function formatPersistedConfigValue(value) {
223
200
  return String(value);
224
201
  }
225
202
  export function getGlobalConfigPath(minicodeHome = MINICODE_HOME) {
226
- return path.join(minicodeHome, "agent.config.json");
203
+ return getHomeEnvPath(minicodeHome);
227
204
  }
228
205
  export async function loadPersistedConfig(minicodeHome = MINICODE_HOME) {
229
- return loadConfigFile(getGlobalConfigPath(minicodeHome));
206
+ return loadHomeEnvValues(minicodeHome);
207
+ }
208
+ function readPersistedEnvValue(definition, persisted) {
209
+ const rawValue = persisted[definition.envVar];
210
+ if (rawValue === undefined) {
211
+ return null;
212
+ }
213
+ try {
214
+ return parseEditableValue(definition, rawValue);
215
+ }
216
+ catch {
217
+ return rawValue;
218
+ }
219
+ }
220
+ function serializeEditableValue(value) {
221
+ return String(value);
230
222
  }
231
223
  export async function buildStructuredConfigPayload(config, minicodeHome = MINICODE_HOME) {
232
224
  const configPath = getGlobalConfigPath(minicodeHome);
@@ -235,10 +227,11 @@ export async function buildStructuredConfigPayload(config, minicodeHome = MINICO
235
227
  return {
236
228
  configPath,
237
229
  entries: EDITABLE_CONFIG_DEFINITIONS.map((definition) => {
238
- const envValue = env.values[definition.envVar];
239
- const envSource = env.sources[definition.envVar] ?? null;
240
- const envSourcePath = envSource === "home-dotenv"
241
- ? env.homeEnvPath
230
+ const envSource = env.sources[definition.envVar] === "process"
231
+ ? "process"
232
+ : null;
233
+ const envValue = envSource === "process"
234
+ ? (env.values[definition.envVar] ?? null)
242
235
  : null;
243
236
  return {
244
237
  key: definition.key,
@@ -247,11 +240,11 @@ export async function buildStructuredConfigPayload(config, minicodeHome = MINICO
247
240
  envVar: definition.envVar,
248
241
  ...(definition.values ? { values: definition.values } : {}),
249
242
  effectiveValue: normalizePersistedValue(config[definition.key]),
250
- persistedValue: normalizePersistedValue(persisted[definition.fileKey]),
251
- envValue: envValue ?? null,
243
+ persistedValue: readPersistedEnvValue(definition, persisted),
244
+ envValue,
252
245
  envSource,
253
- envSourcePath,
254
- overriddenByEnv: envValue !== undefined,
246
+ envSourcePath: envSource === "process" ? null : null,
247
+ overriddenByEnv: envSource === "process",
255
248
  };
256
249
  }),
257
250
  };
@@ -260,26 +253,25 @@ export async function setPersistedConfigValue(options) {
260
253
  const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
261
254
  const definition = getEditableConfigDefinition(options.key);
262
255
  const configPath = getGlobalConfigPath(minicodeHome);
263
- const nextFile = await loadConfigFile(configPath);
264
256
  const storedValue = parseEditableValue(definition, options.rawValue);
265
- nextFile[definition.fileKey] = storedValue;
266
- await mkdir(path.dirname(configPath), { recursive: true });
267
- await writeFile(configPath, JSON.stringify(nextFile, null, 2) + "\n", "utf8");
257
+ await upsertHomeEnvValues({
258
+ minicodeHome,
259
+ values: {
260
+ [definition.envVar]: serializeEditableValue(storedValue),
261
+ },
262
+ });
268
263
  return { path: configPath, storedValue };
269
264
  }
270
265
  export async function unsetPersistedConfigValue(options) {
271
266
  const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
272
267
  const definition = getEditableConfigDefinition(options.key);
273
268
  const configPath = getGlobalConfigPath(minicodeHome);
274
- const nextFile = await loadConfigFile(configPath);
275
- delete nextFile[definition.fileKey];
276
- if (isEmptyConfigFile(nextFile)) {
277
- await rm(configPath, { force: true });
278
- }
279
- else {
280
- await mkdir(path.dirname(configPath), { recursive: true });
281
- await writeFile(configPath, JSON.stringify(nextFile, null, 2) + "\n", "utf8");
282
- }
269
+ await upsertHomeEnvValues({
270
+ minicodeHome,
271
+ values: {
272
+ [definition.envVar]: null,
273
+ },
274
+ });
283
275
  return { path: configPath };
284
276
  }
285
277
  export async function applyPersistedConfigUpdates(options) {
@@ -291,22 +283,22 @@ export async function applyPersistedConfigUpdates(options) {
291
283
  return { key: rawKey, value };
292
284
  });
293
285
  const saved = [];
286
+ const envUpdates = {};
294
287
  for (const item of planned) {
288
+ const definition = getEditableConfigDefinition(item.key);
295
289
  if (item.value === null) {
296
- await unsetPersistedConfigValue({
297
- key: item.key,
298
- minicodeHome,
299
- });
290
+ envUpdates[definition.envVar] = null;
300
291
  saved.push({ key: item.key, value: null });
301
292
  continue;
302
293
  }
303
- await setPersistedConfigValue({
304
- key: item.key,
305
- rawValue: String(item.value),
306
- minicodeHome,
307
- });
308
- saved.push({ key: item.key, value: item.value });
294
+ const storedValue = parseEditableValue(definition, String(item.value));
295
+ envUpdates[definition.envVar] = serializeEditableValue(storedValue);
296
+ saved.push({ key: item.key, value: storedValue });
309
297
  }
298
+ await upsertHomeEnvValues({
299
+ minicodeHome,
300
+ values: envUpdates,
301
+ });
310
302
  return {
311
303
  path: getGlobalConfigPath(minicodeHome),
312
304
  saved,
@@ -0,0 +1,74 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import dotenv from "dotenv";
4
+ import { MINICODE_HOME } from "./config.js";
5
+ export function getHomeEnvPath(minicodeHome = MINICODE_HOME) {
6
+ return path.join(minicodeHome, ".env");
7
+ }
8
+ export async function loadHomeEnvValues(minicodeHome = MINICODE_HOME) {
9
+ const envPath = getHomeEnvPath(minicodeHome);
10
+ try {
11
+ const existing = await readFile(envPath, "utf8");
12
+ return dotenv.parse(existing);
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ function formatEnvValue(value) {
19
+ return value;
20
+ }
21
+ export async function upsertHomeEnvValues(options) {
22
+ const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
23
+ const envPath = getHomeEnvPath(minicodeHome);
24
+ await mkdir(path.dirname(envPath), { recursive: true });
25
+ let existing = "";
26
+ try {
27
+ existing = await readFile(envPath, "utf8");
28
+ }
29
+ catch {
30
+ existing = "";
31
+ }
32
+ const pending = new Map(Object.entries(options.values));
33
+ const managedKeys = new Set(pending.keys());
34
+ const assignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
35
+ const normalizedLines = existing === ""
36
+ ? []
37
+ : existing.split(/\r?\n/);
38
+ const nextLines = [];
39
+ const seen = new Set();
40
+ for (const line of normalizedLines) {
41
+ const match = line.match(assignmentPattern);
42
+ if (!match) {
43
+ nextLines.push(line);
44
+ continue;
45
+ }
46
+ const key = match[1];
47
+ if (!managedKeys.has(key)) {
48
+ nextLines.push(line);
49
+ continue;
50
+ }
51
+ if (seen.has(key)) {
52
+ continue;
53
+ }
54
+ const nextValue = pending.get(key);
55
+ if (nextValue !== null) {
56
+ nextLines.push(`${key}=${formatEnvValue(nextValue)}`);
57
+ }
58
+ seen.add(key);
59
+ pending.delete(key);
60
+ }
61
+ const pendingEntries = [...pending.entries()].filter((entry) => entry[1] !== null);
62
+ if (pendingEntries.length > 0 && nextLines.length > 0 && nextLines[nextLines.length - 1] !== "") {
63
+ nextLines.push("");
64
+ }
65
+ for (const [key, value] of pendingEntries) {
66
+ nextLines.push(`${key}=${formatEnvValue(value)}`);
67
+ }
68
+ const fileContent = `${nextLines.join("\n").replace(/\n+$/, "")}\n`;
69
+ await writeFile(envPath, fileContent, "utf8");
70
+ return {
71
+ path: envPath,
72
+ updatedKeys: Object.keys(options.values),
73
+ };
74
+ }
@@ -5,7 +5,17 @@
5
5
  * over tool-call instrumentation and trace capture.
6
6
  */
7
7
  import { execSync } from "node:child_process";
8
+ import { cp, mkdtemp, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import path from "node:path";
8
11
  import { CodingAgent, Session, ToolRegistry, } from "@minicode/agent-sdk";
12
+ const COPY_SKIP_NAMES = new Set([
13
+ ".git",
14
+ "node_modules",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ ]);
9
19
  function getGitCommitSha() {
10
20
  try {
11
21
  return execSync("git rev-parse HEAD", { encoding: "utf8" }).trim();
@@ -19,73 +29,146 @@ const STRUCTURAL_TOOLS = new Set([
19
29
  "find_references",
20
30
  "get_dependencies",
21
31
  "search_code_map",
32
+ "find_path",
22
33
  ]);
34
+ function sanitizeTaskId(taskId) {
35
+ return taskId.replace(/[^a-z0-9-_]+/gi, "-");
36
+ }
37
+ function resolveSourceWorkspaceRoot(task, options) {
38
+ if (!task.workspaceRoot) {
39
+ return path.resolve(options.config.workspaceRoot);
40
+ }
41
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
42
+ return path.resolve(repoRoot, task.workspaceRoot);
43
+ }
44
+ function shouldCopyPath(src) {
45
+ const name = path.basename(src);
46
+ return !COPY_SKIP_NAMES.has(name);
47
+ }
48
+ async function prepareTaskWorkspace(task, options) {
49
+ const sourceWorkspaceRoot = resolveSourceWorkspaceRoot(task, options);
50
+ if (options.isolateWorkspace === false) {
51
+ return {
52
+ sourceWorkspaceRoot,
53
+ workspaceRoot: sourceWorkspaceRoot,
54
+ cleanup: async () => { },
55
+ };
56
+ }
57
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "minicode-benchmark-"));
58
+ const isolatedWorkspaceRoot = path.join(tempRoot, sanitizeTaskId(task.id));
59
+ await cp(sourceWorkspaceRoot, isolatedWorkspaceRoot, {
60
+ recursive: true,
61
+ filter: shouldCopyPath,
62
+ });
63
+ return {
64
+ sourceWorkspaceRoot,
65
+ workspaceRoot: isolatedWorkspaceRoot,
66
+ cleanup: async () => {
67
+ await rm(tempRoot, { recursive: true, force: true });
68
+ },
69
+ };
70
+ }
71
+ function getTrackedSymbolNames(toolName, input) {
72
+ if (toolName === "find_path") {
73
+ const names = [input.from, input.to]
74
+ .filter((value) => typeof value === "string" && value.length > 0);
75
+ return [...new Set(names)];
76
+ }
77
+ const name = input.symbol ?? input.symbolName ?? input.name ?? input.query;
78
+ return typeof name === "string" && name.length > 0 ? [name] : [];
79
+ }
80
+ function trackStructuralFileReads(toolName, projectIndex, input, filesRead) {
81
+ if (!projectIndex || !STRUCTURAL_TOOLS.has(toolName)) {
82
+ return;
83
+ }
84
+ for (const symbolName of getTrackedSymbolNames(toolName, input)) {
85
+ const symbol = projectIndex.getSymbol(symbolName);
86
+ if (symbol) {
87
+ filesRead.add(symbol.filePath);
88
+ }
89
+ }
90
+ }
23
91
  /**
24
92
  * Run a single benchmark task and return the captured trace.
25
93
  */
26
94
  export async function runBenchmarkTask(task, options) {
95
+ const workspace = await prepareTaskWorkspace(task, options);
27
96
  const captured = [];
28
97
  const filesRead = new Set();
29
98
  const symbolsQueried = new Set();
30
- // Wrap each tool to capture calls
31
- const instrumentedTools = options.tools.map((tool) => ({
32
- ...tool,
33
- execute: async (input) => {
34
- const start = performance.now();
35
- const output = await tool.execute(input);
36
- const durationMs = performance.now() - start;
37
- captured.push({
38
- name: tool.name,
39
- input,
40
- output: output.length > 2000 ? output.slice(0, 2000) + "…[truncated]" : output,
41
- durationMs,
42
- });
43
- // Track files read
44
- if (tool.name === "read_file" || tool.name === "read_symbol") {
45
- const filePath = input.path ?? input.file_path ?? input.filePath;
46
- if (typeof filePath === "string")
47
- filesRead.add(filePath);
48
- }
49
- // Track symbol queries
50
- if (STRUCTURAL_TOOLS.has(tool.name)) {
51
- const sym = input.symbol ?? input.symbolName ?? input.name ?? input.query;
52
- if (typeof sym === "string")
53
- symbolsQueried.add(sym);
54
- }
55
- return output;
56
- },
57
- }));
58
- const registry = new ToolRegistry(instrumentedTools);
59
- const session = new Session();
60
- const agent = new CodingAgent({
61
- config: options.config,
62
- modelClient: options.modelClient,
63
- toolRegistry: registry,
64
- session,
65
- });
66
- const startedAt = new Date().toISOString();
67
- const start = performance.now();
68
- const { text, usage } = await agent.runTurn(task.prompt);
69
- const durationMs = performance.now() - start;
70
- const trace = {
71
- taskId: task.id,
72
- model: options.config.model,
73
- variant: options.variant,
74
- commitSha: getGitCommitSha(),
75
- response: text,
76
- toolCalls: captured,
77
- filesRead: [...filesRead],
78
- symbolsQueried: [...symbolsQueried],
79
- usage: {
80
- inputTokens: usage?.inputTokens ?? 0,
81
- outputTokens: usage?.outputTokens ?? 0,
82
- totalTokens: (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
83
- },
84
- durationMs,
85
- startedAt,
86
- };
87
- options.onTaskComplete?.(task.id, trace);
88
- return trace;
99
+ try {
100
+ const taskConfig = {
101
+ ...options.config,
102
+ workspaceRoot: workspace.workspaceRoot,
103
+ };
104
+ const toolset = options.createToolset
105
+ ? await options.createToolset(taskConfig, task)
106
+ : { tools: options.tools ?? [] };
107
+ // Wrap each tool to capture calls
108
+ const instrumentedTools = toolset.tools.map((tool) => ({
109
+ ...tool,
110
+ execute: async (input) => {
111
+ const start = performance.now();
112
+ const output = await tool.execute(input);
113
+ const durationMs = performance.now() - start;
114
+ captured.push({
115
+ name: tool.name,
116
+ input,
117
+ output: output.length > 2000 ? output.slice(0, 2000) + "…[truncated]" : output,
118
+ durationMs,
119
+ });
120
+ if (tool.name === "read_file") {
121
+ const filePath = input.path ?? input.file_path ?? input.filePath;
122
+ if (typeof filePath === "string") {
123
+ filesRead.add(filePath);
124
+ }
125
+ }
126
+ trackStructuralFileReads(tool.name, toolset.projectIndex, input, filesRead);
127
+ if (STRUCTURAL_TOOLS.has(tool.name)) {
128
+ for (const symbolName of getTrackedSymbolNames(tool.name, input)) {
129
+ symbolsQueried.add(symbolName);
130
+ }
131
+ }
132
+ return output;
133
+ },
134
+ }));
135
+ const registry = new ToolRegistry(instrumentedTools);
136
+ const session = new Session();
137
+ const agent = new CodingAgent({
138
+ config: taskConfig,
139
+ modelClient: options.modelClient,
140
+ toolRegistry: registry,
141
+ session,
142
+ });
143
+ const startedAt = new Date().toISOString();
144
+ const start = performance.now();
145
+ const { text, usage } = await agent.runTurn(task.prompt);
146
+ const durationMs = performance.now() - start;
147
+ const trace = {
148
+ taskId: task.id,
149
+ model: taskConfig.model,
150
+ variant: options.variant,
151
+ commitSha: getGitCommitSha(),
152
+ sourceWorkspaceRoot: workspace.sourceWorkspaceRoot,
153
+ workspaceRoot: workspace.workspaceRoot,
154
+ response: text,
155
+ toolCalls: captured,
156
+ filesRead: [...filesRead],
157
+ symbolsQueried: [...symbolsQueried],
158
+ usage: {
159
+ inputTokens: usage?.inputTokens ?? 0,
160
+ outputTokens: usage?.outputTokens ?? 0,
161
+ totalTokens: (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
162
+ },
163
+ durationMs,
164
+ startedAt,
165
+ };
166
+ options.onTaskComplete?.(task.id, trace);
167
+ return trace;
168
+ }
169
+ finally {
170
+ await workspace.cleanup();
171
+ }
89
172
  }
90
173
  /**
91
174
  * Run all provided benchmark tasks sequentially.
@@ -1,5 +1,5 @@
1
1
  import { formatConfigForDisplay, MINICODE_HOME, resolveConfigEnv } from "../agent/config.js";
2
- import { formatPersistedConfigValue, getEditableConfigDefinition, getEffectiveEditableConfigValue, isEditableConfigKey, listEditableConfigDefinitions, loadPersistedConfig, setPersistedConfigValue, unsetPersistedConfigValue, } from "../agent/editable-config.js";
2
+ import { buildStructuredConfigPayload, formatPersistedConfigValue, getEditableConfigDefinition, getEffectiveEditableConfigValue, isEditableConfigKey, listEditableConfigDefinitions, setPersistedConfigValue, unsetPersistedConfigValue, } from "../agent/editable-config.js";
3
3
  function renderUsage() {
4
4
  return [
5
5
  'Usage:',
@@ -12,7 +12,7 @@ function renderUsage() {
12
12
  }
13
13
  function renderEditableKeys() {
14
14
  const lines = [
15
- "Editable config keys (persisted in ~/.minicode/agent.config.json; environment variables take precedence):",
15
+ "Editable config keys (persisted in ~/.minicode/.env; exported shell environment variables take precedence):",
16
16
  ];
17
17
  for (const definition of listEditableConfigDefinitions()) {
18
18
  const valueHint = definition.type === "enum"
@@ -21,7 +21,7 @@ function renderEditableKeys() {
21
21
  lines.push(` ${definition.key} ${valueHint} — ${definition.description} (env: ${definition.envVar})`);
22
22
  }
23
23
  lines.push("");
24
- lines.push('Use "/config set <key> <value>" to update your global config.');
24
+ lines.push('Use "/config set <key> <value>" to update ~/.minicode/.env.');
25
25
  lines.push("Secrets like API keys stay env-only for now.");
26
26
  return lines.join("\n");
27
27
  }
@@ -31,14 +31,16 @@ async function renderConfigValue(key, context) {
31
31
  }
32
32
  const minicodeHome = context.minicodeHome ?? MINICODE_HOME;
33
33
  const definition = getEditableConfigDefinition(key);
34
- const persisted = await loadPersistedConfig(minicodeHome);
35
- const env = await resolveConfigEnv({ minicodeHome });
36
- const envValue = env.values[definition.envVar];
34
+ const payload = await buildStructuredConfigPayload(context.config, minicodeHome);
35
+ const entry = payload.entries.find((item) => item.key === key);
36
+ if (!entry) {
37
+ return `Unknown editable config key "${key}".\n\n${renderEditableKeys()}`;
38
+ }
37
39
  return [
38
40
  `${definition.key}`,
39
41
  ` effective: ${getEffectiveEditableConfigValue(context.config, key)}`,
40
- ` config file: ${formatPersistedConfigValue(persisted[definition.fileKey])}`,
41
- ` env override (${definition.envVar}): ${formatPersistedConfigValue(envValue)}`,
42
+ ` saved in ~/.minicode/.env: ${formatPersistedConfigValue(entry.persistedValue)}`,
43
+ ` exported env override (${definition.envVar}): ${formatPersistedConfigValue(entry.envValue)}`,
42
44
  ].join("\n");
43
45
  }
44
46
  async function persistConfigValue(key, rawValue, context) {
@@ -59,8 +61,8 @@ async function persistConfigValue(key, rawValue, context) {
59
61
  `File: ${result.path}`,
60
62
  "Restart minicode to pick up persisted config changes in a new session.",
61
63
  ];
62
- if (env.values[definition.envVar] !== undefined) {
63
- lines.push(`Note: ${definition.envVar} is currently set and will override this persisted value until it is unset.`);
64
+ if (env.sources[definition.envVar] === "process") {
65
+ lines.push(`Note: ${definition.envVar} is currently exported in your shell and will override this saved value until it is unset.`);
64
66
  }
65
67
  return lines.join("\n");
66
68
  }
@@ -82,11 +84,11 @@ async function removeConfigValue(key, context) {
82
84
  });
83
85
  const lines = [
84
86
  `Removed persisted value for "${key}".`,
85
- `File: ${minicodeHome}/agent.config.json`,
87
+ `File: ${minicodeHome}/.env`,
86
88
  "Restart minicode to ensure the updated config is applied in a new session.",
87
89
  ];
88
- if (env.values[definition.envVar] !== undefined) {
89
- lines.push(`Note: ${definition.envVar} is still set in the environment, so the effective value may remain unchanged.`);
90
+ if (env.sources[definition.envVar] === "process") {
91
+ lines.push(`Note: ${definition.envVar} is still exported in your shell, so the effective value may remain unchanged.`);
90
92
  }
91
93
  return lines.join("\n");
92
94
  }