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
80 changes: 38 additions & 42 deletions .mise/tasks/agent/_default
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
#MISE description="Start an agent session (interactive or headless)"
#MISE quiet=true
#USAGE flag "--headless" default=#false help="Non-interactive mode: process message and exit"
#USAGE flag "--session <session>" default="" help="Session file path to resume"
#USAGE flag "--session <session>" default="" help="Session ID or name to resume"
#USAGE flag "--timeout <seconds>" default="" help="Timeout in seconds (headless only, stored as session metadata — not yet enforced by sessions wake)"
#USAGE flag "--model <model>" default="" help="Model to use (required for --headless; provider-qualified, e.g. openai-codex/gpt-5.5)"
#USAGE flag "--model <model>" default="" help="Model to use (required; provider-qualified, e.g. openai-codex/gpt-5.5)"
#USAGE arg "[message]" default="" help="Initial message (required for --headless, optional for interactive)"

set -e
Expand All @@ -31,59 +31,55 @@ if [ "$HEADLESS" = "true" ] && [ -z "$MESSAGE" ]; then
exit 1
fi

if [ "$HEADLESS" = "true" ] && [ -z "$MODEL" ]; then
echo "Error: --headless requires --model <provider/model>" >&2
echo "Usage: shimmer agent --headless --model <provider/model> \"do something\"" >&2
if [ -z "$MODEL" ]; then
if [ "$HEADLESS" = "true" ]; then
echo "Error: --headless requires --model <provider/model>" >&2
echo "Usage: shimmer agent --headless --model <provider/model> \"do something\"" >&2
else
echo "Error: --model <provider/model> is required" >&2
echo "Usage: shimmer agent --model <provider/model> [\"message\"]" >&2
fi
exit 1
fi

if [ "$HEADLESS" = "true" ] && [[ "$MODEL" != */* ]]; then
if [[ "$MODEL" != */* ]]; then
echo "Error: --model must be provider-qualified (for example: openai-codex/gpt-5.5)" >&2
exit 1
fi

CWD="${SHIMMER_CALLER_PWD:-${CALLER_PWD:-.}}"
if [ "$HEADLESS" = "true" ]; then
# Headless mode creates a tracked session and launches it in the background
# via sessions new + sessions wake. The agent runs inside zmx (backgrounded),
# and can be monitored with `sessions read`.
CWD="${SHIMMER_CALLER_PWD:-${CALLER_PWD:-.}}"
SESSION_NAME="${GIT_AUTHOR_NAME}-headless-$(date +%s)"
else
SESSION_NAME="${GIT_AUTHOR_NAME}-interactive-$(date +%s)"
fi

# Prepare the long-lived agent runtime without discarding selected-agent
# environment such as GIT_AUTHOR_NAME.
shimmer_prepare_agent_child_env
# Prepare the long-lived agent runtime without discarding selected-agent
# environment such as GIT_AUTHOR_NAME.
shimmer_prepare_agent_child_env

if ! command -v sessions &>/dev/null; then
echo "sessions not found on PATH. Install it: shiv install sessions" >&2
exit 1
fi
if ! command -v sessions &>/dev/null; then
echo "sessions not found on PATH. Install it: shiv install sessions" >&2
exit 1
fi

# Create a tracked session (or resume an existing one)
if [ -n "$SESSION" ]; then
# Resuming an existing session file
SESSION_ID="$SESSION"
else
NEW_ARGS=("$SESSION_NAME" --cwd "$CWD" --meta "agent.name=$GIT_AUTHOR_NAME")
SESSION_ID=$(sessions new "${NEW_ARGS[@]}")
fi
# Create a tracked session (or resume an existing one).
if [ -n "$SESSION" ]; then
SESSION_ID="$SESSION"
else
NEW_ARGS=("$SESSION_NAME" --cwd "$CWD" --meta "agent.name=$GIT_AUTHOR_NAME")
SESSION_ID=$(sessions new "${NEW_ARGS[@]}")
fi

# Wake the session — foreground + headless (blocks until done)
WAKE_ARGS=("$SESSION_ID" --headless --message "$MESSAGE" --model "$MODEL")
# Keep foreground interactive wakes attached to the caller's TTY. Headless
# wakes also remain foreground so the command blocks until the agent exits.
WAKE_ARGS=("$SESSION_ID")
if [ "$HEADLESS" = "true" ]; then
WAKE_ARGS+=(--headless --message "$MESSAGE" --model "$MODEL")
[ -n "$TIMEOUT" ] && WAKE_ARGS+=(--meta "timeout=$TIMEOUT")
sessions wake "${WAKE_ARGS[@]}"
else
# Interactive mode calls the agent harness directly.
CWD="${SHIMMER_CALLER_PWD:-${CALLER_PWD:-.}}"
cd "$CWD"
shimmer_prepare_agent_child_env
HARNESS="${AGENT_HARNESS:-pi}"
if ! command -v "$HARNESS" &>/dev/null; then
echo "$HARNESS not found on PATH." >&2
exit 1
fi

ARGS=()
[ -n "$SESSION" ] && ARGS+=(--session "$SESSION")
[ -n "$MESSAGE" ] && ARGS+=("$MESSAGE")
exec "$HARNESS" "${ARGS[@]}"
WAKE_ARGS+=(--model "$MODEL")
[ -n "$MESSAGE" ] && WAKE_ARGS+=(--message "$MESSAGE")
fi

exec sessions wake "${WAKE_ARGS[@]}"
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Identity, dispatch, generated CI, and session plumbing for agent homes.
<p dir="rtl"><em>إلى ريموند — العمل شرف</em></p>

![tasks: 86](https://img.shields.io/badge/tasks-86-4EAA25?style=flat&logo=gnubash&logoColor=white)
[![tests: 180](https://img.shields.io/badge/tests-180-brightgreen?style=flat)](test/)
[![tests: 181](https://img.shields.io/badge/tests-181-brightgreen?style=flat)](test/)
![lints: 9](https://img.shields.io/badge/lints-9-blue?style=flat)
![workflow templates: 3](https://img.shields.io/badge/workflow%20templates-3-8b5cf6?style=flat)
[![sessions: 659](https://img.shields.io/badge/sessions-659-64748b?style=flat)](https://github.com/KnickKnackLabs/shimmer/issues/794)
Expand Down Expand Up @@ -81,6 +81,8 @@ shimmer whoami
shimmer agent --model openai-codex/gpt-5.5 "Inspect the failing workflow."
```

Interactive and headless wakes require a provider-qualified model. Interactive messages are optional; pass `--session` with a session ID or name to resume an existing conversation.

### Hosted dispatch

Dispatch through the repo that owns the target agent workflow, and put the actual target PR or issue in the packet. Use a message file for anything longer than a scalar.
Expand Down Expand Up @@ -109,13 +111,13 @@ git diff -- .github/workflows/

## What shimmer owns

| Surface | Contract |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `shimmer as <agent>` | Exports local identity, token, home path, B2 settings, and command-scope git signing config. |
| `shimmer agent` | Starts interactive or headless sessions while scrubbing task-scoped mise/caller environment before handing control to pi/sessions. |
| `agent:dispatch` | Finds the right home repo, validates provider-qualified models, preserves file-backed messages, and returns the workflow run id. |
| `workflows:generate` | Turns agent rosters and workflows.yaml manifests into reusable runner workflows, per-agent entrypoints, schedules, and mention wakes. |
| `sessions:backup` | Exports local session bundles and uploads snapshots/latest pointers when blob credentials are configured. |
| Surface | Contract |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shimmer as <agent>` | Exports local identity, token, home path, B2 settings, and command-scope git signing config. |
| `shimmer agent` | Creates or resumes interactive and headless sessions, scrubs task-scoped mise/caller environment, and wakes through the sessions-owned Pi runtime. |
| `agent:dispatch` | Finds the right home repo, validates provider-qualified models, preserves file-backed messages, and returns the workflow run id. |
| `workflows:generate` | Turns agent rosters and workflows.yaml manifests into reusable runner workflows, per-agent entrypoints, schedules, and mention wakes. |
| `sessions:backup` | Exports local session bundles and uploads snapshots/latest pointers when blob credentials are configured. |

## Task map

Expand Down
7 changes: 6 additions & 1 deletion README.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ shimmer whoami`}</CodeBlock>
{" when a local shell needs the same identity and signing posture as a hosted agent run."}
</Paragraph>
<CodeBlock lang="bash">{localIdentityFlow}</CodeBlock>
<Paragraph>
{"Interactive and headless wakes require a provider-qualified model. Interactive messages are optional; pass "}
<Code>--session</Code>
{" with a session ID or name to resume an existing conversation."}
</Paragraph>

<Heading level={3}>Hosted dispatch</Heading>
<Paragraph>
Expand All @@ -331,7 +336,7 @@ shimmer whoami`}</CodeBlock>
</TableRow>
<TableRow>
<Cell><Code>shimmer agent</Code></Cell>
<Cell>{"Starts interactive or headless sessions while scrubbing task-scoped mise/caller environment before handing control to pi/sessions."}</Cell>
<Cell>{"Creates or resumes interactive and headless sessions, scrubs task-scoped mise/caller environment, and wakes through the sessions-owned Pi runtime."}</Cell>
</TableRow>
<TableRow>
<Cell><Code>agent:dispatch</Code></Cell>
Expand Down
138 changes: 76 additions & 62 deletions test/agent/agent.bats
Original file line number Diff line number Diff line change
Expand Up @@ -351,114 +351,128 @@ MOCK

# --- Interactive mode ---

@test "interactive: calls harness without prompt injection" {
@test "interactive: requires model" {
setup_agent
mock_harness
mock_shimmer

run shimmer agent
[ "$status" -eq 0 ]

! grep -q -- "--append-system-prompt" "$HARNESS_LOG"
[ "$status" -ne 0 ]
[[ "$output" == *"--model"* ]]
}

@test "interactive: ignores inherited usage env from parent task" {
@test "interactive: requires provider-qualified model" {
setup_agent
export usage_headless="true"
export usage_model="openai-codex/gpt-5.5"
export usage_message="stale parent message"
mock_harness
mock_shimmer

run shimmer agent
[ "$status" -eq 0 ]

! grep -q -- "--append-system-prompt" "$HARNESS_LOG"
! grep -q "stale parent message" "$HARNESS_LOG"
run shimmer agent --model "gpt-5.5"
[ "$status" -ne 0 ]
[[ "$output" == *"provider-qualified"* ]]
}

@test "interactive: uses SHIMMER_CALLER_PWD as harness cwd before scrubbing" {
@test "interactive: creates and wakes a sessions-owned runtime without a message" {
setup_agent
local caller_dir="$BATS_TEST_TMPDIR/shimmer-caller"
mkdir -p "$caller_dir"
unset CALLER_PWD
export SHIMMER_CALLER_PWD="$caller_dir"
mock_harness
export AGENT_HARNESS="/usr/bin/false"
mock_sessions_binary
mock_shimmer

run shimmer agent
run shimmer agent --model "openai-codex/gpt-5.5"
[ "$status" -eq 0 ]

grep -q "^PWD=$caller_dir$" "$HARNESS_ENV_LOG"
grep -q "^new test-agent-interactive-" "$SESSIONS_LOG"
grep "^new " "$SESSIONS_LOG" | grep -q "agent.name=test-agent"
grep -q "^wake mock-session-id-001 --model openai-codex/gpt-5.5$" "$SESSIONS_LOG"
}

@test "interactive: fails clearly when sessions is unavailable after runtime PATH cleanup" {
local home="$BATS_TEST_TMPDIR/path-boundary-home"
local direct_sessions="$home/.local/share/mise/installs/shiv-sessions/0.4.1/bin"
mkdir -p "$direct_sessions"
cat > "$direct_sessions/sessions" <<'MOCK'
#!/usr/bin/env bash
echo "stale direct sessions should not run" >&2
exit 99
MOCK
chmod +x "$direct_sessions/sessions"

run env -i \
HOME="$home" \
PATH="$direct_sessions:/usr/bin:/bin" \
MISE_CONFIG_ROOT="$SHIMMER_DIR" \
GIT_AUTHOR_NAME="test-agent" \
GIT_AUTHOR_EMAIL="test-agent@ricon.family" \
usage_headless="false" \
usage_model="openai-codex/gpt-5.5" \
usage_message="" \
bash "$SHIMMER_DIR/.mise/tasks/agent/_default" # codebase:ignore bats-test-helper — isolates post-cleanup PATH without mise-added shims
[ "$status" -ne 0 ]
[[ "$output" == *"sessions not found on PATH"* ]]
[[ "$output" != *"stale direct sessions should not run"* ]]
}

@test "interactive: scrubs caller context before invoking harness" {
@test "interactive: ignores inherited usage env from parent task" {
setup_agent
local caller_dir="$BATS_TEST_TMPDIR/scrub-caller"
mkdir -p "$caller_dir"
export SHIMMER_CALLER_PWD="$caller_dir"
export OTHER_CALLER_PWD="/stale/other/caller"
mock_harness
export usage_headless="true"
export usage_model="stale-provider/stale-model"
export usage_message="stale parent message"
mock_sessions_binary
mock_shimmer

run shimmer agent
run shimmer agent --model "openai-codex/gpt-5.5"
[ "$status" -eq 0 ]

grep -q '^CALLER_PWD=$' "$HARNESS_ENV_LOG"
grep -q '^SHIMMER_CALLER_PWD=$' "$HARNESS_ENV_LOG"
grep -q '^OTHER_CALLER_PWD=$' "$HARNESS_ENV_LOG"
grep -q "^wake mock-session-id-001 --model openai-codex/gpt-5.5$" "$SESSIONS_LOG"
! grep -q "stale parent message" "$SESSIONS_LOG"
}

@test "interactive: removes mise task env and direct install PATH before invoking harness" {
@test "interactive: uses SHIMMER_CALLER_PWD as session cwd before scrubbing" {
setup_agent
local caller_dir="$BATS_TEST_TMPDIR/scrub-caller"
local caller_dir="$BATS_TEST_TMPDIR/shimmer-caller"
mkdir -p "$caller_dir"
unset CALLER_PWD
export SHIMMER_CALLER_PWD="$caller_dir"
local installs="$HOME/.local/share/mise/installs"
local stale_sessions="$installs/shiv-sessions/0.4.1/bin"
local current_sessions="$installs/shiv-sessions/0.4.4/bin"
export PATH="$stale_sessions:/before:$current_sessions:$PATH"
export MISE_PROJECT_ROOT="/stale/project"
export MISE_ORIGINAL_CWD="/stale/original"
mock_harness
mock_sessions_binary
mock_shimmer

run shimmer agent
run shimmer agent --model "openai-codex/gpt-5.5"
[ "$status" -eq 0 ]

grep -q '^MISE_CONFIG_ROOT=$' "$HARNESS_ENV_LOG"
grep -q '^MISE_PROJECT_ROOT=$' "$HARNESS_ENV_LOG"
grep -q '^MISE_TASK_NAME=$' "$HARNESS_ENV_LOG"
grep -q '^usage_headless=$' "$HARNESS_ENV_LOG"
grep -q '^usage_model=$' "$HARNESS_ENV_LOG"
grep -q '^usage_message=$' "$HARNESS_ENV_LOG"
grep -q '^GIT_AUTHOR_NAME=test-agent$' "$HARNESS_ENV_LOG"
grep -q '^GIT_AUTHOR_EMAIL=test-agent@ricon.family$' "$HARNESS_ENV_LOG"
grep -q '^PATH=.*/before' "$HARNESS_ENV_LOG"
! grep -q "^PATH=.*$stale_sessions" "$HARNESS_ENV_LOG"
! grep -q "^PATH=.*$current_sessions" "$HARNESS_ENV_LOG"
grep "^new " "$SESSIONS_LOG" | grep -q -- "--cwd $caller_dir"
}

@test "interactive: forwards session flag to harness" {
@test "interactive: preserves identity while scrubbing caller and mise task context" {
setup_agent
mock_harness
export OTHER_CALLER_PWD="/stale/other/caller"
export MISE_PROJECT_ROOT="/stale/project"
export MISE_ORIGINAL_CWD="/stale/original"
mock_sessions_binary
mock_shimmer

run shimmer agent --session "/tmp/my-session"
run shimmer agent --model "openai-codex/gpt-5.5"
[ "$status" -eq 0 ]

grep -q -- "--session /tmp/my-session" "$HARNESS_LOG"
grep -q '^CALLER_PWD=$' "$SESSIONS_ENV_LOG"
grep -q '^SHIMMER_CALLER_PWD=$' "$SESSIONS_ENV_LOG"
grep -q '^OTHER_CALLER_PWD=$' "$SESSIONS_ENV_LOG"
grep -q '^MISE_CONFIG_ROOT=$' "$SESSIONS_ENV_LOG"
grep -q '^MISE_PROJECT_ROOT=$' "$SESSIONS_ENV_LOG"
grep -q '^MISE_TASK_NAME=$' "$SESSIONS_ENV_LOG"
grep -q '^usage_headless=$' "$SESSIONS_ENV_LOG"
grep -q '^usage_model=$' "$SESSIONS_ENV_LOG"
grep -q '^usage_message=$' "$SESSIONS_ENV_LOG"
grep -q '^GIT_AUTHOR_NAME=test-agent$' "$SESSIONS_ENV_LOG"
grep -q '^GIT_AUTHOR_EMAIL=test-agent@ricon.family$' "$SESSIONS_ENV_LOG"
}

@test "interactive: forwards message to harness" {
@test "interactive: resumes an existing session and forwards the initial message" {
setup_agent
mock_harness
mock_sessions_binary
mock_shimmer

run shimmer agent "hello there"
run shimmer agent --session "existing-session-42" --model "openai-codex/gpt-5.5" "continue work"
[ "$status" -eq 0 ]

grep -q "hello there" "$HARNESS_LOG"
! grep -q "^new " "$SESSIONS_LOG"
grep -q "^wake existing-session-42 --model openai-codex/gpt-5.5 --message continue work$" "$SESSIONS_LOG"
}

@test "agent:dispatch requires model" {
Expand Down
Loading
Loading