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
22 changes: 13 additions & 9 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ machine-readable", not for parallelism.

| Component | Trigger | Responsibility | Depends on |
|---|---|---|---|
| `session-start.mjs` | `SessionStart` | Reads the Codex model/effort pin; emits the setup question (when armed via `~/.maestro/ask-on-start`); appends a validated `MAESTRO_SESSION_ID` export to `$CLAUDE_ENV_FILE` for session attribution | `~/.codex/config.toml`, `~/.codex/maestro-impl-effort` |
| `session-start.mjs` | `SessionStart` | Reads the Codex model/effort pin; emits the setup question (when armed via `~/.maestro/ask-on-start`); appends a validated `MAESTRO_SESSION_ID` export to `$CLAUDE_ENV_FILE` for session attribution | `~/.codex/config.toml`, `~/.codex/maestro-impl-effort`, `~/.codex/maestro-impl-model` |
| `orchestrator-inject.mjs` | `UserPromptSubmit` | Resets the direct-edit authorization flag on new tasks; sets it only on an explicit "edit it yourself"-class imperative; emits the orchestrator/implementer directive only when the prompt carries a code/design signal; intercepts "codex model" setup phrasing | `maestro-policy.mjs`, `~/.maestro/direct-edit/` |
| `orchestrator-gate.mjs` | `PreToolUse` (`Edit\|Write\|MultiEdit`) | Blocks the orchestrator's direct source edits; allows non-code allowlisted files, anchored scratch/Desktop paths, any path carrying a `.claude` or `.codex` segment (harness exemption — segment-based, not root-anchored), and only a session-scoped, owner-private, exact-content direct-edit flag; refuses overrides inside subagents; fails closed on unreadable payloads | `maestro-policy.mjs`, `~/.maestro/direct-edit/` |
| `maestro-policy.mjs` | (shared module) | Single source of truth for file classification (non-code extensions/basenames), session-id validation, and direct-edit directive parsing with negation/quote guards | none |
Expand Down Expand Up @@ -128,7 +128,7 @@ Layering is strict and directional: `lib-process.sh` → `lib-companion.sh` →

| Component | Role |
|---|---|
| `codex-model-select.sh` | Transactionally pins Codex model + debate effort (in `~/.codex/config.toml`) and implementation effort (in `~/.codex/maestro-impl-effort`); serialized by its own lock; `--show`, `--pin`, `--ask-on-start on\|off` |
| `codex-model-select.sh` | Transactionally pins Codex model + debate effort (in `~/.codex/config.toml`) and implementation effort/model (in `~/.codex/maestro-impl-effort` and `~/.codex/maestro-impl-model`); serialized by its own lock; `--show`, `--pin`, `--ask-on-start on\|off` |
| `codex-mcp-check.sh` | Reports which MCP servers background Codex jobs inherit from `~/.codex/config.toml`, env keys masked |

### 2.6 Installer / uninstaller (package entry points)
Expand All @@ -143,7 +143,7 @@ Layering is strict and directional: `lib-process.sh` → `lib-companion.sh` →
| Dependency | Role | Boundary |
|---|---|---|
| Claude Code | Host runtime; fires hooks; runs the orchestrator model | Contract: hook JSON payloads on stdin, exit codes |
| Codex CLI + `openai-codex` plugin (`codex-companion.mjs`) | Executes background agent jobs | Contract: `codex task --background [--write] --model … --effort …`, `status --json`, `cancel`, `result`, `status --all --json`; a **compatibility probe** on `--help` refuses write dispatch if `task` is described without `--write` |
| Codex CLI + `openai-codex` plugin (`codex-companion.mjs`) | Executes background agent jobs | Contract: `codex task --background [--write] --model … [--effort …]`, `status --json`, `cancel`, `result`, `status --all --json`; wrapper-expressible write efforts use `--effort`, while max/ultra omit it only when identical to the pinned top-level `model_reasoning_effort`; a **compatibility probe** on `--help` refuses write dispatch if `task` is described without `--write` |
| `~/.codex/config.toml` | Codex configuration: model, reasoning efforts, MCP servers, `web_search` | Parsed as TOML; only true top-level keys are trusted |
| Git | Repository scope, tree digests (`git hash-object --no-filters`), worktree/submodule discovery | Must be present in the working directory |

Expand Down Expand Up @@ -203,7 +203,7 @@ kill without a recovery daemon.
| Session-start preference | `~/.maestro/ask-on-start` | Empty marker file; armed by default on first install, managed by `codex-model-select.sh --ask-on-start on\|off` and removed by uninstall | `install.mjs`, `codex-model-select.sh` | `session-start.mjs`, `uninstall.mjs` |
| Installer ownership manifest | `~/.maestro/install-manifest.json` | `{ "version": 1, "files": { "<key>": "<sha256>" } }`, `0600` | `install.mjs` | `install.mjs`, `uninstall.mjs` |
| Session identity | `$CLAUDE_ENV_FILE` | `export MAESTRO_SESSION_ID=<validated id>` appended at session start | `session-start.mjs` | All lease/provenance code via `write_lock_session_id` |
| Codex pin | `~/.codex/config.toml` (top-level `model`, `model_reasoning_effort`), `~/.codex/maestro-impl-effort` | TOML preamble / single line | `codex-model-select.sh` | `companion_pin`, `session-start.mjs` |
| Codex pin | `~/.codex/config.toml` (top-level `model`, `model_reasoning_effort`), `~/.codex/maestro-impl-effort`, `~/.codex/maestro-impl-model` | TOML preamble / single line | `codex-model-select.sh` | `companion_pin`, `session-start.mjs` |
| Hook registrations | `~/.claude/settings.json` | `hooks.<event>` blocks with exact `# maestro-managed:<script>` command markers | `install.mjs` | Claude Code |
| Backups | `*.maestro.bak` beside settings/config | Byte copy of the pre-merge state | `install.mjs` | Operator, uninstall |

Expand Down Expand Up @@ -289,7 +289,7 @@ implementer-loop.sh --clear-lease
implementer-watchdog.sh --file <plan-file> [max_idle] [poll]
discussion-loop.sh --new "<topic>" [slug]
discussion-loop.sh --turn <file> [slug] [max_idle] [poll]
codex-model-select.sh --show | --pin | <model> <debate-effort> [impl-effort] | --ask-on-start on|off|status
codex-model-select.sh --show | --pin | <model> <debate-effort> [impl-effort] [impl-model] | --ask-on-start on|off|status
codex-mcp-check.sh
node install.mjs [--with-workflow]
node uninstall.mjs
Expand Down Expand Up @@ -325,7 +325,10 @@ cancellation (unconfirmed quiescence).
### 4.4 Companion transport (Maestro → Codex)

Implemented in `lib-companion.sh` via the plugin's `codex-companion.mjs`:
`task --background [--write] --model <m> --effort <e> <prompt>`,
`task --background [--write] --model <m> [--effort <e>] <prompt>`;
write jobs omit `--effort` only for an exact max/ultra match with the pinned
top-level `model_reasoning_effort`, otherwise the wrapper-expressible effort is
passed explicitly.
`status <job> --json`, `cancel <job>`, `result <job>`, `status --all --json`
(repository-global writer visibility, parsed strictly, session filter
deliberately stripped). Jobs are identified as `task-<id>-<id>`.
Expand Down Expand Up @@ -377,7 +380,7 @@ agent processes. The "production environment" is:
macOS (primary) / Linux ──► Claude Code (ships Node) ──► Codex CLI (ChatGPT login)
├── ~/.claude/ hooks/, rules/, skills/, settings.json
├── ~/.maestro/ install-manifest, discussions/, direct-edit/, ask-on-start
├── ~/.codex/ config.toml, maestro-impl-effort
├── ~/.codex/ config.toml, maestro-impl-effort, maestro-impl-model
└── <repo>/.git/ maestro-write.lock/, maestro-provenance.log
```

Expand Down Expand Up @@ -432,7 +435,8 @@ Pull requests and pushes to `main` both trigger it. There is no CD — the
`task` synopsis lacks `--write` (guards against upstream flag drift).
- The model pin is verified against the launched job's recorded
`request.model`/`request.effort` (`companion_verify_pin`), warning on
mismatch.
mismatch; an absent/null effort is accepted only for an exact-match write
dispatch that omitted the wrapper flag.

**Boundary statement.** The write path is coupled to the companion transport's
JSON contract: status fields (`phase`, `elapsed`, `progressPreview`,
Expand Down Expand Up @@ -599,7 +603,7 @@ flowchart LR
HOOKS["Maestro hooks & adapters<br/>(installed into ~/.claude)"]
GIT[("Working tree + .git<br/>lease lock, provenance log)")]
MAE[("~/.maestro<br/>manifest, discussions, direct-edit")]
COD[("~/.codex<br/>config.toml, effort pin")]
COD[("~/.codex<br/>config.toml, effort/model pins")]

U --> CC
CC --> HOOKS
Expand Down
156 changes: 129 additions & 27 deletions hooks/codex-model-select.sh
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
#!/usr/bin/env bash
# Maestro Codex model + debate/implementation effort selector.
# Pins top-level model settings in ~/.codex/config.toml and keeps the
# implementation effort in ~/.codex/maestro-impl-effort and the scout pin in
# ~/.codex/maestro-scout.
# implementation effort/model in ~/.codex/maestro-impl-effort and
# ~/.codex/maestro-impl-model, plus the scout pin in ~/.codex/maestro-scout.
#
# Usage:
# codex-model-select.sh --show
# codex-model-select.sh --pin
# codex-model-select.sh --scout <model> <effort>
# codex-model-select.sh --scout-pin
# codex-model-select.sh <model> <debate-effort> [impl-effort]
# codex-model-select.sh <model> <debate-effort> [impl-effort] [impl-model]
# codex-model-select.sh --ask-on-start on|off|status
#
# Debate effort: none | minimal | low | medium | high | xhigh | max | ultra
# Implementation effort: none | minimal | low | medium | high | xhigh
# Implementation effort: none | minimal | low | medium | high | xhigh, or
# max | ultra only when it exactly matches the top-level debate effort.
# Exit codes: 0 = ok | 3 = bad args, invalid values, or failed publication
set -uo pipefail

CODEX_CONF="$HOME/.codex/config.toml"
IMPL_EFFORT_FILE="$HOME/.codex/maestro-impl-effort"
IMPL_MODEL_FILE="$HOME/.codex/maestro-impl-model"
SCOUT_FILE="$HOME/.codex/maestro-scout"
MAESTRO_DIR="$HOME/.maestro"
ASK_FLAG="$MAESTRO_DIR/ask-on-start"
Expand Down Expand Up @@ -122,6 +124,14 @@ valid_impl_effort() {
esac
}

impl_effort_acceptable() { # implementation-effort debate-effort
valid_impl_effort "$1" && return 0
case "$1" in
max|ultra) [ "$1" = "$2" ] ;;
*) return 1 ;;
esac
}

valid_model() {
[[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]]
}
Expand Down Expand Up @@ -218,17 +228,41 @@ read_impl_effort() {
printf -v "$1" '%s' "${VALUE:-medium}"
}

read_impl_model() {
local VALUE=""
if [ -f "$IMPL_MODEL_FILE" ]; then
IFS= read -r VALUE < "$IMPL_MODEL_FILE" || :
fi
printf -v "$1" '%s' "$VALUE"
}

show() {
local M E I SM SE
local M E I IM SM SE
read_pin M E || return 3
read_impl_effort I
read_impl_model IM
echo "model=${M:-(not pinned — Codex default)}"
if [ -n "$E" ] && ! valid_effort "$E"; then
echo "effort=$E (invalid)"
else
echo "effort=${E:-(not pinned — Codex default)}"
fi
echo "impl-effort=$I"
case "$I" in
max|ultra)
if [ "$I" = "$E" ]; then
echo "impl-effort=$I (via top-level config)"
else
echo "impl-effort=$I (invalid — top-level effort is $E)"
fi ;;
*) echo "impl-effort=$I" ;;
esac
if [ -n "$IM" ] && valid_model "$IM"; then
echo "impl-model=$IM"
elif [ -f "$IMPL_MODEL_FILE" ] && [ -n "$IM" ]; then
echo "impl-model=$IM (invalid)"
else
echo "impl-model=${M:-(not pinned — Codex default)} (inherited)"
fi
if [ ! -f "$SCOUT_FILE" ]; then
echo "scout=(not pinned — scout dispatch disabled)"
else
Expand All @@ -243,9 +277,10 @@ show() {
}

pin() {
local M E I
local M E I IM
read_pin M E || return 3
read_impl_effort I
read_impl_model IM
if [ -z "$M" ] || [ -z "$E" ]; then
echo "SELECT_ERROR: Codex model and effort must both be pinned in the config.toml preamble" >&2
return 3
Expand All @@ -254,11 +289,16 @@ pin() {
echo "SELECT_ERROR: invalid debate effort '$E' (expected: none | minimal | low | medium | high | xhigh | max | ultra)" >&2
return 3
fi
if ! valid_impl_effort "$I"; then
echo "SELECT_ERROR: invalid implementation effort '$I' (expected: none | minimal | low | medium | high | xhigh; max/ultra are debate-only because the companion wrapper cannot express them for write jobs)" >&2
if ! impl_effort_acceptable "$I" "$E"; then
echo "SELECT_ERROR: implementation effort '$I' cannot be expressed per write job (the companion accepts none|minimal|low|medium|high|xhigh); max/ultra are usable only when the top-level debate effort is the same value, currently '$E'" >&2
return 3
fi
printf '%s\t%s\t%s\n' "$M" "$E" "$I"
if [ -n "$IM" ] && ! valid_model "$IM"; then
echo "SELECT_ERROR: invalid implementation model '$IM' (expected letters, digits, . _ -)" >&2
return 3
fi
[ -n "$IM" ] || IM="$M"
printf '%s\t%s\t%s\t%s\n' "$M" "$E" "$I" "$IM"
}

stat_mode() {
Expand Down Expand Up @@ -319,15 +359,19 @@ render_config() { # source destination model effort
' "$source" > "$destination"
}

publish_pin() { # model debate-effort impl-effort
local model="$1" effort="$2" impl="$3"
local config_source config_tmp impl_tmp config_original impl_original
local config_existed=0 impl_existed=0 config_mode=600 impl_mode=600
publish_pin() { # model debate-effort impl-effort impl-model
local model="$1" effort="$2" impl="$3" impl_model="$4"
local config_source config_tmp impl_tmp model_tmp
local config_original impl_original model_original
local config_existed=0 impl_existed=0 model_existed=0
local config_mode=600 impl_mode=600 model_mode=600
mkdir -p "$HOME/.codex" || return 3
config_tmp="$CODEX_CONF.mtmp.$$"
impl_tmp="$IMPL_EFFORT_FILE.mtmp.$$"
model_tmp="$IMPL_MODEL_FILE.mtmp.$$"
config_original="$CODEX_CONF.moriginal.$$"
impl_original="$IMPL_EFFORT_FILE.moriginal.$$"
model_original="$IMPL_MODEL_FILE.moriginal.$$"

if [ -f "$CODEX_CONF" ]; then
config_existed=1
Expand All @@ -348,27 +392,43 @@ publish_pin() { # model debate-effort impl-effort
return 3
}
fi
if [ -f "$IMPL_MODEL_FILE" ]; then
model_existed=1
model_mode=$(stat_mode "$IMPL_MODEL_FILE") || {
rm -f "$config_original" "$impl_original"
return 3
}
cp -p "$IMPL_MODEL_FILE" "$model_original" || {
rm -f "$config_original" "$impl_original"
return 3
}
fi

if ! render_config "$config_source" "$config_tmp" "$model" "$effort" ||
! chmod "$config_mode" "$config_tmp" ||
! printf '%s\n' "$impl" > "$impl_tmp" ||
! chmod "$impl_mode" "$impl_tmp"; then
rm -f "$config_tmp" "$impl_tmp" "$config_original" "$impl_original"
! chmod "$impl_mode" "$impl_tmp" ||
! printf '%s\n' "$impl_model" > "$model_tmp" ||
! chmod "$model_mode" "$model_tmp"; then
rm -f "$config_tmp" "$impl_tmp" "$model_tmp" \
"$config_original" "$impl_original" "$model_original"
echo "SELECT_ERROR: could not stage Codex pin files" >&2
return 3
fi

if [ "$config_existed" -eq 1 ] && [ ! -f "$CODEX_CONF.maestro.bak" ]; then
cp -p "$CODEX_CONF" "$CODEX_CONF.maestro.bak" || {
rm -f "$config_tmp" "$impl_tmp" "$config_original" "$impl_original"
rm -f "$config_tmp" "$impl_tmp" "$model_tmp" \
"$config_original" "$impl_original" "$model_original"
echo "SELECT_ERROR: could not back up config.toml" >&2
return 3
}
echo "SELECT: backed up config.toml → config.toml.maestro.bak"
fi

if ! mv -f "$config_tmp" "$CODEX_CONF"; then
rm -f "$config_tmp" "$impl_tmp" "$config_original" "$impl_original"
rm -f "$config_tmp" "$impl_tmp" "$model_tmp" \
"$config_original" "$impl_original" "$model_original"
echo "SELECT_ERROR: could not publish config.toml" >&2
return 3
fi
Expand All @@ -379,14 +439,42 @@ publish_pin() { # model debate-effort impl-effort
else
rm -f "$CODEX_CONF"
fi
if [ "$impl_existed" -eq 1 ] && [ ! -f "$IMPL_EFFORT_FILE" ]; then
cp -p "$impl_original" "$IMPL_EFFORT_FILE" 2>/dev/null || :
if [ "$impl_existed" -eq 1 ]; then
[ -f "$IMPL_EFFORT_FILE" ] ||
cp -p "$impl_original" "$IMPL_EFFORT_FILE" 2>/dev/null || :
else
rm -f "$IMPL_EFFORT_FILE"
fi
rm -f "$config_tmp" "$impl_tmp" "$config_original" "$impl_original"
rm -f "$config_tmp" "$impl_tmp" "$model_tmp" \
"$config_original" "$impl_original" "$model_original"
echo "SELECT_ERROR: could not publish implementation effort; previous pin restored" >&2
return 3
fi
rm -f "$config_original" "$impl_original"
if ! mv -f "$model_tmp" "$IMPL_MODEL_FILE"; then
if [ "$config_existed" -eq 1 ]; then
mv -f "$config_original" "$CODEX_CONF" 2>/dev/null ||
cp -p "$config_original" "$CODEX_CONF" 2>/dev/null || :
else
rm -f "$CODEX_CONF"
fi
if [ "$impl_existed" -eq 1 ]; then
mv -f "$impl_original" "$IMPL_EFFORT_FILE" 2>/dev/null ||
cp -p "$impl_original" "$IMPL_EFFORT_FILE" 2>/dev/null || :
else
rm -f "$IMPL_EFFORT_FILE"
fi
if [ "$model_existed" -eq 1 ]; then
mv -f "$model_original" "$IMPL_MODEL_FILE" 2>/dev/null ||
cp -p "$model_original" "$IMPL_MODEL_FILE" 2>/dev/null || :
else
rm -f "$IMPL_MODEL_FILE"
fi
rm -f "$config_tmp" "$impl_tmp" "$model_tmp" \
"$config_original" "$impl_original" "$model_original"
echo "SELECT_ERROR: could not publish implementation model; previous pin restored" >&2
return 3
fi
rm -f "$config_original" "$impl_original" "$model_original"
return 0
}

Expand Down Expand Up @@ -438,18 +526,28 @@ case "${1:-}" in
esac
exit $? ;;
""|--help|-h)
echo "usage: codex-model-select.sh --show | --pin | --scout <model> <effort> | --scout-pin | <model> <debate-effort> [impl-effort] | --ask-on-start on|off|status" >&2
echo "usage: codex-model-select.sh --show | --pin | --scout <model> <effort> | --scout-pin | <model> <debate-effort> [impl-effort] [impl-model] | --ask-on-start on|off|status" >&2
exit 3 ;;
esac

pin_lock_acquire || exit 3
[ "$#" -le 4 ] || {
echo "usage: codex-model-select.sh <model> <debate-effort> [impl-effort] [impl-model]" >&2
exit 3
}
MODEL="${1:-}"
EFFORT="${2:-}"
if [ $# -ge 3 ]; then
IMPL_EFFORT="$3"
else
read_impl_effort IMPL_EFFORT
fi
if [ "$#" -eq 4 ]; then
IMPL_MODEL="$4"
else
read_impl_model IMPL_MODEL
[ -n "$IMPL_MODEL" ] || IMPL_MODEL="$MODEL"
fi

if ! valid_model "$MODEL"; then
echo "SELECT_ERROR: invalid model name '$MODEL' (expected e.g. gpt-5.6-sol — letters, digits, . _ -)" >&2
Expand All @@ -459,12 +557,16 @@ if ! valid_effort "$EFFORT"; then
echo "SELECT_ERROR: invalid effort '$EFFORT' (expected: none | minimal | low | medium | high | xhigh | max | ultra)" >&2
exit 3
fi
if ! valid_impl_effort "$IMPL_EFFORT"; then
echo "SELECT_ERROR: invalid implementation effort '$IMPL_EFFORT' (expected: none | minimal | low | medium | high | xhigh; max/ultra are debate-only because the companion wrapper cannot express them for write jobs)" >&2
if ! impl_effort_acceptable "$IMPL_EFFORT" "$EFFORT"; then
echo "SELECT_ERROR: implementation effort '$IMPL_EFFORT' cannot be expressed per write job (the companion accepts none|minimal|low|medium|high|xhigh); max/ultra are usable only when the top-level debate effort is the same value, currently '$EFFORT'" >&2
exit 3
fi
if ! valid_model "$IMPL_MODEL"; then
echo "SELECT_ERROR: invalid implementation model '$IMPL_MODEL' (expected letters, digits, . _ -)" >&2
exit 3
fi

if ! publish_pin "$MODEL" "$EFFORT" "$IMPL_EFFORT"; then
if ! publish_pin "$MODEL" "$EFFORT" "$IMPL_EFFORT" "$IMPL_MODEL"; then
exit 3
fi
echo "SELECT: Codex pin updated → model=$MODEL debate-effort=$EFFORT impl-effort=$IMPL_EFFORT (applies from the next dispatch)"
echo "SELECT: Codex pin updated → model=$MODEL debate-effort=$EFFORT impl-effort=$IMPL_EFFORT impl-model=$IMPL_MODEL (applies from the next dispatch)"
Loading