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
54 changes: 32 additions & 22 deletions .mise/tasks/run
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ SYSTEM_PROMPT_FILE="${usage_system_prompt_file:-}"
TIMEOUT="${usage_timeout:-}"
MODEL="${usage_model:-}"
SESSION="${usage_session:-}"
CWD="${usage_cwd:-${SESSIONS_CALLER_PWD:-${CALLER_PWD:-.}}}"
CALLER_CWD="${SESSIONS_CALLER_PWD:-.}"
CWD="${usage_cwd:-$CALLER_CWD}"
HEADLESS="${usage_headless:-false}"
INTERACTIVE="${usage_interactive:-false}"
PROJECT_TRUST="${usage_project_trust:-inherit}"
Expand Down Expand Up @@ -59,6 +60,8 @@ source "$MISE_CONFIG_ROOT/lib/processes.sh"

PROMPT_TEMP_FILE=""
PROCESS_START_ID=""
HARNESS_NAME=""
HARNESS_EXECUTABLE=""
# shellcheck disable=SC2329 # Invoked by EXIT trap below.
cleanup_prompt_temp() {
if [ -n "$PROMPT_TEMP_FILE" ]; then
Expand Down Expand Up @@ -234,6 +237,29 @@ fi

sessions_validate_project_trust "$PROJECT_TRUST" || exit 1

# Mise tasks run from Sessions' package root. Resolve relative child paths from
# the invoking caller before either launch path changes directories again.
case "$CWD" in
/*) ;;
*) CWD="$CALLER_CWD/$CWD" ;;
esac
CWD=$(cd "$CWD" 2>/dev/null && pwd -P) || {
echo "Error: --cwd directory not found: ${usage_cwd:-$CALLER_CWD}" >&2
exit 1
}

# Resolve the selected harness and its Sessions-owned executable before any
# launch environment is sanitized. Unsupported skeleton adapters may return no
# executable and retain their adapter-level rejection in the Elixir boundary.
if [ -n "$SESSION" ]; then
HARNESS_NAME=$(harness_resolve --session "$SESSION") || exit 1
else
HARNESS_NAME=$(harness_resolve) || exit 1
fi
# shellcheck source=/dev/null
source "$MISE_CONFIG_ROOT/lib/harness/$HARNESS_NAME.sh"
HARNESS_EXECUTABLE=$(harness_call "$HARNESS_NAME" executable "$MISE_CONFIG_ROOT") || exit 1

# Prompt resolution order:
# 1. Explicit --system-prompt-file
# 2. Latest system_prompt entry baked into --session by `sessions new`
Expand Down Expand Up @@ -284,33 +310,16 @@ HEADLESS_EOF
fi

run_interactive() {
# shellcheck source=../../lib/harness/dispatch.sh
source "$MISE_CONFIG_ROOT/lib/harness/dispatch.sh"

local harness_name
if [ -n "$SESSION" ]; then
harness_name=$(harness_resolve --session "$SESSION") || exit 1
else
harness_name=$(harness_resolve) || exit 1
fi
# shellcheck source=/dev/null
source "$MISE_CONFIG_ROOT/lib/harness/$harness_name.sh"

if [ "$harness_name" != "pi" ]; then
echo "sessions: '$harness_name' harness does not support interactive no-message run yet" >&2
if [ "$HARNESS_NAME" != "pi" ]; then
echo "sessions: '$HARNESS_NAME' harness does not support interactive no-message run yet" >&2
exit "$HARNESS_UNSUPPORTED_EXIT"
fi

sessions_prepare_harness_env

local -a pi_command=(mise -C "$MISE_CONFIG_ROOT" exec -- pi)
if ! command -v mise >/dev/null 2>&1; then
echo "Error: mise not found on PATH for sessions-owned pi harness" >&2
exit 1
fi

local -a pi_command=("$HARNESS_EXECUTABLE")
local project_trust_flag
project_trust_flag=$(harness_call "$harness_name" project_trust_flag "$PROJECT_TRUST") || exit 1
project_trust_flag=$(harness_call "$HARNESS_NAME" project_trust_flag "$PROJECT_TRUST") || exit 1

local pi_args=(--model "$MODEL")
[ -n "$project_trust_flag" ] && pi_args+=("$project_trust_flag")
Expand Down Expand Up @@ -354,6 +363,7 @@ CLI_ARGS=(--cwd "$CWD")
[ -n "$TIMEOUT" ] && CLI_ARGS+=(--timeout "$TIMEOUT")
CLI_ARGS+=(--model "$MODEL")
[ -n "$SESSION" ] && CLI_ARGS+=(--session "$SESSION")
[ -n "$HARNESS_EXECUTABLE" ] && CLI_ARGS+=(--harness-executable "$HARNESS_EXECUTABLE")
CLI_ARGS+=(--project-trust "$PROJECT_TRUST")
[ "$EXTENSIONS" != "true" ] && CLI_ARGS+=(--no-extensions)
[ "$SKILLS" != "true" ] && CLI_ARGS+=(--no-skills)
Expand Down
11 changes: 3 additions & 8 deletions .mise/tasks/wake
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,8 @@ NO_EXTENSIONS="${usage_no_extensions:-false}"
NO_SKILLS="${usage_no_skills:-false}"
NO_PROMPT_TEMPLATES="${usage_no_prompt_templates:-false}"

scrub_caller_pwd_env() {
local name
while IFS= read -r name; do
case "$name" in
CALLER_PWD|*_CALLER_PWD) unset "$name" ;;
esac
done < <(compgen -e)
scrub_sessions_caller_pwd_env() {
unset SESSIONS_CALLER_PWD
}

if [ -z "$SESSION_ID" ]; then
Expand Down Expand Up @@ -246,7 +241,7 @@ if [ -n "$OS_USER" ]; then
RUN_CMD=("$HOST_RUN_AS_USER" --user "$OS_USER" -- "${RUN_CMD[@]}")
fi

scrub_caller_pwd_env
scrub_sessions_caller_pwd_env

if [ "$BACKGROUND" = "true" ]; then
# Background: launch via shell/zmx (fire-and-forget)
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Keep these generic in core task/library code:

Keep harness-specific knowledge in `lib/harness/<name>.sh` and `lib/harness/<name>.py`:

- exact executable resolution from Sessions' declared tool context;
- session file locations and path encoding;
- native JSONL message schemas;
- launch arguments for a harness binary;
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Create sessions with structured metadata, wake agents into them,
observe transcripts in real time, and query your history.

![lang: bash + python](https://img.shields.io/badge/lang-bash%20%2B%20python-4EAA25?style=flat&logo=gnubash&logoColor=white)
[![tests: 321 passing](https://img.shields.io/badge/tests-321%20passing-brightgreen?style=flat)](test/)
[![tests: 322 passing](https://img.shields.io/badge/tests-322%20passing-brightgreen?style=flat)](test/)
![commands: 19](https://img.shields.io/badge/commands-19-blue?style=flat)
![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat)

Expand Down Expand Up @@ -115,6 +115,8 @@ sessions wake review/pr-50 --model openai-codex/gpt-5.5 --message "You missed th

The spawning stack uses [shell](https://github.com/KnickKnackLabs/shell) for persistent zmx sessions. `sessions wake` calls `sessions run` as its hidden low-level executor. For profile-specific sessions, use `new` + `wake`: bake profile or task instructions into the session with `--system-prompt-file` at creation, then wake it with task messages. If no explicit or baked prompt exists, the harness starts without an appended prompt and can rely on its native cwd context discovery.

Before launch, Sessions resolves the selected harness executable from its own declared toolchain. The child then starts in the requested `--cwd` with Sessions' mise task context and direct tool-install paths removed. This keeps the harness version pinned without replacing the target project or agent home's own tool and resource context.

`sessions run` remains available as an advanced/compatibility command. It accepts an explicit `--system-prompt-file`, uses any prompt baked into the session, and otherwise starts without appending a system prompt. Caller-provided context belongs to the caller, not to `sessions`.

`--model` on `sessions wake` is required and is not remembered across wakes — pass a provider-qualified model (for example `openai-codex/gpt-5.5`) on each wake.
Expand Down Expand Up @@ -272,7 +274,7 @@ cd sessions && mise trust && mise install
mise run test
```

**321 tests** across 21 suites, using [BATS 1.13.0](https://github.com/bats-core/bats-core). Tasks are bash scripts (session creation, wake, metadata) and Python scripts with [Rich](https://github.com/Textualize/rich) output (list, read, wait, usage, inspect, search). The shared Python support library is 2740 lines in `lib/`.
**322 tests** across 21 suites, using [BATS 1.13.0](https://github.com/bats-core/bats-core). Tasks are bash scripts (session creation, wake, metadata) and Python scripts with [Rich](https://github.com/Textualize/rich) output (list, read, wait, usage, inspect, search). The shared Python support library is 2740 lines in `lib/`.

Python code is checked with [Ruff](https://docs.astral.sh/ruff/) via `mise run lint:python`, and CI runs the same lint/format check in addition to the BATS and Elixir suites.

Expand Down Expand Up @@ -311,7 +313,7 @@ sessions/
│ └── harness/ # Per-harness adapters (pi, …)
├── queries/ # Packaged sessions query SQL presets
└── test/
└── *.bats # 321 tests
└── *.bats # 322 tests
```

</details>
Expand Down
6 changes: 6 additions & 0 deletions README.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ sessions wake review/pr-50 --model openai-codex/gpt-5.5 --message "You missed th
{" at creation, then wake it with task messages. If no explicit or baked prompt exists, the harness starts without an appended prompt and can rely on its native cwd context discovery."}
</Paragraph>

<Paragraph>
{"Before launch, Sessions resolves the selected harness executable from its own declared toolchain. The child then starts in the requested "}
<Code>--cwd</Code>
{" with Sessions' mise task context and direct tool-install paths removed. This keeps the harness version pinned without replacing the target project or agent home's own tool and resource context."}
</Paragraph>

<Paragraph>
<Code>sessions run</Code>
{" remains available as an advanced/compatibility command. It accepts an explicit "}
Expand Down
5 changes: 4 additions & 1 deletion cli/lib/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ defmodule Cli do
skills = opts[:no_skills] != true
prompt_templates = opts[:no_prompt_templates] != true
project_trust = opts[:project_trust] || "inherit"
harness_executable = opts[:harness_executable]

print_header(opts, message, timeout, model)

Expand All @@ -75,7 +76,8 @@ defmodule Cli do
extensions: extensions,
skills: skills,
prompt_templates: prompt_templates,
project_trust: project_trust
project_trust: project_trust,
harness_executable: harness_executable
)
end
end
Expand Down Expand Up @@ -129,6 +131,7 @@ defmodule Cli do
no_skills: :boolean,
no_prompt_templates: :boolean,
project_trust: :string,
harness_executable: :string,
help: :boolean
],
aliases: [h: :help]
Expand Down
13 changes: 9 additions & 4 deletions cli/lib/cli/engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ defmodule Cli.Engine do
extensions: boolean(),
skills: boolean(),
prompt_templates: boolean(),
project_trust: String.t()
project_trust: String.t(),
harness_executable: String.t() | nil
]

@doc """
Expand Down Expand Up @@ -91,13 +92,17 @@ defmodule Cli.Engine do
end

defp harness_env do
caller_pwd_env_scrub() ++ path_env()
inherited_task_env_scrub() ++ path_env()
end

defp caller_pwd_env_scrub do
defp inherited_task_env_scrub do
System.get_env()
|> Map.keys()
|> Enum.filter(&(&1 == "CALLER_PWD" or String.ends_with?(&1, "_CALLER_PWD")))
|> Enum.filter(fn name ->
name == "SESSIONS_CALLER_PWD" or
(String.starts_with?(name, "MISE_") and name != "MISE_DATA_DIR") or
String.starts_with?(name, "usage_")
end)
|> Enum.map(&{String.to_charlist(&1), false})
end

Expand Down
29 changes: 24 additions & 5 deletions cli/lib/harness/pi/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ defmodule Cli.Harness.Pi.Command do
Pi command construction — builds the shell invocation passed to the
port under `/bin/sh -c <script> -- $1 $2 ...`.

User-controlled strings (message, model, system prompt file, session
path) are passed as positional `$1`/`$2`/... args so they never enter
the shell script as interpolated text.
User-controlled strings and the Sessions-selected Pi executable are passed
as positional `$1`/`$2`/... args so they never enter the shell script as
interpolated text.

Part of the pi harness adapter — see sessions#50 for the
multi-harness plan.
Expand All @@ -15,7 +15,8 @@ defmodule Cli.Harness.Pi.Command do
extensions: boolean(),
skills: boolean(),
prompt_templates: boolean(),
project_trust: String.t()
project_trust: String.t(),
harness_executable: String.t()
]

@doc """
Expand All @@ -40,6 +41,7 @@ defmodule Cli.Harness.Pi.Command do
skills = Keyword.get(opts, :skills, true)
prompt_templates = Keyword.get(opts, :prompt_templates, true)
project_trust_flag = project_trust_flag(Keyword.get(opts, :project_trust, "inherit"))
executable = harness_executable(opts)

qualified_model = model

Expand All @@ -59,6 +61,9 @@ defmodule Cli.Harness.Pi.Command do
{" --no-session", positional_after_prompt}
end

executable_arg = "$#{length(positional) + 1}"
positional = positional ++ [executable]

pi_flags =
[
prompt_flag,
Expand All @@ -72,7 +77,7 @@ defmodule Cli.Harness.Pi.Command do
]
|> Enum.join("")

pi_cmd = ~s(mise -C "$MISE_CONFIG_ROOT" exec -- pi -p "$1"#{pi_flags})
pi_cmd = ~s("#{executable_arg}" -p "$1"#{pi_flags})

# `echo |` pipes empty stdin so pi doesn't block waiting for a TTY.
shell_script =
Expand All @@ -85,6 +90,20 @@ defmodule Cli.Harness.Pi.Command do
{shell_script, positional}
end

defp harness_executable(opts) do
case Keyword.fetch(opts, :harness_executable) do
{:ok, executable} when is_binary(executable) and executable != "" ->
if Path.type(executable) == :absolute do
executable
else
raise ArgumentError, "pi harness executable must be an absolute path"
end

_ ->
raise ArgumentError, "pi harness executable is required"
end
end

defp project_trust_flag("inherit"), do: ""
defp project_trust_flag("approve"), do: " --approve"
defp project_trust_flag("deny"), do: " --no-approve"
Expand Down
Loading