@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.
- package/dist/chimphands/run.js +96 -12
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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