diff --git a/.mise/tasks/run b/.mise/tasks/run index 6a655f1..ae69e1e 100755 --- a/.mise/tasks/run +++ b/.mise/tasks/run @@ -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}" @@ -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 @@ -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` @@ -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") @@ -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) diff --git a/.mise/tasks/wake b/.mise/tasks/wake index bd42516..1524686 100755 --- a/.mise/tasks/wake +++ b/.mise/tasks/wake @@ -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 @@ -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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7008edb..2dce80b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,7 @@ Keep these generic in core task/library code: Keep harness-specific knowledge in `lib/harness/.sh` and `lib/harness/.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; diff --git a/README.md b/README.md index 4e4fd5c..5b21b47 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. @@ -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. @@ -311,7 +313,7 @@ sessions/ │ └── harness/ # Per-harness adapters (pi, …) ├── queries/ # Packaged sessions query SQL presets └── test/ - └── *.bats # 321 tests + └── *.bats # 322 tests ``` diff --git a/README.tsx b/README.tsx index 175ae5c..17bcfd3 100644 --- a/README.tsx +++ b/README.tsx @@ -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."} + + {"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 "} diff --git a/cli/lib/cli.ex b/cli/lib/cli.ex index f959b72..c8be92c 100644 --- a/cli/lib/cli.ex +++ b/cli/lib/cli.ex @@ -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) @@ -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 @@ -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] diff --git a/cli/lib/cli/engine.ex b/cli/lib/cli/engine.ex index 0ee5655..0cd0480 100644 --- a/cli/lib/cli/engine.ex +++ b/cli/lib/cli/engine.ex @@ -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 """ @@ -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 diff --git a/cli/lib/harness/pi/command.ex b/cli/lib/harness/pi/command.ex index 76f8d6b..9fdcaae 100644 --- a/cli/lib/harness/pi/command.ex +++ b/cli/lib/harness/pi/command.ex @@ -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