@testchimp/cli 0.1.73 → 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
|
|
@@ -1048,8 +1052,10 @@ function isAssistantEchoOfSentPrompt(assistant, ...sentPrompts) {
|
|
|
1048
1052
|
}
|
|
1049
1053
|
return false;
|
|
1050
1054
|
}
|
|
1055
|
+
/** First line of host-injected OpenCode prompts — UI filters on this marker. */
|
|
1056
|
+
const HOST_PROMPT_WRAPPER_MARKER = "[[[chimphands.host_prompt.v1]]]";
|
|
1051
1057
|
function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl, baseBranch) {
|
|
1052
|
-
const parts = [];
|
|
1058
|
+
const parts = [HOST_PROMPT_WRAPPER_MARKER];
|
|
1053
1059
|
if (baseBranch?.trim()) {
|
|
1054
1060
|
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
1061
|
}
|
|
@@ -1062,11 +1068,11 @@ function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSes
|
|
|
1062
1068
|
// user message and the UI shows host control text in chat.
|
|
1063
1069
|
if (isNewOpencodeSession && conversationSummary.trim()) {
|
|
1064
1070
|
parts.push(`Conversation so far:\n${conversationSummary.trim()}`, "", `Current task:\n${task}`);
|
|
1065
|
-
return parts.filter(
|
|
1071
|
+
return parts.filter((p) => p !== "").join("\n");
|
|
1066
1072
|
}
|
|
1067
|
-
if (parts.length) {
|
|
1073
|
+
if (parts.length > 1) {
|
|
1068
1074
|
parts.push(`Current task:\n${task}`);
|
|
1069
|
-
return parts.filter(
|
|
1075
|
+
return parts.filter((p) => p !== "").join("\n");
|
|
1070
1076
|
}
|
|
1071
1077
|
return task;
|
|
1072
1078
|
}
|
|
@@ -1310,7 +1316,7 @@ async function restartLocalOpencodeServer(attachUrl) {
|
|
|
1310
1316
|
const port = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
1311
1317
|
killListenersOnPort(port);
|
|
1312
1318
|
await sleep(300);
|
|
1313
|
-
const logFd = openSync(
|
|
1319
|
+
const logFd = openSync(OPENCODE_SERVER_LOG_PATH, "a");
|
|
1314
1320
|
const child = spawn("opencode", ["serve", "--port", port, "--hostname", hostname], {
|
|
1315
1321
|
detached: true,
|
|
1316
1322
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -1325,6 +1331,7 @@ async function restartLocalOpencodeServer(attachUrl) {
|
|
|
1325
1331
|
/* best effort */
|
|
1326
1332
|
}
|
|
1327
1333
|
}
|
|
1334
|
+
console.error(`ChimpHands opencode serve log: ${OPENCODE_SERVER_LOG_PATH}`);
|
|
1328
1335
|
console.error(`ChimpHands restarted OpenCode serve on ${hostname}:${port} (pid ${child.pid ?? "?"})`);
|
|
1329
1336
|
await waitForOpencodeHttp(attachUrl, 60_000);
|
|
1330
1337
|
}
|
|
@@ -1625,8 +1632,13 @@ async function ackWorktreeFileWrite(backend, apiKey, sessionId, requestId, ok, e
|
|
|
1625
1632
|
requestId,
|
|
1626
1633
|
ok,
|
|
1627
1634
|
};
|
|
1628
|
-
if (errorMessage)
|
|
1629
|
-
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
|
+
}
|
|
1630
1642
|
await postJson(backend, apiKey, "/api/chimphands/ack_worktree_file_write", body).catch((err) => {
|
|
1631
1643
|
console.error(`ChimpHands ack_worktree_file_write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1632
1644
|
});
|
|
@@ -1649,7 +1661,9 @@ async function applyUserFileEdit(backend, apiKey, sessionId, edit, turnActive) {
|
|
|
1649
1661
|
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, true);
|
|
1650
1662
|
return;
|
|
1651
1663
|
}
|
|
1652
|
-
const pushed = await commitAndPushDirtyWorktree("User Update"
|
|
1664
|
+
const pushed = await commitAndPushDirtyWorktree("User Update", {
|
|
1665
|
+
paths: [relative],
|
|
1666
|
+
});
|
|
1653
1667
|
if (!pushed.ok) {
|
|
1654
1668
|
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, false, pushed.error || "commit/push failed");
|
|
1655
1669
|
return;
|
|
@@ -2387,10 +2401,20 @@ export async function runChimphands(opts) {
|
|
|
2387
2401
|
if (result.opencodeSessionId) {
|
|
2388
2402
|
opencodeSessionId = result.opencodeSessionId;
|
|
2389
2403
|
}
|
|
2390
|
-
//
|
|
2391
|
-
//
|
|
2392
|
-
|
|
2393
|
-
if (attachUrl && opencodeSessionId) {
|
|
2404
|
+
// Checkpoint dirty worktree before unlocking the UI so Files (GitHub tip
|
|
2405
|
+
// of workingBranch) matches what the user sees when WAITING_USER lands.
|
|
2406
|
+
await commitAndPushDirtyWorktree("chimphands: commit after turn");
|
|
2407
|
+
if (attachUrl && opencodeSessionId && midTurnInjectedById.size) {
|
|
2408
|
+
try {
|
|
2409
|
+
await releaseUnansweredMidTurnMessages(attachUrl, opencodeSessionId, midTurnInjectedById, queue, wakePromptWaiter, midTurnSidecarAnswered);
|
|
2410
|
+
}
|
|
2411
|
+
catch (err) {
|
|
2412
|
+
console.error(`ChimpHands mid-turn steering release failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
const reconcileTurn = async () => {
|
|
2416
|
+
if (!attachUrl || !opencodeSessionId)
|
|
2417
|
+
return;
|
|
2394
2418
|
try {
|
|
2395
2419
|
await reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, (role, content, opts) => {
|
|
2396
2420
|
turnPostEvent(role, content, {
|
|
@@ -2404,18 +2428,7 @@ export async function runChimphands(opts) {
|
|
|
2404
2428
|
catch (err) {
|
|
2405
2429
|
console.error(`ChimpHands turn-end reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2406
2430
|
}
|
|
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
|
-
}
|
|
2431
|
+
};
|
|
2419
2432
|
if (result.code !== 0 && !result.cancelled) {
|
|
2420
2433
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
2421
2434
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
@@ -2437,16 +2450,25 @@ export async function runChimphands(opts) {
|
|
|
2437
2450
|
postEvent(ROLE_STATUS, errMsg, { status: STATUS_FAILED });
|
|
2438
2451
|
await complete(STATUS_FAILED, errMsg);
|
|
2439
2452
|
}
|
|
2453
|
+
// Persist partial transcript even on failure (after push / status report).
|
|
2454
|
+
await reconcileTurn();
|
|
2440
2455
|
exitCode = result.code || 1;
|
|
2441
2456
|
break;
|
|
2442
2457
|
}
|
|
2458
|
+
const waitingOpts = {
|
|
2459
|
+
status: STATUS_WAITING_USER,
|
|
2460
|
+
...(workingBranch ? { workingBranch } : {}),
|
|
2461
|
+
...(pullRequestUrl ? { pullRequestUrl } : {}),
|
|
2462
|
+
};
|
|
2443
2463
|
if (result.cancelled) {
|
|
2444
|
-
postEvent(ROLE_STATUS, "Turn stopped",
|
|
2464
|
+
postEvent(ROLE_STATUS, "Turn stopped", waitingOpts);
|
|
2445
2465
|
}
|
|
2446
2466
|
else {
|
|
2447
|
-
postEvent(ROLE_STATUS, "Waiting for user input",
|
|
2467
|
+
postEvent(ROLE_STATUS, "Waiting for user input", waitingOpts);
|
|
2448
2468
|
}
|
|
2449
2469
|
await poster.flush();
|
|
2470
|
+
// Durable reconcile after unlock so Interrupt→Send is not blocked on PG writes.
|
|
2471
|
+
await reconcileTurn();
|
|
2450
2472
|
lastUserActivity = Date.now();
|
|
2451
2473
|
idle = false;
|
|
2452
2474
|
prompt = (await waitForNextPrompt()) || "";
|
|
@@ -2499,8 +2521,15 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId, onUiAttached) {
|
|
|
2499
2521
|
stopped = true;
|
|
2500
2522
|
};
|
|
2501
2523
|
}
|
|
2502
|
-
/**
|
|
2503
|
-
|
|
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) {
|
|
2504
2533
|
const run = (args, env) => runGit(args, env);
|
|
2505
2534
|
try {
|
|
2506
2535
|
const branch = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -2524,22 +2553,70 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2524
2553
|
console.error(`ChimpHands git status failed: ${error}`);
|
|
2525
2554
|
return { ok: false, error };
|
|
2526
2555
|
}
|
|
2527
|
-
|
|
2528
|
-
|
|
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
|
+
}
|
|
2529
2564
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2565
|
+
// Drop scaffolding the agent may have staged (git add -A, etc.).
|
|
2566
|
+
const staged = await run(["diff", "--cached", "--name-only"]);
|
|
2567
|
+
if (staged.code !== 0) {
|
|
2568
|
+
const error = staged.err || staged.out || "git diff --cached failed";
|
|
2569
|
+
console.error(`ChimpHands git staged check failed: ${error}`);
|
|
2534
2570
|
return { ok: false, error };
|
|
2535
2571
|
}
|
|
2572
|
+
const stagedPaths = staged.out
|
|
2573
|
+
.split("\n")
|
|
2574
|
+
.map((line) => line.trim())
|
|
2575
|
+
.filter(Boolean);
|
|
2576
|
+
const internalStaged = stagedPaths.filter(isChimpHandsInternalCommitPath);
|
|
2577
|
+
if (internalStaged.length) {
|
|
2578
|
+
await run(["reset", "-q", "HEAD", "--", ...internalStaged]);
|
|
2579
|
+
}
|
|
2580
|
+
const keepStaged = stagedPaths.filter((p) => !isChimpHandsInternalCommitPath(p));
|
|
2581
|
+
const pathScoped = !!opts?.paths?.length;
|
|
2582
|
+
if (pathScoped) {
|
|
2583
|
+
if (!toStage.length) {
|
|
2584
|
+
return { ok: true };
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
else if (!keepStaged.length) {
|
|
2588
|
+
return { ok: true };
|
|
2589
|
+
}
|
|
2536
2590
|
const commitEnv = {
|
|
2537
2591
|
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "ChimpHands",
|
|
2538
2592
|
GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || "chimphands@testchimp.io",
|
|
2539
2593
|
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "ChimpHands",
|
|
2540
2594
|
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "chimphands@testchimp.io",
|
|
2541
2595
|
};
|
|
2542
|
-
|
|
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);
|
|
2543
2620
|
if (commit.code !== 0) {
|
|
2544
2621
|
const error = commit.err || commit.out || "git commit failed";
|
|
2545
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"
|