@testchimp/cli 0.1.72 → 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.
- package/dist/chimphands/run.js +124 -23
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
//
|
|
2391
|
-
//
|
|
2392
|
-
|
|
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",
|
|
2452
|
+
postEvent(ROLE_STATUS, "Turn stopped", waitingOpts);
|
|
2445
2453
|
}
|
|
2446
2454
|
else {
|
|
2447
|
-
postEvent(ROLE_STATUS, "Waiting for user input",
|
|
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([
|
|
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",
|
|
@@ -2682,11 +2763,31 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
|
|
|
2682
2763
|
}
|
|
2683
2764
|
};
|
|
2684
2765
|
try {
|
|
2766
|
+
// Prefer JSON for workspace-scoped APIs — OpenCode SPA fallback is text/html.
|
|
2767
|
+
if (opencodeProxyNeedsDirectory(pathPart) && !headers.Accept && !headers.accept) {
|
|
2768
|
+
headers.Accept = "application/json";
|
|
2769
|
+
}
|
|
2685
2770
|
const upstream = await fetch(target, init);
|
|
2686
2771
|
const respHeaders = {};
|
|
2687
2772
|
upstream.headers.forEach((v, k) => {
|
|
2688
2773
|
respHeaders[k] = v;
|
|
2689
2774
|
});
|
|
2775
|
+
const contentType = String(respHeaders["content-type"] || respHeaders["Content-Type"] || "").toLowerCase();
|
|
2776
|
+
// Root-cause: never tunnel OpenCode SPA HTML as a successful VCS/API response.
|
|
2777
|
+
if (opencodeProxyNeedsDirectory(pathPart) &&
|
|
2778
|
+
contentType.includes("text/html")) {
|
|
2779
|
+
const msg = `OpenCode returned HTML SPA shell for ${pathPart} ` +
|
|
2780
|
+
"(expected JSON API; check directory routing / API path)";
|
|
2781
|
+
console.error(`ChimpHands ${msg}`);
|
|
2782
|
+
send({
|
|
2783
|
+
type: "http_response",
|
|
2784
|
+
requestId,
|
|
2785
|
+
status: 502,
|
|
2786
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
2787
|
+
bodyBase64: Buffer.from(msg, "utf8").toString("base64"),
|
|
2788
|
+
});
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2690
2791
|
send({
|
|
2691
2792
|
type: "http_response_start",
|
|
2692
2793
|
requestId,
|
package/package.json
CHANGED