Compare commits

..

3 Commits

Author SHA1 Message Date
cexll
716d1eb173 fix(do): isolate stop hook by task_id to prevent concurrent task interference
When running multiple do tasks concurrently in worktrees, the stop hook
would scan all do.*.local.md files and block exit for unrelated tasks.

Changes:
- setup-do.sh: export DO_TASK_ID for hook environment
- stop-hook.sh: filter state files by DO_TASK_ID when set, fallback to
  scanning all files for backward compatibility

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 16:01:24 +08:00
cexll
4bc9ffa907 fix(cli): resolve process hang after install and sync version with tag
- Add process.stdin.pause() in cleanup() to properly exit event loop
- Pass tag via CODEAGENT_WRAPPER_VERSION env to install.sh
- Support versioned release URL in install.sh

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-01-28 15:08:24 +08:00
cexll
c6c2f93e02 fix(codeagent-wrapper): skip tmpdir tests on Windows
ensureExecutableTempDir is intentionally no-op on Windows,
so tests should be skipped on that platform.

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-01-28 13:10:42 +08:00
5 changed files with 34 additions and 10 deletions

View File

@@ -501,7 +501,7 @@ async function updateInstalledModules(installDir, tag, config, dryRun) {
await fs.promises.mkdir(installDir, { recursive: true }); await fs.promises.mkdir(installDir, { recursive: true });
for (const name of toUpdate) { for (const name of toUpdate) {
process.stdout.write(`Updating module: ${name}\n`); process.stdout.write(`Updating module: ${name}\n`);
const r = await applyModule(name, config, repoRoot, installDir, true); const r = await applyModule(name, config, repoRoot, installDir, true, tag);
upsertModuleStatus(installDir, r); upsertModuleStatus(installDir, r);
} }
} finally { } finally {
@@ -560,6 +560,7 @@ async function promptMultiSelect(items, title) {
function cleanup() { function cleanup() {
process.stdin.setRawMode(false); process.stdin.setRawMode(false);
process.stdin.removeListener("keypress", onKey); process.stdin.removeListener("keypress", onKey);
process.stdin.pause();
} }
function onKey(_, key) { function onKey(_, key) {
@@ -749,14 +750,16 @@ async function mergeDir(src, installDir, force) {
return installed; return installed;
} }
function runInstallSh(repoRoot, installDir) { function runInstallSh(repoRoot, installDir, tag) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cmd = process.platform === "win32" ? "cmd.exe" : "bash"; const cmd = process.platform === "win32" ? "cmd.exe" : "bash";
const args = process.platform === "win32" ? ["/c", "install.bat"] : ["install.sh"]; const args = process.platform === "win32" ? ["/c", "install.bat"] : ["install.sh"];
const env = { ...process.env, INSTALL_DIR: installDir };
if (tag) env.CODEAGENT_WRAPPER_VERSION = tag;
const p = spawn(cmd, args, { const p = spawn(cmd, args, {
cwd: repoRoot, cwd: repoRoot,
stdio: "inherit", stdio: "inherit",
env: { ...process.env, INSTALL_DIR: installDir }, env,
}); });
p.on("exit", (code) => { p.on("exit", (code) => {
if (code === 0) resolve(); if (code === 0) resolve();
@@ -774,7 +777,7 @@ async function rmTree(p) {
await fs.promises.rmdir(p, { recursive: true }); await fs.promises.rmdir(p, { recursive: true });
} }
async function applyModule(moduleName, config, repoRoot, installDir, force) { async function applyModule(moduleName, config, repoRoot, installDir, force, tag) {
const mod = config && config.modules && config.modules[moduleName]; const mod = config && config.modules && config.modules[moduleName];
if (!mod) throw new Error(`Unknown module: ${moduleName}`); if (!mod) throw new Error(`Unknown module: ${moduleName}`);
const ops = Array.isArray(mod.operations) ? mod.operations : []; const ops = Array.isArray(mod.operations) ? mod.operations : [];
@@ -800,7 +803,7 @@ async function applyModule(moduleName, config, repoRoot, installDir, force) {
if (cmd !== "bash install.sh") { if (cmd !== "bash install.sh") {
throw new Error(`Refusing run_command: ${cmd || "(empty)"}`); throw new Error(`Refusing run_command: ${cmd || "(empty)"}`);
} }
await runInstallSh(repoRoot, installDir); await runInstallSh(repoRoot, installDir, tag);
} else { } else {
throw new Error(`Unsupported operation type: ${type}`); throw new Error(`Unsupported operation type: ${type}`);
} }
@@ -964,12 +967,12 @@ async function installSelected(picks, tag, config, installDir, force, dryRun) {
for (const p of picks) { for (const p of picks) {
if (p.kind === "wrapper") { if (p.kind === "wrapper") {
process.stdout.write("Installing codeagent-wrapper...\n"); process.stdout.write("Installing codeagent-wrapper...\n");
await runInstallSh(repoRoot, installDir); await runInstallSh(repoRoot, installDir, tag);
continue; continue;
} }
if (p.kind === "module") { if (p.kind === "module") {
process.stdout.write(`Installing module: ${p.moduleName}\n`); process.stdout.write(`Installing module: ${p.moduleName}\n`);
const r = await applyModule(p.moduleName, config, repoRoot, installDir, force); const r = await applyModule(p.moduleName, config, repoRoot, installDir, force, tag);
upsertModuleStatus(installDir, r); upsertModuleStatus(installDir, r);
continue; continue;
} }

View File

@@ -3,10 +3,14 @@ package wrapper
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
) )
func TestEnsureExecutableTempDir_Override(t *testing.T) { func TestEnsureExecutableTempDir_Override(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("ensureExecutableTempDir is no-op on Windows")
}
restore := captureTempEnv() restore := captureTempEnv()
t.Cleanup(restore) t.Cleanup(restore)
@@ -37,6 +41,9 @@ func TestEnsureExecutableTempDir_Override(t *testing.T) {
} }
func TestEnsureExecutableTempDir_FallbackWhenCurrentNotExecutable(t *testing.T) { func TestEnsureExecutableTempDir_FallbackWhenCurrentNotExecutable(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("ensureExecutableTempDir is no-op on Windows")
}
restore := captureTempEnv() restore := captureTempEnv()
t.Cleanup(restore) t.Cleanup(restore)

View File

@@ -24,9 +24,13 @@ esac
# Build download URL # Build download URL
REPO="cexll/myclaude" REPO="cexll/myclaude"
VERSION="latest" VERSION="${CODEAGENT_WRAPPER_VERSION:-latest}"
BINARY_NAME="codeagent-wrapper-${OS}-${ARCH}" BINARY_NAME="codeagent-wrapper-${OS}-${ARCH}"
URL="https://github.com/${REPO}/releases/${VERSION}/download/${BINARY_NAME}" if [ "$VERSION" = "latest" ]; then
URL="https://github.com/${REPO}/releases/latest/download/${BINARY_NAME}"
else
URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}"
fi
echo "Downloading codeagent-wrapper from ${URL}..." echo "Downloading codeagent-wrapper from ${URL}..."
if ! curl -fsSL "$URL" -o /tmp/codeagent-wrapper; then if ! curl -fsSL "$URL" -o /tmp/codeagent-wrapper; then

View File

@@ -29,7 +29,16 @@ project_dir="${CLAUDE_PROJECT_DIR:-$PWD}"
state_dir="${project_dir}/.claude" state_dir="${project_dir}/.claude"
shopt -s nullglob shopt -s nullglob
state_files=("${state_dir}"/do.*.local.md) if [ -n "${DO_TASK_ID:-}" ]; then
candidate="${state_dir}/do.${DO_TASK_ID}.local.md"
if [ -f "$candidate" ]; then
state_files=("$candidate")
else
state_files=()
fi
else
state_files=("${state_dir}"/do.*.local.md)
fi
shopt -u nullglob shopt -u nullglob
if [ ${#state_files[@]} -eq 0 ]; then if [ ${#state_files[@]} -eq 0 ]; then

View File

@@ -112,3 +112,4 @@ echo "Initialized: $state_file"
echo "task_id: $task_id" echo "task_id: $task_id"
echo "phase: 1/$max_phases ($phase_name)" echo "phase: 1/$max_phases ($phase_name)"
echo "completion_promise: $completion_promise" echo "completion_promise: $completion_promise"
echo "export DO_TASK_ID=$task_id"