@testchimp/cli 0.1.45 → 0.1.46

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.
@@ -33,6 +33,7 @@ const CHIMPHANDS_AGENT_PROMPT = `You are ChimpHands, TestChimp's coding agent. Y
33
33
  - This conversation uses ONE working branch and ONE pull request. Reuse them for all follow-up work in this chat.
34
34
  - If bootstrap lists a working branch, checkout that branch and push additional commits there — update the same PR.
35
35
  - Only create a NEW branch/PR when (a) no working branch exists yet for this conversation, or (b) the prior PR was merged/closed (verify with \`gh pr view\`).
36
+ - Commit and push on the session working branch after meaningful edit batches. The host also commits any dirty worktree before idle teardown — keep the branch pushed so the UI can show diffs from GitHub.
36
37
  - Branch names MUST start with \`testchimp-\` or \`chimphands-\`.
37
38
  - When creating a NEW working branch: create it, then IMMEDIATELY publish it with
38
39
  \`git push -u origin <branch>\` BEFORE calling report-branch. Users open the branch URL in the UI —
@@ -768,6 +769,7 @@ export async function runChimphands(opts) {
768
769
  stopInbound();
769
770
  stopTunnel();
770
771
  stopHeartbeat();
772
+ await commitAndPushDirtyWorktree("chimphands: commit before session idle/shutdown");
771
773
  await poster.flush();
772
774
  await snapshotExport();
773
775
  if (runtimeId) {
@@ -918,6 +920,69 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId) {
918
920
  stopped = true;
919
921
  };
920
922
  }
923
+ /** Commit+push dirty worktree on the session branch before idle/teardown (no default-branch writes). */
924
+ async function commitAndPushDirtyWorktree(message) {
925
+ const run = (args, env) => new Promise((resolve) => {
926
+ const child = spawn("git", args, {
927
+ stdio: ["ignore", "pipe", "pipe"],
928
+ env: env ? { ...process.env, ...env } : process.env,
929
+ });
930
+ let out = "";
931
+ let err = "";
932
+ child.stdout.on("data", (d) => {
933
+ out += d.toString();
934
+ });
935
+ child.stderr.on("data", (d) => {
936
+ err += d.toString();
937
+ });
938
+ child.on("close", (code) => resolve({ code: code ?? 1, out, err }));
939
+ });
940
+ try {
941
+ const branch = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
942
+ if (branch.code !== 0) {
943
+ console.error(`ChimpHands git rev-parse failed: ${branch.err || branch.out}`);
944
+ return;
945
+ }
946
+ const current = branch.out.trim();
947
+ if (!current || current === "HEAD" || /^(main|master)$/i.test(current)) {
948
+ console.error(`ChimpHands skip commit-before-idle: refusing branch "${current || "(unknown)"}"`);
949
+ return;
950
+ }
951
+ const status = await run(["status", "--porcelain"]);
952
+ if (status.code !== 0) {
953
+ console.error(`ChimpHands git status failed: ${status.err || status.out}`);
954
+ return;
955
+ }
956
+ if (!status.out.trim()) {
957
+ return;
958
+ }
959
+ const add = await run(["add", "-A"]);
960
+ if (add.code !== 0) {
961
+ console.error(`ChimpHands git add failed: ${add.err || add.out}`);
962
+ return;
963
+ }
964
+ const commitEnv = {
965
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "ChimpHands",
966
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || "chimphands@testchimp.io",
967
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "ChimpHands",
968
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "chimphands@testchimp.io",
969
+ };
970
+ const commit = await run(["-c", "user.name=ChimpHands", "-c", "user.email=chimphands@testchimp.io", "commit", "-m", message], commitEnv);
971
+ if (commit.code !== 0) {
972
+ console.error(`ChimpHands git commit: ${commit.err || commit.out}`);
973
+ return;
974
+ }
975
+ const push = await run(["push", "-u", "origin", "HEAD"]);
976
+ if (push.code !== 0) {
977
+ console.error(`ChimpHands git push failed: ${push.err || push.out}`);
978
+ return;
979
+ }
980
+ console.error(`ChimpHands committed and pushed dirty worktree on ${current} before shutdown`);
981
+ }
982
+ catch (err) {
983
+ console.error(`ChimpHands commit-before-idle failed: ${err instanceof Error ? err.message : String(err)}`);
984
+ }
985
+ }
921
986
  async function putOpencodeExport(backend, apiKey, sessionId, opencodeSessionId) {
922
987
  const exported = await new Promise((resolve, reject) => {
923
988
  const child = spawn("opencode", ["export", opencodeSessionId, "--sanitize"], {
@@ -1006,6 +1071,18 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
1006
1071
  return;
1007
1072
  socket.send(JSON.stringify(obj));
1008
1073
  };
1074
+ /** Keep each WS text frame small — Tomcat default max is 8KiB; GCLB is happier with modest frames. */
1075
+ const sendBodyChunk = (bytes) => {
1076
+ const MAX = 24 * 1024;
1077
+ for (let offset = 0; offset < bytes.length; offset += MAX) {
1078
+ const slice = bytes.subarray(offset, Math.min(offset + MAX, bytes.length));
1079
+ send({
1080
+ type: "http_response_chunk",
1081
+ requestId,
1082
+ bodyBase64: Buffer.from(slice).toString("base64"),
1083
+ });
1084
+ }
1085
+ };
1009
1086
  try {
1010
1087
  const upstream = await fetch(target, init);
1011
1088
  const respHeaders = {};
@@ -1026,22 +1103,14 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
1026
1103
  if (done)
1027
1104
  break;
1028
1105
  if (value && value.length) {
1029
- send({
1030
- type: "http_response_chunk",
1031
- requestId,
1032
- bodyBase64: Buffer.from(value).toString("base64"),
1033
- });
1106
+ sendBodyChunk(Buffer.from(value));
1034
1107
  }
1035
1108
  }
1036
1109
  }
1037
1110
  else {
1038
1111
  const buf = Buffer.from(await upstream.arrayBuffer());
1039
1112
  if (buf.length) {
1040
- send({
1041
- type: "http_response_chunk",
1042
- requestId,
1043
- bodyBase64: buf.toString("base64"),
1044
- });
1113
+ sendBodyChunk(buf);
1045
1114
  }
1046
1115
  }
1047
1116
  send({ type: "http_response_end", requestId });
@@ -1073,6 +1142,20 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
1073
1142
  socket.on("open", () => {
1074
1143
  backoffMs = 1000;
1075
1144
  console.error(`ChimpHands agent tunnel WS connected: ${tunnelUrl}`);
1145
+ // Application ping keeps LBs from idling out the tunnel (and proves liveness).
1146
+ const ping = () => {
1147
+ if (stopped || socket.readyState !== 1)
1148
+ return;
1149
+ try {
1150
+ socket.send(JSON.stringify({ type: "ping" }));
1151
+ }
1152
+ catch {
1153
+ /* ignore */
1154
+ }
1155
+ };
1156
+ ping();
1157
+ const pingTimer = setInterval(ping, 20_000);
1158
+ socket.once("close", () => clearInterval(pingTimer));
1076
1159
  });
1077
1160
  socket.on("message", (data) => {
1078
1161
  if (stopped)
@@ -1094,11 +1177,12 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
1094
1177
  console.error(`ChimpHands tunnel frame error: ${err instanceof Error ? err.message : String(err)}`);
1095
1178
  }
1096
1179
  });
1097
- socket.on("close", () => {
1180
+ socket.on("close", (code, reason) => {
1098
1181
  ws = null;
1099
1182
  if (stopped)
1100
1183
  return;
1101
- console.error(`ChimpHands agent tunnel WS closed; reconnecting in ${backoffMs}ms`);
1184
+ const why = reason?.toString?.() || "";
1185
+ console.error(`ChimpHands agent tunnel WS closed; code=${code} reason=${why || "(none)"} reconnecting in ${backoffMs}ms`);
1102
1186
  reconnectTimer = setTimeout(() => {
1103
1187
  void connect();
1104
1188
  }, backoffMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
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",