Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ GitHub Releases page; `0.8.0` is the new starting line.

- **Release promotion no longer stalls when the Homebrew tap is broken.** The `promote-release` workflow now gates only on platform assets and PyPI; a lagging or broken Homebrew tap emits a warning annotation and step summary note but no longer blocks the GitHub Release from reaching Latest.
- **Calmer, theme-aligned TUI rendering.** Transcript, recap, and tool-header output now use theme-standardized activity colors instead of hardcoded values, and Markdown tables render as a bordered grid (wide tables no longer collapse into a stacked-record list).
- **Auto-mode tool approval fails closed when unattended.** In auto/non-interactive runs, an action that still needs approval under the active safe-mode/trust policy is denied with guidance instead of waiting indefinitely for an absent user, and outside-workspace writes are never auto-approved. A new `auto_deliberate_destructive_actions` setting can additionally bounce destructive auto-approved actions once for deliberation before they run.
- **Auto-mode tool approval fails closed when unattended.** In auto/non-interactive runs, an action that still needs approval under the active safe-mode/trust policy is denied with guidance instead of waiting indefinitely for an absent user, and outside-workspace writes are never auto-approved. A destructive auto-approved action is now bounced once for deliberation whenever no user is present (regardless of config), so the obvious `--yolo --auto` combination is no longer more dangerous than the `autonomous_coding` profile; the `auto_deliberate_destructive_actions` setting extends that backstop to interactive `--yolo` sessions, where a user is present but approvals are skipped.
- **Yolo + auto mode hardened against silent over-reach.** Entering plan mode now requires confirmation in an interactive `--yolo` session (matching exit), so the plan-review checkpoint is preserved when a user is present. A `--yolo` run no longer clears or persists the workspace's safe-mode/trust state. A new `--no-yolo` flag forces yolo off for a run — overriding the `--yolo` flag, the `default_yolo` config, and any resumed session state. Resuming a session that restores yolo and/or auto now surfaces a startup warning so it is never silent.
- **`pythinker review` validates finding evidence.** Reviewflow assembles prompts from a shared security-knowledge manifest and validates findings, handling invalid ones without failing the whole review.

## 0.30.0 (2026-06-02)
Expand Down
6 changes: 6 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- **Release promotion no longer stalls when the Homebrew tap is broken.** The `promote-release` workflow now gates only on platform assets and PyPI; a lagging or broken Homebrew tap emits a warning annotation and step summary note but no longer blocks the GitHub Release from reaching Latest.
- **Calmer, theme-aligned TUI rendering.** Transcript, recap, and tool-header output now use theme-standardized activity colors instead of hardcoded values, and Markdown tables render as a bordered grid (wide tables no longer collapse into a stacked-record list).
- **Auto-mode tool approval fails closed when unattended.** In auto/non-interactive runs, an action that still needs approval under the active safe-mode/trust policy is denied with guidance instead of waiting indefinitely for an absent user, and outside-workspace writes are never auto-approved. A destructive auto-approved action is now bounced once for deliberation whenever no user is present (regardless of config), so the obvious `--yolo --auto` combination is no longer more dangerous than the `autonomous_coding` profile; the `auto_deliberate_destructive_actions` setting extends that backstop to interactive `--yolo` sessions, where a user is present but approvals are skipped.
- **Yolo + auto mode hardened against silent over-reach.** Entering plan mode now requires confirmation in an interactive `--yolo` session (matching exit), so the plan-review checkpoint is preserved when a user is present. A `--yolo` run no longer clears or persists the workspace's safe-mode/trust state. A new `--no-yolo` flag forces yolo off for a run — overriding the `--yolo` flag, the `default_yolo` config, and any resumed session state. Resuming a session that restores yolo and/or auto now surfaces a startup warning so it is never silent.
- **`pythinker review` validates finding evidence.** Reviewflow assembles prompts from a shared security-knowledge manifest and validates findings, handling invalid ones without failing the whole review.
Comment thread
elkaix marked this conversation as resolved.

## 0.30.0 (2026-06-02)

### What changed in this release
Expand Down
8 changes: 8 additions & 0 deletions packages/linux-installer/pythinker.spec
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ for pkg in (
except Exception:
pass

# Pygments loads a style module dynamically when config.tui.code_theme names a
# stock style (e.g. monokai); collect_submodules("rich") does not pull these in,
# so without this the frozen binary raises ClassNotFound on opted-in code themes.
try:
hiddenimports.extend(collect_submodules("pygments.styles"))
except Exception:
pass

a = Analysis(
["entrypoint.py"],
pathex=[],
Expand Down
8 changes: 8 additions & 0 deletions packages/windows-installer/pythinker.spec
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ for pkg in (
except Exception:
pass

# Pygments loads a style module dynamically when config.tui.code_theme names a
# stock style (e.g. monokai); collect_submodules("rich") does not pull these in,
# so without this the frozen binary raises ClassNotFound on opted-in code themes.
try:
hiddenimports.extend(collect_submodules("pygments.styles"))
except Exception:
pass

a = Analysis(
["entrypoint.py"],
pathex=[],
Expand Down
29 changes: 29 additions & 0 deletions src/pythinker_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ def _safe_git_branch(cwd: str | Path | HostPath) -> str | None:
return branch or None


def _resumed_unsupervised_notice(*, resumed: bool, yolo: bool, auto: bool) -> str | None:
"""Welcome-banner warning when a resumed session is running unsupervised.

Resuming restores ``yolo``/``auto`` from persisted state, so a session can come back
unattended and, under YOLO, auto-approving actions with no prompt. Surface that
prominently at startup (it also fires when the modes were passed explicitly on the
resume command — acceptable over-notification). ``None`` when not a resume or no
unsupervised mode is active.
"""
if not resumed or not (yolo or auto):
return None
modes = " + ".join(name for name, active in (("YOLO", yolo), ("auto", auto)) if active)
if yolo:
return f"{modes} active — actions auto-approved; toggle with /yolo /auto"
return "auto active — interactive approvals still required; toggle with /auto"


def _patch_session_id(record: dict[str, Any]) -> None:
"""Inject the current session ID (from ContextVar) into log records."""
try:
Expand Down Expand Up @@ -153,6 +170,7 @@ async def create(
thinking_effort: ThinkingEffort | None = None,
# Run mode
yolo: bool = False,
no_yolo: bool = False,
auto: bool = False,
runtime_auto: bool = False,
plan_mode: bool = False,
Expand Down Expand Up @@ -183,6 +201,8 @@ async def create(
Defaults to None.
yolo (bool, optional): Dangerously skip permission approvals. The user is still
reachable via ``AskUserQuestion``. Defaults to False.
no_yolo (bool, optional): Force yolo OFF for this run, overriding the ``yolo``
flag, config ``default_yolo``, and persisted session state. Defaults to False.
auto (bool, optional): Invocation-level auto mode (no user is present to answer
questions or approve actions). Implies auto-approve. Defaults to False.
runtime_auto (bool, optional): Internal invocation-only auto-mode overlay, used by
Expand Down Expand Up @@ -307,6 +327,7 @@ async def create(
yolo,
auto=auto,
runtime_auto=runtime_auto,
no_yolo=no_yolo,
skills_dirs=skills_dirs,
scratchpad_section=scratchpad_section,
)
Expand Down Expand Up @@ -783,6 +804,14 @@ async def run_shell(
if branch_name:
welcome_info.append(WelcomeInfoItem(name="Branch", value=branch_name))
welcome_info.append(WelcomeInfoItem(name="Session", value=self._runtime.session.id))
if notice := _resumed_unsupervised_notice(
resumed=self._runtime.resumed,
yolo=self._runtime.approval.is_yolo(),
auto=self._runtime.approval.is_auto(),
):
welcome_info.append(
WelcomeInfoItem(name="Mode", value=notice, level=WelcomeInfoItem.Level.WARN)
)
try:
auto_save_path = str(
shorten_home(HostPath.unsafe_from_local_path(self._runtime.session.context_file))
Expand Down
11 changes: 11 additions & 0 deletions src/pythinker_code/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,16 @@ def pythinker(
help="Automatically approve all actions. Default: no.",
),
] = False,
no_yolo: Annotated[
bool,
typer.Option(
"--no-yolo",
help=(
"Force yolo OFF for this run, overriding --yolo, config default_yolo, and "
"any persisted/resumed yolo state. Default: no."
),
),
] = False,
plan: Annotated[
bool,
typer.Option(
Expand Down Expand Up @@ -844,6 +854,7 @@ async def _run(session_id: str | None, prefill_text: str | None = None) -> tuple
thinking=thinking,
thinking_effort=normalized_thinking_effort,
yolo=yolo,
no_yolo=no_yolo,
auto=auto,
runtime_auto=ui == "print",
plan_mode=plan,
Expand Down
33 changes: 33 additions & 0 deletions src/pythinker_code/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,39 @@ class TUIConfig(BaseModel):
default=True,
description="Show a compact recap line after completed interactive shell turns.",
)
code_theme: str = Field(
default="pythinker-ansi",
description=(
"Syntax-highlighting theme for assistant code blocks. Default "
"'pythinker-ansi' keeps the terminal-adaptive, transparent look. "
"Set to any Pygments style name (e.g. 'monokai', 'material', "
"'dracula', 'one-dark') to render code fences with that style on a "
"solid dark background block."
),
)
smooth_streaming: bool = Field(
default=True,
description=(
"Pace streamed assistant text so it reveals smoothly instead of "
"landing in bursty delta-sized clumps. Keeps up with the model "
"(bounded catch-up). Set false to reveal each delta immediately."
),
)

@field_validator("code_theme")
@classmethod
def _validate_code_theme(cls, value: str) -> str:
from pythinker_code.utils.rich.syntax import available_code_themes

allowed = available_code_themes()
if value in allowed:
return value
if value.lower() in allowed:
return value.lower()
raise ValueError(
f"Unknown code_theme {value!r}. Choose 'pythinker-ansi' or a Pygments "
f"style name. Available: {', '.join(allowed)}"
)


class MCPConfig(BaseModel):
Expand Down
21 changes: 15 additions & 6 deletions src/pythinker_code/soul/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ async def create(
yolo: bool,
auto: bool = False,
runtime_auto: bool = False,
no_yolo: bool = False,
skills_dirs: list[HostPath] | None = None,
scratchpad_section: str | None = None,
) -> Runtime:
Expand Down Expand Up @@ -278,18 +279,26 @@ async def create(
parts.append(f"### `{d}`\n\n```\n{dir_ls}\n```")
additional_dirs_info = "\n\n".join(parts)

# Merge invocation flags with persisted session state.
effective_yolo = yolo or session.state.approval.yolo
# An explicit --yolo invocation is already a deliberate trust decision for
# this run, so it must not deadlock non-interactive/e2e flows behind safe mode.
effective_safe_mode = False if yolo else session.state.trust.safe_mode
# Merge invocation flags with persisted session state. ``--no-yolo`` is an explicit
# force-off that beats the flag, config ``default_yolo``, and persisted state.
original_persisted_yolo = session.state.approval.yolo
effective_yolo = (yolo or original_persisted_yolo) and not no_yolo
# Do NOT force safe_mode off under yolo: yolo already bypasses safe mode in the
# decision path (is_auto_approve / _unattended_denial_feedback short-circuit on
# yolo before reading safe_mode), so there is no deadlock to avoid — and forcing it
# False here used to get persisted back to trust state, silently downgrading the
# workspace's trust posture.
effective_safe_mode = session.state.trust.safe_mode
if auto and not session.state.approval.auto:
session.state.approval.auto = True
session.save_state()
saved_actions = set(session.state.approval.auto_approve_actions)

def _on_approval_change() -> None:
session.state.approval.yolo = approval_state.yolo
if not no_yolo:
session.state.approval.yolo = approval_state.yolo
else:
session.state.approval.yolo = original_persisted_yolo
session.state.approval.auto = approval_state.auto
session.state.approval.auto_approve_actions = set(approval_state.auto_approve_actions)
session.state.trust.safe_mode = approval_state.safe_mode
Expand Down
15 changes: 11 additions & 4 deletions src/pythinker_code/soul/approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,24 @@ def _deliberation_fingerprint(
def deliberation_gate(self, tool_call: ToolCall) -> str | None:
"""Reason a destructive auto-approved action must deliberate once, else ``None``.

Fires only when ``auto_deliberate`` is on, the action would otherwise be
auto-approved (auto *or* yolo — so it gates ahead of the yolo bypass), and the
tool call is destructive per the tool-agnostic classifier in ``permission``
Fires when the action would otherwise be auto-approved (auto *or* yolo — so it
gates ahead of the yolo bypass), it is destructive, and either no user is present
(``is_auto`` — no human to veto, so deliberation is mandatory) or ``auto_deliberate``
is on (which extends deliberation to the interactive-yolo case). Destructiveness is
classified by the tool-agnostic classifier in ``permission``
(today only ``Shell``; other destructive tools register their classifier there).
One-shot, scoped to (execution context, generation): the first sighting and any
same-generation duplicate are bounced; only a re-issue in a later generation of the
same context is let through once, so a deliberated ``rm -rf`` runs without being
permanently whitelisted, while two identical calls in one model response both
deliberate and a subagent cannot consume the main agent's one-shot.
"""
if not self._state.auto_deliberate:
# The destructive backstop must hold whenever an irreversible action would be
# auto-approved with NO user present (``is_auto``): there is no human to veto it,
# so the model must deliberate once first. The ``auto_deliberate`` config flag
# only EXTENDS this to the interactive-yolo case (a user IS present but approvals
# are skipped), where the human would otherwise see the action at approval time.
if not (self._state.auto_deliberate or self.is_auto()):
return None
if not self.is_auto_approve():
return None
Expand Down
36 changes: 7 additions & 29 deletions src/pythinker_code/soul/dynamic_injections/auto_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,6 @@

_AUTO_INJECTION_TYPE = "auto_mode"

_AUTO_PROMPT = (
"You are running in auto mode. No user is present to answer questions or "
"approve actions.\n"
"- Do NOT call AskUserQuestion — it will be auto-dismissed with no answer, "
"wasting a turn. Make your best judgment and proceed.\n"
"- Tool calls are auto-approved only when the current trust/safe-mode policy "
"allows. If approval is unavailable, the tool fails closed instead of "
"waiting forever; choose a safe alternative or explain the required explicit "
"trust/yolo step.\n"
"- Outside-workspace file writes are not auto-approved by auto mode.\n"
"- You CAN use EnterPlanMode / ExitPlanMode normally when available. Planning "
"still helps you think before acting; use it for non-trivial tasks, then "
"exit and execute.\n"
"- Finish the user's request end-to-end in this run. Do not defer decisions "
"to a human."
)

_AUTO_PROMPT_DESTRUCTIVE_DELIBERATE = (
"You are running in auto mode. No user is present to answer questions or "
"approve actions.\n"
Expand Down Expand Up @@ -93,20 +76,15 @@ async def get_injections(
if self._injected:
return []
self._injected = True
# Under the auto_deliberate policy AskUserQuestion self-decides (advisor-
# assisted) instead of being dismissed, so invite it at consequential
# forks. Destructive deliberation can also be enabled independently while
# AskUserQuestion remains auto-dismissed.
ask_deliberate = soul.runtime.config.ask_user_question_policy == "auto_deliberate"
destructive_deliberate = (
soul.runtime.config.auto_deliberate_destructive_actions or ask_deliberate
)
if ask_deliberate:
# No user is present, so a destructive auto-approved action is always bounced once
# for deliberation (see Approval.deliberation_gate) — surface that guidance in
# every auto prompt. Under the auto_deliberate policy AskUserQuestion additionally
# self-decides (advisor-assisted) at consequential forks instead of being
# dismissed, so invite it there.
if soul.runtime.config.ask_user_question_policy == "auto_deliberate":
content = _AUTO_PROMPT_DELIBERATE
elif destructive_deliberate:
content = _AUTO_PROMPT_DESTRUCTIVE_DELIBERATE
else:
content = _AUTO_PROMPT
content = _AUTO_PROMPT_DESTRUCTIVE_DELIBERATE
return [DynamicInjection(type=_AUTO_INJECTION_TYPE, content=content)]

async def on_context_compacted(self) -> None:
Expand Down
5 changes: 4 additions & 1 deletion src/pythinker_code/soul/pythinkersoul.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,10 @@ def path_getter() -> Path | None:
self.toggle_plan_mode,
path_getter,
checker,
self._approval.is_auto_approve,
# Match ExitPlanMode: gate on user presence (is_auto), not is_auto_approve.
# Yolo skips approvals but the user is still present, so an interactive
# yolo session should not silently slip into plan mode without confirming.
self._approval.is_auto,
)

# AskUserQuestion — bind auto-mode checker for auto-dismiss.
Expand Down
Loading
Loading