diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0c30e7..cdd4856e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ GitHub Releases page; `0.8.0` is the new starting line. - **The in-app updater recovers from Homebrew's untrusted-tap refusal.** Homebrew 5.0 (`HOMEBREW_REQUIRE_TAP_TRUST`) refuses to load formulas from third-party taps until `brew trust ` is run once, which made the in-app `brew upgrade` fail with only a generic "run manually" hint. The updater now detects both the hard `Refusing to load … from untrusted tap` refusal and the soft `Skipping … not trusted` warning, offers to run `brew trust pythoughts-labs/pythinker` and retry the upgrade once on an interactive terminal, and otherwise prints the exact remediation. It also catches the silent no-op where an untrusted tap is skipped during `brew update` and `brew upgrade` exits 0 without advancing the version. - **Session exports redact secrets surfaced by tool output.** A tool result (e.g. `grep`/`cat` over a `.env`) could write a secret value into an exported transcript in plaintext. `/export` now redacts the value of secret-named keys (`password`, `token`, `api_key`, `secret`, …) to `[REDACTED]` in both the markdown and YAML formats, while leaving non-secret keys such as `token_count`, usernames, and ports intact. - **The welcome logo's antenna blinks a fixed number of times on launch, then settles.** Replaces the terminal's indefinite slow-blink with a bounded boot animation — the antenna ball blinks seven times after the banner prints and then holds steady. It is skipped under reduced motion, on non-interactive output, and when the terminal is too short to keep the antenna row on screen. +- **Inline `/command` references get acted on, not just explained away.** When a message mentions a slash command mid-sentence (e.g. "your `/goal` today is to `/plan` and build the page"), the command doesn't auto-run — but the agent no longer leads its reply by reporting it as failed. The per-turn reminder and the system prompt now steer the agent to act on the intent: call the real `EnterPlanMode` tool for `/plan` (clarified as a genuine, callable tool so models stop doubting it exists), pursue the described objective for `/goal`, load `/skill:` via `ReadSkill`, and apply equivalent guidance for other commands — only surfacing how to invoke the literal command when genuinely needed. ## 0.41.0 (2026-06-11) diff --git a/src/pythinker_code/agents/default/system.md b/src/pythinker_code/agents/default/system.md index 37cc5461..5b2359a6 100644 --- a/src/pythinker_code/agents/default/system.md +++ b/src/pythinker_code/agents/default/system.md @@ -125,7 +125,7 @@ For research or multimedia tasks (images, video, PDFs, docs, spreadsheets, prese **Skills (`ReadSkill`).** Load a skill's exact instructions before applying its workflow — mandatory for `review-pr`, `diagnose-ci-failures`, `fix-errors`, `implement-specs`, `spec-driven-implementation`, `check-impl-against-spec`, `resolve-merge-conflicts`, and `create-pr`. Read skill details only when needed, to conserve context. Catalog and scope precedence in §12. -**Inline `/command` references.** Slash commands execute only as their own message starting with `/`. A `/command` or `/skill:` mentioned mid-message did not run: treat the reference as part of the request — load a referenced skill via `ReadSkill`, apply referenced guidance yourself, or tell the user to invoke it as a standalone message. Never silently drop such a reference. +**Inline `/command` references.** Slash commands execute only as their own message starting with `/`. A `/command` or `/skill:` mentioned mid-message did not auto-run, but it still expresses intent — act on it rather than leading your reply by reporting it as failed. For `/plan`, call `EnterPlanMode` (a real tool in your toolset, not just prose); for `/goal`, pursue the described objective until it is verifiably done; for `/skill:`, load it via `ReadSkill` and apply it; for other guidance commands, apply the equivalent guidance yourself. Mention invoking the real command only when genuinely needed. Never silently drop such a reference. **MCP.** Connected MCP servers expose their capabilities as ordinary tools already in your toolset (descriptions name the server). To *use* one, invoke its tools directly — never pip-install the server, import it as a module, or search the repo for its config. If a named server has no tools present, it is not connected (loading, failed, or unauthorized), not missing: point the user to `/mcp` for status, and to `pythinker mcp auth ` for an unauthorized OAuth server. diff --git a/src/pythinker_code/soul/dynamic_injections/inline_commands.py b/src/pythinker_code/soul/dynamic_injections/inline_commands.py index 4357c0af..0af2372f 100644 --- a/src/pythinker_code/soul/dynamic_injections/inline_commands.py +++ b/src/pythinker_code/soul/dynamic_injections/inline_commands.py @@ -25,12 +25,16 @@ _REMINDER_TEMPLATE = ( "The user's message references slash commands inline: {refs}. Inline references do " - "NOT execute — a slash command runs only as its own message starting with '/'. Do " - "not silently ignore them; treat each reference as part of the request: for " + "NOT execute as commands — a slash command runs only as its own message starting " + "with '/'. Do not silently ignore them, and do not lead your reply by telling the " + "user they failed to run. Instead act on the intent each reference expresses: for " + "/plan, call the EnterPlanMode tool to enter plan mode before implementing (it is " + "an available tool, not just prose); for /goal, treat the objective the user " + "described as the goal to pursue across this work until it is verifiably done; for " "/skill:, load that skill with ReadSkill and apply its instructions; for " - "guidance-injecting commands (e.g. /best-practices), apply the closest equivalent " - "guidance yourself and tell the user how to run the real command; otherwise tell " - "the user the command did not run and how to invoke it." + "other guidance-injecting commands (e.g. /best-practices), apply the closest " + "equivalent guidance yourself. Only mention how to invoke the real slash command " + "if doing so is genuinely needed to satisfy the request." ) diff --git a/tests/core/test_inline_command_provider.py b/tests/core/test_inline_command_provider.py index 6065c322..ad39a961 100644 --- a/tests/core/test_inline_command_provider.py +++ b/tests/core/test_inline_command_provider.py @@ -24,6 +24,7 @@ def _mock_soul(is_subagent: bool = False) -> MagicMock: _cmd("best-practices", aliases=["bp"]), _cmd("clear"), _cmd("goal"), + _cmd("plan"), ] return soul @@ -42,6 +43,22 @@ async def test_injects_for_inline_known_command() -> None: assert "NOT execute" in result[0].content +async def test_reminder_is_actionable_not_dismissive() -> None: + """The reminder must steer the agent to act on intent, not to lead by telling + the user the command 'did not run'.""" + provider = InlineCommandReminderProvider() + history = [_user("your /goal today is to /plan and build the landing page")] + result = await provider.get_injections(history, _mock_soul()) + assert len(result) == 1 + content = result[0].content + # Names the real EnterPlanMode tool so the agent stops doubting it exists. + assert "EnterPlanMode" in content + # Pursues the described objective for /goal rather than just explaining syntax. + assert "goal to pursue" in content + # Does not instruct the agent to report the commands as failed up front. + assert "did not run" not in content + + async def test_injects_for_alias() -> None: provider = InlineCommandReminderProvider() history = [_user("apply /bp while you build it")]