diff --git a/firstcoder/agent/session.py b/firstcoder/agent/session.py index 77f1089..bc6b2ee 100644 --- a/firstcoder/agent/session.py +++ b/firstcoder/agent/session.py @@ -38,7 +38,7 @@ from firstcoder.skills.session import replay_loaded_skills -DEFAULT_BASE_RULES = "你是 FirstCoder,一个本地 AI coding agent。请遵守项目规则并优先保持上下文可恢复。" +DEFAULT_BASE_RULES = "你是 PytestPilot,一个面向 Python/pytest 项目的本地 AI coding agent。请遵守项目规则并优先保持上下文可恢复。" @dataclass(slots=True) diff --git a/firstcoder/app/tui.py b/firstcoder/app/tui.py index 1930018..bfbe1bd 100644 --- a/firstcoder/app/tui.py +++ b/firstcoder/app/tui.py @@ -84,7 +84,7 @@ class CurrentSessionLike(Protocol): @dataclass(slots=True) class FirstCoderTuiConfig: - title: str = "FirstCoder" + title: str = "PytestPilot" provider_name: str | None = None provider_model: str | None = None project_name: str | None = None @@ -165,7 +165,7 @@ def on_mount(self) -> None: self.title = self.config.title self._refresh_session_subtitle() self._write_line( - "FirstCoder ready. Commands: /sessions, /session, /resume, /share, /rename, " + "PytestPilot ready. Commands: /sessions, /session, /resume, /share, /rename, " "/context, /compact status, /compact", classes="message system-message", ) @@ -443,7 +443,7 @@ def _topbar_width(self) -> int | None: def _topbar_text(self, *, session_id: str | None = None, width: int | None = None) -> str: if session_id is None and self.current_session is not None: session_id = self.current_session.session_id - brand = "[#7bba55]FirstCoder[/]" + brand = "[#7bba55]PytestPilot[/]" status = _activity_markup(self._activity_text) metadata_parts = [f"[#6e6d72]{escape(_short_session_id(session_id) if session_id else 'no session')}[/]"] if self.config.provider_name or self.config.provider_model: @@ -841,7 +841,7 @@ def _append_stream_text(self, text: str) -> None: self._schedule_stream_flush() return if hasattr(output, "write"): - prefix = "FirstCoder:\n" if self._stream_text_buffer == text else "" + prefix = "PytestPilot:\n" if self._stream_text_buffer == text else "" output.write(f"{prefix}{text}") def _close_stream_segment_for_tool(self) -> None: @@ -876,7 +876,7 @@ def _flush_stream_text(self) -> bool: if self._stream_rendered_text == self._stream_text_buffer: return False self._stream_rendered_text = self._stream_text_buffer - _observe_markdown_update(self._stream_text_widget.update(f"FirstCoder:\n\n{self._stream_rendered_text}")) + _observe_markdown_update(self._stream_text_widget.update(f"PytestPilot:\n\n{self._stream_rendered_text}")) output = self.query_one("#output") self._scroll_output_end_if_pinned(output) return True diff --git a/firstcoder/app/tui_state.py b/firstcoder/app/tui_state.py index eec6672..4aff7ba 100644 --- a/firstcoder/app/tui_state.py +++ b/firstcoder/app/tui_state.py @@ -21,7 +21,7 @@ class TuiEntryKind(StrEnum): TuiEntryKind.SYSTEM: "system", TuiEntryKind.COMMAND: "command", TuiEntryKind.USER: "you", - TuiEntryKind.ASSISTANT: "FirstCoder", + TuiEntryKind.ASSISTANT: "PytestPilot", TuiEntryKind.REASONING: "thinking", TuiEntryKind.TOOL: "tool", TuiEntryKind.PERMISSION: "permission", diff --git a/firstcoder/cli.py b/firstcoder/cli.py index 916245f..fa20566 100644 --- a/firstcoder/cli.py +++ b/firstcoder/cli.py @@ -433,12 +433,12 @@ def run_repl( else: response = chat_runner.run_user_turn(line) - print(f"FirstCoder> {response.content}") + print(f"PytestPilot> {response.content}") pending = getattr(chat_runner, "last_pending_input", None) while pending is not None and auto_approve and _pending_kind(pending) == "permission_confirmation": print("Auto-approve> allow_once") response = chat_runner.resume_with_user_input(_pending_id(pending), "allow_once") - print(f"FirstCoder> {response.content}") + print(f"PytestPilot> {response.content}") pending = getattr(chat_runner, "last_pending_input", None) if pending is not None: diff --git a/firstcoder/context/provider_summarizer.py b/firstcoder/context/provider_summarizer.py index 7d6c0a4..3f56b1c 100644 --- a/firstcoder/context/provider_summarizer.py +++ b/firstcoder/context/provider_summarizer.py @@ -40,7 +40,7 @@ def summarize(self, messages: list[AgentMessage], *, summary_mode: str = "defaul ChatMessage( role="system", content=( - "你是 FirstCoder 的上下文压缩器。请只输出一段中文摘要," + "你是 PytestPilot 的上下文压缩器。请只输出一段中文摘要," "保留用户目标、已完成决策、关键文件、未解决问题和下一步。" ), ), diff --git a/firstcoder/context/system_prompt.py b/firstcoder/context/system_prompt.py index 4960a6a..d4ae82f 100644 --- a/firstcoder/context/system_prompt.py +++ b/firstcoder/context/system_prompt.py @@ -132,7 +132,7 @@ def _format_section(title: str, content: str) -> str: def _agent_behavior_rules() -> str: return """# Role and operating context -You are FirstCoder, an interactive local coding agent. Use the available tools to help the user with software engineering tasks in the current workspace. User and project instructions override these default rules. +You are PytestPilot, an interactive local coding agent for Python/pytest diagnosis and repair. Use the available tools to help the user with software engineering tasks in the current workspace. User and project instructions override these default rules. # Working loop - Classify the request first: answer simple questions directly; use tools for code edits, debugging, tests, repository search, and multi-file work. diff --git a/firstcoder/execution.py b/firstcoder/execution.py index 887c47f..6954e4f 100644 --- a/firstcoder/execution.py +++ b/firstcoder/execution.py @@ -131,7 +131,9 @@ def run(self, command: list[str], cwd: Path, limits: ResourceLimits) -> CommandR f"--ulimit=fsize={limits.max_file_size_mb * 1024}:{limits.max_file_size_mb * 1024}", f"--tmpfs=/tmp:rw,noexec,nosuid,nodev,size={limits.tmpfs_mb}m", "--mount", - f"type=bind,src={root},dst=/workspace,rw", + # Bind mounts are read-write by default; `rw` is not a valid + # standalone field in Docker's `--mount` key=value syntax. + f"type=bind,src={root},dst=/workspace", "--workdir=/workspace", ] for key, value in sorted(self.environment.items()): diff --git a/tests/test_app_tui.py b/tests/test_app_tui.py index 3d1c764..3ab34d3 100644 --- a/tests/test_app_tui.py +++ b/tests/test_app_tui.py @@ -150,7 +150,7 @@ def test_firstcoder_app_topbar_text_includes_session_id() -> None: app = FirstCoderApp(current_session=FakeSession()) assert app._topbar_text() == ( - "[#7bba55]FirstCoder[/] [#303238]·[/] [#7bba55]idle · ready[/] " + "[#7bba55]PytestPilot[/] [#303238]·[/] [#7bba55]idle · ready[/] " "[#303238]·[/] [#6e6d72]sess_test[/] " "[#303238]·[/] [#6e6d72]standard[/]" ) @@ -167,7 +167,7 @@ def test_firstcoder_app_topbar_text_includes_provider_model_mode_and_cwd() -> No ) assert app._topbar_text() == ( - "[#7bba55]FirstCoder[/] [#303238]·[/] [#7bba55]idle · ready[/] " + "[#7bba55]PytestPilot[/] [#303238]·[/] [#7bba55]idle · ready[/] " "[#303238]·[/] [#6e6d72]sess_test[/] " "[#303238]·[/] [#6e6d72]yurenapi/gpt-5.5[/] " "[#303238]·[/] [#6e6d72]standard[/] [#303238]·[/] [#6e6d72]cwd FirstCoder[/]" @@ -214,7 +214,7 @@ def test_firstcoder_app_topbar_uses_spacious_two_sided_layout_when_width_is_know text = app._topbar_text(width=120) - assert text.startswith("[#7bba55]FirstCoder[/]") + assert text.startswith("[#7bba55]PytestPilot[/]") assert " [#303238]·[/] [#6e6d72]sess_test[/]" not in text assert "[#7bba55]idle · ready[/]" in text assert "[#6e6d72]sess_test[/]" in text @@ -230,7 +230,7 @@ class BypassSession(FakeSession): app = FirstCoderApp(current_session=BypassSession()) assert app._topbar_text() == ( - "[#7bba55]FirstCoder[/] [#303238]·[/] [#7bba55]idle · ready[/] " + "[#7bba55]PytestPilot[/] [#303238]·[/] [#7bba55]idle · ready[/] " "[#303238]·[/] [#6e6d72]sess_c8d401e2[/] " "[#303238]·[/] [#b28443]bypass[/]" ) @@ -257,7 +257,7 @@ def test_tui_transcript_records_structured_entries_with_stable_labels() -> None: ) assert [entry.id for entry in transcript.entries] == [user.id, assistant.id, tool.id] - assert [entry.label for entry in transcript.entries] == ["you", "FirstCoder", "tool exec_command running"] + assert [entry.label for entry in transcript.entries] == ["you", "PytestPilot", "tool exec_command running"] assert transcript.entries[-1].status == "running" @@ -304,7 +304,7 @@ def test_firstcoder_app_records_rendered_messages_in_transcript(monkeypatch) -> assert [(entry.kind, entry.label, entry.body) for entry in app.transcript.entries] == [ (TuiEntryKind.USER, "you", "> hello"), - (TuiEntryKind.ASSISTANT, "FirstCoder", "**hi**"), + (TuiEntryKind.ASSISTANT, "PytestPilot", "**hi**"), ] @@ -680,7 +680,7 @@ def test_firstcoder_app_streams_text_delta_without_repeating_final_text(monkeypa assert [type(widget).__name__ for widget in output.mounted] == ["FirstCoderMarkdown"] assert output.mounted[0].allow_select is False - assert output.mounted[0].updates[-1] == "FirstCoder:\n\nhello" + assert output.mounted[0].updates[-1] == "PytestPilot:\n\nhello" assert app._stream_text_buffer == "hello" assert runner.seen == [ ChatStreamEvent(kind="text_delta", text="he"), @@ -703,7 +703,7 @@ def test_firstcoder_app_streaming_skips_normalized_duplicate_assistant_line(monk assert [type(widget).__name__ for widget in output.mounted] == ["FirstCoderMarkdown"] assert output.mounted[0].allow_select is False - assert output.mounted[0].updates[-1] == "FirstCoder:\n\nhello" + assert output.mounted[0].updates[-1] == "PytestPilot:\n\nhello" def test_firstcoder_app_paces_stream_markdown_updates(monkeypatch) -> None: @@ -720,12 +720,12 @@ def test_firstcoder_app_paces_stream_markdown_updates(monkeypatch) -> None: markdown = output.mounted[0] assert type(markdown).__name__ == "FirstCoderMarkdown" assert markdown.allow_select is False - assert markdown.updates == ["FirstCoder:\n\n我"] + assert markdown.updates == ["PytestPilot:\n\n我"] assert app._stream_text_buffer == "我在这里" app._flush_stream_text() - assert markdown.updates[-1] == "FirstCoder:\n\n我在这里" + assert markdown.updates[-1] == "PytestPilot:\n\n我在这里" def test_firstcoder_app_does_not_scroll_stream_when_render_is_deferred(monkeypatch) -> None: @@ -1333,8 +1333,8 @@ def test_firstcoder_app_starts_new_stream_block_after_tool_event(monkeypatch) -> first_markdown, _, second_markdown = output.mounted assert first_markdown.allow_select is False assert second_markdown.allow_select is False - assert first_markdown.updates[-1] == "FirstCoder:\n\n我先看看。" - assert second_markdown.updates[-1] == "FirstCoder:\n\n看完了。" + assert first_markdown.updates[-1] == "PytestPilot:\n\n我先看看。" + assert second_markdown.updates[-1] == "PytestPilot:\n\n看完了。" def test_permission_requested_tool_event_uses_permission_style() -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py index 74f9f30..99bc9e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -223,7 +223,7 @@ def test_run_repl_sends_multiple_user_messages(capsys): run_repl(runner, ["hello", "continue"]) assert runner.turns == ["hello", "continue"] - assert capsys.readouterr().out == "FirstCoder> first reply\nFirstCoder> second reply\n" + assert capsys.readouterr().out == "PytestPilot> first reply\nPytestPilot> second reply\n" def test_run_repl_routes_next_line_to_pending_permission(capsys): @@ -240,13 +240,13 @@ def test_run_repl_routes_next_line_to_pending_permission(capsys): assert runner.turns == ["write file"] assert runner.resumes == [("perm_1", "allow_once")] assert capsys.readouterr().out == ( - "FirstCoder> need permission\n" + "PytestPilot> need permission\n" "Permission> Allow?\n" "Choose:\n" " 1. Deny\n" " 2. Allow once\n" " 3. Allow always for same scope\n" - "FirstCoder> done\n" + "PytestPilot> done\n" ) diff --git a/tests/test_context_system_prompt.py b/tests/test_context_system_prompt.py index 29a5679..ae5e106 100644 --- a/tests/test_context_system_prompt.py +++ b/tests/test_context_system_prompt.py @@ -9,7 +9,7 @@ def _inputs(**overrides: object) -> SystemPromptInputs: values = { - "base_rules": "你是 FirstCoder。", + "base_rules": "你是 PytestPilot。", "agents_md": "项目规则:上下文放在 firstcoder/context。", "tools": [ ToolDefinition( @@ -44,7 +44,18 @@ def test_system_prompt_cache_reuses_prefix_when_fingerprint_matches() -> None: assert first.fingerprint == second.fingerprint assert first is second assert first.messages[0].role == "system" - assert "你是 FirstCoder。" in first.messages[0].content + assert "你是 PytestPilot。" in first.messages[0].content + + +def test_default_agent_identity_is_pytestpilot() -> None: + from firstcoder.agent.session import DEFAULT_BASE_RULES + + entry = SystemPromptBuilder().build(_inputs(base_rules=DEFAULT_BASE_RULES)) + content = entry.messages[0].content + + assert "PytestPilot" in content + assert "You are PytestPilot" in content + assert "You are FirstCoder" not in content def test_agents_md_change_invalidates_system_prompt_fingerprint() -> None: diff --git a/tests/test_execution_backends.py b/tests/test_execution_backends.py index d8634b5..f701a96 100644 --- a/tests/test_execution_backends.py +++ b/tests/test_execution_backends.py @@ -62,7 +62,7 @@ def fake_run(command, *, cwd, env, limits, local_resource_limits): assert "--user=65532:65532" in command assert "--ulimit=fsize=8192:8192" in command assert "--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=16m" in command - assert f"type=bind,src={tmp_path.resolve()},dst=/workspace,rw" in command + assert f"type=bind,src={tmp_path.resolve()},dst=/workspace" in command assert "LANG=C" in command assert all("secret" not in part for part in command) assert command[-5:] == ["example/pytest:locked", "python", "-m", "pytest", "-q"]