@testchimp/cli 0.1.73 → 0.1.76

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.
@@ -1048,8 +1048,10 @@ function isAssistantEchoOfSentPrompt(assistant, ...sentPrompts) {
1048
1048
  }
1049
1049
  return false;
1050
1050
  }
1051
+ /** First line of host-injected OpenCode prompts — UI filters on this marker. */
1052
+ const HOST_PROMPT_WRAPPER_MARKER = "[[[chimphands.host_prompt.v1]]]";
1051
1053
  function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl, baseBranch) {
1052
- const parts = [];
1054
+ const parts = [HOST_PROMPT_WRAPPER_MARKER];
1053
1055
  if (baseBranch?.trim()) {
1054
1056
  parts.push("## Base branch (parent — do NOT commit here)", `Base branch: \`${baseBranch.trim()}\``, "Create a `testchimp-*` branch from this parent before any file edits. PRs must target this base branch.", "");
1055
1057
  }
@@ -1062,11 +1064,11 @@ function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSes
1062
1064
  // user message and the UI shows host control text in chat.
1063
1065
  if (isNewOpencodeSession && conversationSummary.trim()) {
1064
1066
  parts.push(`Conversation so far:\n${conversationSummary.trim()}`, "", `Current task:\n${task}`);
1065
- return parts.filter(Boolean).join("\n");
1067
+ return parts.filter((p) => p !== "").join("\n");
1066
1068
  }
1067
- if (parts.length) {
1069
+ if (parts.length > 1) {
1068
1070
  parts.push(`Current task:\n${task}`);
1069
- return parts.filter(Boolean).join("\n");
1071
+ return parts.filter((p) => p !== "").join("\n");
1070
1072
  }
1071
1073
  return task;
1072
1074
  }
@@ -2387,10 +2389,20 @@ export async function runChimphands(opts) {
2387
2389
  if (result.opencodeSessionId) {
2388
2390
  opencodeSessionId = result.opencodeSessionId;
2389
2391
  }
2390
- // Turn-end durable reconcile into PG (idempotent messageIds). Mid-turn was
2391
- // ephemeral-only when UI attached; async runs get their transcript here.
2392
- // Also reconcile on failure so partial assistant/tool output is not lost.
2393
- if (attachUrl && opencodeSessionId) {
2392
+ // Checkpoint dirty worktree before unlocking the UI so Files (GitHub tip
2393
+ // of workingBranch) matches what the user sees when WAITING_USER lands.
2394
+ await commitAndPushDirtyWorktree("chimphands: commit after turn");
2395
+ if (attachUrl && opencodeSessionId && midTurnInjectedById.size) {
2396
+ try {
2397
+ await releaseUnansweredMidTurnMessages(attachUrl, opencodeSessionId, midTurnInjectedById, queue, wakePromptWaiter, midTurnSidecarAnswered);
2398
+ }
2399
+ catch (err) {
2400
+ console.error(`ChimpHands mid-turn steering release failed: ${err instanceof Error ? err.message : String(err)}`);
2401
+ }
2402
+ }
2403
+ const reconcileTurn = async () => {
2404
+ if (!attachUrl || !opencodeSessionId)
2405
+ return;
2394
2406
  try {
2395
2407
  await reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, (role, content, opts) => {
2396
2408
  turnPostEvent(role, content, {
@@ -2404,18 +2416,7 @@ export async function runChimphands(opts) {
2404
2416
  catch (err) {
2405
2417
  console.error(`ChimpHands turn-end reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
2406
2418
  }
2407
- }
2408
- // Always checkpoint dirty worktree after each turn so PR + Files pane
2409
- // (GitHub compare) stay current whether or not a browser UI is attached.
2410
- await commitAndPushDirtyWorktree("chimphands: commit after turn");
2411
- if (attachUrl && opencodeSessionId && midTurnInjectedById.size) {
2412
- try {
2413
- await releaseUnansweredMidTurnMessages(attachUrl, opencodeSessionId, midTurnInjectedById, queue, wakePromptWaiter, midTurnSidecarAnswered);
2414
- }
2415
- catch (err) {
2416
- console.error(`ChimpHands mid-turn steering release failed: ${err instanceof Error ? err.message : String(err)}`);
2417
- }
2418
- }
2419
+ };
2419
2420
  if (result.code !== 0 && !result.cancelled) {
2420
2421
  const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
2421
2422
  console.error(`ChimpHands OpenCode failed: ${errMsg}`);
@@ -2437,16 +2438,25 @@ export async function runChimphands(opts) {
2437
2438
  postEvent(ROLE_STATUS, errMsg, { status: STATUS_FAILED });
2438
2439
  await complete(STATUS_FAILED, errMsg);
2439
2440
  }
2441
+ // Persist partial transcript even on failure (after push / status report).
2442
+ await reconcileTurn();
2440
2443
  exitCode = result.code || 1;
2441
2444
  break;
2442
2445
  }
2446
+ const waitingOpts = {
2447
+ status: STATUS_WAITING_USER,
2448
+ ...(workingBranch ? { workingBranch } : {}),
2449
+ ...(pullRequestUrl ? { pullRequestUrl } : {}),
2450
+ };
2443
2451
  if (result.cancelled) {
2444
- postEvent(ROLE_STATUS, "Turn stopped", { status: STATUS_WAITING_USER });
2452
+ postEvent(ROLE_STATUS, "Turn stopped", waitingOpts);
2445
2453
  }
2446
2454
  else {
2447
- postEvent(ROLE_STATUS, "Waiting for user input", { status: STATUS_WAITING_USER });
2455
+ postEvent(ROLE_STATUS, "Waiting for user input", waitingOpts);
2448
2456
  }
2449
2457
  await poster.flush();
2458
+ // Durable reconcile after unlock so Interrupt→Send is not blocked on PG writes.
2459
+ await reconcileTurn();
2450
2460
  lastUserActivity = Date.now();
2451
2461
  idle = false;
2452
2462
  prompt = (await waitForNextPrompt()) || "";
@@ -2499,6 +2509,39 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId, onUiAttached) {
2499
2509
  stopped = true;
2500
2510
  };
2501
2511
  }
2512
+ /** Paths ChimpHands writes on the runner — never commit into the agent branch. */
2513
+ const CHIMPHANDS_COMMIT_EXCLUDE_PATHSPECS = [
2514
+ ":(exclude)opencode.json",
2515
+ ":(exclude)opencode-server.log",
2516
+ ":(exclude)chimphands-run.log",
2517
+ ":(exclude)remote-branches.txt",
2518
+ ":(exclude)remote-branch-names.txt",
2519
+ ":(exclude)gha-creds*",
2520
+ ":(exclude)**/gha-creds*",
2521
+ ":(exclude).chimphands",
2522
+ ":(exclude).chimphands/**",
2523
+ ":(exclude).agents",
2524
+ ":(exclude).agents/**",
2525
+ ];
2526
+ function isChimpHandsInternalCommitPath(filePath) {
2527
+ let normalized = filePath.replace(/\\/g, "/").toLowerCase();
2528
+ while (normalized.startsWith("./"))
2529
+ normalized = normalized.slice(2);
2530
+ const slash = normalized.lastIndexOf("/");
2531
+ const name = slash >= 0 ? normalized.slice(slash + 1) : normalized;
2532
+ if (name === "opencode.json" ||
2533
+ name === "opencode-server.log" ||
2534
+ name === "chimphands-run.log" ||
2535
+ name === "remote-branches.txt" ||
2536
+ name === "remote-branch-names.txt" ||
2537
+ name.startsWith("gha-creds")) {
2538
+ return true;
2539
+ }
2540
+ return (normalized === ".chimphands" ||
2541
+ normalized.startsWith(".chimphands/") ||
2542
+ normalized === ".agents" ||
2543
+ normalized.startsWith(".agents/"));
2544
+ }
2502
2545
  /** Commit+push dirty worktree on the session agent branch before idle/teardown. */
2503
2546
  async function commitAndPushDirtyWorktree(message) {
2504
2547
  const run = (args, env) => runGit(args, env);
@@ -2527,12 +2570,50 @@ async function commitAndPushDirtyWorktree(message) {
2527
2570
  if (!status.out.trim()) {
2528
2571
  return { ok: true };
2529
2572
  }
2530
- const add = await run(["add", "-A"]);
2573
+ const add = await run([
2574
+ "add",
2575
+ "-A",
2576
+ "--",
2577
+ ".",
2578
+ ...CHIMPHANDS_COMMIT_EXCLUDE_PATHSPECS,
2579
+ ]);
2531
2580
  if (add.code !== 0) {
2532
2581
  const error = add.err || add.out || "git add failed";
2533
2582
  console.error(`ChimpHands git add failed: ${error}`);
2534
2583
  return { ok: false, error };
2535
2584
  }
2585
+ // Drop already-tracked scaffolding that may still be staged from older commits.
2586
+ await run([
2587
+ "reset",
2588
+ "-q",
2589
+ "HEAD",
2590
+ "--",
2591
+ "opencode.json",
2592
+ "opencode-server.log",
2593
+ "chimphands-run.log",
2594
+ "remote-branches.txt",
2595
+ "remote-branch-names.txt",
2596
+ ".chimphands",
2597
+ ".agents",
2598
+ ]);
2599
+ const staged = await run(["diff", "--cached", "--name-only"]);
2600
+ if (staged.code !== 0) {
2601
+ const error = staged.err || staged.out || "git diff --cached failed";
2602
+ console.error(`ChimpHands git staged check failed: ${error}`);
2603
+ return { ok: false, error };
2604
+ }
2605
+ const stagedPaths = staged.out
2606
+ .split("\n")
2607
+ .map((line) => line.trim())
2608
+ .filter(Boolean);
2609
+ const internalStaged = stagedPaths.filter(isChimpHandsInternalCommitPath);
2610
+ if (internalStaged.length) {
2611
+ await run(["reset", "-q", "HEAD", "--", ...internalStaged]);
2612
+ }
2613
+ const keepStaged = stagedPaths.filter((p) => !isChimpHandsInternalCommitPath(p));
2614
+ if (!keepStaged.length) {
2615
+ return { ok: true };
2616
+ }
2536
2617
  const commitEnv = {
2537
2618
  GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "ChimpHands",
2538
2619
  GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || "chimphands@testchimp.io",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.73",
3
+ "version": "0.1.76",
4
4
  "description": "TestChimp CLI and MCP server — coverage, plans, EaaS, TrueCoverage, API operations (calls /api/mcp/*)",
5
5
  "type": "module",
6
6
  "main": "dist/bin/testchimp.js",