@testchimp/cli 0.1.76 → 0.1.77
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
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { mkdirSync, openSync, writeFileSync } from "node:fs";
|
|
8
8
|
import fs from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import http from "node:http";
|
|
11
12
|
import https from "node:https";
|
|
@@ -15,6 +16,9 @@ import { isChimphandsLlmRetryEnabled, startLlmRetryProxy } from "./llmRetryProxy
|
|
|
15
16
|
import { midTurnSteeringKey, releaseUnansweredMidTurnMessages, } from "./midTurnSteering.js";
|
|
16
17
|
import { runSteeringSidecarReply } from "./steeringSidecar.js";
|
|
17
18
|
import { ensureAgentSessionBranch, ensurePullRequest, isAgentBranchName, isPrOnlyMode, resolveSessionBaseBranch, runGit, } from "./agentBranch.js";
|
|
19
|
+
import { isChimpHandsInternalCommitPath, resolveCommitPaths, } from "./worktreeCommitPaths.js";
|
|
20
|
+
/** Runner logs live outside the git worktree so they never enter commit status. */
|
|
21
|
+
const OPENCODE_SERVER_LOG_PATH = path.join(tmpdir(), "chimphands-opencode-server.log");
|
|
18
22
|
const ROLE_ASSISTANT = "CHIMPHANDS_MESSAGE_ROLE_ASSISTANT";
|
|
19
23
|
const ROLE_TOOL = "CHIMPHANDS_MESSAGE_ROLE_TOOL";
|
|
20
24
|
const ROLE_REASONING = "CHIMPHANDS_MESSAGE_ROLE_REASONING";
|
|
@@ -98,7 +102,7 @@ const CHIMPHANDS_AGENT_PROMPT = `You are ChimpHands, TestChimp's coding agent. Y
|
|
|
98
102
|
|
|
99
103
|
## CI runner hygiene (GitHub Actions)
|
|
100
104
|
- On hosted runners: load skill \`references/chimphands-ci-runner.md\` before compile, Docker builds, or large installs.
|
|
101
|
-
- Never \`git fetch origin\` (full fetch) — shallow single-branch fetch only; branch names are in
|
|
105
|
+
- Never \`git fetch origin\` (full fetch) — shallow single-branch fetch only; branch names are in \`/tmp/chimphands/remote-branch-names.txt\` (or \`$CHIMPHANDS_REMOTE_BRANCH_NAMES\`).
|
|
102
106
|
- Reclaim disk between heavy phases (\`docker builder prune -af\` after image builds, stop compile daemons, delete temp/test artifacts). One reclaim+retry on ENOSPC — no blind loops.
|
|
103
107
|
|
|
104
108
|
## Honesty
|
|
@@ -1312,7 +1316,7 @@ async function restartLocalOpencodeServer(attachUrl) {
|
|
|
1312
1316
|
const port = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
1313
1317
|
killListenersOnPort(port);
|
|
1314
1318
|
await sleep(300);
|
|
1315
|
-
const logFd = openSync(
|
|
1319
|
+
const logFd = openSync(OPENCODE_SERVER_LOG_PATH, "a");
|
|
1316
1320
|
const child = spawn("opencode", ["serve", "--port", port, "--hostname", hostname], {
|
|
1317
1321
|
detached: true,
|
|
1318
1322
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -1327,6 +1331,7 @@ async function restartLocalOpencodeServer(attachUrl) {
|
|
|
1327
1331
|
/* best effort */
|
|
1328
1332
|
}
|
|
1329
1333
|
}
|
|
1334
|
+
console.error(`ChimpHands opencode serve log: ${OPENCODE_SERVER_LOG_PATH}`);
|
|
1330
1335
|
console.error(`ChimpHands restarted OpenCode serve on ${hostname}:${port} (pid ${child.pid ?? "?"})`);
|
|
1331
1336
|
await waitForOpencodeHttp(attachUrl, 60_000);
|
|
1332
1337
|
}
|
|
@@ -1627,8 +1632,13 @@ async function ackWorktreeFileWrite(backend, apiKey, sessionId, requestId, ok, e
|
|
|
1627
1632
|
requestId,
|
|
1628
1633
|
ok,
|
|
1629
1634
|
};
|
|
1630
|
-
if (errorMessage)
|
|
1631
|
-
body.errorMessage = errorMessage
|
|
1635
|
+
if (errorMessage) {
|
|
1636
|
+
body.errorMessage = errorMessage
|
|
1637
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
1638
|
+
.replace(/ +/g, " ")
|
|
1639
|
+
.trim()
|
|
1640
|
+
.slice(0, 2000);
|
|
1641
|
+
}
|
|
1632
1642
|
await postJson(backend, apiKey, "/api/chimphands/ack_worktree_file_write", body).catch((err) => {
|
|
1633
1643
|
console.error(`ChimpHands ack_worktree_file_write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1634
1644
|
});
|
|
@@ -1651,7 +1661,9 @@ async function applyUserFileEdit(backend, apiKey, sessionId, edit, turnActive) {
|
|
|
1651
1661
|
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, true);
|
|
1652
1662
|
return;
|
|
1653
1663
|
}
|
|
1654
|
-
const pushed = await commitAndPushDirtyWorktree("User Update"
|
|
1664
|
+
const pushed = await commitAndPushDirtyWorktree("User Update", {
|
|
1665
|
+
paths: [relative],
|
|
1666
|
+
});
|
|
1655
1667
|
if (!pushed.ok) {
|
|
1656
1668
|
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, false, pushed.error || "commit/push failed");
|
|
1657
1669
|
return;
|
|
@@ -2509,41 +2521,15 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId, onUiAttached) {
|
|
|
2509
2521
|
stopped = true;
|
|
2510
2522
|
};
|
|
2511
2523
|
}
|
|
2512
|
-
/**
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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
|
-
}
|
|
2545
|
-
/** Commit+push dirty worktree on the session agent branch before idle/teardown. */
|
|
2546
|
-
async function commitAndPushDirtyWorktree(message) {
|
|
2524
|
+
/**
|
|
2525
|
+
* Commit+push product changes on the session agent branch.
|
|
2526
|
+
*
|
|
2527
|
+
* Policy: allowlist staging from `git status` (never `git add -A` / exclude pathspecs).
|
|
2528
|
+
* Runner scaffolding is filtered out; anything already staged by the agent that is
|
|
2529
|
+
* scaffolding is unstaged before commit. Session logs are written under os.tmpdir(),
|
|
2530
|
+
* not the worktree.
|
|
2531
|
+
*/
|
|
2532
|
+
async function commitAndPushDirtyWorktree(message, opts) {
|
|
2547
2533
|
const run = (args, env) => runGit(args, env);
|
|
2548
2534
|
try {
|
|
2549
2535
|
const branch = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -2567,35 +2553,16 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2567
2553
|
console.error(`ChimpHands git status failed: ${error}`);
|
|
2568
2554
|
return { ok: false, error };
|
|
2569
2555
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
...CHIMPHANDS_COMMIT_EXCLUDE_PATHSPECS,
|
|
2579
|
-
]);
|
|
2580
|
-
if (add.code !== 0) {
|
|
2581
|
-
const error = add.err || add.out || "git add failed";
|
|
2582
|
-
console.error(`ChimpHands git add failed: ${error}`);
|
|
2583
|
-
return { ok: false, error };
|
|
2556
|
+
const toStage = resolveCommitPaths(status.out, opts?.paths);
|
|
2557
|
+
if (toStage.length) {
|
|
2558
|
+
const add = await run(["add", "--", ...toStage]);
|
|
2559
|
+
if (add.code !== 0) {
|
|
2560
|
+
const error = add.err || add.out || "git add failed";
|
|
2561
|
+
console.error(`ChimpHands git add failed: ${error}`);
|
|
2562
|
+
return { ok: false, error };
|
|
2563
|
+
}
|
|
2584
2564
|
}
|
|
2585
|
-
// Drop
|
|
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
|
-
]);
|
|
2565
|
+
// Drop scaffolding the agent may have staged (git add -A, etc.).
|
|
2599
2566
|
const staged = await run(["diff", "--cached", "--name-only"]);
|
|
2600
2567
|
if (staged.code !== 0) {
|
|
2601
2568
|
const error = staged.err || staged.out || "git diff --cached failed";
|
|
@@ -2611,7 +2578,13 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2611
2578
|
await run(["reset", "-q", "HEAD", "--", ...internalStaged]);
|
|
2612
2579
|
}
|
|
2613
2580
|
const keepStaged = stagedPaths.filter((p) => !isChimpHandsInternalCommitPath(p));
|
|
2614
|
-
|
|
2581
|
+
const pathScoped = !!opts?.paths?.length;
|
|
2582
|
+
if (pathScoped) {
|
|
2583
|
+
if (!toStage.length) {
|
|
2584
|
+
return { ok: true };
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
else if (!keepStaged.length) {
|
|
2615
2588
|
return { ok: true };
|
|
2616
2589
|
}
|
|
2617
2590
|
const commitEnv = {
|
|
@@ -2620,7 +2593,30 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2620
2593
|
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "ChimpHands",
|
|
2621
2594
|
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "chimphands@testchimp.io",
|
|
2622
2595
|
};
|
|
2623
|
-
|
|
2596
|
+
// Pathspec form is `git commit --only` — commits just those paths even if
|
|
2597
|
+
// the index has other staged product files.
|
|
2598
|
+
const commitArgs = pathScoped
|
|
2599
|
+
? [
|
|
2600
|
+
"-c",
|
|
2601
|
+
"user.name=ChimpHands",
|
|
2602
|
+
"-c",
|
|
2603
|
+
"user.email=chimphands@testchimp.io",
|
|
2604
|
+
"commit",
|
|
2605
|
+
"-m",
|
|
2606
|
+
message,
|
|
2607
|
+
"--",
|
|
2608
|
+
...toStage,
|
|
2609
|
+
]
|
|
2610
|
+
: [
|
|
2611
|
+
"-c",
|
|
2612
|
+
"user.name=ChimpHands",
|
|
2613
|
+
"-c",
|
|
2614
|
+
"user.email=chimphands@testchimp.io",
|
|
2615
|
+
"commit",
|
|
2616
|
+
"-m",
|
|
2617
|
+
message,
|
|
2618
|
+
];
|
|
2619
|
+
const commit = await run(commitArgs, commitEnv);
|
|
2624
2620
|
if (commit.code !== 0) {
|
|
2625
2621
|
const error = commit.err || commit.out || "git commit failed";
|
|
2626
2622
|
console.error(`ChimpHands git commit: ${error}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths that must not be committed on the agent branch.
|
|
3
|
+
*
|
|
4
|
+
* Worktree-resident (needed at runtime, never product):
|
|
5
|
+
* - opencode.json, .agents/**
|
|
6
|
+
*
|
|
7
|
+
* Legacy / accidental worktree pollution (now written under /tmp when possible):
|
|
8
|
+
* - *.log runner logs, .chimphands/**, remote-branch*.txt, gha-creds*
|
|
9
|
+
*/
|
|
10
|
+
export declare function isChimpHandsInternalCommitPath(filePath: string): boolean;
|
|
11
|
+
/** Parse paths from `git status --porcelain` (newline form, not -z). */
|
|
12
|
+
export declare function pathsFromGitPorcelain(porcelain: string): string[];
|
|
13
|
+
/** Dirty worktree paths that are eligible to commit on the agent branch. */
|
|
14
|
+
export declare function commitCandidatePathsFromPorcelain(porcelain: string): string[];
|
|
15
|
+
export declare function normalizeWorktreePath(filePath: string): string;
|
|
16
|
+
/** Intersect status candidates with an optional path allowlist (e.g. a single UI save). */
|
|
17
|
+
export declare function resolveCommitPaths(porcelain: string, onlyPaths?: string[]): string[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths that must not be committed on the agent branch.
|
|
3
|
+
*
|
|
4
|
+
* Worktree-resident (needed at runtime, never product):
|
|
5
|
+
* - opencode.json, .agents/**
|
|
6
|
+
*
|
|
7
|
+
* Legacy / accidental worktree pollution (now written under /tmp when possible):
|
|
8
|
+
* - *.log runner logs, .chimphands/**, remote-branch*.txt, gha-creds*
|
|
9
|
+
*/
|
|
10
|
+
export function isChimpHandsInternalCommitPath(filePath) {
|
|
11
|
+
let normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
12
|
+
while (normalized.startsWith("./"))
|
|
13
|
+
normalized = normalized.slice(2);
|
|
14
|
+
const slash = normalized.lastIndexOf("/");
|
|
15
|
+
const name = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
16
|
+
if (name === "opencode.json" ||
|
|
17
|
+
name === "opencode-server.log" ||
|
|
18
|
+
name === "chimphands-run.log" ||
|
|
19
|
+
name === "remote-branches.txt" ||
|
|
20
|
+
name === "remote-branch-names.txt" ||
|
|
21
|
+
name.startsWith("gha-creds")) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return (normalized === ".chimphands" ||
|
|
25
|
+
normalized.startsWith(".chimphands/") ||
|
|
26
|
+
normalized === ".agents" ||
|
|
27
|
+
normalized.startsWith(".agents/"));
|
|
28
|
+
}
|
|
29
|
+
/** Parse paths from `git status --porcelain` (newline form, not -z). */
|
|
30
|
+
export function pathsFromGitPorcelain(porcelain) {
|
|
31
|
+
const paths = [];
|
|
32
|
+
for (const raw of porcelain.split("\n")) {
|
|
33
|
+
if (!raw || raw.length < 4)
|
|
34
|
+
continue;
|
|
35
|
+
let body = raw.slice(3);
|
|
36
|
+
if (body.includes(" -> ")) {
|
|
37
|
+
body = body.slice(body.lastIndexOf(" -> ") + 4);
|
|
38
|
+
}
|
|
39
|
+
if (body.startsWith('"') && body.endsWith('"')) {
|
|
40
|
+
body = body
|
|
41
|
+
.slice(1, -1)
|
|
42
|
+
.replace(/\\([\\"nt])/g, (_m, c) => {
|
|
43
|
+
if (c === "n")
|
|
44
|
+
return "\n";
|
|
45
|
+
if (c === "t")
|
|
46
|
+
return "\t";
|
|
47
|
+
return c;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (body)
|
|
51
|
+
paths.push(body);
|
|
52
|
+
}
|
|
53
|
+
return paths;
|
|
54
|
+
}
|
|
55
|
+
/** Dirty worktree paths that are eligible to commit on the agent branch. */
|
|
56
|
+
export function commitCandidatePathsFromPorcelain(porcelain) {
|
|
57
|
+
return pathsFromGitPorcelain(porcelain).filter((p) => !isChimpHandsInternalCommitPath(p));
|
|
58
|
+
}
|
|
59
|
+
export function normalizeWorktreePath(filePath) {
|
|
60
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
61
|
+
while (normalized.startsWith("./"))
|
|
62
|
+
normalized = normalized.slice(2);
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
/** Intersect status candidates with an optional path allowlist (e.g. a single UI save). */
|
|
66
|
+
export function resolveCommitPaths(porcelain, onlyPaths) {
|
|
67
|
+
const candidates = commitCandidatePathsFromPorcelain(porcelain);
|
|
68
|
+
if (!onlyPaths?.length)
|
|
69
|
+
return candidates;
|
|
70
|
+
const want = new Set(onlyPaths.map(normalizeWorktreePath));
|
|
71
|
+
return candidates.filter((p) => want.has(normalizeWorktreePath(p)));
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { commitCandidatePathsFromPorcelain, isChimpHandsInternalCommitPath, pathsFromGitPorcelain, resolveCommitPaths, } from "./worktreeCommitPaths.js";
|
|
4
|
+
describe("worktreeCommitPaths", () => {
|
|
5
|
+
it("parses porcelain paths and rename destinations", () => {
|
|
6
|
+
assert.deepEqual(pathsFromGitPorcelain([" M ui/plans/foo.md", "?? chimphands-run.log", "R old.md -> new.md"].join("\n")), ["ui/plans/foo.md", "chimphands-run.log", "new.md"]);
|
|
7
|
+
});
|
|
8
|
+
it("allowlists only product paths for commit", () => {
|
|
9
|
+
assert.deepEqual(commitCandidatePathsFromPorcelain([
|
|
10
|
+
" M ui/plans/stories/x.md",
|
|
11
|
+
"?? chimphands-run.log",
|
|
12
|
+
"?? opencode-server.log",
|
|
13
|
+
"?? opencode.json",
|
|
14
|
+
" M src/app.ts",
|
|
15
|
+
"?? .agents/skills/testchimp/SKILL.md",
|
|
16
|
+
].join("\n")), ["ui/plans/stories/x.md", "src/app.ts"]);
|
|
17
|
+
});
|
|
18
|
+
it("can narrow commit to an explicit path set", () => {
|
|
19
|
+
const porcelain = [" M ui/plans/a.md", " M src/app.ts"].join("\n");
|
|
20
|
+
assert.deepEqual(resolveCommitPaths(porcelain, ["ui/plans/a.md"]), ["ui/plans/a.md"]);
|
|
21
|
+
assert.deepEqual(resolveCommitPaths(porcelain), ["ui/plans/a.md", "src/app.ts"]);
|
|
22
|
+
});
|
|
23
|
+
it("recognizes internal commit paths", () => {
|
|
24
|
+
assert.equal(isChimpHandsInternalCommitPath("chimphands-run.log"), true);
|
|
25
|
+
assert.equal(isChimpHandsInternalCommitPath(".chimphands/tmp"), true);
|
|
26
|
+
assert.equal(isChimpHandsInternalCommitPath("ui/plans/x.md"), false);
|
|
27
|
+
});
|
|
28
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testchimp/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"test": "npm run build && node --test dist/chimphands/llmRetryProxy.test.js dist/chimphands/midTurnSteering.test.js dist/chimphands/steeringSidecar.test.js dist/chimphands/agentBranch.test.js",
|
|
16
|
+
"test": "npm run build && node --test dist/chimphands/llmRetryProxy.test.js dist/chimphands/midTurnSteering.test.js dist/chimphands/steeringSidecar.test.js dist/chimphands/agentBranch.test.js dist/chimphands/worktreeCommitPaths.test.js",
|
|
17
17
|
"watch": "tsc --watch",
|
|
18
18
|
"start": "node dist/bin/testchimp.js",
|
|
19
19
|
"prepublishOnly": "npm run build"
|