From b89077d12d09d42de60cfe063039d45f7659b0be Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 25 Jul 2026 00:05:50 +0000 Subject: [PATCH 1/2] fix: preserve Rust release commits --- js/test/rust-release-script.test.mjs | 25 ++++++++ rust/CHANGELOG.md | 59 +++++++++++++++++++ rust/Cargo.lock | 2 +- rust/Cargo.toml | 2 +- ...20260426_181000_read_only_planning_mode.md | 10 ---- ...260426_193000_sync_hive_mind_2026_04_26.md | 6 -- .../20260430_ci_docs_release_parity.md | 11 ---- .../changelog.d/20260430_prompt_file_input.md | 11 ---- .../20260501_071500_execution_passthrough.md | 7 --- ...60501_071700_normalized_result_metadata.md | 8 --- ...20260501_091500_release_latest_recovery.md | 7 --- ...1_123000_qwen_gemini_prompt_passthrough.md | 7 --- .../20260617_120000_agent_permission_mode.md | 7 --- ...60617_140000_per_command_approval_relay.md | 7 --- ...20260724_223500_interactive_tui_capture.md | 7 --- scripts/rust/staged-changes.mjs | 22 +++++++ scripts/rust/version-and-commit.mjs | 10 ++-- 17 files changed, 112 insertions(+), 96 deletions(-) create mode 100644 js/test/rust-release-script.test.mjs delete mode 100644 rust/changelog.d/20260426_181000_read_only_planning_mode.md delete mode 100644 rust/changelog.d/20260426_193000_sync_hive_mind_2026_04_26.md delete mode 100644 rust/changelog.d/20260430_ci_docs_release_parity.md delete mode 100644 rust/changelog.d/20260430_prompt_file_input.md delete mode 100644 rust/changelog.d/20260501_071500_execution_passthrough.md delete mode 100644 rust/changelog.d/20260501_071700_normalized_result_metadata.md delete mode 100644 rust/changelog.d/20260501_091500_release_latest_recovery.md delete mode 100644 rust/changelog.d/20260501_123000_qwen_gemini_prompt_passthrough.md delete mode 100644 rust/changelog.d/20260617_120000_agent_permission_mode.md delete mode 100644 rust/changelog.d/20260617_140000_per_command_approval_relay.md delete mode 100644 rust/changelog.d/20260724_223500_interactive_tui_capture.md create mode 100644 scripts/rust/staged-changes.mjs diff --git a/js/test/rust-release-script.test.mjs b/js/test/rust-release-script.test.mjs new file mode 100644 index 0000000..b9f3399 --- /dev/null +++ b/js/test/rust-release-script.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { hasStagedChanges } from '../../scripts/rust/staged-changes.mjs'; + +function commandStreamResult(exitCode, stderr = '') { + return () => ({ + run: async () => ({ exitCode, stderr }), + }); +} + +test('Rust release recognizes git diff exit 1 as staged changes', async () => { + assert.equal(await hasStagedChanges(commandStreamResult(1)), true); +}); + +test('Rust release recognizes git diff exit 0 as a clean index', async () => { + assert.equal(await hasStagedChanges(commandStreamResult(0)), false); +}); + +test('Rust release preserves unexpected git failures', async () => { + await assert.rejects( + hasStagedChanges(commandStreamResult(128, 'fatal: corrupt index')), + /exit code 128: fatal: corrupt index/u + ); +}); diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index 8bcae4a..f23f7d2 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -6,3 +6,62 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.7] - 2026-07-24 + +### Added + +- Added enforceable read-only planning mode for supported tools and clear rejection for unsupported tools. + +### Fixed + +- Removed JavaScript command execution's dependency on runtime CDN imports, avoiding Deno CI failures when `esm.sh` is unavailable. + +### Changed + +- Synced model maps from hive-mind v1.57.2 (issue #22): Claude `opus` -> `claude-opus-4-7`; Codex default `gpt-5` -> `gpt-5.5` and added the `gpt-5.5/5.4/5.3/5.2/5.1` family; Agent default `minimax-m2.5-free` -> `nemotron-3-super-free` with `qwen3.6-plus-free` retained as deprecated. + +### Added + +- Added Rust package README metadata and language-specific GitHub Release tags. + +### Fixed + +- Fixed Rust CLI pass-through for Claude fallback, session, prompt append, verbose, and replay options. + +### Fixed + +- Pipe prompts for stdin-based tools through temporary prompt files during execution so large generated prompts are not embedded in nested shell commands. + +### Added + +- Added `--prompt-file` / `prompt_file` support for callers that already have prompt content on disk. + +### Added + +- Added native execution passthrough controls for Claude, Codex, OpenCode, and @link-assistant/agent, including custom executables, raw tool environment variables, raw tool arguments, and suppressible default safety bypass flags. + +### Added + +- Expose normalized result metadata on `AgentResult`, including success classification, session IDs, usage-limit details, summaries, cost estimates, stream token usage, sub-agent calls, and execution error information for `claude`, `codex`, `opencode`, and `agent`. +- Add `AgentResult::usage` for Rust parity with the JavaScript result shape. + +### Fixed + +- Fixed Rust release recovery when a GitHub release tag exists without a matching crates.io publication, and made Rust GitHub releases explicitly become the repository latest release. + +### Fixed + +- Added Qwen and Gemini prompt-file stdin handling plus native executable, environment, and raw argument passthrough parity. + +### Added + +- Map `--read-only` and `--plan-only` for the `agent` tool to its native `--permission-mode` (agent v0.24.0): `--read-only` → `readonly` and `--plan-only` → `plan`. The `agent` tool now supports enforceable read-only/planning mode instead of being rejected, and exposes typed `permission_mode` and `permission` (OpenCode-compatible JSON policy) passthrough options. + +### Added + +- Add a uniform per-command approval relay ("ask" mode). The new `--approve-each` flag (alias `--permission-mode ask`, `AgentOptions.approve_each`) maps to each backend's native per-command approval mechanism and relays native permission prompts as normalized `permission_request` events (carrying an opaque `id`, `tool`, `command`/`pattern`, `title`, and a `scope`), forwarding the consumer's normalized decision (`once` | `always` | `reject`) back in the native wire format. Supported (relayable) for `agent` (`--permission-mode ask` + `--input-format stream-json`, scope `session`) and `claude` (stream-json `can_use_tool`, scope `tool-input`); rejected with a clear error for `codex`, `qwen`, `gemini`, and `opencode`. Adds a `permissions` module (`normalize_permission_request`, `build_permission_response`, `PermissionRelay`, `permission_parity`, `supports_ask`, `ASK_DECISIONS`, `ask_scope`). + +### Added + +- Add real PTY-backed interactive agent capture with input, control-key, resize, unrolled transcripts, semantic events, and replay artifacts for Claude, Codex, OpenCode, Agent, Gemini, and Qwen. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 26474d6..c0a2bdd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agent-commander" -version = "0.1.0" +version = "0.2.7" dependencies = [ "command-stream", "serde", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9a6c6bf..c136e23 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-commander" -version = "0.1.0" +version = "0.2.7" edition = "2021" description = "A Rust library to control agents enclosed in CLI commands like Anthropic Claude Code CLI" license = "Unlicense" diff --git a/rust/changelog.d/20260426_181000_read_only_planning_mode.md b/rust/changelog.d/20260426_181000_read_only_planning_mode.md deleted file mode 100644 index b86977b..0000000 --- a/rust/changelog.d/20260426_181000_read_only_planning_mode.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -bump: minor ---- - -### Added -- Added enforceable read-only planning mode for supported tools and clear rejection for unsupported tools. - -### Fixed -- Removed JavaScript command execution's dependency on runtime CDN imports, avoiding Deno CI failures when `esm.sh` is unavailable. - diff --git a/rust/changelog.d/20260426_193000_sync_hive_mind_2026_04_26.md b/rust/changelog.d/20260426_193000_sync_hive_mind_2026_04_26.md deleted file mode 100644 index 30e0240..0000000 --- a/rust/changelog.d/20260426_193000_sync_hive_mind_2026_04_26.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -bump: minor ---- - -### Changed -- Synced model maps from hive-mind v1.57.2 (issue #22): Claude `opus` -> `claude-opus-4-7`; Codex default `gpt-5` -> `gpt-5.5` and added the `gpt-5.5/5.4/5.3/5.2/5.1` family; Agent default `minimax-m2.5-free` -> `nemotron-3-super-free` with `qwen3.6-plus-free` retained as deprecated. diff --git a/rust/changelog.d/20260430_ci_docs_release_parity.md b/rust/changelog.d/20260430_ci_docs_release_parity.md deleted file mode 100644 index c14af38..0000000 --- a/rust/changelog.d/20260430_ci_docs_release_parity.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -bump: minor ---- - -### Added - -- Added Rust package README metadata and language-specific GitHub Release tags. - -### Fixed - -- Fixed Rust CLI pass-through for Claude fallback, session, prompt append, verbose, and replay options. diff --git a/rust/changelog.d/20260430_prompt_file_input.md b/rust/changelog.d/20260430_prompt_file_input.md deleted file mode 100644 index 841240c..0000000 --- a/rust/changelog.d/20260430_prompt_file_input.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -bump: patch ---- - -### Fixed - -- Pipe prompts for stdin-based tools through temporary prompt files during execution so large generated prompts are not embedded in nested shell commands. - -### Added - -- Added `--prompt-file` / `prompt_file` support for callers that already have prompt content on disk. diff --git a/rust/changelog.d/20260501_071500_execution_passthrough.md b/rust/changelog.d/20260501_071500_execution_passthrough.md deleted file mode 100644 index c708209..0000000 --- a/rust/changelog.d/20260501_071500_execution_passthrough.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -bump: minor ---- - -### Added - -- Added native execution passthrough controls for Claude, Codex, OpenCode, and @link-assistant/agent, including custom executables, raw tool environment variables, raw tool arguments, and suppressible default safety bypass flags. diff --git a/rust/changelog.d/20260501_071700_normalized_result_metadata.md b/rust/changelog.d/20260501_071700_normalized_result_metadata.md deleted file mode 100644 index 540ed2d..0000000 --- a/rust/changelog.d/20260501_071700_normalized_result_metadata.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -bump: minor ---- - -### Added - -- Expose normalized result metadata on `AgentResult`, including success classification, session IDs, usage-limit details, summaries, cost estimates, stream token usage, sub-agent calls, and execution error information for `claude`, `codex`, `opencode`, and `agent`. -- Add `AgentResult::usage` for Rust parity with the JavaScript result shape. diff --git a/rust/changelog.d/20260501_091500_release_latest_recovery.md b/rust/changelog.d/20260501_091500_release_latest_recovery.md deleted file mode 100644 index 5ec0a81..0000000 --- a/rust/changelog.d/20260501_091500_release_latest_recovery.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -bump: patch ---- - -### Fixed - -- Fixed Rust release recovery when a GitHub release tag exists without a matching crates.io publication, and made Rust GitHub releases explicitly become the repository latest release. diff --git a/rust/changelog.d/20260501_123000_qwen_gemini_prompt_passthrough.md b/rust/changelog.d/20260501_123000_qwen_gemini_prompt_passthrough.md deleted file mode 100644 index 651a600..0000000 --- a/rust/changelog.d/20260501_123000_qwen_gemini_prompt_passthrough.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -bump: patch ---- - -### Fixed - -- Added Qwen and Gemini prompt-file stdin handling plus native executable, environment, and raw argument passthrough parity. diff --git a/rust/changelog.d/20260617_120000_agent_permission_mode.md b/rust/changelog.d/20260617_120000_agent_permission_mode.md deleted file mode 100644 index a44a99c..0000000 --- a/rust/changelog.d/20260617_120000_agent_permission_mode.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -bump: minor ---- - -### Added - -- Map `--read-only` and `--plan-only` for the `agent` tool to its native `--permission-mode` (agent v0.24.0): `--read-only` → `readonly` and `--plan-only` → `plan`. The `agent` tool now supports enforceable read-only/planning mode instead of being rejected, and exposes typed `permission_mode` and `permission` (OpenCode-compatible JSON policy) passthrough options. diff --git a/rust/changelog.d/20260617_140000_per_command_approval_relay.md b/rust/changelog.d/20260617_140000_per_command_approval_relay.md deleted file mode 100644 index 5cf74cc..0000000 --- a/rust/changelog.d/20260617_140000_per_command_approval_relay.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -bump: minor ---- - -### Added - -- Add a uniform per-command approval relay ("ask" mode). The new `--approve-each` flag (alias `--permission-mode ask`, `AgentOptions.approve_each`) maps to each backend's native per-command approval mechanism and relays native permission prompts as normalized `permission_request` events (carrying an opaque `id`, `tool`, `command`/`pattern`, `title`, and a `scope`), forwarding the consumer's normalized decision (`once` | `always` | `reject`) back in the native wire format. Supported (relayable) for `agent` (`--permission-mode ask` + `--input-format stream-json`, scope `session`) and `claude` (stream-json `can_use_tool`, scope `tool-input`); rejected with a clear error for `codex`, `qwen`, `gemini`, and `opencode`. Adds a `permissions` module (`normalize_permission_request`, `build_permission_response`, `PermissionRelay`, `permission_parity`, `supports_ask`, `ASK_DECISIONS`, `ask_scope`). diff --git a/rust/changelog.d/20260724_223500_interactive_tui_capture.md b/rust/changelog.d/20260724_223500_interactive_tui_capture.md deleted file mode 100644 index 27e8c09..0000000 --- a/rust/changelog.d/20260724_223500_interactive_tui_capture.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -bump: minor ---- - -### Added - -- Add real PTY-backed interactive agent capture with input, control-key, resize, unrolled transcripts, semantic events, and replay artifacts for Claude, Codex, OpenCode, Agent, Gemini, and Qwen. diff --git a/scripts/rust/staged-changes.mjs b/scripts/rust/staged-changes.mjs new file mode 100644 index 0000000..068cbba --- /dev/null +++ b/scripts/rust/staged-changes.mjs @@ -0,0 +1,22 @@ +/** + * Report whether the index contains changes while preserving real git errors. + * + * `git diff --quiet` uses exit 1 as a normal "different" result. Command-stream + * resolves that result, so callers must inspect the exit code explicitly. + */ +export async function hasStagedChanges($) { + const result = await $`git diff --cached --quiet`.run({ + capture: true, + mirror: false, + }); + + if (result.exitCode === 0) return false; + if (result.exitCode === 1) return true; + + const detail = result.stderr?.trim(); + throw new Error( + `git diff --cached --quiet failed with exit code ${result.exitCode}${ + detail ? `: ${detail}` : "" + }`, + ); +} diff --git a/scripts/rust/version-and-commit.mjs b/scripts/rust/version-and-commit.mjs index 96e8147..7c00302 100644 --- a/scripts/rust/version-and-commit.mjs +++ b/scripts/rust/version-and-commit.mjs @@ -25,6 +25,7 @@ import { fetchCratesVersions, selectRustReleaseVersion, } from "./release-registry.mjs"; +import { hasStagedChanges } from "./staged-changes.mjs"; // Load use-m dynamically const { use } = eval( @@ -305,16 +306,13 @@ async function main() { // Stage Cargo.toml, collected changelog, and removed fragments await $`git add Cargo.toml CHANGELOG.md changelog.d`; - // Check if there are changes to commit - try { - await $`git diff --cached --quiet`.run({ capture: true }); - // No changes to commit + // command-stream returns non-zero exit codes instead of throwing. Exit 1 + // from `git diff --quiet` means that the staged release commit is needed. + if (!(await hasStagedChanges($))) { console.log("No changes to commit"); setOutput("version_committed", "false"); setOutput("new_version", newVersion); return; - } catch { - // There are changes to commit (git diff exits with 1 when there are differences) } // Commit changes From b19e3f97babb332fe202590e20ec92e5fed93a87 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 25 Jul 2026 00:14:27 +0000 Subject: [PATCH 2/2] fix(js): drive startup prompts before TUI input --- js/.changeset/fresh-waves-start.md | 7 +++++++ js/README.md | 4 ++++ js/src/tui.mjs | 15 +++++++++++++-- js/test/fixtures/fake-agent-tui.mjs | 18 ++++++++++++------ js/test/tui.test.mjs | 22 ++++++++++++++++++++++ 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 js/.changeset/fresh-waves-start.md diff --git a/js/.changeset/fresh-waves-start.md b/js/.changeset/fresh-waves-start.md new file mode 100644 index 0000000..7fdaa9e --- /dev/null +++ b/js/.changeset/fresh-waves-start.md @@ -0,0 +1,7 @@ +--- +'agent-commander': patch +--- + +Add pre-prompt TUI interactions for first-run and workspace-trust dialogs, and +default interactive launches to `TERM=xterm-256color` while preserving explicit +caller overrides. diff --git a/js/README.md b/js/README.md index 604a1cd..f18c1fd 100644 --- a/js/README.md +++ b/js/README.md @@ -97,6 +97,10 @@ const capture = await captureAgentTui({ tool: 'codex', workingDirectory: '/tmp/project', prompt: 'Inspect the failing test', + promptAfter: 'What do you want to work on?', + startupInteractions: [ + { after: 'Do you trust the contents of this directory?', key: 'ENTER' }, + ], cols: 100, rows: 30, interactions: [ diff --git a/js/src/tui.mjs b/js/src/tui.mjs index 89bffcc..d2bb8d2 100644 --- a/js/src/tui.mjs +++ b/js/src/tui.mjs @@ -137,7 +137,13 @@ const launchEnvironment = ({ extraEnv, tool, readOnly }) => { if (tool === 'opencode' && readOnly) { entries.push(['OPENCODE_PERMISSION', READ_ONLY_OPENCODE_PERMISSION]); } - return { ...process.env, ...Object.fromEntries(entries) }; + return { + ...process.env, + // Interactive clients such as Codex refuse to start their TUI when the + // parent process inherited TERM=dumb (common in CI). + TERM: 'xterm-256color', + ...Object.fromEntries(entries), + }; }; /** @@ -227,6 +233,7 @@ export const captureAgentTui = async (options) => { systemPrompt, promptAfter, promptKey = 'ENTER', + startupInteractions = [], interactions = [], cols, rows, @@ -250,7 +257,11 @@ export const captureAgentTui = async (options) => { cols, rows, settleMilliseconds, - interactions: [...promptInteraction, ...interactions], + interactions: [ + ...startupInteractions, + ...promptInteraction, + ...interactions, + ], stopMarker, stopMarkerGraceMilliseconds, timeoutMilliseconds, diff --git a/js/test/fixtures/fake-agent-tui.mjs b/js/test/fixtures/fake-agent-tui.mjs index 6ae0624..ea74275 100644 --- a/js/test/fixtures/fake-agent-tui.mjs +++ b/js/test/fixtures/fake-agent-tui.mjs @@ -1,7 +1,8 @@ const clear = '\u001b[2J\u001b[H'; const tool = process.argv[2]; const initialDimensions = `${process.stdout.columns}x${process.stdout.rows}`; -let state = `ready:${tool}:${initialDimensions}`; +let phase = 'startup'; +let state = `trust:${tool}`; process.stdin.setRawMode?.(true); process.stdin.resume(); @@ -11,11 +12,16 @@ let input = ''; process.stdin.on('data', (chunk) => { for (const character of chunk.toString()) { if (character === '\r') { - state = [ - `user:${input}`, - 'tool_call:read README.md', - `waiting-resize:${process.stdout.columns}x${process.stdout.rows}`, - ].join('\n'); + if (phase === 'startup') { + phase = 'prompt'; + state = `ready:${tool}:${initialDimensions}:accepted=${input}`; + } else { + state = [ + `user:${input}`, + 'tool_call:read README.md', + `waiting-resize:${process.stdout.columns}x${process.stdout.rows}`, + ].join('\n'); + } process.stdout.write(`${clear}${state}`); input = ''; } else { diff --git a/js/test/tui.test.mjs b/js/test/tui.test.mjs index 82ff7b0..a8e4946 100644 --- a/js/test/tui.test.mjs +++ b/js/test/tui.test.mjs @@ -26,11 +26,20 @@ test('buildAgentTuiLaunch selects interactive mode for every supported tool', () assert.strictEqual(launch.file, `${tool}-test`); assert.strictEqual(launch.cwd, '/workspace'); + assert.strictEqual(launch.env.TERM, 'xterm-256color'); assert.ok(!launch.args.includes('exec'), tool); assert.ok(!launch.args.includes('run'), tool); assert.ok(!launch.args.includes('--json'), tool); assert.ok(!launch.args.includes('stream-json'), tool); } + + const explicitTerm = buildAgentTuiLaunch({ + tool: 'codex', + workingDirectory: '/workspace', + executable: 'codex-test', + extraEnv: { TERM: 'screen-256color' }, + }); + assert.strictEqual(explicitTerm.env.TERM, 'screen-256color'); }); test('buildAgentTuiLaunch maps interactive safety and resume options', () => { @@ -105,6 +114,14 @@ test( executable: process.execPath, prefixArgs: [join(directory, 'fixtures/fake-agent-tui.mjs'), tool], prompt: 'hello', + promptAfter: `ready:${tool}`, + startupInteractions: [ + { + after: `trust:${tool}`, + text: 'y', + key: 'ENTER', + }, + ], cols: 24, rows: 6, interactions: [ @@ -117,6 +134,11 @@ test( }); assert.strictEqual(capture.exitCode, 0, tool); + assert.strictEqual(capture.interactionCount, 3, tool); + assert.ok( + capture.transcript.replace(/\s/gu, '').includes('accepted=y'), + tool + ); assert.deepStrictEqual( capture.events.map(({ type }) => type), ['message', 'tool_call', 'message'],