Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions js/.changeset/fresh-waves-start.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
15 changes: 13 additions & 2 deletions js/src/tui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};

/**
Expand Down Expand Up @@ -227,6 +233,7 @@ export const captureAgentTui = async (options) => {
systemPrompt,
promptAfter,
promptKey = 'ENTER',
startupInteractions = [],
interactions = [],
cols,
rows,
Expand All @@ -250,7 +257,11 @@ export const captureAgentTui = async (options) => {
cols,
rows,
settleMilliseconds,
interactions: [...promptInteraction, ...interactions],
interactions: [
...startupInteractions,
...promptInteraction,
...interactions,
],
stopMarker,
stopMarkerGraceMilliseconds,
timeoutMilliseconds,
Expand Down
18 changes: 12 additions & 6 deletions js/test/fixtures/fake-agent-tui.mjs
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions js/test/rust-release-script.test.mjs
Original file line number Diff line number Diff line change
@@ -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
);
});
22 changes: 22 additions & 0 deletions js/test/tui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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: [
Expand All @@ -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'],
Expand Down
59 changes: 59 additions & 0 deletions rust/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<!-- changelog-insert-here -->

## [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.
2 changes: 1 addition & 1 deletion rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 0 additions & 10 deletions rust/changelog.d/20260426_181000_read_only_planning_mode.md

This file was deleted.

6 changes: 0 additions & 6 deletions rust/changelog.d/20260426_193000_sync_hive_mind_2026_04_26.md

This file was deleted.

11 changes: 0 additions & 11 deletions rust/changelog.d/20260430_ci_docs_release_parity.md

This file was deleted.

11 changes: 0 additions & 11 deletions rust/changelog.d/20260430_prompt_file_input.md

This file was deleted.

7 changes: 0 additions & 7 deletions rust/changelog.d/20260501_071500_execution_passthrough.md

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions rust/changelog.d/20260501_091500_release_latest_recovery.md

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions rust/changelog.d/20260617_120000_agent_permission_mode.md

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions rust/changelog.d/20260724_223500_interactive_tui_capture.md

This file was deleted.

22 changes: 22 additions & 0 deletions scripts/rust/staged-changes.mjs
Original file line number Diff line number Diff line change
@@ -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}` : ""
}`,
);
}
10 changes: 4 additions & 6 deletions scripts/rust/version-and-commit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
fetchCratesVersions,
selectRustReleaseVersion,
} from "./release-registry.mjs";
import { hasStagedChanges } from "./staged-changes.mjs";

// Load use-m dynamically
const { use } = eval(
Expand Down Expand Up @@ -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
Expand Down
Loading