mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 23:19:43 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d00229ee29 |
@@ -137,7 +137,6 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
|||||||
- **Global regex `lastIndex`** — Shared `g`-flag patterns in loops must reset `lastIndex = 0` first, or use the `execPattern()` helper in `utils/regex-patterns.ts` (resets automatically)
|
- **Global regex `lastIndex`** — Shared `g`-flag patterns in loops must reset `lastIndex = 0` first, or use the `execPattern()` helper in `utils/regex-patterns.ts` (resets automatically)
|
||||||
- **`envOverrides` flow `CLAUDE_CODE_*` / `OPENCODE_*` / `CODEX_*` / `GEMINI_*` / `GOOGLE_*` / `ANTIGRAVITY_*` / `PI_*` / `GROK_*` / `XAI_*` / `DSH_*` / `DEEPSEEK_*` env vars, plus exact-key `CLAUDE_CONFIG_DIR`** — Set via `POST /api/sessions { envOverrides }`, stored on `Session._envOverrides`, exported by `tmux-manager.buildEnvExports()` at spawn time, persisted in `SessionState.envOverrides`. **Do NOT** write these to `<case>/.claude/settings.local.json` — that's the old path and creates UI/disk drift. (`GOOGLE_*` is the deliberately-broad Vertex-AI namespace for Gemini — see Multi-CLI prefix discipline.) `CLAUDE_CONFIG_DIR` (#255, exact match via `ALLOWED_ENV_KEYS` in `schemas.ts`) points a session at a separate Claude account/config dir for per-client subscriptions; it persists to state.json (a path, not a secret; losing it on restart would silently switch accounts). ⚠️ A relocated config dir writes transcripts outside `~/.claude/projects`, so the response viewer, subagent windows, ultracode panel and Read My Mind capture go blind for that session unless the user symlinks `projects` back into the shared tree (`ln -s ~/.claude/projects <configDir>/projects`). ⚠️ It is also one of claude's `privilegedEnvKeys` (Custom Model Endpoint Profiles, since it can redirect a session's traffic same as any other injected var), so in multi-user mode setting it via `envOverrides` is admin-only, and a non-granted owner's already-persisted `CLAUDE_CONFIG_DIR` is stripped on reboot-restore — silently returning that session to the default Claude account rather than the one it was pointed at (see `session-env-clamp.ts`). → [architecture-invariants#per-session-env-overrides-exact-key-allowlist-and-claude_config_dir](docs/architecture-invariants.md#per-session-env-overrides-exact-key-allowlist-and-claude_config_dir)
|
- **`envOverrides` flow `CLAUDE_CODE_*` / `OPENCODE_*` / `CODEX_*` / `GEMINI_*` / `GOOGLE_*` / `ANTIGRAVITY_*` / `PI_*` / `GROK_*` / `XAI_*` / `DSH_*` / `DEEPSEEK_*` env vars, plus exact-key `CLAUDE_CONFIG_DIR`** — Set via `POST /api/sessions { envOverrides }`, stored on `Session._envOverrides`, exported by `tmux-manager.buildEnvExports()` at spawn time, persisted in `SessionState.envOverrides`. **Do NOT** write these to `<case>/.claude/settings.local.json` — that's the old path and creates UI/disk drift. (`GOOGLE_*` is the deliberately-broad Vertex-AI namespace for Gemini — see Multi-CLI prefix discipline.) `CLAUDE_CONFIG_DIR` (#255, exact match via `ALLOWED_ENV_KEYS` in `schemas.ts`) points a session at a separate Claude account/config dir for per-client subscriptions; it persists to state.json (a path, not a secret; losing it on restart would silently switch accounts). ⚠️ A relocated config dir writes transcripts outside `~/.claude/projects`, so the response viewer, subagent windows, ultracode panel and Read My Mind capture go blind for that session unless the user symlinks `projects` back into the shared tree (`ln -s ~/.claude/projects <configDir>/projects`). ⚠️ It is also one of claude's `privilegedEnvKeys` (Custom Model Endpoint Profiles, since it can redirect a session's traffic same as any other injected var), so in multi-user mode setting it via `envOverrides` is admin-only, and a non-granted owner's already-persisted `CLAUDE_CONFIG_DIR` is stripped on reboot-restore — silently returning that session to the default Claude account rather than the one it was pointed at (see `session-env-clamp.ts`). → [architecture-invariants#per-session-env-overrides-exact-key-allowlist-and-claude_config_dir](docs/architecture-invariants.md#per-session-env-overrides-exact-key-allowlist-and-claude_config_dir)
|
||||||
- **Effort is NOT an env var** — never carry effort as `CLAUDE_CODE_EFFORT_LEVEL`: the env var hard-locks effort and blocks in-session `/effort` switching (incl. ultracode). It flows as the dedicated `effort` payload field → `Session._effort` → `claude --effort <level>` for regular levels incl. `max` (the settings `effortLevel` key is `enum(["low","medium","high","xhigh"]).catch(undefined)` — `max` gets SILENTLY dropped there), or `claude --settings '{"ultracode":true}'` for ultracode (rejected by `--effort`). Both are soft defaults the user can override anytime. Legacy env-var entries are auto-migrated by the Session constructor and unset from tmux sessions in `applyEnvOverrides()`. See `buildEffortCliArgs()` in `session-cli-builder.ts`, tests in `test/effort-injection.test.ts`
|
- **Effort is NOT an env var** — never carry effort as `CLAUDE_CODE_EFFORT_LEVEL`: the env var hard-locks effort and blocks in-session `/effort` switching (incl. ultracode). It flows as the dedicated `effort` payload field → `Session._effort` → `claude --effort <level>` for regular levels incl. `max` (the settings `effortLevel` key is `enum(["low","medium","high","xhigh"]).catch(undefined)` — `max` gets SILENTLY dropped there), or `claude --settings '{"ultracode":true}'` for ultracode (rejected by `--effort`). Both are soft defaults the user can override anytime. Legacy env-var entries are auto-migrated by the Session constructor and unset from tmux sessions in `applyEnvOverrides()`. See `buildEffortCliArgs()` in `session-cli-builder.ts`, tests in `test/effort-injection.test.ts`
|
||||||
- **The advisor rides `--settings`, NEVER the `--advisor` flag**: Claude Code's advisor tool (a stronger model consulted at decision points, code.claude.com/docs/en/advisor) flows as the `advisorModel` payload field → `Session._advisorModel` (persisted, so respawn and reboot restore keep it) → the `advisorModel` key in the launch's ONE `--settings` JSON, merged with ultracode and the statusLine exporter by `buildAdvisorSettings()` (`session-cli-builder.ts`). ⚠️ The flag EXITS at launch on any pairing the CLI refuses (`claude --advisor haiku` exits 1, so does Fable before its usage-credit consent), which would leave a dead pane on every respawn; the settings key degrades to "no advisor" instead. ⚠️ `isAdvisorModel()` (fable/opus/sonnet aliases or full ids, no haiku) is also the injection guard for the single-quoted argument. Soft default: `/advisor` still switches it in-session. App Settings key `claudeAdvisorModel` (SYNCED, `''` = leave it to the CLI). Remote/docker quick-start refuses it, like `effort`. Tests: `test/advisor-model.test.ts`
|
|
||||||
- **Model choice flows via `settings.local.json`, NOT `--model` or env** — the App Settings **Claude Model** picker (`claudeModel` in `settings.json`) is read by `session-ui.js` at session create (wins over the legacy 1M-Opus toggles `opusContext1m`/`opusContext1mEnabled`), sent as the `modelOverride` payload field, and `updateCaseModel()` (`hooks-config.ts`) writes/deletes the `model` key in `<case>/.claude/settings.local.json`. This is the intended exception to the envOverrides rule above: model legitimately lives in `settings.local.json` (a soft default — in-session `/model` still works); env vars do not
|
- **Model choice flows via `settings.local.json`, NOT `--model` or env** — the App Settings **Claude Model** picker (`claudeModel` in `settings.json`) is read by `session-ui.js` at session create (wins over the legacy 1M-Opus toggles `opusContext1m`/`opusContext1mEnabled`), sent as the `modelOverride` payload field, and `updateCaseModel()` (`hooks-config.ts`) writes/deletes the `model` key in `<case>/.claude/settings.local.json`. This is the intended exception to the envOverrides rule above: model legitimately lives in `settings.local.json` (a soft default — in-session `/model` still works); env vars do not
|
||||||
- **Multi-CLI prefix discipline** — env-var prefix is CLI-specific (`CLAUDE_CODE_*` vs `OPENCODE_*` vs `CODEX_*` vs `GEMINI_*` vs `ANTIGRAVITY_*` vs `PI_*` vs `GROK_*` vs `DSH_*`) and the `ALLOWED_ENV_PREFIXES` allowlist in `schemas.ts` enforces this; non-prefix exceptions are exact keys in `ALLOWED_ENV_KEYS` (currently only `CLAUDE_CONFIG_DIR`), never a widened prefix. Gemini additionally allowlists the **broad `GOOGLE_*`** namespace (intentional: Vertex AI auth needs `GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI`; it is the loosest allowlist entry, affecting only the user's own spawned CLI), and Grok allowlists **`XAI_*`** for the same vendor-namespace reason (`XAI_API_KEY` is grok's documented auth var). When adding a setting, decide which CLI(s) it applies to and gate the env export accordingly. Never blanket-forward all prefixes. ⚠️ Pi is the case that proves the rule: its ~34 provider keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `HF_TOKEN`, …) share NO prefix, and the allowlist is one GLOBAL list applied by a refine with no mode context, so admitting them for pi would widen it for every mode at once — they stay out, and pi users authenticate via `/login` or the server process's own env. ⚠️ DeepSeek repeats pi's lesson exactly: a dsh `settings.yaml` can nominate ANY env var as a provider credential (`apiKeyEnv`), so only the vendor namespaces `DSH_*` (launcher inputs incl. `DSH_PERMISSION_MODE`) and `DEEPSEEK_*` (`DEEPSEEK_API_KEY`/`DEEPSEEK_BASE_URL`) are admitted; foreign provider keys authenticate from dsh's own files or the server env. Resolver design pattern: `docs/opencode-integration.md`, `docs/pi-integration.md`, `docs/grok-integration.md`, `docs/deepseek-integration.md`
|
- **Multi-CLI prefix discipline** — env-var prefix is CLI-specific (`CLAUDE_CODE_*` vs `OPENCODE_*` vs `CODEX_*` vs `GEMINI_*` vs `ANTIGRAVITY_*` vs `PI_*` vs `GROK_*` vs `DSH_*`) and the `ALLOWED_ENV_PREFIXES` allowlist in `schemas.ts` enforces this; non-prefix exceptions are exact keys in `ALLOWED_ENV_KEYS` (currently only `CLAUDE_CONFIG_DIR`), never a widened prefix. Gemini additionally allowlists the **broad `GOOGLE_*`** namespace (intentional: Vertex AI auth needs `GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI`; it is the loosest allowlist entry, affecting only the user's own spawned CLI), and Grok allowlists **`XAI_*`** for the same vendor-namespace reason (`XAI_API_KEY` is grok's documented auth var). When adding a setting, decide which CLI(s) it applies to and gate the env export accordingly. Never blanket-forward all prefixes. ⚠️ Pi is the case that proves the rule: its ~34 provider keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `HF_TOKEN`, …) share NO prefix, and the allowlist is one GLOBAL list applied by a refine with no mode context, so admitting them for pi would widen it for every mode at once — they stay out, and pi users authenticate via `/login` or the server process's own env. ⚠️ DeepSeek repeats pi's lesson exactly: a dsh `settings.yaml` can nominate ANY env var as a provider credential (`apiKeyEnv`), so only the vendor namespaces `DSH_*` (launcher inputs incl. `DSH_PERMISSION_MODE`) and `DEEPSEEK_*` (`DEEPSEEK_API_KEY`/`DEEPSEEK_BASE_URL`) are admitted; foreign provider keys authenticate from dsh's own files or the server env. Resolver design pattern: `docs/opencode-integration.md`, `docs/pi-integration.md`, `docs/grok-integration.md`, `docs/deepseek-integration.md`
|
||||||
- **Zod `.optional()` rejects `null`** — accepts `undefined` only. When the frontend builds a request body with `JSON.stringify`, an explicit `null` field is preserved on the wire and fails validation with `INVALID_INPUT`. Convert `null` → `undefined` before stringifying (e.g. `field: value ?? undefined`), or declare the schema `.nullish()`. This has caused real shipped bugs twice
|
- **Zod `.optional()` rejects `null`** — accepts `undefined` only. When the frontend builds a request body with `JSON.stringify`, an explicit `null` field is preserved on the wire and fails validation with `INVALID_INPUT`. Convert `null` → `undefined` before stringifying (e.g. `field: value ?? undefined`), or declare the schema `.nullish()`. This has caused real shipped bugs twice
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ output. The other CLIs expose no equivalent.
|
|||||||
| Read My Mind | Yes | No |
|
| Read My Mind | Yes | No |
|
||||||
| Ralph loop and its task tracker | Yes | No |
|
| Ralph loop and its task tracker | Yes | No |
|
||||||
| Subagent and team windows | Yes | No |
|
| Subagent and team windows | Yes | No |
|
||||||
| Model, effort, advisor, and ultracode controls | Yes | No |
|
| Model, effort, and ultracode controls | Yes | No |
|
||||||
| `stop` and `blocked` wait signals | Yes | DeepSeek yes; elsewhere 400 if you ask for them explicitly |
|
| `stop` and `blocked` wait signals | Yes | DeepSeek yes; elsewhere 400 if you ask for them explicitly |
|
||||||
| The bundled agent skill | Yes | No |
|
| The bundled agent skill | Yes | No |
|
||||||
|
|
||||||
@@ -96,9 +96,6 @@ The defaults you will care about, all under **App Settings**:
|
|||||||
- **Effort** (`low` through `max`) or **ultracode** for dynamic multi-agent workflows. Also
|
- **Effort** (`low` through `max`) or **ultracode** for dynamic multi-agent workflows. Also
|
||||||
a soft default: `/effort` overrides it any time. Effort is deliberately not passed as an
|
a soft default: `/effort` overrides it any time. Effort is deliberately not passed as an
|
||||||
environment variable, because that would hard-lock it and block in-session switching.
|
environment variable, because that would hard-lock it and block in-session switching.
|
||||||
- **Advisor** (Sonnet, Opus or Fable): a stronger model Claude consults at decision points,
|
|
||||||
via Claude Code's [advisor tool](https://code.claude.com/docs/en/advisor). Also a soft
|
|
||||||
default: `/advisor` switches it or turns it off inside the session.
|
|
||||||
- **Startup permission mode** (Agents & CLIs section). The default is
|
- **Startup permission mode** (Agents & CLIs section). The default is
|
||||||
`--dangerously-skip-permissions`, which is why the security model matters. You can switch
|
`--dangerously-skip-permissions`, which is why the security model matters. You can switch
|
||||||
new sessions to Anthropic's classifier-guarded `auto` mode, normal prompting, or an
|
new sessions to Anthropic's classifier-guarded `auto` mode, normal prompting, or an
|
||||||
|
|||||||
@@ -87,22 +87,13 @@ every session or only the active tab.
|
|||||||
|
|
||||||
### Models
|
### Models
|
||||||
|
|
||||||
Claude model cards, the 1M context window switch, the thinking effort segment and the
|
Claude model cards, the 1M context window switch, and the thinking effort segment. The cards
|
||||||
advisor segment. The cards and the switch compose into one model choice, so there is no
|
and the switch compose into one model choice, so there is no separate "which one wins"
|
||||||
separate "which one wins" question.
|
question.
|
||||||
|
|
||||||
Model, effort and advisor are all **soft defaults**: the model is written into the case's
|
Model and effort are both **soft defaults**: the model is written into the case's
|
||||||
`.claude/settings.local.json` and effort and advisor are passed at start, so `/model`,
|
`.claude/settings.local.json` and effort is passed at start, so `/model` and `/effort`
|
||||||
`/effort` and `/advisor` inside a session override them at any time.
|
inside a session override them at any time.
|
||||||
|
|
||||||
**Advisor** gives new Claude sessions Claude Code's
|
|
||||||
[advisor tool](https://code.claude.com/docs/en/advisor): a second, stronger model that Claude
|
|
||||||
consults before committing to an approach, when an error keeps coming back, and before it
|
|
||||||
calls a task done. A common pairing is a Sonnet main model with an Opus or Fable advisor,
|
|
||||||
which costs less than running the stronger model all the time. **Default** leaves it to
|
|
||||||
whatever you picked with `/advisor` yourself. The advisor needs the Anthropic API (not
|
|
||||||
Bedrock or Vertex), and an advisor that ranks below the session's model is simply not
|
|
||||||
attached.
|
|
||||||
|
|
||||||
**Custom model endpoints** (off by default) adds a saved-endpoint list plus a matching
|
**Custom model endpoints** (off by default) adds a saved-endpoint list plus a matching
|
||||||
section to the Run dropdown, for pointing a harness at your own OpenAI-compatible server
|
section to the Run dropdown, for pointing a harness at your own OpenAI-compatible server
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ later call opens with, and your first REAL call performs them anyway:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
||||||
@@ -75,8 +75,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
|
|||||||
mkdir -p "$(dirname "$PRE")"
|
mkdir -p "$(dirname "$PRE")"
|
||||||
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
||||||
# half-written file self-heals here instead of costing you a round trip to rm it.
|
# half-written file self-heals here instead of costing you a round trip to rm it.
|
||||||
grep -qs '^CODEMAN_PREAMBLE=1.33.4$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
grep -qs '^CODEMAN_PREAMBLE=1.30.1$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
||||||
# ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
# ---- Codeman agent preamble 1.30.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
||||||
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
||||||
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
||||||
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
||||||
@@ -196,11 +196,6 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
|
|||||||
# rather than handed back, because a worker that never drew its composer would eat the
|
# rather than handed back, because a worker that never drew its composer would eat the
|
||||||
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
||||||
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
||||||
# CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
|
|
||||||
# it is set Claude Code's advisor tool: a stronger model the worker consults before
|
|
||||||
# committing to an approach, on a recurring error and before declaring the task done.
|
|
||||||
# Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
|
|
||||||
# advisor that ranks below the worker's model is accepted but never attached by claude.
|
|
||||||
spawn_worker() {
|
spawn_worker() {
|
||||||
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
||||||
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
||||||
@@ -212,19 +207,12 @@ spawn_worker() {
|
|||||||
# server clamps this back to `workspace-write` for an owner without the grant.
|
# server clamps this back to `workspace-write` for an owner without the grant.
|
||||||
# Spawn by hand (§5.1) when you want a worker that asks.
|
# Spawn by hand (§5.1) when you want a worker that asks.
|
||||||
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
||||||
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
|
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
|
||||||
'{caseName:$n,mode:$m,parentSessionId:$p}
|
'{caseName:$n,mode:$m,parentSessionId:$p}
|
||||||
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
|
||||||
+ (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
|
|
||||||
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
||||||
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
||||||
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
||||||
# A server without advisor support DROPS the field instead of refusing it, so read it
|
|
||||||
# back: a worker silently missing the advisor it was asked for is worth one line.
|
|
||||||
if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
|
|
||||||
[ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
|
|
||||||
echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
|
|
||||||
fi
|
|
||||||
if [ "$mode" = deepseek ]; then
|
if [ "$mode" = deepseek ]; then
|
||||||
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
||||||
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
||||||
@@ -384,10 +372,10 @@ last_text() {
|
|||||||
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
||||||
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
||||||
# here would fail that match and rewrite this file on every single bootstrap.
|
# here would fail that match and rewrite this file on every single bootstrap.
|
||||||
CODEMAN_PREAMBLE=1.33.4
|
CODEMAN_PREAMBLE=1.30.1
|
||||||
PREAMBLE
|
PREAMBLE
|
||||||
)
|
)
|
||||||
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
Every later Bash call that touches the API starts with the same two loader lines from
|
Every later Bash call that touches the API starts with the same two loader lines from
|
||||||
@@ -438,7 +426,7 @@ and no per-call body to hand-build.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
||||||
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
||||||
# (a name may carry a mode: `beta:deepseek`, see below)
|
# (a name may carry a mode: `beta:deepseek`, see below)
|
||||||
T=('reply with one line: the absolute path of your working directory'
|
T=('reply with one line: the absolute path of your working directory'
|
||||||
@@ -505,12 +493,6 @@ Four things this block leans on, each one link away, no detour needed to run it:
|
|||||||
`sendwait` reads the composer and keeps pressing Enter until the prompt has left it.
|
`sendwait` reads the composer and keeps pressing Enter until the prompt has left it.
|
||||||
All three are reasons to let `sendwait` build the call rather than hand-rolling it.
|
All three are reasons to let `sendwait` build the call rather than hand-rolling it.
|
||||||
- Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
|
- Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
|
||||||
- For long or high-stakes worker tasks, `CODEMAN_WORKER_ADVISOR=opus spawn_workers "${N[@]}"`
|
|
||||||
(`fable`, `opus` or `sonnet`) gives each claude worker Claude Code's advisor tool: a
|
|
||||||
stronger model it consults before committing to an approach, on a recurring error and
|
|
||||||
before declaring the task done. Advisor calls bill extra tokens, and an advisor ranked
|
|
||||||
below the worker's own model is never attached (on an Opus worker only `opus` and
|
|
||||||
`fable` do anything).
|
|
||||||
- Deleting the sessions does **not** remove the case directories. They are marked as
|
- Deleting the sessions does **not** remove the case directories. They are marked as
|
||||||
agent-created, so `GET /api/v1/cases/agent-created` lists them for cleanup: §5.14.
|
agent-created, so `GET /api/v1/cases/agent-created` lists them for cleanup: §5.14.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
# ---- Codeman agent preamble 1.30.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
||||||
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
||||||
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
||||||
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
||||||
@@ -118,11 +118,6 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
|
|||||||
# rather than handed back, because a worker that never drew its composer would eat the
|
# rather than handed back, because a worker that never drew its composer would eat the
|
||||||
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
||||||
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
||||||
# CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
|
|
||||||
# it is set Claude Code's advisor tool: a stronger model the worker consults before
|
|
||||||
# committing to an approach, on a recurring error and before declaring the task done.
|
|
||||||
# Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
|
|
||||||
# advisor that ranks below the worker's model is accepted but never attached by claude.
|
|
||||||
spawn_worker() {
|
spawn_worker() {
|
||||||
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
||||||
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
||||||
@@ -134,19 +129,12 @@ spawn_worker() {
|
|||||||
# server clamps this back to `workspace-write` for an owner without the grant.
|
# server clamps this back to `workspace-write` for an owner without the grant.
|
||||||
# Spawn by hand (§5.1) when you want a worker that asks.
|
# Spawn by hand (§5.1) when you want a worker that asks.
|
||||||
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
||||||
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
|
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
|
||||||
'{caseName:$n,mode:$m,parentSessionId:$p}
|
'{caseName:$n,mode:$m,parentSessionId:$p}
|
||||||
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
|
||||||
+ (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
|
|
||||||
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
||||||
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
||||||
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
||||||
# A server without advisor support DROPS the field instead of refusing it, so read it
|
|
||||||
# back: a worker silently missing the advisor it was asked for is worth one line.
|
|
||||||
if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
|
|
||||||
[ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
|
|
||||||
echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
|
|
||||||
fi
|
|
||||||
if [ "$mode" = deepseek ]; then
|
if [ "$mode" = deepseek ]; then
|
||||||
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
||||||
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
||||||
@@ -306,4 +294,4 @@ last_text() {
|
|||||||
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
||||||
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
||||||
# here would fail that match and rewrite this file on every single bootstrap.
|
# here would fail that match and rewrite this file on every single bootstrap.
|
||||||
CODEMAN_PREAMBLE=1.33.4
|
CODEMAN_PREAMBLE=1.30.1
|
||||||
|
|||||||
@@ -345,13 +345,6 @@ ESC=$(printf '\033')
|
|||||||
`.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
|
`.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
|
||||||
on the user's disk) if missing, do not retry it in a loop, and remember the name.
|
on the user's disk) if missing, do not retry it in a loop, and remember the name.
|
||||||
|
|
||||||
A claude worker also takes `"advisorModel":"opus"` (`fable`, `opus`, `sonnet` or a full
|
|
||||||
model id): Claude Code's advisor tool, a stronger model the worker consults before
|
|
||||||
committing to an approach, on a recurring error and before declaring the task done. It is
|
|
||||||
a soft default the worker can change with `/advisor`. Remote and docker cases refuse it
|
|
||||||
(400), as they refuse `effort`. `spawn_worker` and `spawn_workers` send it for you when
|
|
||||||
`CODEMAN_WORKER_ADVISOR` is set.
|
|
||||||
|
|
||||||
⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
|
⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
|
||||||
`OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
|
`OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
|
||||||
the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
|
the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
|
||||||
@@ -391,7 +384,7 @@ every claude create path installs them, so a linked case and a raw path both get
|
|||||||
|
|
||||||
**The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
|
**The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
|
||||||
a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
|
a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
|
||||||
`advisorModel`, `envOverrides`). Three differences that break copied code:
|
`envOverrides`). Three differences that break copied code:
|
||||||
|
|
||||||
- The id is at **`.data.session.id`**, not quick-start's `.data.sessionId`
|
- The id is at **`.data.session.id`**, not quick-start's `.data.sessionId`
|
||||||
(`session-routes.ts:878` returns `{ session: lightState }`).
|
(`session-routes.ts:878` returns `{ session: lightState }`).
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
|
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
|
||||||
|
|||||||
+8
-26
@@ -47,7 +47,7 @@ later call opens with, and your first REAL call performs them anyway:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
||||||
@@ -75,8 +75,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
|
|||||||
mkdir -p "$(dirname "$PRE")"
|
mkdir -p "$(dirname "$PRE")"
|
||||||
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
||||||
# half-written file self-heals here instead of costing you a round trip to rm it.
|
# half-written file self-heals here instead of costing you a round trip to rm it.
|
||||||
grep -qs '^CODEMAN_PREAMBLE=1.33.4$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
grep -qs '^CODEMAN_PREAMBLE=1.30.1$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
||||||
# ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
# ---- Codeman agent preamble 1.30.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
||||||
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
||||||
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
||||||
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
||||||
@@ -196,11 +196,6 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
|
|||||||
# rather than handed back, because a worker that never drew its composer would eat the
|
# rather than handed back, because a worker that never drew its composer would eat the
|
||||||
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
||||||
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
||||||
# CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
|
|
||||||
# it is set Claude Code's advisor tool: a stronger model the worker consults before
|
|
||||||
# committing to an approach, on a recurring error and before declaring the task done.
|
|
||||||
# Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
|
|
||||||
# advisor that ranks below the worker's model is accepted but never attached by claude.
|
|
||||||
spawn_worker() {
|
spawn_worker() {
|
||||||
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
||||||
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
||||||
@@ -212,19 +207,12 @@ spawn_worker() {
|
|||||||
# server clamps this back to `workspace-write` for an owner without the grant.
|
# server clamps this back to `workspace-write` for an owner without the grant.
|
||||||
# Spawn by hand (§5.1) when you want a worker that asks.
|
# Spawn by hand (§5.1) when you want a worker that asks.
|
||||||
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
||||||
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
|
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
|
||||||
'{caseName:$n,mode:$m,parentSessionId:$p}
|
'{caseName:$n,mode:$m,parentSessionId:$p}
|
||||||
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
|
||||||
+ (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
|
|
||||||
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
||||||
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
||||||
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
||||||
# A server without advisor support DROPS the field instead of refusing it, so read it
|
|
||||||
# back: a worker silently missing the advisor it was asked for is worth one line.
|
|
||||||
if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
|
|
||||||
[ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
|
|
||||||
echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
|
|
||||||
fi
|
|
||||||
if [ "$mode" = deepseek ]; then
|
if [ "$mode" = deepseek ]; then
|
||||||
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
||||||
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
||||||
@@ -384,10 +372,10 @@ last_text() {
|
|||||||
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
||||||
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
||||||
# here would fail that match and rewrite this file on every single bootstrap.
|
# here would fail that match and rewrite this file on every single bootstrap.
|
||||||
CODEMAN_PREAMBLE=1.33.4
|
CODEMAN_PREAMBLE=1.30.1
|
||||||
PREAMBLE
|
PREAMBLE
|
||||||
)
|
)
|
||||||
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
Every later Bash call that touches the API starts with the same two loader lines from
|
Every later Bash call that touches the API starts with the same two loader lines from
|
||||||
@@ -438,7 +426,7 @@ and no per-call body to hand-build.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
||||||
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
||||||
# (a name may carry a mode: `beta:deepseek`, see below)
|
# (a name may carry a mode: `beta:deepseek`, see below)
|
||||||
T=('reply with one line: the absolute path of your working directory'
|
T=('reply with one line: the absolute path of your working directory'
|
||||||
@@ -505,12 +493,6 @@ Four things this block leans on, each one link away, no detour needed to run it:
|
|||||||
`sendwait` reads the composer and keeps pressing Enter until the prompt has left it.
|
`sendwait` reads the composer and keeps pressing Enter until the prompt has left it.
|
||||||
All three are reasons to let `sendwait` build the call rather than hand-rolling it.
|
All three are reasons to let `sendwait` build the call rather than hand-rolling it.
|
||||||
- Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
|
- Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
|
||||||
- For long or high-stakes worker tasks, `CODEMAN_WORKER_ADVISOR=opus spawn_workers "${N[@]}"`
|
|
||||||
(`fable`, `opus` or `sonnet`) gives each claude worker Claude Code's advisor tool: a
|
|
||||||
stronger model it consults before committing to an approach, on a recurring error and
|
|
||||||
before declaring the task done. Advisor calls bill extra tokens, and an advisor ranked
|
|
||||||
below the worker's own model is never attached (on an Opus worker only `opus` and
|
|
||||||
`fable` do anything).
|
|
||||||
- Deleting the sessions does **not** remove the case directories. They are marked as
|
- Deleting the sessions does **not** remove the case directories. They are marked as
|
||||||
agent-created, so `GET /api/v1/cases/agent-created` lists them for cleanup: §5.14.
|
agent-created, so `GET /api/v1/cases/agent-created` lists them for cleanup: §5.14.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
# ---- Codeman agent preamble 1.30.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
||||||
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
||||||
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
||||||
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
||||||
@@ -118,11 +118,6 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
|
|||||||
# rather than handed back, because a worker that never drew its composer would eat the
|
# rather than handed back, because a worker that never drew its composer would eat the
|
||||||
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
# task prompt with its trust dialog. There is deliberately no pid poll: wait-output
|
||||||
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
# already blocks until the composer draws, and pid!=null proved startup, never readiness.
|
||||||
# CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
|
|
||||||
# it is set Claude Code's advisor tool: a stronger model the worker consults before
|
|
||||||
# committing to an approach, on a recurring error and before declaring the task done.
|
|
||||||
# Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
|
|
||||||
# advisor that ranks below the worker's model is accepted but never attached by claude.
|
|
||||||
spawn_worker() {
|
spawn_worker() {
|
||||||
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
||||||
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
||||||
@@ -134,19 +129,12 @@ spawn_worker() {
|
|||||||
# server clamps this back to `workspace-write` for an owner without the grant.
|
# server clamps this back to `workspace-write` for an owner without the grant.
|
||||||
# Spawn by hand (§5.1) when you want a worker that asks.
|
# Spawn by hand (§5.1) when you want a worker that asks.
|
||||||
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
||||||
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
|
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
|
||||||
'{caseName:$n,mode:$m,parentSessionId:$p}
|
'{caseName:$n,mode:$m,parentSessionId:$p}
|
||||||
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
|
+ (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
|
||||||
+ (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
|
|
||||||
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
||||||
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
||||||
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
||||||
# A server without advisor support DROPS the field instead of refusing it, so read it
|
|
||||||
# back: a worker silently missing the advisor it was asked for is worth one line.
|
|
||||||
if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
|
|
||||||
[ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
|
|
||||||
echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
|
|
||||||
fi
|
|
||||||
if [ "$mode" = deepseek ]; then
|
if [ "$mode" = deepseek ]; then
|
||||||
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
# The one non-claude mode with REAL end-of-turn signals: its TUI reports
|
||||||
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
# idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
|
||||||
@@ -306,4 +294,4 @@ last_text() {
|
|||||||
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
||||||
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
||||||
# here would fail that match and rewrite this file on every single bootstrap.
|
# here would fail that match and rewrite this file on every single bootstrap.
|
||||||
CODEMAN_PREAMBLE=1.33.4
|
CODEMAN_PREAMBLE=1.30.1
|
||||||
|
|||||||
@@ -345,13 +345,6 @@ ESC=$(printf '\033')
|
|||||||
`.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
|
`.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
|
||||||
on the user's disk) if missing, do not retry it in a loop, and remember the name.
|
on the user's disk) if missing, do not retry it in a loop, and remember the name.
|
||||||
|
|
||||||
A claude worker also takes `"advisorModel":"opus"` (`fable`, `opus`, `sonnet` or a full
|
|
||||||
model id): Claude Code's advisor tool, a stronger model the worker consults before
|
|
||||||
committing to an approach, on a recurring error and before declaring the task done. It is
|
|
||||||
a soft default the worker can change with `/advisor`. Remote and docker cases refuse it
|
|
||||||
(400), as they refuse `effort`. `spawn_worker` and `spawn_workers` send it for you when
|
|
||||||
`CODEMAN_WORKER_ADVISOR` is set.
|
|
||||||
|
|
||||||
⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
|
⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
|
||||||
`OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
|
`OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
|
||||||
the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
|
the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
|
||||||
@@ -391,7 +384,7 @@ every claude create path installs them, so a linked case and a raw path both get
|
|||||||
|
|
||||||
**The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
|
**The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
|
||||||
a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
|
a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
|
||||||
`advisorModel`, `envOverrides`). Three differences that break copied code:
|
`envOverrides`). Three differences that break copied code:
|
||||||
|
|
||||||
- The id is at **`.data.session.id`**, not quick-start's `.data.sessionId`
|
- The id is at **`.data.session.id`**, not quick-start's `.data.sessionId`
|
||||||
(`session-routes.ts:878` returns `{ session: lightState }`).
|
(`session-routes.ts:878` returns `{ session: lightState }`).
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
|
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
|
||||||
|
|||||||
@@ -115,8 +115,6 @@ export interface CreateSessionOptions {
|
|||||||
envOverrides?: Record<string, string>;
|
envOverrides?: Record<string, string>;
|
||||||
/** Claude CLI effort level, injected as a `--settings` soft default (overridable via /effort in-session) */
|
/** Claude CLI effort level, injected as a `--settings` soft default (overridable via /effort in-session) */
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
/** Claude advisor model, merged into the same `--settings` JSON (overridable via /advisor in-session) */
|
|
||||||
advisorModel?: string;
|
|
||||||
/** tmux history-limit (scrollback lines) allocated when this session is created. */
|
/** tmux history-limit (scrollback lines) allocated when this session is created. */
|
||||||
historyLimit?: number;
|
historyLimit?: number;
|
||||||
/** Remote execution metadata for local tmux sessions wrapping SSH */
|
/** Remote execution metadata for local tmux sessions wrapping SSH */
|
||||||
@@ -166,8 +164,6 @@ export interface RespawnPaneOptions {
|
|||||||
unsetEnvKeys?: string[];
|
unsetEnvKeys?: string[];
|
||||||
/** Claude CLI effort level (preserved across respawns, injected via `--settings`) */
|
/** Claude CLI effort level (preserved across respawns, injected via `--settings`) */
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
/** Claude advisor model (preserved across respawns, merged into the same `--settings` JSON) */
|
|
||||||
advisorModel?: string;
|
|
||||||
/** Original tmux history-limit retained for config parity; respawn cannot resize the existing pane. */
|
/** Original tmux history-limit retained for config parity; respawn cannot resize the existing pane. */
|
||||||
historyLimit?: number;
|
historyLimit?: number;
|
||||||
/** Remote execution metadata for local tmux sessions wrapping SSH */
|
/** Remote execution metadata for local tmux sessions wrapping SSH */
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ClaudeMode, EffortLevel } from './types.js';
|
import type { ClaudeMode, EffortLevel } from './types.js';
|
||||||
import { isAdvisorModel, isEffortLevel } from './types.js';
|
import { isEffortLevel } from './types.js';
|
||||||
import { getAugmentedPath } from './utils/index.js';
|
import { getAugmentedPath } from './utils/index.js';
|
||||||
import { compareVersions } from './utils/dependency-checker.js';
|
import { compareVersions } from './utils/dependency-checker.js';
|
||||||
import { dataPath } from './config/instance.js';
|
import { dataPath } from './config/instance.js';
|
||||||
@@ -54,25 +54,6 @@ export function buildEffortCliArgs(effort?: EffortLevel): string[] {
|
|||||||
return effort === 'ultracode' ? ['--settings', '{"ultracode":true}'] : ['--effort', effort];
|
return effort === 'ultracode' ? ['--settings', '{"ultracode":true}'] : ['--effort', effort];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The `--settings` keys that switch on Claude Code's advisor tool for one session: a
|
|
||||||
* stronger model the main model consults at decision points (code.claude.com/docs/en/advisor).
|
|
||||||
* Returns `{}` for an absent or non-allowlisted value, so callers can spread it unconditionally.
|
|
||||||
*
|
|
||||||
* ⚠️ Carried as the `advisorModel` SETTINGS key, never the `--advisor` flag. The flag EXITS at
|
|
||||||
* launch on any pairing the CLI refuses (`claude --advisor haiku` prints "cannot be used as an
|
|
||||||
* advisor" and exits 1, as does a Fable advisor still awaiting usage-credit consent), which
|
|
||||||
* would leave a dead pane on every spawn and respawn. The settings key degrades instead: the
|
|
||||||
* CLI simply does not attach an advisor it cannot use. It is a SOFT default either way:
|
|
||||||
* `/advisor` still switches or turns it off inside the running session.
|
|
||||||
*
|
|
||||||
* ⚠️ Claude Code reads only ONE `--settings` flag per invocation, so this must be merged into
|
|
||||||
* the same JSON object as ultracode and the statusLine exporter, never rendered on its own.
|
|
||||||
*/
|
|
||||||
export function buildAdvisorSettings(advisorModel?: string): { advisorModel?: string } {
|
|
||||||
return isAdvisorModel(advisorModel) ? { advisorModel } : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum Claude CLI version for passing `--name` at spawn. 2.1.224 is the release
|
* Minimum Claude CLI version for passing `--name` at spawn. 2.1.224 is the release
|
||||||
* that ships cross-session messaging (the feature that makes the peer name matter),
|
* that ships cross-session messaging (the feature that makes the peer name matter),
|
||||||
@@ -130,7 +111,6 @@ export function buildNameCliArgs(sessionName: string | undefined, cliVersion: st
|
|||||||
* @param effort - Optional effort level, injected via --settings (overridable in-session)
|
* @param effort - Optional effort level, injected via --settings (overridable in-session)
|
||||||
* @param sessionName - Optional Codeman session name, passed as `--name` (version-gated)
|
* @param sessionName - Optional Codeman session name, passed as `--name` (version-gated)
|
||||||
* @param cliVersion - Installed Claude CLI version for the `--name` gate (null = omit the flag)
|
* @param cliVersion - Installed Claude CLI version for the `--name` gate (null = omit the flag)
|
||||||
* @param advisorModel - Optional advisor model, merged into the one `--settings` JSON (see buildAdvisorSettings)
|
|
||||||
* @returns Array of CLI arguments
|
* @returns Array of CLI arguments
|
||||||
*/
|
*/
|
||||||
export function buildInteractiveArgs(
|
export function buildInteractiveArgs(
|
||||||
@@ -140,21 +120,11 @@ export function buildInteractiveArgs(
|
|||||||
allowedTools?: string,
|
allowedTools?: string,
|
||||||
effort?: EffortLevel,
|
effort?: EffortLevel,
|
||||||
sessionName?: string,
|
sessionName?: string,
|
||||||
cliVersion?: string | null,
|
cliVersion?: string | null
|
||||||
advisorModel?: string
|
|
||||||
): string[] {
|
): string[] {
|
||||||
const args = [...buildPermissionArgs(claudeMode, allowedTools), '--session-id', sessionId];
|
const args = [...buildPermissionArgs(claudeMode, allowedTools), '--session-id', sessionId];
|
||||||
if (model) args.push('--model', model);
|
if (model) args.push('--model', model);
|
||||||
const effortArgs = buildEffortCliArgs(effort);
|
args.push(...buildEffortCliArgs(effort));
|
||||||
const advisor = buildAdvisorSettings(advisorModel);
|
|
||||||
if (advisor.advisorModel === undefined) {
|
|
||||||
args.push(...effortArgs);
|
|
||||||
} else if (effortArgs[0] === '--settings') {
|
|
||||||
// One --settings flag only: fold the advisor into ultracode's JSON object.
|
|
||||||
args.push('--settings', JSON.stringify({ ...JSON.parse(effortArgs[1]), ...advisor }));
|
|
||||||
} else {
|
|
||||||
args.push(...effortArgs, '--settings', JSON.stringify(advisor));
|
|
||||||
}
|
|
||||||
args.push(...buildNameCliArgs(sessionName, cliVersion));
|
args.push(...buildNameCliArgs(sessionName, cliVersion));
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
import type { CliEntry } from './config/cli-registry/types.js';
|
import type { CliEntry } from './config/cli-registry/types.js';
|
||||||
import { renderLaunch, type EngineValues, type ParamValues } from './config/cli-registry/argv.js';
|
import { renderLaunch, type EngineValues, type ParamValues } from './config/cli-registry/argv.js';
|
||||||
import { matchesPattern } from './config/cli-registry/patterns.js';
|
import { matchesPattern } from './config/cli-registry/patterns.js';
|
||||||
import { buildAdvisorSettings, buildEffortCliArgs, sanitizeCliSessionName } from './session-cli-builder.js';
|
import { buildEffortCliArgs, sanitizeCliSessionName } from './session-cli-builder.js';
|
||||||
import { compareVersions } from './utils/dependency-checker.js';
|
import { compareVersions } from './utils/dependency-checker.js';
|
||||||
import { getClaudeCliVersion } from './utils/claude-cli-resolver.js';
|
import { getClaudeCliVersion } from './utils/claude-cli-resolver.js';
|
||||||
import { launcherDefaultTarget } from './utils/cli-launcher.js';
|
import { launcherDefaultTarget } from './utils/cli-launcher.js';
|
||||||
@@ -54,8 +54,6 @@ export interface SpawnBridgeOptions {
|
|||||||
ompConfig?: OmpConfig;
|
ompConfig?: OmpConfig;
|
||||||
resumeSessionId?: string;
|
resumeSessionId?: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
/** Claude advisor model; rides the same `--settings` JSON as ultracode (see buildAdvisorSettings). */
|
|
||||||
advisorModel?: string;
|
|
||||||
sessionName?: string;
|
sessionName?: string;
|
||||||
claudeCliVersion?: string | null;
|
claudeCliVersion?: string | null;
|
||||||
/**
|
/**
|
||||||
@@ -200,20 +198,14 @@ export function buildSpawnCommandFromRegistry(entry: CliEntry, options: SpawnBri
|
|||||||
engineValues.effortLevel = effortValue;
|
engineValues.effortLevel = effortValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fold the advisor model and the ephemeral plan-usage statusLine exporter (see
|
// Fold the ephemeral plan-usage statusLine exporter (see resolveStatusLineCliCommand in
|
||||||
// resolveStatusLineCliCommand in hooks-config.ts) into the SAME `--settings` JSON object as
|
// hooks-config.ts) into the SAME `--settings` JSON object as ultracode/ effort, since Claude
|
||||||
// ultracode/ effort, since Claude Code accepts only one `--settings` flag per invocation:
|
// Code accepts only one `--settings` flag per invocation — rendering them as two independent
|
||||||
// rendering them as independent params would let the last one silently win. Claude-only in
|
// params would let the second one silently win. Claude-only in practice (statusLineCommand
|
||||||
// practice: only claude's launch template renders this engine value, so another CLI's
|
// is resolved claude-mode-only upstream), but this merge is mode-agnostic.
|
||||||
// session carrying an advisorModel launches exactly as before.
|
if ((effortFlag === '--settings' && effortValue) || options.statusLineCommand) {
|
||||||
// Key order (ultracode, advisorModel, statusLine) keeps a launch without an advisor
|
const settingsObj: Record<string, unknown> =
|
||||||
// byte-identical to one from before the advisor existed.
|
effortFlag === '--settings' && effortValue ? JSON.parse(effortValue) : {};
|
||||||
const advisorSettings = buildAdvisorSettings(options.advisorModel);
|
|
||||||
if ((effortFlag === '--settings' && effortValue) || advisorSettings.advisorModel || options.statusLineCommand) {
|
|
||||||
const settingsObj: Record<string, unknown> = {
|
|
||||||
...(effortFlag === '--settings' && effortValue ? JSON.parse(effortValue) : {}),
|
|
||||||
...advisorSettings,
|
|
||||||
};
|
|
||||||
if (options.statusLineCommand) {
|
if (options.statusLineCommand) {
|
||||||
settingsObj.statusLine = { type: 'command', command: options.statusLineCommand };
|
settingsObj.statusLine = { type: 'command', command: options.statusLineCommand };
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-16
@@ -42,7 +42,6 @@ import {
|
|||||||
NiceConfig,
|
NiceConfig,
|
||||||
DEFAULT_NICE_CONFIG,
|
DEFAULT_NICE_CONFIG,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
isAdvisorModel,
|
|
||||||
isEffortLevel,
|
isEffortLevel,
|
||||||
type ClaudeMode,
|
type ClaudeMode,
|
||||||
type SessionMode,
|
type SessionMode,
|
||||||
@@ -659,11 +658,6 @@ export class Session extends EventEmitter {
|
|||||||
// the CLAUDE_CODE_EFFORT_LEVEL env var, which would hard-lock the session.
|
// the CLAUDE_CODE_EFFORT_LEVEL env var, which would hard-lock the session.
|
||||||
private _effort: EffortLevel | undefined;
|
private _effort: EffortLevel | undefined;
|
||||||
|
|
||||||
// Claude advisor model (code.claude.com/docs/en/advisor), merged into the same launch
|
|
||||||
// `--settings` JSON as ultracode, never the `--advisor` flag (which exits on a refused
|
|
||||||
// pairing). A soft default: /advisor still switches or disables it in-session.
|
|
||||||
private _advisorModel: string | undefined;
|
|
||||||
|
|
||||||
// Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md). `envKeys`,
|
// Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md). `envKeys`,
|
||||||
// `configDir` and `launchModel` are internal bookkeeping ONLY (never surfaced via
|
// `configDir` and `launchModel` are internal bookkeeping ONLY (never surfaced via
|
||||||
// toState()/the customModel getter): they are what setCustomModel() needs to undo a
|
// toState()/the customModel getter): they are what setCustomModel() needs to undo a
|
||||||
@@ -780,8 +774,6 @@ export class Session extends EventEmitter {
|
|||||||
envOverrides?: Record<string, string>;
|
envOverrides?: Record<string, string>;
|
||||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
/** Claude advisor model (soft default via --settings, switchable in-session via /advisor) */
|
|
||||||
advisorModel?: string;
|
|
||||||
/** tmux history-limit (scrollback lines) allocated when this session's pane is created. */
|
/** tmux history-limit (scrollback lines) allocated when this session's pane is created. */
|
||||||
tmuxHistoryLimit?: number;
|
tmuxHistoryLimit?: number;
|
||||||
/** Restored per-session attachment history. May include server-private external paths. */
|
/** Restored per-session attachment history. May include server-private external paths. */
|
||||||
@@ -942,9 +934,6 @@ export class Session extends EventEmitter {
|
|||||||
if (config.effort && isEffortLevel(config.effort)) {
|
if (config.effort && isEffortLevel(config.effort)) {
|
||||||
this._effort = config.effort;
|
this._effort = config.effort;
|
||||||
}
|
}
|
||||||
if (isAdvisorModel(config.advisorModel)) {
|
|
||||||
this._advisorModel = config.advisorModel;
|
|
||||||
}
|
|
||||||
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
||||||
this._remote = config.remote;
|
this._remote = config.remote;
|
||||||
this._docker = config.docker;
|
this._docker = config.docker;
|
||||||
@@ -1838,7 +1827,6 @@ export class Session extends EventEmitter {
|
|||||||
ompConfig: this._ompConfig,
|
ompConfig: this._ompConfig,
|
||||||
resumeSessionId: this._resumeSessionId,
|
resumeSessionId: this._resumeSessionId,
|
||||||
effort: this._effort,
|
effort: this._effort,
|
||||||
advisorModel: this._advisorModel,
|
|
||||||
customModel: this.customModel,
|
customModel: this.customModel,
|
||||||
// COD-118: runtime-only — surfaced so the frontend can require explicit user
|
// COD-118: runtime-only — surfaced so the frontend can require explicit user
|
||||||
// intent before restarting a crash-looped session. Deliberately NOT restored
|
// intent before restarting a crash-looped session. Deliberately NOT restored
|
||||||
@@ -2217,7 +2205,6 @@ export class Session extends EventEmitter {
|
|||||||
envOverrides: this._envOverrides,
|
envOverrides: this._envOverrides,
|
||||||
unsetEnvKeys: this._pendingEnvUnsets.size > 0 ? [...this._pendingEnvUnsets] : undefined,
|
unsetEnvKeys: this._pendingEnvUnsets.size > 0 ? [...this._pendingEnvUnsets] : undefined,
|
||||||
effort: this._effort,
|
effort: this._effort,
|
||||||
advisorModel: this._advisorModel,
|
|
||||||
historyLimit: this._tmuxHistoryLimit,
|
historyLimit: this._tmuxHistoryLimit,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
docker: this._docker,
|
docker: this._docker,
|
||||||
@@ -2680,7 +2667,6 @@ export class Session extends EventEmitter {
|
|||||||
resumeSessionId: this._resumeSessionId,
|
resumeSessionId: this._resumeSessionId,
|
||||||
envOverrides: this._envOverrides,
|
envOverrides: this._envOverrides,
|
||||||
effort: this._effort,
|
effort: this._effort,
|
||||||
advisorModel: this._advisorModel,
|
|
||||||
historyLimit: this._tmuxHistoryLimit,
|
historyLimit: this._tmuxHistoryLimit,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
docker: this._docker,
|
docker: this._docker,
|
||||||
@@ -2805,8 +2791,7 @@ export class Session extends EventEmitter {
|
|||||||
this._allowedTools,
|
this._allowedTools,
|
||||||
this._effort,
|
this._effort,
|
||||||
this.cliPinnedName,
|
this.cliPinnedName,
|
||||||
getClaudeCliVersion(),
|
getClaudeCliVersion()
|
||||||
this._advisorModel
|
|
||||||
);
|
);
|
||||||
this.ptyProcess = spawnPtyWithHelperRepair(() =>
|
this.ptyProcess = spawnPtyWithHelperRepair(() =>
|
||||||
pty.spawn(getClaudeBinaryPath(), args, {
|
pty.spawn(getClaudeBinaryPath(), args, {
|
||||||
|
|||||||
@@ -882,8 +882,6 @@ export function buildSpawnCommand(options: {
|
|||||||
ompConfig?: OmpConfig;
|
ompConfig?: OmpConfig;
|
||||||
resumeSessionId?: string;
|
resumeSessionId?: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
/** Claude advisor model, merged into the launch's one `--settings` JSON (see buildAdvisorSettings). */
|
|
||||||
advisorModel?: string;
|
|
||||||
/** Resolved by resolveStatusLineCliCommand (hooks-config.ts) — undefined skips the exporter. Claude only. */
|
/** Resolved by resolveStatusLineCliCommand (hooks-config.ts) — undefined skips the exporter. Claude only. */
|
||||||
statusLineCommand?: string;
|
statusLineCommand?: string;
|
||||||
/** Name pinned on claude as `--name` (version-gated, sanitized; local spawns only). Only a user-chosen name: see `Session.cliPinnedName`. */
|
/** Name pinned on claude as `--name` (version-gated, sanitized; local spawns only). Only a user-chosen name: see `Session.cliPinnedName`. */
|
||||||
@@ -2085,7 +2083,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
resumeSessionId,
|
resumeSessionId,
|
||||||
envOverrides,
|
envOverrides,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
historyLimit = DEFAULT_TMUX_HISTORY_LIMIT,
|
historyLimit = DEFAULT_TMUX_HISTORY_LIMIT,
|
||||||
remote,
|
remote,
|
||||||
docker,
|
docker,
|
||||||
@@ -2173,7 +2170,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
ompConfig,
|
ompConfig,
|
||||||
resumeSessionId,
|
resumeSessionId,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
statusLineCommand,
|
statusLineCommand,
|
||||||
sessionName: cliName,
|
sessionName: cliName,
|
||||||
});
|
});
|
||||||
@@ -2401,7 +2397,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
envOverrides,
|
envOverrides,
|
||||||
unsetEnvKeys,
|
unsetEnvKeys,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
remote,
|
remote,
|
||||||
docker,
|
docker,
|
||||||
cliName,
|
cliName,
|
||||||
@@ -2440,7 +2435,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
ompConfig,
|
ompConfig,
|
||||||
resumeSessionId,
|
resumeSessionId,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
statusLineCommand,
|
statusLineCommand,
|
||||||
sessionName: cliName,
|
sessionName: cliName,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -377,26 +377,6 @@ export function isEffortLevel(value: string | undefined): value is EffortLevel {
|
|||||||
return value !== undefined && (EFFORT_LEVELS as readonly string[]).includes(value);
|
return value !== undefined && (EFFORT_LEVELS as readonly string[]).includes(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Model aliases Claude Code accepts for its advisor tool (a stronger model the session's
|
|
||||||
* main model consults at decision points; code.claude.com/docs/en/advisor). Haiku is left
|
|
||||||
* out on purpose: it can call an advisor but never act as one.
|
|
||||||
*/
|
|
||||||
export const ADVISOR_MODEL_ALIASES = ['fable', 'opus', 'sonnet'] as const;
|
|
||||||
|
|
||||||
/** A full model id in one of the advisor-capable families, e.g. `claude-opus-5-5`. */
|
|
||||||
const ADVISOR_MODEL_ID_PATTERN = /^claude-(?:fable|opus|sonnet)-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard: is the value an advisor model Codeman will pass to claude? An alias from
|
|
||||||
* ADVISOR_MODEL_ALIASES or a full fable/opus/sonnet model id. ⚠️ This allowlist is also the
|
|
||||||
* injection guard: the value is rendered inside the single-quoted `--settings` JSON argument.
|
|
||||||
*/
|
|
||||||
export function isAdvisorModel(value: unknown): value is string {
|
|
||||||
if (typeof value !== 'string' || value.length > 64) return false;
|
|
||||||
return (ADVISOR_MODEL_ALIASES as readonly string[]).includes(value) || ADVISOR_MODEL_ID_PATTERN.test(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** OpenCode session configuration */
|
/** OpenCode session configuration */
|
||||||
export interface OpenCodeConfig {
|
export interface OpenCodeConfig {
|
||||||
/** Model identifier (e.g., "anthropic/claude-sonnet-4-5", "openai/gpt-5.2", "ollama/codellama") */
|
/** Model identifier (e.g., "anthropic/claude-sonnet-4-5", "openai/gpt-5.2", "ollama/codellama") */
|
||||||
@@ -815,8 +795,6 @@ export interface SessionState {
|
|||||||
resumeSessionId?: string;
|
resumeSessionId?: string;
|
||||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
/** Claude advisor model (`advisorModel` in the launch `--settings`, switchable in-session via /advisor) */
|
|
||||||
advisorModel?: string;
|
|
||||||
/**
|
/**
|
||||||
* Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md): the custom
|
* Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md): the custom
|
||||||
* OpenAI-compatible endpoint (local or cloud) this session's CLI is currently pointed
|
* OpenAI-compatible endpoint (local or cloud) this session's CLI is currently pointed
|
||||||
|
|||||||
@@ -2172,19 +2172,6 @@
|
|||||||
<option value="ultracode">Ultracode</option>
|
<option value="ultracode">Ultracode</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-row set-row-block" data-search="advisor fable opus sonnet second opinion review consult">
|
|
||||||
<div class="set-row-text">
|
|
||||||
<span class="set-row-label">Advisor</span>
|
|
||||||
<span class="set-row-desc">A stronger model Claude consults before big decisions, on repeated errors and before calling a task done. Uses extra tokens. Switchable in-session with /advisor.</span>
|
|
||||||
</div>
|
|
||||||
<div class="set-segment" id="appSettingsAdvisorSegment" role="radiogroup" aria-label="Advisor"></div>
|
|
||||||
<select id="appSettingsClaudeAdvisor" class="set-select set-field-hidden" aria-hidden="true" tabindex="-1">
|
|
||||||
<option value="">Default</option>
|
|
||||||
<option value="sonnet">Sonnet</option>
|
|
||||||
<option value="opus">Opus</option>
|
|
||||||
<option value="fable">Fable</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1038,7 +1038,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const ralphGlobalSettings = this.loadAppSettingsFromStorage();
|
const ralphGlobalSettings = this.loadAppSettingsFromStorage();
|
||||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(config.caseName), ralphGlobalSettings);
|
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(config.caseName), ralphGlobalSettings);
|
||||||
const effort = this.getEffortSetting(ralphGlobalSettings);
|
const effort = this.getEffortSetting(ralphGlobalSettings);
|
||||||
const advisorModel = this.getAdvisorSetting(ralphGlobalSettings);
|
|
||||||
const res = await fetch('/api/ralph-loop/start', {
|
const res = await fetch('/api/ralph-loop/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -1051,7 +1050,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
planItems: enabledItems?.length ? enabledItems : undefined,
|
planItems: enabledItems?.length ? enabledItems : undefined,
|
||||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||||
...(effort ? { effort } : {}),
|
...(effort ? { effort } : {}),
|
||||||
...(advisorModel ? { advisorModel } : {}),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -169,17 +169,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
return valid.includes(effort) ? effort : undefined;
|
return valid.includes(effort) ? effort : undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the advisor model for new Claude sessions from global settings.
|
|
||||||
* Returns 'fable' | 'opus' | 'sonnet', or undefined (= leave it to the CLI's own
|
|
||||||
* /advisor choice). Sent as the `advisorModel` payload field; the backend merges it
|
|
||||||
* into the launch's `claude --settings` JSON, so /advisor still switches it in-session.
|
|
||||||
*/
|
|
||||||
getAdvisorSetting(globalSettings) {
|
|
||||||
const advisor = globalSettings?.claudeAdvisorModel;
|
|
||||||
return ['fable', 'opus', 'sonnet'].includes(advisor) ? advisor : undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Quick Start
|
// Quick Start
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -1976,7 +1965,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const envOverrides = this.buildEnvOverrides(caseSettings, globalSettings);
|
const envOverrides = this.buildEnvOverrides(caseSettings, globalSettings);
|
||||||
const hasEnvOverrides = Object.keys(envOverrides).length > 0;
|
const hasEnvOverrides = Object.keys(envOverrides).length > 0;
|
||||||
const effort = this.getEffortSetting(globalSettings);
|
const effort = this.getEffortSetting(globalSettings);
|
||||||
const advisorModel = this.getAdvisorSetting(globalSettings);
|
|
||||||
// Explicit Claude Model choice (App Settings) wins over the legacy 1M Opus
|
// Explicit Claude Model choice (App Settings) wins over the legacy 1M Opus
|
||||||
// toggles; both flow as `modelOverride` → the case's .claude/settings.local.json
|
// toggles; both flow as `modelOverride` → the case's .claude/settings.local.json
|
||||||
const useOpus1m = caseSettings.opusContext1m || globalSettings.opusContext1mEnabled;
|
const useOpus1m = caseSettings.opusContext1m || globalSettings.opusContext1mEnabled;
|
||||||
@@ -1992,7 +1980,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
workingDir, name,
|
workingDir, name,
|
||||||
...(hasEnvOverrides ? { envOverrides } : {}),
|
...(hasEnvOverrides ? { envOverrides } : {}),
|
||||||
...(effort ? { effort } : {}),
|
...(effort ? { effort } : {}),
|
||||||
...(advisorModel ? { advisorModel } : {}),
|
|
||||||
...(modelOverride !== undefined ? { modelOverride } : {}),
|
...(modelOverride !== undefined ? { modelOverride } : {}),
|
||||||
})
|
})
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
|
|||||||
@@ -527,7 +527,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
document.getElementById('appSettingsOpusContext1m').checked = settings.opusContext1mEnabled ?? false;
|
document.getElementById('appSettingsOpusContext1m').checked = settings.opusContext1mEnabled ?? false;
|
||||||
document.getElementById('appSettingsRemoteAutoReconnect').checked = settings.remoteAutoReconnect ?? true;
|
document.getElementById('appSettingsRemoteAutoReconnect').checked = settings.remoteAutoReconnect ?? true;
|
||||||
document.getElementById('appSettingsThinkingEffort').value = settings.thinkingEffort ?? '';
|
document.getElementById('appSettingsThinkingEffort').value = settings.thinkingEffort ?? '';
|
||||||
document.getElementById('appSettingsClaudeAdvisor').value = settings.claudeAdvisorModel ?? '';
|
|
||||||
// CPU Priority settings
|
// CPU Priority settings
|
||||||
const niceSettings = settings.nice || {};
|
const niceSettings = settings.nice || {};
|
||||||
document.getElementById('appSettingsNiceEnabled').checked = niceSettings.enabled ?? false;
|
document.getElementById('appSettingsNiceEnabled').checked = niceSettings.enabled ?? false;
|
||||||
@@ -628,7 +627,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this._syncSettingsChips();
|
this._syncSettingsChips();
|
||||||
this._syncModelCards();
|
this._syncModelCards();
|
||||||
this._syncEffortSegment();
|
this._syncEffortSegment();
|
||||||
this._syncAdvisorSegment();
|
|
||||||
// Back to the top of the document (one scroll, not a tab reset). Updates is
|
// Back to the top of the document (one scroll, not a tab reset). Updates is
|
||||||
// first now: the version this install is running, and whether a newer one is
|
// first now: the version this install is running, and whether a newer one is
|
||||||
// waiting, are the two things worth seeing before any preference. The rest of
|
// waiting, are the two things worth seeing before any preference. The rest of
|
||||||
@@ -720,7 +718,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (!modal || !doc || typeof modal.querySelectorAll !== 'function') return;
|
if (!modal || !doc || typeof modal.querySelectorAll !== 'function') return;
|
||||||
this._buildModelCards();
|
this._buildModelCards();
|
||||||
this._buildEffortSegment();
|
this._buildEffortSegment();
|
||||||
this._buildAdvisorSegment();
|
|
||||||
// Rebuilt on every open: admin-ui.js appends its Users entry to the rail
|
// Rebuilt on every open: admin-ui.js appends its Users entry to the rail
|
||||||
// after the first open, and the menu must not drift from the rail.
|
// after the first open, and the menu must not drift from the rail.
|
||||||
this._buildSettingsJumpMenu();
|
this._buildSettingsJumpMenu();
|
||||||
@@ -996,28 +993,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_buildEffortSegment() {
|
_buildEffortSegment() {
|
||||||
this._buildSelectSegment('appSettingsThinkingEffort', 'appSettingsEffortSegment');
|
const select = document.getElementById('appSettingsThinkingEffort');
|
||||||
},
|
const seg = document.getElementById('appSettingsEffortSegment');
|
||||||
|
|
||||||
_syncEffortSegment() {
|
|
||||||
this._syncSelectSegment('appSettingsThinkingEffort', 'appSettingsEffortSegment');
|
|
||||||
},
|
|
||||||
|
|
||||||
_buildAdvisorSegment() {
|
|
||||||
this._buildSelectSegment('appSettingsClaudeAdvisor', 'appSettingsAdvisorSegment');
|
|
||||||
},
|
|
||||||
|
|
||||||
_syncAdvisorSegment() {
|
|
||||||
this._syncSelectSegment('appSettingsClaudeAdvisor', 'appSettingsAdvisorSegment');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a radio segment as a view over a hidden <select>, which stays the single
|
|
||||||
* source of truth for load/save (the same contract as the model cards).
|
|
||||||
*/
|
|
||||||
_buildSelectSegment(selectId, segId) {
|
|
||||||
const select = document.getElementById(selectId);
|
|
||||||
const seg = document.getElementById(segId);
|
|
||||||
if (!select || !seg || seg.dataset.built === '1' || !select.options) return;
|
if (!select || !seg || seg.dataset.built === '1' || !select.options) return;
|
||||||
seg.innerHTML = '';
|
seg.innerHTML = '';
|
||||||
[...select.options].forEach(opt => {
|
[...select.options].forEach(opt => {
|
||||||
@@ -1028,16 +1005,16 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
btn.textContent = opt.textContent;
|
btn.textContent = opt.textContent;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
select.value = opt.value;
|
select.value = opt.value;
|
||||||
this._syncSelectSegment(selectId, segId);
|
this._syncEffortSegment();
|
||||||
});
|
});
|
||||||
seg.appendChild(btn);
|
seg.appendChild(btn);
|
||||||
});
|
});
|
||||||
seg.dataset.built = '1';
|
seg.dataset.built = '1';
|
||||||
},
|
},
|
||||||
|
|
||||||
_syncSelectSegment(selectId, segId) {
|
_syncEffortSegment() {
|
||||||
const select = document.getElementById(selectId);
|
const select = document.getElementById('appSettingsThinkingEffort');
|
||||||
const seg = document.getElementById(segId);
|
const seg = document.getElementById('appSettingsEffortSegment');
|
||||||
if (!select || !seg) return;
|
if (!select || !seg) return;
|
||||||
seg.querySelectorAll('button').forEach(btn => {
|
seg.querySelectorAll('button').forEach(btn => {
|
||||||
const on = btn.dataset.value === (select.value || '');
|
const on = btn.dataset.value === (select.value || '');
|
||||||
@@ -2237,7 +2214,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
opusContext1mEnabled: document.getElementById('appSettingsOpusContext1m').checked,
|
opusContext1mEnabled: document.getElementById('appSettingsOpusContext1m').checked,
|
||||||
remoteAutoReconnect: document.getElementById('appSettingsRemoteAutoReconnect').checked,
|
remoteAutoReconnect: document.getElementById('appSettingsRemoteAutoReconnect').checked,
|
||||||
thinkingEffort: document.getElementById('appSettingsThinkingEffort').value,
|
thinkingEffort: document.getElementById('appSettingsThinkingEffort').value,
|
||||||
claudeAdvisorModel: document.getElementById('appSettingsClaudeAdvisor').value,
|
|
||||||
// CPU Priority settings
|
// CPU Priority settings
|
||||||
nice: {
|
nice: {
|
||||||
enabled: document.getElementById('appSettingsNiceEnabled').checked,
|
enabled: document.getElementById('appSettingsNiceEnabled').checked,
|
||||||
|
|||||||
+64
-24
File diff suppressed because one or more lines are too long
@@ -3155,7 +3155,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const globalSettings = this.loadAppSettingsFromStorage();
|
const globalSettings = this.loadAppSettingsFromStorage();
|
||||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
|
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
|
||||||
const effort = this.getEffortSetting(globalSettings);
|
const effort = this.getEffortSetting(globalSettings);
|
||||||
const advisorModel = this.getAdvisorSetting(globalSettings);
|
|
||||||
// `resumeSessionId` is a Claude conversation UUID (server reads it from
|
// `resumeSessionId` is a Claude conversation UUID (server reads it from
|
||||||
// ~/.claude/projects); an external-CLI row has no such thing, so sending
|
// ~/.claude/projects); an external-CLI row has no such thing, so sending
|
||||||
// it there gets silently ignored while the OMITTED `mode` field defaults
|
// it there gets silently ignored while the OMITTED `mode` field defaults
|
||||||
@@ -3205,8 +3204,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
...modeConfig,
|
...modeConfig,
|
||||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||||
...(effort ? { effort } : {}),
|
...(effort ? { effort } : {}),
|
||||||
// The advisor is a claude-only feature; other CLIs would carry it inertly.
|
|
||||||
...(advisorModel && effectiveMode === 'claude' ? { advisorModel } : {}),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const createData = await createRes.json();
|
const createData = await createRes.json();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
* session record involved. A dropped plan therefore returns the user to
|
* session record involved. A dropped plan therefore returns the user to
|
||||||
* resuming by hand, one at a time, which is where they are without this
|
* resuming by hand, one at a time, which is where they are without this
|
||||||
* feature. What the plan held that a transcript does not is the owner, the
|
* feature. What the plan held that a transcript does not is the owner, the
|
||||||
* name, the env overrides, the effort, the advisor model and the lineage.
|
* name, the env overrides, the effort and the lineage.
|
||||||
* - Module-level singleton in the style of `web/approval-inbox.ts`: no `Session`
|
* - Module-level singleton in the style of `web/approval-inbox.ts`: no `Session`
|
||||||
* import and no IO, which keeps it unit-testable and cycle-free.
|
* import and no IO, which keeps it unit-testable and cycle-free.
|
||||||
* - Spending is take-then-build: `take()` removes entries synchronously, before
|
* - Spending is take-then-build: `take()` removes entries synchronously, before
|
||||||
|
|||||||
@@ -293,7 +293,6 @@ export function registerRalphRoutes(
|
|||||||
planItems,
|
planItems,
|
||||||
envOverrides,
|
envOverrides,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
} = parseBody(RalphLoopStartSchema, req.body);
|
} = parseBody(RalphLoopStartSchema, req.body);
|
||||||
|
|
||||||
// Multi-user: cases live in the requesting user's space.
|
// Multi-user: cases live in the requesting user's space.
|
||||||
@@ -344,7 +343,6 @@ export function registerRalphRoutes(
|
|||||||
allowedTools: rlClaudeModeConfig.allowedTools,
|
allowedTools: rlClaudeModeConfig.allowedTools,
|
||||||
envOverrides,
|
envOverrides,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
owner: rlOwner,
|
owner: rlOwner,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ export function registerRebootRestoreRoutes(app: FastifyInstance, ctx: RebootRes
|
|||||||
(saved as { __envOverrides?: Record<string, string> }).__envOverrides
|
(saved as { __envOverrides?: Record<string, string> }).__envOverrides
|
||||||
),
|
),
|
||||||
effort: saved.effort,
|
effort: saved.effort,
|
||||||
advisorModel: saved.advisorModel,
|
|
||||||
attachmentHistory:
|
attachmentHistory:
|
||||||
(saved as { __attachmentHistory?: SessionAttachmentHistoryItem[] }).__attachmentHistory ??
|
(saved as { __attachmentHistory?: SessionAttachmentHistoryItem[] }).__attachmentHistory ??
|
||||||
saved.attachmentHistory,
|
saved.attachmentHistory,
|
||||||
|
|||||||
@@ -1130,7 +1130,6 @@ export function registerSessionRoutes(
|
|||||||
resumeSessionId: validatedResumeId,
|
resumeSessionId: validatedResumeId,
|
||||||
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
|
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
|
||||||
effort: body.effort,
|
effort: body.effort,
|
||||||
advisorModel: body.advisorModel,
|
|
||||||
tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
|
tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
|
||||||
remote,
|
remote,
|
||||||
owner,
|
owner,
|
||||||
@@ -3394,7 +3393,6 @@ export function registerSessionRoutes(
|
|||||||
ompConfig,
|
ompConfig,
|
||||||
envOverrides,
|
envOverrides,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
parentSessionId,
|
parentSessionId,
|
||||||
agentOrigin,
|
agentOrigin,
|
||||||
customModel,
|
customModel,
|
||||||
@@ -3442,7 +3440,6 @@ export function registerSessionRoutes(
|
|||||||
if (
|
if (
|
||||||
(envOverrides && Object.keys(envOverrides).length > 0) ||
|
(envOverrides && Object.keys(envOverrides).length > 0) ||
|
||||||
effort ||
|
effort ||
|
||||||
advisorModel ||
|
|
||||||
modelOverride !== undefined ||
|
modelOverride !== undefined ||
|
||||||
codexConfig ||
|
codexConfig ||
|
||||||
geminiConfig ||
|
geminiConfig ||
|
||||||
@@ -3456,7 +3453,7 @@ export function registerSessionRoutes(
|
|||||||
) {
|
) {
|
||||||
return createErrorResponse(
|
return createErrorResponse(
|
||||||
ApiErrorCode.INVALID_INPUT,
|
ApiErrorCode.INVALID_INPUT,
|
||||||
'envOverrides, effort, advisorModel, modelOverride, per-CLI config, and custom model endpoints are not supported for remote cases (they do not cross ssh). Configure the remote command via the host command override instead.'
|
'envOverrides, effort, modelOverride, per-CLI config, and custom model endpoints are not supported for remote cases (they do not cross ssh). Configure the remote command via the host command override instead.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3513,7 +3510,6 @@ export function registerSessionRoutes(
|
|||||||
if (
|
if (
|
||||||
(envOverrides && Object.keys(envOverrides).length > 0) ||
|
(envOverrides && Object.keys(envOverrides).length > 0) ||
|
||||||
effort ||
|
effort ||
|
||||||
advisorModel ||
|
|
||||||
codexConfig ||
|
codexConfig ||
|
||||||
geminiConfig ||
|
geminiConfig ||
|
||||||
antigravityConfig ||
|
antigravityConfig ||
|
||||||
@@ -3526,7 +3522,7 @@ export function registerSessionRoutes(
|
|||||||
) {
|
) {
|
||||||
return createErrorResponse(
|
return createErrorResponse(
|
||||||
ApiErrorCode.INVALID_INPUT,
|
ApiErrorCode.INVALID_INPUT,
|
||||||
'envOverrides, effort, advisorModel, per-CLI config, and custom model endpoints are not supported for docker cases (they do not cross into the container). Configure the container via the docker host command override instead.'
|
'envOverrides, effort, per-CLI config, and custom model endpoints are not supported for docker cases (they do not cross into the container). Configure the container via the docker host command override instead.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3981,7 +3977,6 @@ export function registerSessionRoutes(
|
|||||||
ompConfig: qsResolvedOmpConfig,
|
ompConfig: qsResolvedOmpConfig,
|
||||||
envOverrides: qsCustomModelEnvOverrides,
|
envOverrides: qsCustomModelEnvOverrides,
|
||||||
effort,
|
effort,
|
||||||
advisorModel,
|
|
||||||
remote,
|
remote,
|
||||||
docker,
|
docker,
|
||||||
resumeSessionId: dockerResumeId,
|
resumeSessionId: dockerResumeId,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { MAX_WAKE_MACS } from '../config/remote-wake-limits.js';
|
|||||||
import { MAX_INPUT_LENGTH } from '../config/terminal-limits.js';
|
import { MAX_INPUT_LENGTH } from '../config/terminal-limits.js';
|
||||||
import { enabledCliIds, enabledClis } from '../config/cli-registry/registry.js';
|
import { enabledCliIds, enabledClis } from '../config/cli-registry/registry.js';
|
||||||
import type { SessionMode } from '../types.js';
|
import type { SessionMode } from '../types.js';
|
||||||
import { isAdvisorModel } from '../types/session.js';
|
|
||||||
|
|
||||||
// ========== Path Validation ==========
|
// ========== Path Validation ==========
|
||||||
|
|
||||||
@@ -252,20 +251,6 @@ const safeEnvOverridesSchema = z
|
|||||||
*/
|
*/
|
||||||
const effortLevelSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max', 'ultracode']).optional();
|
const effortLevelSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max', 'ultracode']).optional();
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude advisor model for new sessions: `fable`/`opus`/`sonnet` or a full model id in one of
|
|
||||||
* those families (isAdvisorModel). Merged into the launch `--settings` JSON as `advisorModel`,
|
|
||||||
* a soft default that /advisor still switches in-session. The allowlist is also the injection
|
|
||||||
* guard for the single-quoted `--settings` argument.
|
|
||||||
*/
|
|
||||||
const advisorModelSchema = z
|
|
||||||
.string()
|
|
||||||
.max(64)
|
|
||||||
.refine((value) => isAdvisorModel(value), {
|
|
||||||
message: 'advisorModel must be fable, opus, sonnet or a full claude-fable/opus/sonnet model id',
|
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
// ========== Session Routes ==========
|
// ========== Session Routes ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -539,8 +524,6 @@ export const CreateSessionSchema = z.object({
|
|||||||
envOverrides: safeEnvOverridesSchema,
|
envOverrides: safeEnvOverridesSchema,
|
||||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||||
effort: effortLevelSchema,
|
effort: effortLevelSchema,
|
||||||
/** Claude advisor model (soft default via --settings, switchable in-session via /advisor) */
|
|
||||||
advisorModel: advisorModelSchema,
|
|
||||||
/** Model override to write to .claude/settings.local.json (e.g., "opus[1m]"). Empty string clears. */
|
/** Model override to write to .claude/settings.local.json (e.g., "opus[1m]"). Empty string clears. */
|
||||||
modelOverride: z.string().max(50).optional(),
|
modelOverride: z.string().max(50).optional(),
|
||||||
openCodeConfig: OpenCodeConfigSchema,
|
openCodeConfig: OpenCodeConfigSchema,
|
||||||
@@ -1072,8 +1055,6 @@ export const QuickStartSchema = z.object({
|
|||||||
envOverrides: safeEnvOverridesSchema,
|
envOverrides: safeEnvOverridesSchema,
|
||||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||||
effort: effortLevelSchema,
|
effort: effortLevelSchema,
|
||||||
/** Claude advisor model (soft default via --settings, switchable in-session via /advisor) */
|
|
||||||
advisorModel: advisorModelSchema,
|
|
||||||
/**
|
/**
|
||||||
* Who is spawning this worker (`codeman-skill` from the packaged agent skill), or,
|
* Who is spawning this worker (`codeman-skill` from the packaged agent skill), or,
|
||||||
* equivalently, the `X-Codeman-Agent-Origin` header; the body wins when both are
|
* equivalently, the `X-Codeman-Agent-Origin` header; the body wins when both are
|
||||||
@@ -1393,12 +1374,6 @@ export const SettingsUpdateSchema = z
|
|||||||
// auto-reattached.
|
// auto-reattached.
|
||||||
remoteAutoReconnect: z.boolean().optional(),
|
remoteAutoReconnect: z.boolean().optional(),
|
||||||
thinkingEffort: z.string().max(20).optional(),
|
thinkingEffort: z.string().max(20).optional(),
|
||||||
/** Advisor model for new Claude sessions ('' = leave it to the CLI's own /advisor choice). */
|
|
||||||
claudeAdvisorModel: z
|
|
||||||
.string()
|
|
||||||
.max(64)
|
|
||||||
.refine((value) => value === '' || isAdvisorModel(value), { message: 'Invalid advisor model' })
|
|
||||||
.optional(),
|
|
||||||
// UI visibility
|
// UI visibility
|
||||||
showFontControls: z.boolean().optional(),
|
showFontControls: z.boolean().optional(),
|
||||||
showSystemStats: z.boolean().optional(),
|
showSystemStats: z.boolean().optional(),
|
||||||
@@ -1887,8 +1862,6 @@ export const RalphLoopStartSchema = z.object({
|
|||||||
envOverrides: safeEnvOverridesSchema,
|
envOverrides: safeEnvOverridesSchema,
|
||||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||||
effort: effortLevelSchema,
|
effort: effortLevelSchema,
|
||||||
/** Claude advisor model (soft default via --settings, switchable in-session via /advisor) */
|
|
||||||
advisorModel: advisorModelSchema,
|
|
||||||
planItems: z
|
planItems: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -3489,7 +3489,6 @@ export class WebServer extends EventEmitter {
|
|||||||
ompConfig: muxSession.mode === 'omp' ? savedState?.ompConfig : undefined,
|
ompConfig: muxSession.mode === 'omp' ? savedState?.ompConfig : undefined,
|
||||||
envOverrides: savedEnvOverrides,
|
envOverrides: savedEnvOverrides,
|
||||||
effort: savedState?.effort,
|
effort: savedState?.effort,
|
||||||
advisorModel: savedState?.advisorModel,
|
|
||||||
attachmentHistory: savedAttachmentHistory,
|
attachmentHistory: savedAttachmentHistory,
|
||||||
// The pane's last Enter. Without it the response viewer would show
|
// The pane's last Enter. Without it the response viewer would show
|
||||||
// the launch conversation until the user types again, even though
|
// the launch conversation until the user types again, even though
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Tests for Claude Code's advisor tool (code.claude.com/docs/en/advisor)
|
|
||||||
* carried as a per-session `advisorModel`.
|
|
||||||
*
|
|
||||||
* The advisor rides the launch's ONE `--settings` JSON object as the `advisorModel` key,
|
|
||||||
* never the `--advisor` flag: the flag exits at launch on a pairing the CLI refuses
|
|
||||||
* (`claude --advisor haiku` prints "cannot be used as an advisor" and exits 1), while the
|
|
||||||
* settings key degrades to "no advisor". Verified against Claude Code 2.1.289 with
|
|
||||||
* `claude -p --settings '{"advisorModel":"opus"}' /advisor` → "Advisor: Opus 5.5".
|
|
||||||
*
|
|
||||||
* `--settings` is extracted through a REAL shell, as in statusline-cli-flag.test.ts, so the
|
|
||||||
* assertions see exactly what a spawned pane would.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { execFileSync } from 'node:child_process';
|
|
||||||
import { buildAdvisorSettings, buildInteractiveArgs } from '../src/session-cli-builder.js';
|
|
||||||
import { buildSpawnCommand } from '../src/tmux-manager.js';
|
|
||||||
import { isAdvisorModel, ADVISOR_MODEL_ALIASES } from '../src/types.js';
|
|
||||||
import { Session } from '../src/session.js';
|
|
||||||
import {
|
|
||||||
CreateSessionSchema,
|
|
||||||
QuickStartSchema,
|
|
||||||
RalphLoopStartSchema,
|
|
||||||
SettingsUpdateSchema,
|
|
||||||
} from '../src/web/schemas.js';
|
|
||||||
|
|
||||||
const EXPORTER_CMD = 'curl -sfk -X POST "$CODEMAN_API_URL/api/status-telemetry" --data @- 2>/dev/null || true';
|
|
||||||
|
|
||||||
function extractSettingsJson(cmd: string): unknown {
|
|
||||||
const idx = cmd.indexOf('--settings ');
|
|
||||||
expect(idx).toBeGreaterThan(-1);
|
|
||||||
const out = execFileSync('bash', ['-c', `set -- ${cmd.slice(idx)}; printf '%s' "$2"`]).toString();
|
|
||||||
return JSON.parse(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('isAdvisorModel', () => {
|
|
||||||
it('accepts the documented aliases', () => {
|
|
||||||
for (const alias of ADVISOR_MODEL_ALIASES) expect(isAdvisorModel(alias)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts full model ids in the advisor-capable families', () => {
|
|
||||||
expect(isAdvisorModel('claude-opus-5-5')).toBe(true);
|
|
||||||
expect(isAdvisorModel('claude-fable-5-1')).toBe(true);
|
|
||||||
expect(isAdvisorModel('claude-sonnet-5-5')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects haiku, which can call an advisor but never act as one', () => {
|
|
||||||
expect(isAdvisorModel('haiku')).toBe(false);
|
|
||||||
expect(isAdvisorModel('claude-haiku-4-5-20251001')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects anything that could break out of the quoted --settings argument', () => {
|
|
||||||
for (const bad of [
|
|
||||||
'',
|
|
||||||
'OPUS',
|
|
||||||
'opus[1m]',
|
|
||||||
"opus'; rm -rf /; '",
|
|
||||||
'opus"}',
|
|
||||||
'claude-opus-5-5 --dangerously-skip-permissions',
|
|
||||||
`claude-opus-${'5-'.repeat(40)}5`,
|
|
||||||
undefined,
|
|
||||||
null,
|
|
||||||
42,
|
|
||||||
]) {
|
|
||||||
expect(isAdvisorModel(bad)).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('buildAdvisorSettings', () => {
|
|
||||||
it('returns the settings key for a valid model and nothing otherwise', () => {
|
|
||||||
expect(buildAdvisorSettings('opus')).toEqual({ advisorModel: 'opus' });
|
|
||||||
expect(buildAdvisorSettings(undefined)).toEqual({});
|
|
||||||
expect(buildAdvisorSettings('haiku')).toEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('buildSpawnCommand advisorModel (tmux launch, claude mode)', () => {
|
|
||||||
it('rides --settings as the advisorModel key, never the --advisor flag', () => {
|
|
||||||
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', advisorModel: 'opus' });
|
|
||||||
expect(cmd).not.toContain('--advisor');
|
|
||||||
expect(cmd.match(/--settings/g)).toHaveLength(1);
|
|
||||||
expect(extractSettingsJson(cmd)).toEqual({ advisorModel: 'opus' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('merges ultracode, advisor and the statusLine exporter into ONE --settings object', () => {
|
|
||||||
const cmd = buildSpawnCommand({
|
|
||||||
mode: 'claude',
|
|
||||||
sessionId: 'sid-1',
|
|
||||||
effort: 'ultracode',
|
|
||||||
advisorModel: 'fable',
|
|
||||||
statusLineCommand: EXPORTER_CMD,
|
|
||||||
});
|
|
||||||
expect(cmd.match(/--settings/g)).toHaveLength(1);
|
|
||||||
expect(extractSettingsJson(cmd)).toEqual({
|
|
||||||
ultracode: true,
|
|
||||||
advisorModel: 'fable',
|
|
||||||
statusLine: { type: 'command', command: EXPORTER_CMD },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps a regular --effort flag beside the advisor settings', () => {
|
|
||||||
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', effort: 'high', advisorModel: 'sonnet' });
|
|
||||||
expect(cmd).toContain("--effort 'high'");
|
|
||||||
expect(extractSettingsJson(cmd)).toEqual({ advisorModel: 'sonnet' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('carries the advisor on the resume variant too', () => {
|
|
||||||
const cmd = buildSpawnCommand({
|
|
||||||
mode: 'claude',
|
|
||||||
sessionId: 'sid-1',
|
|
||||||
resumeSessionId: '11111111-2222-3333-4444-555555555555',
|
|
||||||
advisorModel: 'opus',
|
|
||||||
});
|
|
||||||
expect(cmd).toContain('--resume');
|
|
||||||
expect(extractSettingsJson(cmd)).toEqual({ advisorModel: 'opus' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('leaves the command byte-identical when no advisor (or an invalid one) is set', () => {
|
|
||||||
for (const base of [
|
|
||||||
{ mode: 'claude', sessionId: 'sid-1' },
|
|
||||||
{ mode: 'claude', sessionId: 'sid-1', effort: 'ultracode' as const, statusLineCommand: EXPORTER_CMD },
|
|
||||||
]) {
|
|
||||||
const without = buildSpawnCommand(base);
|
|
||||||
expect(buildSpawnCommand({ ...base, advisorModel: undefined })).toBe(without);
|
|
||||||
expect(buildSpawnCommand({ ...base, advisorModel: 'haiku' })).toBe(without);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is inert for a CLI that has no --settings carrier', () => {
|
|
||||||
const cmd = buildSpawnCommand({ mode: 'codex', sessionId: 'sid-1', advisorModel: 'opus' });
|
|
||||||
expect(cmd).not.toContain('advisorModel');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('buildInteractiveArgs advisorModel (direct-PTY fallback)', () => {
|
|
||||||
const settingsOf = (args: string[]) => {
|
|
||||||
expect(args.filter((a) => a === '--settings')).toHaveLength(1);
|
|
||||||
return JSON.parse(args[args.indexOf('--settings') + 1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
it('adds a --settings object holding only the advisor', () => {
|
|
||||||
const args = buildInteractiveArgs('sid', 'normal', undefined, undefined, undefined, undefined, null, 'opus');
|
|
||||||
expect(args).not.toContain('--advisor');
|
|
||||||
expect(settingsOf(args)).toEqual({ advisorModel: 'opus' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("folds the advisor into ultracode's --settings object", () => {
|
|
||||||
const args = buildInteractiveArgs('sid', 'normal', undefined, undefined, 'ultracode', undefined, null, 'fable');
|
|
||||||
expect(settingsOf(args)).toEqual({ ultracode: true, advisorModel: 'fable' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps --effort beside it for a regular level', () => {
|
|
||||||
const args = buildInteractiveArgs('sid', 'normal', undefined, undefined, 'max', undefined, null, 'sonnet');
|
|
||||||
expect(args).toEqual(expect.arrayContaining(['--effort', 'max']));
|
|
||||||
expect(settingsOf(args)).toEqual({ advisorModel: 'sonnet' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is unchanged without an advisor', () => {
|
|
||||||
expect(buildInteractiveArgs('sid', 'normal', undefined, undefined, 'high', undefined, null, undefined)).toEqual(
|
|
||||||
buildInteractiveArgs('sid', 'normal', undefined, undefined, 'high', undefined, null)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Session advisorModel', () => {
|
|
||||||
it('stores a valid advisor and persists it through toState()', () => {
|
|
||||||
const session = new Session({ workingDir: '/tmp', advisorModel: 'opus' });
|
|
||||||
expect(session.toState().advisorModel).toBe('opus');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('drops an invalid value instead of forwarding it to the launch', () => {
|
|
||||||
expect(new Session({ workingDir: '/tmp', advisorModel: 'haiku' }).toState().advisorModel).toBeUndefined();
|
|
||||||
expect(new Session({ workingDir: '/tmp' }).toState().advisorModel).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('advisorModel request validation', () => {
|
|
||||||
it.each([
|
|
||||||
['CreateSessionSchema', CreateSessionSchema, { workingDir: '/tmp' }],
|
|
||||||
['QuickStartSchema', QuickStartSchema, {}],
|
|
||||||
['RalphLoopStartSchema', RalphLoopStartSchema, { taskDescription: 'x' }],
|
|
||||||
] as const)('%s accepts advisor models and rejects the rest', (_name, schema, base) => {
|
|
||||||
expect(schema.safeParse({ ...base, advisorModel: 'opus' }).success).toBe(true);
|
|
||||||
expect(schema.safeParse({ ...base, advisorModel: 'claude-fable-5-1' }).success).toBe(true);
|
|
||||||
expect(schema.safeParse({ ...base }).success).toBe(true);
|
|
||||||
expect(schema.safeParse({ ...base, advisorModel: 'haiku' }).success).toBe(false);
|
|
||||||
expect(schema.safeParse({ ...base, advisorModel: "opus'" }).success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('SettingsUpdateSchema takes claudeAdvisorModel, with "" meaning the CLI default', () => {
|
|
||||||
expect(SettingsUpdateSchema.safeParse({ claudeAdvisorModel: '' }).success).toBe(true);
|
|
||||||
expect(SettingsUpdateSchema.safeParse({ claudeAdvisorModel: 'fable' }).success).toBe(true);
|
|
||||||
expect(SettingsUpdateSchema.safeParse({ claudeAdvisorModel: 'haiku' }).success).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -55,7 +55,6 @@ function makeApp() {
|
|||||||
getCaseSettings: () => ({}),
|
getCaseSettings: () => ({}),
|
||||||
buildEnvOverrides: () => ({}),
|
buildEnvOverrides: () => ({}),
|
||||||
getEffortSetting: () => undefined,
|
getEffortSetting: () => undefined,
|
||||||
getAdvisorSetting: () => undefined,
|
|
||||||
selectSession: vi.fn(async () => {}),
|
selectSession: vi.fn(async () => {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Static guard for the Run menus' per-CLI logos (styles.css).
|
||||||
|
*
|
||||||
|
* Every launch surface (toolbar Run menu, phone overview picker, Custom Endpoint
|
||||||
|
* rows, model picker) renders `<span class="run-mode-dot <id>">`, and styles.css
|
||||||
|
* turns that slot into the CLI's logo through a `--run-mode-logo` data URI plus
|
||||||
|
* one of two paint groups: brand-coloured marks paint as a background image,
|
||||||
|
* monochrome marks as a mask over the row's text colour. Three ways that breaks
|
||||||
|
* silently, each pinned here:
|
||||||
|
* - a new stock CLI lands with no logo rule (its row shows a bare grey dot);
|
||||||
|
* - a logo rule exists but the id is in neither paint group (the variable is
|
||||||
|
* set and never painted, so the slot is a blank 15px gap);
|
||||||
|
* - a later `background:` shorthand on `.run-mode-dot.<id>` (e.g. a skin
|
||||||
|
* override like the ones the non-og block used to carry) resets
|
||||||
|
* background-image and wipes the logo.
|
||||||
|
*
|
||||||
|
* Port: none (pure static analysis).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { STOCK_CLIS } from '../src/config/cli-registry/stock.js';
|
||||||
|
|
||||||
|
// Comments stripped: they sit between rules and carry commas, which would bleed
|
||||||
|
// into the selector lists split below.
|
||||||
|
const styles = readFileSync(resolve('src/web/public/styles.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
|
|
||||||
|
/** Every id a Run menu renders as `.run-mode-dot <id>`: the stock agents, Shell, and web tabs. */
|
||||||
|
const RUN_MENU_IDS = [
|
||||||
|
...STOCK_CLIS.filter((cli) => cli.kind === 'agent').map((cli) => cli.id as string),
|
||||||
|
'shell',
|
||||||
|
'web',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** The selector list of the one rule whose body contains `marker`. */
|
||||||
|
function groupIds(marker: string): string[] {
|
||||||
|
const rule = [...styles.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter((m) => m[2].includes(marker));
|
||||||
|
expect(rule, `exactly one rule paints via "${marker}"`).toHaveLength(1);
|
||||||
|
return rule[0][1]
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim().match(/^\.run-mode-dot\.([a-z0-9-]+)$/)?.[1])
|
||||||
|
.filter((id): id is string => Boolean(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoSvg(id: string): string {
|
||||||
|
const m = styles.match(
|
||||||
|
new RegExp(`\\.run-mode-dot\\.${id} \\{ --run-mode-logo: url\\("data:image/svg\\+xml,([^"]+)"\\); \\}`)
|
||||||
|
);
|
||||||
|
expect(m, `.run-mode-dot.${id} has a --run-mode-logo rule`).not.toBeNull();
|
||||||
|
return decodeURIComponent(m![1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Run menu CLI logos', () => {
|
||||||
|
const colourIds = groupIds('background: var(--run-mode-logo)');
|
||||||
|
const maskIds = groupIds('mask: var(--run-mode-logo)');
|
||||||
|
|
||||||
|
it.each(RUN_MENU_IDS)('%s has a logo that one paint group draws', (id) => {
|
||||||
|
const svg = logoSvg(id);
|
||||||
|
expect(svg.startsWith('<svg')).toBe(true);
|
||||||
|
expect(svg).toMatch(/viewBox='[\d. ]+'/);
|
||||||
|
expect(colourIds.includes(id) !== maskIds.includes(id), `${id} is in exactly one paint group`).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines no logo that no surface renders, and no group member without a logo', () => {
|
||||||
|
expect([...colourIds, ...maskIds].sort()).toEqual([...RUN_MENU_IDS].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('carries no script or event handler inside a data URI', () => {
|
||||||
|
for (const id of RUN_MENU_IDS) {
|
||||||
|
const svg = logoSvg(id);
|
||||||
|
expect(svg).not.toMatch(/<script|\son[a-z]+=|javascript:|href=/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never resets a logo slot with a background shorthand outside the colour group', () => {
|
||||||
|
const offenders = [...styles.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter(
|
||||||
|
([, selector, body]) =>
|
||||||
|
/\.run-mode-dot\.[a-z]/.test(selector) &&
|
||||||
|
/(^|[;\s])background\s*:/.test(body) &&
|
||||||
|
!body.includes('var(--run-mode-logo)')
|
||||||
|
);
|
||||||
|
expect(offenders.map((m) => m[1].trim())).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user