topchester-ai 0.65.0 → 0.67.0

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/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runTopchesterCli } from "./cli-2v1iJ0YF.mjs";
2
+ import { t as runTopchesterCli } from "./cli-DfgLjXLl.mjs";
3
3
  //#region src/bin.ts
4
4
  await runTopchesterCli();
5
5
  //#endregion
@@ -9,8 +9,9 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
9
9
  import { generateText, stepCountIs, streamText, tool } from "ai";
10
10
  import { ZodError, z } from "zod";
11
11
  import { execFile, spawn } from "node:child_process";
12
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
12
+ import { accessSync, constants, createReadStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
13
13
  import { createHash, randomUUID } from "node:crypto";
14
+ import { TextDecoder as TextDecoder$1 } from "node:util";
14
15
  import { parseDocument } from "yaml";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import pino from "pino";
@@ -21,6 +22,13 @@ import { diffWords } from "diff";
21
22
  import { highlight, supportsLanguage } from "cli-highlight";
22
23
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
24
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
25
+ //#region src/agent/benchmark-profile.ts
26
+ const BENCHMARK_PROFILES = ["terminal-bench"];
27
+ function parseBenchmarkProfile(value) {
28
+ if (BENCHMARK_PROFILES.includes(value)) return value;
29
+ throw new Error(`Unsupported benchmark profile '${value}'. Supported profiles: ${BENCHMARK_PROFILES.join(", ")}.`);
30
+ }
31
+ //#endregion
24
32
  //#region src/auth/codex.ts
25
33
  const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
26
34
  const CODEX_ISSUER = "https://auth.openai.com";
@@ -638,9 +646,10 @@ async function validateBashPolicy(args, context) {
638
646
  if (!cwd.allowed) return reject(command, cwd.reason, false);
639
647
  const deniedRule = findMatchingBashRule(command, context.permissions?.deny ?? []);
640
648
  if (deniedRule) return reject(command, `bash policy rejected '${command}' because it matches deny rule '${deniedRule}'.`, false);
649
+ const shell = resolveBashShell(context.permissions?.shell);
650
+ if (context.benchmarkProfile === "terminal-bench") return allow(command, cwd.path, realWorkspaceRoot, shell, "benchmark_terminal", "terminal-bench");
641
651
  const destructive = getDestructiveReason(command);
642
652
  if (destructive) return reject(command, `bash policy rejected '${command}' because it looks destructive: ${destructive}.`, false);
643
- const shell = resolveBashShell(context.permissions?.shell);
644
653
  const allowedExactRule = findExactBashRule(command, context.permissions?.allowExact ?? []);
645
654
  if (allowedExactRule) return allow(command, cwd.path, realWorkspaceRoot, shell, "allow_exact", allowedExactRule);
646
655
  const approvedRule = findExactBashRule(command, context.approvedCommands ?? []);
@@ -668,7 +677,7 @@ function getBashApprovalCandidates(command) {
668
677
  };
669
678
  }
670
679
  function allow(command, cwd, workspaceRoot, shell, kind, matchedRule) {
671
- const reason = kind === "allow_exact" ? `bash exact command allowed by '${matchedRule}'` : kind === "allow_prefix" ? `bash command allowed by prefix '${matchedRule}'` : `bash exact command approved by '${matchedRule}'`;
680
+ const reason = kind === "allow_exact" ? `bash exact command allowed by '${matchedRule}'` : kind === "benchmark_terminal" ? `bash command allowed by benchmark profile '${matchedRule}'` : kind === "allow_prefix" ? `bash command allowed by prefix '${matchedRule}'` : `bash exact command approved by '${matchedRule}'`;
672
681
  return {
673
682
  allowed: true,
674
683
  reason,
@@ -758,14 +767,16 @@ const bashTool = defineTool({
758
767
  pathEnv: context.pathEnv,
759
768
  abortSignal: context.abortSignal,
760
769
  permissions: context.config?.tools?.bash,
761
- approvedCommands: context.bashApprovals?.allowExactCommands
770
+ approvedCommands: context.bashApprovals?.allowExactCommands,
771
+ benchmarkProfile: context.benchmarkProfile
762
772
  })
763
773
  });
764
774
  async function runBashCommand(workspaceRoot, args, options = {}) {
765
775
  const decision = await validateBashPolicy(args, {
766
776
  workspaceRoot,
767
777
  permissions: options.permissions,
768
- approvedCommands: options.approvedCommands
778
+ approvedCommands: options.approvedCommands,
779
+ benchmarkProfile: options.benchmarkProfile
769
780
  });
770
781
  if (!decision.allowed) throw new Error(decision.reason);
771
782
  const result = await runProcess({
@@ -1843,7 +1854,7 @@ async function deleteWorkspaceFile(workspaceRoot, path) {
1843
1854
  const finishTaskTool = defineTool({
1844
1855
  name: "finish_task",
1845
1856
  description: "Finish the current task only after the requested work has actually been completed with tools. Do not use this to claim edits, file reads, or validation that did not happen.",
1846
- prompt: "finish_task: complete the task with a brief final response only after tool results prove the work is done. In benchmark or require-finish mode, this is the only valid terminal action; normal assistant messages are progress notes and do not finish the task. For implementation tasks, do not call finish_task until source files were changed by edit_file, write_file, apply_patch, or another mutating tool, unless no code change is truly required. Example: {\"tool\":\"finish_task\",\"args\":{\"final_response\":\"Changed src/foo.ts and ran pnpm test foo.test.ts.\",\"files_changed\":[\"src/foo.ts\"],\"validation\":[\"pnpm test foo.test.ts\"],\"remaining_issues\":[]}}",
1857
+ prompt: "finish_task: complete the task with a brief final response only after tool results prove the work is done. In benchmark or require-finish mode, this is the only valid terminal action; normal assistant messages are progress notes and do not finish the task, and remaining_issues must be empty. For implementation tasks, do not call finish_task until source files were changed by edit_file, write_file, apply_patch, or another mutating tool, unless no code change is truly required. Example: {\"tool\":\"finish_task\",\"args\":{\"final_response\":\"Changed src/foo.ts and ran pnpm test foo.test.ts.\",\"files_changed\":[\"src/foo.ts\"],\"validation\":[\"pnpm test foo.test.ts\"],\"remaining_issues\":[]}}",
1847
1858
  argsSchema: z.object({
1848
1859
  final_response: z.string().describe("Brief final response for the user."),
1849
1860
  files_changed: z.array(z.string()).optional().describe("Workspace-relative files actually changed by successful edit tools."),
@@ -4097,6 +4108,10 @@ const planTodoTool = defineTool({
4097
4108
  };
4098
4109
  }
4099
4110
  });
4111
+ //#endregion
4112
+ //#region src/agent/tools/read-file.ts
4113
+ const MAX_UTF8_READ_BYTES = 512 * 1024;
4114
+ const SAMPLE_BYTES = 256;
4100
4115
  const readFileTool = defineTool({
4101
4116
  name: "read_file",
4102
4117
  description: "Read a UTF-8 file inside the workspace.",
@@ -4123,15 +4138,94 @@ async function readWorkspaceFile(workspaceRoot, path) {
4123
4138
  const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
4124
4139
  const relativePath = relative(resolvedWorkspace, resolvedPath);
4125
4140
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error(`read_file can only read files inside the workspace: ${path}`);
4141
+ const fileStat = await stat(resolvedPath);
4142
+ if (!fileStat.isFile()) throw new Error(`read_file can only read regular files: ${path}`);
4143
+ if (fileStat.size > MAX_UTF8_READ_BYTES) {
4144
+ const [sample, hash] = await Promise.all([readFileSample(resolvedPath, SAMPLE_BYTES), hashFile$2(resolvedPath)]);
4145
+ return {
4146
+ tool: "read_file",
4147
+ path: relativePath || ".",
4148
+ content: formatSkippedReadSummary({
4149
+ reason: "too_large",
4150
+ relativePath: relativePath || ".",
4151
+ bytes: fileStat.size,
4152
+ sample
4153
+ }),
4154
+ hash,
4155
+ skipped: "too_large",
4156
+ bytes: fileStat.size,
4157
+ warning: `Skipped reading ${relativePath || "."}: file is ${fileStat.size} bytes, above the ${MAX_UTF8_READ_BYTES} byte read_file limit.`
4158
+ };
4159
+ }
4126
4160
  const bytes = await readFile(resolvedPath);
4161
+ const hash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
4162
+ if (looksBinary$1(bytes) || !isValidUtf8(bytes)) {
4163
+ const sample = bytes.subarray(0, SAMPLE_BYTES);
4164
+ return {
4165
+ tool: "read_file",
4166
+ path: relativePath || ".",
4167
+ content: formatSkippedReadSummary({
4168
+ reason: "binary",
4169
+ relativePath: relativePath || ".",
4170
+ bytes: fileStat.size,
4171
+ sample
4172
+ }),
4173
+ hash,
4174
+ skipped: "binary",
4175
+ bytes: fileStat.size,
4176
+ warning: `Skipped reading ${relativePath || "."}: file appears to be binary or non-UTF-8.`
4177
+ };
4178
+ }
4127
4179
  const content = bytes.toString("utf8");
4128
4180
  return {
4129
4181
  tool: "read_file",
4130
4182
  path: relativePath || ".",
4131
4183
  content,
4132
- hash: `sha256:${createHash("sha256").update(bytes).digest("hex")}`
4184
+ hash
4133
4185
  };
4134
4186
  }
4187
+ function looksBinary$1(bytes) {
4188
+ return bytes.subarray(0, Math.min(bytes.length, SAMPLE_BYTES)).includes(0);
4189
+ }
4190
+ function isValidUtf8(bytes) {
4191
+ try {
4192
+ new TextDecoder$1("utf-8", { fatal: true }).decode(bytes);
4193
+ return true;
4194
+ } catch {
4195
+ return false;
4196
+ }
4197
+ }
4198
+ function formatSkippedReadSummary(options) {
4199
+ const reason = options.reason === "binary" ? "read_file did not return file contents because this file appears to be binary or non-UTF-8." : `read_file did not return file contents because this file is ${options.bytes} bytes, above the ${MAX_UTF8_READ_BYTES} byte limit.`;
4200
+ const sampleHex = options.sample.length > 0 ? options.sample.toString("hex").match(/.{1,2}/g)?.join(" ") : "";
4201
+ return [
4202
+ reason,
4203
+ `path: ${options.relativePath}`,
4204
+ `bytes: ${options.bytes}`,
4205
+ sampleHex ? `first_${options.sample.length}_bytes_hex: ${sampleHex}` : "first_bytes_hex: <empty>",
4206
+ "Use shell inspection tools such as `file`, `xxd`, `readelf`, `objdump`, `strings`, or a focused parser instead of reading the whole file into context."
4207
+ ].join("\n");
4208
+ }
4209
+ async function readFileSample(path, maxBytes) {
4210
+ const handle = await open(path, "r");
4211
+ try {
4212
+ const sample = Buffer.alloc(maxBytes);
4213
+ const { bytesRead } = await handle.read(sample, 0, maxBytes, 0);
4214
+ return sample.subarray(0, bytesRead);
4215
+ } finally {
4216
+ await handle.close();
4217
+ }
4218
+ }
4219
+ async function hashFile$2(path) {
4220
+ const hash = createHash("sha256");
4221
+ await new Promise((resolvePromise, reject) => {
4222
+ const stream = createReadStream(path);
4223
+ stream.on("data", (chunk) => hash.update(chunk));
4224
+ stream.on("error", reject);
4225
+ stream.on("end", resolvePromise);
4226
+ });
4227
+ return `sha256:${hash.digest("hex")}`;
4228
+ }
4135
4229
  //#endregion
4136
4230
  //#region src/agent/tools/command-policy.ts
4137
4231
  const PACKAGE_MANAGERS = new Set([
@@ -12054,6 +12148,7 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
12054
12148
  subagents: options.subagents,
12055
12149
  projectInstructions: options.projectInstructions,
12056
12150
  currentUserMessage: options.currentUserMessage,
12151
+ benchmarkProfile: options.benchmarkProfile,
12057
12152
  eventSink: options.eventSink,
12058
12153
  abortSignal: options.abortSignal,
12059
12154
  toolCallId: options.toolCallId
@@ -13492,6 +13587,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13492
13587
  subagents,
13493
13588
  projectInstructions: projectInstructionToolState,
13494
13589
  currentUserMessage: message,
13590
+ benchmarkProfile: options.benchmarkProfile,
13495
13591
  abortSignal,
13496
13592
  toolCallId: entry.toolCallId,
13497
13593
  toolCatalog,
@@ -13510,7 +13606,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13510
13606
  const call = taskCalls[index];
13511
13607
  const toolResult = taskResults[index];
13512
13608
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
13513
- if (isSourceMutationResult(toolResult)) hasSourceMutation = true;
13609
+ if (isSourceMutationResult(toolResult) || isBenchmarkMutationResult(toolResult, options.benchmarkProfile)) hasSourceMutation = true;
13514
13610
  yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult), getToolCallDisplayDiff(toolResult));
13515
13611
  const postHookRun = this.startPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
13516
13612
  for await (const event of postHookRun.statusEvents) yield event;
@@ -13562,6 +13658,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13562
13658
  subagents,
13563
13659
  projectInstructions: projectInstructionToolState,
13564
13660
  currentUserMessage: message,
13661
+ benchmarkProfile: options.benchmarkProfile,
13565
13662
  abortSignal,
13566
13663
  toolCallId: entry.toolCallId,
13567
13664
  toolCatalog
@@ -13574,7 +13671,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13574
13671
  const call = parallelCalls[index];
13575
13672
  const toolResult = parallelResults[index];
13576
13673
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
13577
- if (isSourceMutationResult(toolResult)) hasSourceMutation = true;
13674
+ if (isSourceMutationResult(toolResult) || isBenchmarkMutationResult(toolResult, options.benchmarkProfile)) hasSourceMutation = true;
13578
13675
  yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult), getToolCallDisplayDiff(toolResult));
13579
13676
  const postHookRun = this.startPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
13580
13677
  for await (const event of postHookRun.statusEvents) yield event;
@@ -13639,6 +13736,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13639
13736
  subagents,
13640
13737
  projectInstructions: projectInstructionToolState,
13641
13738
  currentUserMessage: message,
13739
+ benchmarkProfile: options.benchmarkProfile,
13642
13740
  abortSignal,
13643
13741
  toolCallId: toolCall.id,
13644
13742
  toolCatalog,
@@ -13657,11 +13755,12 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13657
13755
  implementationTask,
13658
13756
  hasSourceMutation,
13659
13757
  hasOpenPlan: planTodoMode === "normal" && hasOpenTaskPlan(this.taskPlan.get()),
13660
- canUseSourceEditTool: canStillUseSourceEditTool(permissions)
13758
+ canUseSourceEditTool: canStillUseSourceEditTool(permissions),
13759
+ requireFinishTask
13661
13760
  });
13662
13761
  if (finishError) toolResult = createToolErrorResult(executableToolCall.tool, finishError);
13663
13762
  }
13664
- if (isSourceMutationResult(toolResult)) hasSourceMutation = true;
13763
+ if (isSourceMutationResult(toolResult) || isBenchmarkMutationResult(toolResult, options.benchmarkProfile)) hasSourceMutation = true;
13665
13764
  yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult), getToolCallDisplayDiff(toolResult));
13666
13765
  if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") {
13667
13766
  planTodoUpdates += 1;
@@ -13870,7 +13969,8 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13870
13969
  const decision = await validateBashPolicy(parsed.data, {
13871
13970
  workspaceRoot: this.context.workspaceRoot,
13872
13971
  permissions: this.context.config.tools?.bash,
13873
- approvedCommands
13972
+ approvedCommands,
13973
+ benchmarkProfile: options.benchmarkProfile
13874
13974
  });
13875
13975
  if (decision.allowed) return {
13876
13976
  cancelled: false,
@@ -14071,6 +14171,14 @@ function getToolCallDisplayDiff(result) {
14071
14171
  function readMaxToolCallsPerTurn() {
14072
14172
  const raw = process.env[MAX_TOOL_CALLS_PER_TURN_ENV]?.trim();
14073
14173
  if (!raw) return DEFAULT_MAX_TOOL_CALLS_PER_TURN;
14174
+ if ([
14175
+ "0",
14176
+ "off",
14177
+ "false",
14178
+ "none",
14179
+ "unlimited",
14180
+ "disabled"
14181
+ ].includes(raw.toLowerCase())) return Number.POSITIVE_INFINITY;
14074
14182
  const parsed = Number(raw);
14075
14183
  if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_MAX_TOOL_CALLS_PER_TURN;
14076
14184
  return parsed;
@@ -14125,6 +14233,10 @@ function isSourceMutationResult(result) {
14125
14233
  if (result.tool === "apply_patch") return result.changedFiles.some(isSourcePath);
14126
14234
  return false;
14127
14235
  }
14236
+ function isBenchmarkMutationResult(result, benchmarkProfile) {
14237
+ if (benchmarkProfile !== "terminal-bench" || isToolErrorResult(result)) return false;
14238
+ return result.tool === "bash" && "workspaceMayHaveChanged" in result && result.workspaceMayHaveChanged === true;
14239
+ }
14128
14240
  function isSourcePath(path) {
14129
14241
  if (!path) return false;
14130
14242
  return !path.startsWith(".agents/") && !path.startsWith(".git/") && !path.startsWith("topchester-kb/");
@@ -14133,6 +14245,7 @@ function validateFinishTaskResult(result, state) {
14133
14245
  if (isToolErrorResult(result) || result.tool !== "finish_task") return;
14134
14246
  if (state.hasOpenPlan) return "finish_task rejected because the visible plan is still open. Close or update plan_todo first.";
14135
14247
  if (state.implementationTask && state.canUseSourceEditTool && !state.hasSourceMutation) return "finish_task rejected because this appears to be an implementation task but no successful source-file edit has occurred. Use edit_file, write_file, or apply_patch first, unless you can prove no code change is required.";
14248
+ if (state.requireFinishTask && result.remainingIssues.length > 0) return "finish_task rejected because benchmark mode cannot finish with known remaining issues. Continue implementing or validating until remaining_issues is empty.";
14136
14249
  }
14137
14250
  function isL1ContextDisabledByEnv() {
14138
14251
  const value = process.env.TOPCHESTER_DISABLE_L1_CONTEXT?.trim().toLowerCase();
@@ -15710,12 +15823,14 @@ async function executeRunCommand(context, options) {
15710
15823
  json: Boolean(options.json),
15711
15824
  outputJson: options.outputJson,
15712
15825
  timeoutMs,
15713
- dangerouslyAutoApprove: Boolean(options.dangerouslyAutoApprove)
15826
+ dangerouslyAutoApprove: Boolean(options.dangerouslyAutoApprove),
15827
+ benchmarkProfile: options.benchmarkProfile
15714
15828
  }, "run started");
15715
15829
  pushJson(jsonEvents, runId, session.sessionId, "run.started", {
15716
15830
  workspaceRoot: runContext.workspaceRoot,
15717
15831
  timeoutMs,
15718
- dangerouslyAutoApprove: Boolean(options.dangerouslyAutoApprove)
15832
+ dangerouslyAutoApprove: Boolean(options.dangerouslyAutoApprove),
15833
+ benchmarkProfile: options.benchmarkProfile
15719
15834
  });
15720
15835
  try {
15721
15836
  if (!options.resume) await persistStartupMessages(session, runContext);
@@ -15772,7 +15887,8 @@ async function executeRunCommand(context, options) {
15772
15887
  });
15773
15888
  for await (const event of runtime.submitMessageStream(conversation, options.prompt, abortController.signal, {
15774
15889
  session,
15775
- userApprovalMode: options.dangerouslyAutoApprove ? "auto_allow" : "interactive"
15890
+ userApprovalMode: options.dangerouslyAutoApprove ? "auto_allow" : "interactive",
15891
+ benchmarkProfile: options.benchmarkProfile
15776
15892
  })) await applyRuntimeEvent({
15777
15893
  event,
15778
15894
  session,
@@ -16228,7 +16344,7 @@ function createTopchesterProgram() {
16228
16344
  process.exitCode = 1;
16229
16345
  }
16230
16346
  });
16231
- program.command("run").description("run one prompt or slash command without opening the TUI").argument("<prompt...>", "prompt text or slash command").option("--model <model>", "override the agent.primary model for this run").option("--timeout <ms>", "timeout for the run in milliseconds", parsePositiveInteger).option("--json", "write JSONL run events to stdout").option("--output-json <path>", "write JSONL run events to a file").option("--dangerously-auto-approve", "auto-approve prompt-gated tool calls for this non-interactive run").action(async (promptParts, options) => {
16347
+ program.command("run").description("run one prompt or slash command without opening the TUI").argument("<prompt...>", "prompt text or slash command").option("--model <model>", "override the agent.primary model for this run").option("--timeout <ms>", "timeout for the run in milliseconds", parsePositiveInteger).option("--json", "write JSONL run events to stdout").option("--output-json <path>", "write JSONL run events to a file").option("--dangerously-auto-approve", "auto-approve prompt-gated tool calls for this non-interactive run").option("--benchmark-profile <profile>", "enable an explicit benchmark runtime profile", parseBenchmarkProfile).action(async (promptParts, options) => {
16232
16348
  const context = createContextFromOptions(program);
16233
16349
  const globalOptions = program.opts();
16234
16350
  try {
@@ -16239,7 +16355,8 @@ function createTopchesterProgram() {
16239
16355
  json: options.json,
16240
16356
  outputJson: options.outputJson,
16241
16357
  resume: globalOptions.resume,
16242
- dangerouslyAutoApprove: options.dangerouslyAutoApprove
16358
+ dangerouslyAutoApprove: options.dangerouslyAutoApprove,
16359
+ benchmarkProfile: options.benchmarkProfile
16243
16360
  });
16244
16361
  } catch (error) {
16245
16362
  console.error(formatStartupError(error));
@@ -16529,4 +16646,4 @@ function formatDryRunSyncStatus(status) {
16529
16646
  //#endregion
16530
16647
  export { runTopchesterCli as t };
16531
16648
 
16532
- //# sourceMappingURL=cli-2v1iJ0YF.mjs.map
16649
+ //# sourceMappingURL=cli-DfgLjXLl.mjs.map