@testchimp/cli 0.1.69 → 0.1.71
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.
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChimpHands CI git: base branch (parent) vs agent branch (testchimp-* only).
|
|
3
|
+
*/
|
|
4
|
+
export declare const BASE_BRANCH_PREFIX = "Base branch: ";
|
|
5
|
+
/** @deprecated Legacy platform line; still parsed for in-flight sessions. */
|
|
6
|
+
export declare const LEGACY_WORKING_BRANCH_PREFIX = "Working branch: ";
|
|
7
|
+
export declare function isAgentBranchName(branch: string): boolean;
|
|
8
|
+
export declare function parseBaseBranchFromPrompt(prompt: string): string | undefined;
|
|
9
|
+
export declare function resolveSessionBaseBranch(prompt: string | undefined, envBaseBranch?: string): string | undefined;
|
|
10
|
+
export declare function buildAgentBranchName(sessionId: string): string;
|
|
11
|
+
type GitRunResult = {
|
|
12
|
+
code: number;
|
|
13
|
+
out: string;
|
|
14
|
+
err: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function runGit(args: string[], env?: NodeJS.ProcessEnv): Promise<GitRunResult>;
|
|
17
|
+
export type EnsureAgentBranchOptions = {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
baseBranch?: string;
|
|
20
|
+
/** Agent branch already reported for this session (bootstrap). */
|
|
21
|
+
existingAgentBranch?: string;
|
|
22
|
+
};
|
|
23
|
+
export type EnsureAgentBranchResult = {
|
|
24
|
+
agentBranch: string;
|
|
25
|
+
baseBranch?: string;
|
|
26
|
+
created: boolean;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Ensures the runner is on a published testchimp-* agent branch.
|
|
30
|
+
* When checked out on the user's base branch, creates testchimp-chimphands-<session> from it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureAgentSessionBranch(opts: EnsureAgentBranchOptions): Promise<EnsureAgentBranchResult>;
|
|
33
|
+
export type EnsurePullRequestOptions = {
|
|
34
|
+
agentBranch: string;
|
|
35
|
+
baseBranch: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
body?: string;
|
|
38
|
+
};
|
|
39
|
+
/** Opens a PR from agent branch → base branch when none exists. Returns PR URL if created/found. */
|
|
40
|
+
export declare function ensurePullRequest(opts: EnsurePullRequestOptions): Promise<string | undefined>;
|
|
41
|
+
export declare function isPrOnlyMode(): boolean;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChimpHands CI git: base branch (parent) vs agent branch (testchimp-* only).
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
export const BASE_BRANCH_PREFIX = "Base branch: ";
|
|
6
|
+
/** @deprecated Legacy platform line; still parsed for in-flight sessions. */
|
|
7
|
+
export const LEGACY_WORKING_BRANCH_PREFIX = "Working branch: ";
|
|
8
|
+
const BASE_BRANCH_LINE = /^(?:Base branch|Working branch):\s*(.+)$/im;
|
|
9
|
+
const AGENT_BRANCH_PREFIX = /^testchimp-/i;
|
|
10
|
+
export function isAgentBranchName(branch) {
|
|
11
|
+
return AGENT_BRANCH_PREFIX.test(branch.trim());
|
|
12
|
+
}
|
|
13
|
+
export function parseBaseBranchFromPrompt(prompt) {
|
|
14
|
+
if (!prompt?.trim())
|
|
15
|
+
return undefined;
|
|
16
|
+
let last;
|
|
17
|
+
for (const line of prompt.split("\n")) {
|
|
18
|
+
const m = line.match(BASE_BRANCH_LINE);
|
|
19
|
+
if (!m?.[1])
|
|
20
|
+
continue;
|
|
21
|
+
const candidate = m[1].trim();
|
|
22
|
+
if (candidate)
|
|
23
|
+
last = candidate;
|
|
24
|
+
}
|
|
25
|
+
return last;
|
|
26
|
+
}
|
|
27
|
+
export function resolveSessionBaseBranch(prompt, envBaseBranch) {
|
|
28
|
+
const fromPrompt = prompt ? parseBaseBranchFromPrompt(prompt) : undefined;
|
|
29
|
+
if (fromPrompt)
|
|
30
|
+
return fromPrompt;
|
|
31
|
+
const fromEnv = envBaseBranch?.trim();
|
|
32
|
+
return fromEnv || undefined;
|
|
33
|
+
}
|
|
34
|
+
export function buildAgentBranchName(sessionId) {
|
|
35
|
+
const slug = sessionId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 10).toLowerCase() || "session";
|
|
36
|
+
return `testchimp-chimphands-${slug}`;
|
|
37
|
+
}
|
|
38
|
+
export function runGit(args, env) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const child = spawn("git", args, {
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
43
|
+
});
|
|
44
|
+
let out = "";
|
|
45
|
+
let err = "";
|
|
46
|
+
child.stdout.on("data", (d) => {
|
|
47
|
+
out += d.toString();
|
|
48
|
+
});
|
|
49
|
+
child.stderr.on("data", (d) => {
|
|
50
|
+
err += d.toString();
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => resolve({ code: code ?? 1, out, err }));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function currentBranch() {
|
|
56
|
+
const r = await runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
57
|
+
if (r.code !== 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
const b = r.out.trim();
|
|
60
|
+
return b && b !== "HEAD" ? b : undefined;
|
|
61
|
+
}
|
|
62
|
+
async function isDirtyWorktree() {
|
|
63
|
+
const r = await runGit(["status", "--porcelain"]);
|
|
64
|
+
return r.code === 0 && !!r.out.trim();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ensures the runner is on a published testchimp-* agent branch.
|
|
68
|
+
* When checked out on the user's base branch, creates testchimp-chimphands-<session> from it.
|
|
69
|
+
*/
|
|
70
|
+
export async function ensureAgentSessionBranch(opts) {
|
|
71
|
+
const baseBranch = opts.baseBranch?.trim() || undefined;
|
|
72
|
+
const existing = opts.existingAgentBranch?.trim();
|
|
73
|
+
if (existing && isAgentBranchName(existing)) {
|
|
74
|
+
await checkoutLocalBranch(existing);
|
|
75
|
+
return { agentBranch: existing, baseBranch, created: false };
|
|
76
|
+
}
|
|
77
|
+
const current = await currentBranch();
|
|
78
|
+
if (current && isAgentBranchName(current)) {
|
|
79
|
+
return { agentBranch: current, baseBranch, created: false };
|
|
80
|
+
}
|
|
81
|
+
const agentBranch = buildAgentBranchName(opts.sessionId);
|
|
82
|
+
const create = await runGit(["checkout", "-b", agentBranch]);
|
|
83
|
+
if (create.code !== 0) {
|
|
84
|
+
throw new Error(`git checkout -b ${agentBranch} failed: ${create.err || create.out || "unknown"}`);
|
|
85
|
+
}
|
|
86
|
+
const push = await runGit(["push", "-u", "origin", "HEAD"]);
|
|
87
|
+
if (push.code !== 0) {
|
|
88
|
+
throw new Error(`git push failed for new agent branch: ${push.err || push.out || "unknown"}`);
|
|
89
|
+
}
|
|
90
|
+
console.error(`ChimpHands created agent branch ${agentBranch}` +
|
|
91
|
+
(baseBranch ? ` from base ${baseBranch}` : ""));
|
|
92
|
+
return { agentBranch, baseBranch, created: true };
|
|
93
|
+
}
|
|
94
|
+
async function checkoutLocalBranch(branch) {
|
|
95
|
+
const r = await runGit(["checkout", branch]);
|
|
96
|
+
if (r.code !== 0) {
|
|
97
|
+
throw new Error(`git checkout ${branch} failed: ${r.err || r.out || "unknown"}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Opens a PR from agent branch → base branch when none exists. Returns PR URL if created/found. */
|
|
101
|
+
export async function ensurePullRequest(opts) {
|
|
102
|
+
const agentBranch = opts.agentBranch.trim();
|
|
103
|
+
const baseBranch = opts.baseBranch.trim();
|
|
104
|
+
if (!agentBranch || !baseBranch || agentBranch === baseBranch)
|
|
105
|
+
return undefined;
|
|
106
|
+
const list = await runGh([
|
|
107
|
+
"pr",
|
|
108
|
+
"list",
|
|
109
|
+
"--head",
|
|
110
|
+
agentBranch,
|
|
111
|
+
"--base",
|
|
112
|
+
baseBranch,
|
|
113
|
+
"--state",
|
|
114
|
+
"open",
|
|
115
|
+
"--json",
|
|
116
|
+
"url",
|
|
117
|
+
"--jq",
|
|
118
|
+
".[0].url",
|
|
119
|
+
]);
|
|
120
|
+
if (list.code === 0 && list.out.trim()) {
|
|
121
|
+
return list.out.trim();
|
|
122
|
+
}
|
|
123
|
+
const dirty = await isDirtyWorktree();
|
|
124
|
+
if (dirty) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const headAhead = await runGit(["rev-list", "--count", `${baseBranch}..${agentBranch}`]);
|
|
128
|
+
const aheadCount = Number.parseInt(headAhead.out.trim(), 10);
|
|
129
|
+
if (headAhead.code !== 0 || !Number.isFinite(aheadCount) || aheadCount <= 0) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const title = opts.title?.trim() || `ChimpHands: ${agentBranch}`;
|
|
133
|
+
const body = opts.body?.trim() ||
|
|
134
|
+
`Automated changes from ChimpHands session on branch \`${agentBranch}\`.\n\nMerge target: \`${baseBranch}\`.`;
|
|
135
|
+
const create = await runGh([
|
|
136
|
+
"pr",
|
|
137
|
+
"create",
|
|
138
|
+
"--base",
|
|
139
|
+
baseBranch,
|
|
140
|
+
"--head",
|
|
141
|
+
agentBranch,
|
|
142
|
+
"--title",
|
|
143
|
+
title,
|
|
144
|
+
"--body",
|
|
145
|
+
body,
|
|
146
|
+
]);
|
|
147
|
+
if (create.code !== 0) {
|
|
148
|
+
console.error(`ChimpHands gh pr create failed: ${create.err || create.out}`);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const url = (create.out || create.err).trim().match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/)?.[0];
|
|
152
|
+
return url;
|
|
153
|
+
}
|
|
154
|
+
function runGh(args) {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const child = spawn("gh", args, {
|
|
157
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
158
|
+
env: process.env,
|
|
159
|
+
});
|
|
160
|
+
let out = "";
|
|
161
|
+
let err = "";
|
|
162
|
+
child.stdout.on("data", (d) => {
|
|
163
|
+
out += d.toString();
|
|
164
|
+
});
|
|
165
|
+
child.stderr.on("data", (d) => {
|
|
166
|
+
err += d.toString();
|
|
167
|
+
});
|
|
168
|
+
child.on("close", (code) => resolve({ code: code ?? 1, out, err }));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
export function isPrOnlyMode() {
|
|
172
|
+
const v = (process.env.CHIMPHANDS_PR_ONLY || "").trim().toLowerCase();
|
|
173
|
+
return v === "1" || v === "true" || v === "yes";
|
|
174
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { buildAgentBranchName, isAgentBranchName, parseBaseBranchFromPrompt, resolveSessionBaseBranch, } from "./agentBranch.js";
|
|
4
|
+
describe("agentBranch", () => {
|
|
5
|
+
it("isAgentBranchName accepts testchimp- only", () => {
|
|
6
|
+
assert.equal(isAgentBranchName("testchimp-foo"), true);
|
|
7
|
+
assert.equal(isAgentBranchName("chimphands-foo"), false);
|
|
8
|
+
assert.equal(isAgentBranchName("feature/foo"), false);
|
|
9
|
+
});
|
|
10
|
+
it("parseBaseBranchFromPrompt reads Base and legacy Working lines", () => {
|
|
11
|
+
assert.equal(parseBaseBranchFromPrompt("run qa\nBase branch: dashboard"), "dashboard");
|
|
12
|
+
assert.equal(parseBaseBranchFromPrompt("Working branch: main"), "main");
|
|
13
|
+
assert.equal(parseBaseBranchFromPrompt("run qa only"), undefined);
|
|
14
|
+
});
|
|
15
|
+
it("resolveSessionBaseBranch prefers prompt over env", () => {
|
|
16
|
+
assert.equal(resolveSessionBaseBranch("Base branch: a", "b"), "a");
|
|
17
|
+
assert.equal(resolveSessionBaseBranch(undefined, "b"), "b");
|
|
18
|
+
});
|
|
19
|
+
it("buildAgentBranchName uses testchimp prefix", () => {
|
|
20
|
+
assert.match(buildAgentBranchName("01ARZ3NDEKTSV4RRFFQ69G5FAV"), /^testchimp-chimphands-/);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/dist/chimphands/run.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getBackendUrl, requireApiKey } from "../core/client.js";
|
|
|
14
14
|
import { isChimphandsLlmRetryEnabled, startLlmRetryProxy } from "./llmRetryProxy.js";
|
|
15
15
|
import { midTurnSteeringKey, releaseUnansweredMidTurnMessages, } from "./midTurnSteering.js";
|
|
16
16
|
import { runSteeringSidecarReply } from "./steeringSidecar.js";
|
|
17
|
+
import { ensureAgentSessionBranch, ensurePullRequest, isAgentBranchName, isPrOnlyMode, resolveSessionBaseBranch, runGit, } from "./agentBranch.js";
|
|
17
18
|
const ROLE_ASSISTANT = "CHIMPHANDS_MESSAGE_ROLE_ASSISTANT";
|
|
18
19
|
const ROLE_TOOL = "CHIMPHANDS_MESSAGE_ROLE_TOOL";
|
|
19
20
|
const ROLE_REASONING = "CHIMPHANDS_MESSAGE_ROLE_REASONING";
|
|
@@ -24,8 +25,10 @@ const STATUS_IDLE = "CHIMPHANDS_SESSION_STATUS_IDLE";
|
|
|
24
25
|
const STATUS_FAILED = "CHIMPHANDS_SESSION_STATUS_FAILED";
|
|
25
26
|
const OPENCODE_AGENT_ID = "chimphands";
|
|
26
27
|
const MID_TURN_DELIVERED_STATUS = "Answering your question while the current step continues…";
|
|
27
|
-
/** Coalesce live token fanout (~
|
|
28
|
-
const
|
|
28
|
+
/** Coalesce live token fanout (~30fps) on tunnel WS / HTTP fallback while UI is attached. */
|
|
29
|
+
const STREAM_FANOUT_MIN_INTERVAL_MS = 33;
|
|
30
|
+
/** Set for the active ChimpHands session (PR base branch + callbacks). */
|
|
31
|
+
let activeSessionGitContext = null;
|
|
29
32
|
function isStreamFanoutRole(role) {
|
|
30
33
|
return role === ROLE_ASSISTANT || role === ROLE_TOOL || role === ROLE_REASONING;
|
|
31
34
|
}
|
|
@@ -66,18 +69,24 @@ const CHIMPHANDS_AGENT_PROMPT = `You are ChimpHands, TestChimp's coding agent. Y
|
|
|
66
69
|
- When you need clarification (e.g. import plans/tests, env strategy, CI choices) or plan approval: write the questions / plan summary as assistant text, then **stop this turn**. Do not invent answers or continue into Execute. The host will wait for the next chat message and revive you.
|
|
67
70
|
- Prefer a short numbered list of concrete questions over a long monologue. One decision gate at a time when possible (especially \`/testchimp project init\` Phase 1).
|
|
68
71
|
|
|
72
|
+
## Paths (mandatory)
|
|
73
|
+
- Process cwd **is** the git worktree root. On GitHub Actions that is \`$GITHUB_WORKSPACE\`, which looks like \`/home/runner/work/<repo>/<repo>\` — the repeated folder name is the normal Actions layout, **not** a nested second checkout.
|
|
74
|
+
- User \`ref:\` paths are **repo-relative to cwd** (e.g. \`ref: ui/plans/foo.md\` → open \`ui/plans/foo.md\` from cwd). Never prepend the repository name. Never open or create \`<repo>/<repo>/…\` under cwd. Never treat \`/home/runner/work\` (the parent of the twin) as the worktree root.
|
|
75
|
+
- If a tool returns an absolute path under cwd, relativize with the worktree root before reusing it. Do **not** strip a leading \`/\` and re-join under cwd — that nests the workspace path inside itself.
|
|
76
|
+
|
|
69
77
|
## Repo changes (mandatory)
|
|
70
|
-
- NEVER commit or push directly to the default branch (main/master).
|
|
71
|
-
- This conversation uses ONE
|
|
72
|
-
- If bootstrap lists
|
|
73
|
-
- Only create a NEW branch/PR when (a) no
|
|
74
|
-
- Commit and push on the session
|
|
75
|
-
- Branch names MUST start with \`testchimp-\`
|
|
76
|
-
- When
|
|
78
|
+
- NEVER commit or push directly to the default branch (main/master) or to a user **base branch** (the \`Base branch:\` line in the prompt is the parent to branch *from*, not where you commit).
|
|
79
|
+
- This conversation uses ONE **agent branch** (\`testchimp-*\` only) and ONE pull request targeting the base branch. Reuse them for all follow-up work in this chat.
|
|
80
|
+
- If bootstrap lists an agent branch (\`testchimp-*\`), checkout that branch and push additional commits there — update the same PR.
|
|
81
|
+
- Only create a NEW agent branch/PR when (a) no \`testchimp-*\` branch exists yet for this conversation, or (b) the prior PR was merged/closed (verify with \`gh pr view\`).
|
|
82
|
+
- Commit and push on the session agent branch after meaningful edit batches. When \`CHIMPHANDS_UI_ATTACHED\` is \`false\` (no browser watching live), **commit and push before ending every turn** so the user can review async via PR / Files changed. The host also commits any remaining dirty worktree after each turn and before idle teardown.
|
|
83
|
+
- Branch names for commits and PRs MUST start with \`testchimp-\` (never \`chimphands-*\` or the base branch name).
|
|
84
|
+
- When the prompt includes \`Base branch: <name>\`: the workflow may already have that branch checked out (\`CHIMPHANDS_WORK_BRANCH\`). Create \`git checkout -b testchimp-<scope>\` from it, push, then work only on the \`testchimp-*\` branch. Open PRs with \`gh pr create --base <base-branch> --head <testchimp-branch>\`.
|
|
85
|
+
- When creating a NEW agent branch: create it, then IMMEDIATELY publish it with
|
|
77
86
|
\`git push -u origin <branch>\` BEFORE calling report-branch. Users open the branch URL in the UI —
|
|
78
87
|
do not report a branch that only exists locally (that causes GitHub 404).
|
|
79
88
|
- After the branch is on the remote (and after opening a PR), IMMEDIATELY run:
|
|
80
|
-
\`testchimp chimphands report-branch --branch <
|
|
89
|
+
\`testchimp chimphands report-branch --branch <testchimp-branch> [--pr-url <url>]\`
|
|
81
90
|
- Tell the user which branch you are on and include the PR URL when available.
|
|
82
91
|
- **GitHub auth expiry (self-fix):** Job-start App installation tokens expire after ~1 hour. On \`git push\` / \`gh\` auth failures (401/403 / Authentication failed / write access not granted), run \`testchimp chimphands refresh-git-auth\` then retry — **never** ask the user to paste or reconnect a GitHub token in chat. See skill \`references/chimphands-faq.md\`.
|
|
83
92
|
|
|
@@ -119,6 +128,9 @@ export async function reportWorkingBranch(opts) {
|
|
|
119
128
|
if (!branch) {
|
|
120
129
|
throw new Error("branch is required");
|
|
121
130
|
}
|
|
131
|
+
if (!isAgentBranchName(branch)) {
|
|
132
|
+
throw new Error("branch must start with testchimp- (agent feature branch, not the base branch)");
|
|
133
|
+
}
|
|
122
134
|
const body = {
|
|
123
135
|
sessionId: opts.sessionId.trim(),
|
|
124
136
|
workingBranch: branch,
|
|
@@ -155,23 +167,42 @@ async function postJson(backend, apiKey, path, body) {
|
|
|
155
167
|
clearTimeout(timer);
|
|
156
168
|
}
|
|
157
169
|
}
|
|
158
|
-
/** Serializes agent event posts
|
|
170
|
+
/** Serializes durable agent event posts; live mid-turn tokens prefer the agent tunnel WS. */
|
|
159
171
|
class AgentEventPoster {
|
|
160
172
|
backend;
|
|
161
173
|
apiKey;
|
|
162
174
|
sessionId;
|
|
163
175
|
chain = Promise.resolve();
|
|
164
|
-
|
|
176
|
+
lastLiveFanoutAt = 0;
|
|
165
177
|
/** When false, liveStream tokens are dropped; completed events still persist. */
|
|
166
178
|
uiAttached = false;
|
|
179
|
+
/**
|
|
180
|
+
* Prefer agent tunnel WS for live tokens. Returns true when the frame was queued on an open socket.
|
|
181
|
+
* Set by {@link startTunnelWorker} once the runtime tunnel is connected.
|
|
182
|
+
*/
|
|
183
|
+
sendUiMessage = null;
|
|
167
184
|
ephemeralFailLogAt = 0;
|
|
168
185
|
ephemeralFailCount = 0;
|
|
186
|
+
/** Latest pending live snapshot per messageId (coalesce-to-latest). */
|
|
187
|
+
pendingLive = new Map();
|
|
188
|
+
liveFlushTimer = null;
|
|
189
|
+
liveFlushInFlight = false;
|
|
169
190
|
constructor(backend, apiKey, sessionId) {
|
|
170
191
|
this.backend = backend;
|
|
171
192
|
this.apiKey = apiKey;
|
|
172
193
|
this.sessionId = sessionId;
|
|
173
194
|
}
|
|
174
195
|
enqueue(role, content, opts) {
|
|
196
|
+
const streamRole = isStreamFanoutRole(role);
|
|
197
|
+
const live = !!opts?.liveStream && !opts?.durable;
|
|
198
|
+
// Mid-turn tokens: coalesce + WS (HTTP fallback). Never hit the durable chain.
|
|
199
|
+
if (live && streamRole) {
|
|
200
|
+
if (!this.uiAttached) {
|
|
201
|
+
return Promise.resolve();
|
|
202
|
+
}
|
|
203
|
+
this.enqueueLive(role, content, opts);
|
|
204
|
+
return Promise.resolve();
|
|
205
|
+
}
|
|
175
206
|
const body = {
|
|
176
207
|
sessionId: this.sessionId,
|
|
177
208
|
role,
|
|
@@ -189,48 +220,95 @@ class AgentEventPoster {
|
|
|
189
220
|
body.pullRequestUrl = opts.pullRequestUrl;
|
|
190
221
|
if (opts?.toolExecution)
|
|
191
222
|
body.toolExecution = protoJsonToolExecution(opts.toolExecution);
|
|
192
|
-
const streamRole = isStreamFanoutRole(role);
|
|
193
223
|
this.chain = this.chain.then(async () => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
224
|
+
await postJson(this.backend, this.apiKey, "/api/chimphands/post_agent_event", body);
|
|
225
|
+
});
|
|
226
|
+
return this.chain;
|
|
227
|
+
}
|
|
228
|
+
enqueueLive(role, content, opts) {
|
|
229
|
+
const key = (opts?.messageId || "").trim() || `${role}:default`;
|
|
230
|
+
this.pendingLive.set(key, { role, content: String(content || "").slice(0, 20000), opts });
|
|
231
|
+
this.scheduleLiveFlush();
|
|
232
|
+
}
|
|
233
|
+
scheduleLiveFlush() {
|
|
234
|
+
if (this.liveFlushInFlight || this.liveFlushTimer)
|
|
235
|
+
return;
|
|
236
|
+
const wait = Math.max(0, STREAM_FANOUT_MIN_INTERVAL_MS - (Date.now() - this.lastLiveFanoutAt));
|
|
237
|
+
this.liveFlushTimer = setTimeout(() => {
|
|
238
|
+
this.liveFlushTimer = null;
|
|
239
|
+
void this.flushLivePending();
|
|
240
|
+
}, wait);
|
|
241
|
+
}
|
|
242
|
+
async flushLivePending() {
|
|
243
|
+
if (this.liveFlushInFlight)
|
|
244
|
+
return;
|
|
245
|
+
if (this.pendingLive.size === 0)
|
|
246
|
+
return;
|
|
247
|
+
if (!this.uiAttached) {
|
|
248
|
+
this.pendingLive.clear();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.liveFlushInFlight = true;
|
|
252
|
+
try {
|
|
253
|
+
const batch = [...this.pendingLive.values()];
|
|
254
|
+
this.pendingLive.clear();
|
|
255
|
+
this.lastLiveFanoutAt = Date.now();
|
|
256
|
+
for (const item of batch) {
|
|
257
|
+
await this.deliverLive(item.role, item.content, item.opts);
|
|
198
258
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
this.liveFlushInFlight = false;
|
|
262
|
+
if (this.pendingLive.size > 0 && this.uiAttached) {
|
|
263
|
+
this.scheduleLiveFlush();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async deliverLive(role, content, opts) {
|
|
268
|
+
const frame = {
|
|
269
|
+
type: "ui_message",
|
|
270
|
+
sessionId: this.sessionId,
|
|
271
|
+
role,
|
|
272
|
+
content,
|
|
273
|
+
};
|
|
274
|
+
if (opts?.messageId)
|
|
275
|
+
frame.messageId = opts.messageId;
|
|
276
|
+
if (opts?.toolExecution)
|
|
277
|
+
frame.toolExecution = protoJsonToolExecution(opts.toolExecution);
|
|
278
|
+
if (this.sendUiMessage) {
|
|
279
|
+
try {
|
|
280
|
+
if (this.sendUiMessage(frame)) {
|
|
221
281
|
this.ephemeralFailCount = 0;
|
|
282
|
+
return;
|
|
222
283
|
}
|
|
223
|
-
catch (err) {
|
|
224
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
225
|
-
this.logEphemeralIssue(`ephemeral post failed — not persisting liveStream: ${detail}`);
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
284
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
285
|
+
catch (err) {
|
|
286
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
287
|
+
this.logEphemeralIssue(`tunnel ui_message send failed — falling back to HTTP: ${detail}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// HTTP ephemeral fallback when tunnel is down / unavailable.
|
|
291
|
+
const eph = {
|
|
292
|
+
sessionId: this.sessionId,
|
|
293
|
+
role,
|
|
294
|
+
content,
|
|
295
|
+
};
|
|
296
|
+
if (opts?.messageId)
|
|
297
|
+
eph.messageId = opts.messageId;
|
|
298
|
+
if (opts?.opencodeSessionId)
|
|
299
|
+
eph.opencodeSessionId = opts.opencodeSessionId;
|
|
300
|
+
if (opts?.toolExecution)
|
|
301
|
+
eph.toolExecution = protoJsonToolExecution(opts.toolExecution);
|
|
302
|
+
try {
|
|
303
|
+
await postJson(this.backend, this.apiKey, "/api/chimphands/post_ephemeral_agent_event", eph);
|
|
304
|
+
this.ephemeralFailCount = 0;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
308
|
+
this.logEphemeralIssue(`ephemeral post failed — not persisting liveStream: ${detail}`);
|
|
309
|
+
}
|
|
232
310
|
}
|
|
233
|
-
/** Avoid flooding GHA logs when
|
|
311
|
+
/** Avoid flooding GHA logs when live fanout fails repeatedly. */
|
|
234
312
|
logEphemeralIssue(message) {
|
|
235
313
|
this.ephemeralFailCount += 1;
|
|
236
314
|
const now = Date.now();
|
|
@@ -247,7 +325,11 @@ class AgentEventPoster {
|
|
|
247
325
|
});
|
|
248
326
|
}
|
|
249
327
|
flush() {
|
|
250
|
-
|
|
328
|
+
if (this.liveFlushTimer) {
|
|
329
|
+
clearTimeout(this.liveFlushTimer);
|
|
330
|
+
this.liveFlushTimer = null;
|
|
331
|
+
}
|
|
332
|
+
return this.flushLivePending().then(() => this.chain);
|
|
251
333
|
}
|
|
252
334
|
reportWorkingBranch(branch, pullRequestUrl) {
|
|
253
335
|
this.chain = this.chain.then(async () => {
|
|
@@ -966,10 +1048,13 @@ function isAssistantEchoOfSentPrompt(assistant, ...sentPrompts) {
|
|
|
966
1048
|
}
|
|
967
1049
|
return false;
|
|
968
1050
|
}
|
|
969
|
-
function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl) {
|
|
1051
|
+
function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl, baseBranch) {
|
|
970
1052
|
const parts = [];
|
|
971
|
-
if (
|
|
972
|
-
parts.push("##
|
|
1053
|
+
if (baseBranch?.trim()) {
|
|
1054
|
+
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
|
+
}
|
|
1056
|
+
if (workingBranch?.trim() && isAgentBranchName(workingBranch)) {
|
|
1057
|
+
parts.push("## Conversation agent branch (reuse for this thread)", `Branch: \`${workingBranch.trim()}\``, pullRequestUrl?.trim() ? `PR: ${pullRequestUrl.trim()}` : "", "Checkout this testchimp-* branch, commit and push here. Do NOT open a new PR unless the one above was merged/closed.", "");
|
|
973
1058
|
}
|
|
974
1059
|
const task = normalizeUserMessage(userPrompt);
|
|
975
1060
|
// Do NOT inject "Interactive turn reminder" into the user prompt — it is already in
|
|
@@ -992,7 +1077,7 @@ function detectWorkingBranchFromToolOutput(output) {
|
|
|
992
1077
|
const prMatch = text.match(/https:\/\/github\.com\/[^\s)\]]+\/pull\/\d+/);
|
|
993
1078
|
// Only auto-detect after a successful push to origin — local checkout -b alone would
|
|
994
1079
|
// report a branch URL that 404s until the remote ref exists.
|
|
995
|
-
const pushMatch = text.match(/push\s+(?:--set-upstream\s+|-u\s+)?origin\s+((?:testchimp
|
|
1080
|
+
const pushMatch = text.match(/push\s+(?:--set-upstream\s+|-u\s+)?origin\s+((?:testchimp-)[^\s'"\[\]]+)/i);
|
|
996
1081
|
const branch = pushMatch?.[1]?.replace(/[`'"\[\]]/g, "");
|
|
997
1082
|
return {
|
|
998
1083
|
branch,
|
|
@@ -1487,12 +1572,36 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1487
1572
|
});
|
|
1488
1573
|
});
|
|
1489
1574
|
}
|
|
1490
|
-
function normalizeWorktreeRelativePath(filePath) {
|
|
1491
|
-
|
|
1575
|
+
function normalizeWorktreeRelativePath(filePath, worktreeRoot = process.cwd()) {
|
|
1576
|
+
const rootResolved = path.resolve(worktreeRoot);
|
|
1577
|
+
const rawInput = String(filePath || "").trim();
|
|
1578
|
+
if (!rawInput || rawInput.includes("\0")) {
|
|
1579
|
+
throw new Error("invalid path");
|
|
1580
|
+
}
|
|
1581
|
+
// Absolute path under the worktree → repo-relative (never strip "/" then re-join).
|
|
1582
|
+
if (path.isAbsolute(rawInput)) {
|
|
1583
|
+
const abs = path.resolve(rawInput);
|
|
1584
|
+
if (abs === rootResolved) {
|
|
1585
|
+
throw new Error("invalid path");
|
|
1586
|
+
}
|
|
1587
|
+
if (abs.startsWith(rootResolved + path.sep)) {
|
|
1588
|
+
return path.relative(rootResolved, abs).split(path.sep).join("/");
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
let p = rawInput.replace(/\\/g, "/");
|
|
1492
1592
|
while (p.startsWith("/"))
|
|
1493
1593
|
p = p.slice(1);
|
|
1494
|
-
|
|
1495
|
-
|
|
1594
|
+
// GHA workspace is /home/runner/work/<repo>/<repo>. Strip twin prefixes that agents/OpenCode
|
|
1595
|
+
// sometimes emit as if relative to /home/runner/work.
|
|
1596
|
+
const base = path.basename(rootResolved);
|
|
1597
|
+
const parentBase = path.basename(path.dirname(rootResolved));
|
|
1598
|
+
if (base && base === parentBase) {
|
|
1599
|
+
const twin = `${base}/${base}/`;
|
|
1600
|
+
const gha = `home/runner/work/${base}/${base}/`;
|
|
1601
|
+
if (p.startsWith(gha))
|
|
1602
|
+
p = p.slice(gha.length);
|
|
1603
|
+
else if (p.startsWith(twin))
|
|
1604
|
+
p = p.slice(twin.length);
|
|
1496
1605
|
}
|
|
1497
1606
|
const segments = [];
|
|
1498
1607
|
for (const part of p.split("/")) {
|
|
@@ -1844,9 +1953,16 @@ export async function runChimphands(opts) {
|
|
|
1844
1953
|
const prIsNew = !!prUrl?.trim() && prUrl.trim() !== pullRequestUrl;
|
|
1845
1954
|
if (workingBranch === normalizedBranch && !prIsNew)
|
|
1846
1955
|
return;
|
|
1956
|
+
if (!isAgentBranchName(normalizedBranch))
|
|
1957
|
+
return;
|
|
1847
1958
|
workingBranch = normalizedBranch;
|
|
1848
1959
|
if (prUrl?.trim())
|
|
1849
1960
|
pullRequestUrl = prUrl.trim();
|
|
1961
|
+
if (activeSessionGitContext) {
|
|
1962
|
+
activeSessionGitContext.agentBranch = normalizedBranch;
|
|
1963
|
+
if (pullRequestUrl)
|
|
1964
|
+
activeSessionGitContext.pullRequestUrl = pullRequestUrl;
|
|
1965
|
+
}
|
|
1850
1966
|
if (branchIsNew || prIsNew) {
|
|
1851
1967
|
poster.reportWorkingBranch(normalizedBranch, nextPr);
|
|
1852
1968
|
}
|
|
@@ -2082,11 +2198,16 @@ export async function runChimphands(opts) {
|
|
|
2082
2198
|
void pollPendingUserMessages();
|
|
2083
2199
|
}, 1500);
|
|
2084
2200
|
if (runtimeId && attachUrl) {
|
|
2085
|
-
|
|
2201
|
+
const tunnel = startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, {
|
|
2086
2202
|
onUserFileEdit: handleUserFileEdit,
|
|
2087
2203
|
onCancelTurn: handleCancelTurn,
|
|
2088
2204
|
onUserMessage: enqueueUserMessage,
|
|
2089
2205
|
});
|
|
2206
|
+
poster.sendUiMessage = (frame) => tunnel.sendUiMessage(frame);
|
|
2207
|
+
stopTunnel = () => {
|
|
2208
|
+
poster.sendUiMessage = null;
|
|
2209
|
+
tunnel.stop();
|
|
2210
|
+
};
|
|
2090
2211
|
}
|
|
2091
2212
|
const shutdownRuntime = async () => {
|
|
2092
2213
|
sessionActive = false;
|
|
@@ -2113,6 +2234,7 @@ export async function runChimphands(opts) {
|
|
|
2113
2234
|
status: "CHIMPHANDS_RUNTIME_STATUS_TERMINATED",
|
|
2114
2235
|
}).catch(() => { });
|
|
2115
2236
|
}
|
|
2237
|
+
activeSessionGitContext = null;
|
|
2116
2238
|
};
|
|
2117
2239
|
try {
|
|
2118
2240
|
poster.fireAndForget(ROLE_STATUS, "Agent ready", { status: STATUS_RUNNING });
|
|
@@ -2134,6 +2256,32 @@ export async function runChimphands(opts) {
|
|
|
2134
2256
|
undefined;
|
|
2135
2257
|
enqueueUserMessage({ id: id || undefined, content });
|
|
2136
2258
|
}
|
|
2259
|
+
let sessionBaseBranch = resolveSessionBaseBranch(prompt, process.env.CHIMPHANDS_WORK_BRANCH);
|
|
2260
|
+
activeSessionGitContext = {
|
|
2261
|
+
baseBranch: sessionBaseBranch,
|
|
2262
|
+
agentBranch: workingBranch && isAgentBranchName(workingBranch) ? workingBranch : undefined,
|
|
2263
|
+
pullRequestUrl,
|
|
2264
|
+
onPullRequest: (url) => {
|
|
2265
|
+
if (workingBranch)
|
|
2266
|
+
noteWorkingBranch(workingBranch, url);
|
|
2267
|
+
},
|
|
2268
|
+
};
|
|
2269
|
+
try {
|
|
2270
|
+
const branchSetup = await ensureAgentSessionBranch({
|
|
2271
|
+
sessionId,
|
|
2272
|
+
baseBranch: sessionBaseBranch,
|
|
2273
|
+
existingAgentBranch: workingBranch && isAgentBranchName(workingBranch) ? workingBranch : undefined,
|
|
2274
|
+
});
|
|
2275
|
+
sessionBaseBranch = branchSetup.baseBranch || sessionBaseBranch;
|
|
2276
|
+
activeSessionGitContext.baseBranch = sessionBaseBranch;
|
|
2277
|
+
activeSessionGitContext.agentBranch = branchSetup.agentBranch;
|
|
2278
|
+
if (!workingBranch || workingBranch !== branchSetup.agentBranch) {
|
|
2279
|
+
noteWorkingBranch(branchSetup.agentBranch);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
catch (err) {
|
|
2283
|
+
console.error(`ChimpHands ensure agent branch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2284
|
+
}
|
|
2137
2285
|
const waitForNextPrompt = () => new Promise((resolve) => {
|
|
2138
2286
|
let lastPollAt = 0;
|
|
2139
2287
|
let settled = false;
|
|
@@ -2189,7 +2337,7 @@ export async function runChimphands(opts) {
|
|
|
2189
2337
|
flushPendingMidTurnInject();
|
|
2190
2338
|
let useOpencodeSessionId = opencodeSessionId;
|
|
2191
2339
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
2192
|
-
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
2340
|
+
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl, sessionBaseBranch);
|
|
2193
2341
|
const turnPostEvent = (role, content, opts) => postEventForTurn(prompt, effectivePrompt, role, content, opts);
|
|
2194
2342
|
// Visible in chat (not filtered as routine). OpenCode may not emit text until a
|
|
2195
2343
|
// part completes — without this the UI looks empty while the turn is running.
|
|
@@ -2213,7 +2361,7 @@ export async function runChimphands(opts) {
|
|
|
2213
2361
|
console.error(`ChimpHands OpenCode session ${useOpencodeSessionId} missing on runner; starting fresh thread.`);
|
|
2214
2362
|
opencodeSessionId = undefined;
|
|
2215
2363
|
isNewOpencodeSession = true;
|
|
2216
|
-
effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, true, workingBranch, pullRequestUrl);
|
|
2364
|
+
effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, true, workingBranch, pullRequestUrl, sessionBaseBranch);
|
|
2217
2365
|
result = await runOpencode(effectivePrompt, opencodeModel, childEnv, undefined, {
|
|
2218
2366
|
onSessionId: (id) => {
|
|
2219
2367
|
opencodeSessionId = id;
|
|
@@ -2350,23 +2498,9 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId, onUiAttached) {
|
|
|
2350
2498
|
stopped = true;
|
|
2351
2499
|
};
|
|
2352
2500
|
}
|
|
2353
|
-
/** Commit+push dirty worktree on the session branch before idle/teardown
|
|
2501
|
+
/** Commit+push dirty worktree on the session agent branch before idle/teardown. */
|
|
2354
2502
|
async function commitAndPushDirtyWorktree(message) {
|
|
2355
|
-
const run = (args, env) =>
|
|
2356
|
-
const child = spawn("git", args, {
|
|
2357
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2358
|
-
env: env ? { ...process.env, ...env } : process.env,
|
|
2359
|
-
});
|
|
2360
|
-
let out = "";
|
|
2361
|
-
let err = "";
|
|
2362
|
-
child.stdout.on("data", (d) => {
|
|
2363
|
-
out += d.toString();
|
|
2364
|
-
});
|
|
2365
|
-
child.stderr.on("data", (d) => {
|
|
2366
|
-
err += d.toString();
|
|
2367
|
-
});
|
|
2368
|
-
child.on("close", (code) => resolve({ code: code ?? 1, out, err }));
|
|
2369
|
-
});
|
|
2503
|
+
const run = (args, env) => runGit(args, env);
|
|
2370
2504
|
try {
|
|
2371
2505
|
const branch = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2372
2506
|
if (branch.code !== 0) {
|
|
@@ -2375,11 +2509,14 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2375
2509
|
return { ok: false, error };
|
|
2376
2510
|
}
|
|
2377
2511
|
const current = branch.out.trim();
|
|
2378
|
-
if (!current || current === "HEAD" ||
|
|
2379
|
-
const error = `refusing branch "${current || "(unknown)"}"`;
|
|
2512
|
+
if (!current || current === "HEAD" || !isAgentBranchName(current)) {
|
|
2513
|
+
const error = `refusing non-agent branch "${current || "(unknown)"}" (expected testchimp-*)`;
|
|
2380
2514
|
console.error(`ChimpHands skip commit-before-idle: ${error}`);
|
|
2381
2515
|
return { ok: false, error };
|
|
2382
2516
|
}
|
|
2517
|
+
if (activeSessionGitContext) {
|
|
2518
|
+
activeSessionGitContext.agentBranch = current;
|
|
2519
|
+
}
|
|
2383
2520
|
const status = await run(["status", "--porcelain"]);
|
|
2384
2521
|
if (status.code !== 0) {
|
|
2385
2522
|
const error = status.err || status.out || "git status failed";
|
|
@@ -2434,6 +2571,22 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2434
2571
|
}
|
|
2435
2572
|
}
|
|
2436
2573
|
console.error(`ChimpHands committed and pushed dirty worktree on ${current} before shutdown`);
|
|
2574
|
+
const ctx = activeSessionGitContext;
|
|
2575
|
+
if (ctx &&
|
|
2576
|
+
isPrOnlyMode() &&
|
|
2577
|
+
!ctx.pullRequestUrl &&
|
|
2578
|
+
ctx.baseBranch &&
|
|
2579
|
+
isAgentBranchName(current)) {
|
|
2580
|
+
const prUrl = await ensurePullRequest({
|
|
2581
|
+
agentBranch: current,
|
|
2582
|
+
baseBranch: ctx.baseBranch,
|
|
2583
|
+
});
|
|
2584
|
+
if (prUrl) {
|
|
2585
|
+
ctx.pullRequestUrl = prUrl;
|
|
2586
|
+
ctx.onPullRequest?.(prUrl);
|
|
2587
|
+
console.error(`ChimpHands opened PR: ${prUrl}`);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2437
2590
|
return { ok: true };
|
|
2438
2591
|
}
|
|
2439
2592
|
catch (err) {
|
|
@@ -2482,6 +2635,18 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
|
|
|
2482
2635
|
reconnectTimer = null;
|
|
2483
2636
|
}
|
|
2484
2637
|
};
|
|
2638
|
+
const sendUiMessage = (frame) => {
|
|
2639
|
+
const socket = ws;
|
|
2640
|
+
if (!socket || socket.readyState !== 1)
|
|
2641
|
+
return false;
|
|
2642
|
+
try {
|
|
2643
|
+
socket.send(JSON.stringify(frame));
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2646
|
+
catch {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2485
2650
|
const handleHttpRequest = async (socket, req) => {
|
|
2486
2651
|
if (!req.requestId || socket.readyState !== 1)
|
|
2487
2652
|
return;
|
|
@@ -2661,18 +2826,21 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
|
|
|
2661
2826
|
});
|
|
2662
2827
|
};
|
|
2663
2828
|
void connect();
|
|
2664
|
-
return
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2829
|
+
return {
|
|
2830
|
+
sendUiMessage,
|
|
2831
|
+
stop: () => {
|
|
2832
|
+
stopped = true;
|
|
2833
|
+
clearReconnect();
|
|
2834
|
+
if (ws) {
|
|
2835
|
+
try {
|
|
2836
|
+
ws.close();
|
|
2837
|
+
}
|
|
2838
|
+
catch {
|
|
2839
|
+
// ignore
|
|
2840
|
+
}
|
|
2841
|
+
ws = null;
|
|
2673
2842
|
}
|
|
2674
|
-
|
|
2675
|
-
}
|
|
2843
|
+
},
|
|
2676
2844
|
};
|
|
2677
2845
|
}
|
|
2678
2846
|
/** Runtime-aware entry: register + attach to local OpenCode server (Phase 1+). */
|
package/dist/cli/program.js
CHANGED
|
@@ -1607,7 +1607,7 @@ export function buildCliProgram() {
|
|
|
1607
1607
|
chimphands
|
|
1608
1608
|
.command("report-branch")
|
|
1609
1609
|
.description("Report the conversation working branch (and optional PR URL) to TestChimp")
|
|
1610
|
-
.requiredOption("--branch <name>", "
|
|
1610
|
+
.requiredOption("--branch <name>", "Agent feature branch name (testchimp-* only)")
|
|
1611
1611
|
.option("--pr-url <url>", "Open pull request URL")
|
|
1612
1612
|
.option("--session-id <id>", "ChimpHands session id (or SESSION_ID env)")
|
|
1613
1613
|
.action(async (opts) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testchimp/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.71",
|
|
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",
|
|
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",
|
|
17
17
|
"watch": "tsc --watch",
|
|
18
18
|
"start": "node dist/bin/testchimp.js",
|
|
19
19
|
"prepublishOnly": "npm run build"
|