diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index 812b6ca68..40ca10897 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -722,6 +722,97 @@ def test_skill_slash_fallback_contract() -> None: assert "The five sections are output structure, while the execution contract is the evidence authority" in pr_review_normalized +def test_start_goal_guided_derives_display_name_from_goal_text() -> None: + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "display-name-project" + project.mkdir() + write_connected_goal_fixture(project, goal_id="display-goal", agent_id="codex-test-agent") + + derived = run_json( + "start-goal", + "--guided", + "--project", + str(project), + "--goal-id", + "display-goal", + "--agent-id", + "codex-test-agent", + "--host-surface", + "codex-app", + "--goal-text", + "修复 scheduler state path 覆盖问题", + "--include-command-pack-detail", + ) + connect_command = str( + derived["command_pack"]["commands"]["goal_start_connect_if_needed"] + ) + assert "--display-name" not in connect_command, connect_command + assert derived["command_pack"].get("display_name") is None + + run_json( + "bootstrap", + "--project", + str(project), + "--goal-id", + "derived-display-goal", + "--objective", + "修复 scheduler state path 覆盖问题", + "--no-onboarding-scan", + "--no-global-sync", + ) + registry = json.loads( + (project / ".loopx" / "registry.json").read_text(encoding="utf-8") + ) + registry_goal = next( + goal for goal in registry["goals"] if goal["id"] == "derived-display-goal" + ) + assert registry_goal.get("display_name") == "修复 scheduler state path 覆盖问题" + + explicit = run_json( + "start-goal", + "--guided", + "--project", + str(project), + "--goal-id", + "display-goal", + "--agent-id", + "codex-test-agent", + "--host-surface", + "codex-app", + "--goal-text", + "some objective", + "--display-name", + "Custom Title", + "--include-command-pack-detail", + ) + explicit_command = str( + explicit["command_pack"]["commands"]["goal_start_connect_if_needed"] + ) + assert "--display-name 'Custom Title'" in explicit_command, explicit_command + assert explicit["command_pack"].get("display_name") == "Custom Title" + + rejected = run_json( + "start-goal", + "--guided", + "--project", + str(project), + "--goal-id", + "display-goal", + "--agent-id", + "codex-test-agent", + "--host-surface", + "codex-app", + "--goal-text", + "/private/secret/path should not leak", + "--include-command-pack-detail", + ) + rejected_command = str( + rejected["command_pack"]["commands"]["goal_start_connect_if_needed"] + ) + assert "--display-name" not in rejected_command, rejected_command + assert rejected["command_pack"].get("display_name") is None + + def main() -> int: test_missing_project_stops_before_mutation() test_goal_text_invocation_plans_ranked_todos_before_activation() @@ -730,6 +821,7 @@ def main() -> int: test_connected_project_reuses_existing_state() test_linked_git_worktree_reuses_canonical_source_registry() test_skill_slash_fallback_contract() + test_start_goal_guided_derives_display_name_from_goal_text() print("bootstrap command pack smoke passed") return 0 diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py index 2c7790e38..adacc6180 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -7,6 +7,7 @@ from typing import Any from .control_plane.runtime.time import now_local_iso +from .control_plane.runtime.public_safety import public_safe_compact_text from .control_plane.todos.active_state_editing import ( TODO_SECTION_HEADINGS, insertion_anchor, @@ -76,6 +77,12 @@ def default_goal_id(project: Path) -> str: return f"{slugify_goal_id(project.name)}-goal" +def derive_goal_display_name(goal_text: str | None) -> str | None: + """Derive a public-safe display title from user-supplied goal text.""" + + return public_safe_compact_text(goal_text, limit=132) + + def now_iso() -> str: return now_local_iso() diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index 49ca07ca2..10b8112bf 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -18,6 +18,11 @@ build_goal_start_contract, build_goal_start_prompt, ) +from .control_plane.goals.start_goal_todo_delta import ( + append_todo_delta_render_line, + existing_runnable_agent_frontier, + todo_authoring_steps, +) from .control_plane.scheduler.execution_context import ( GUIDED_START_TURN_RUNTIME_PROFILES, ) @@ -32,16 +37,13 @@ DEFAULT_HANDOFF_ADAPTER_STATUS, render_available_capability_args, render_cli_command_prefix, + render_goal_start_bootstrap_command, + render_optional_cli_arg, render_quota_guard_command, render_refresh_state_command, shell_arg, ) from .paths import resolve_runtime_root -from .control_plane.goals.start_goal_todo_delta import ( - append_todo_delta_render_line, - existing_runnable_agent_frontier, - todo_authoring_steps, -) from .registry import registry_goals, resolve_state_file from .slash_commands import build_slash_command_catalog from .thread_agent_binding import normalize_thread_id, resolve_thread_agent_binding @@ -215,6 +217,7 @@ def _start_goal_command( capability_route: str | None, fine_grained: bool, include_command_pack_detail: bool, + display_name: str | None = None, ) -> str: return ( f"{render_cli_command_prefix(cli_bin=cli_bin, runtime_root=runtime_root)} " @@ -233,6 +236,7 @@ def _start_goal_command( else "" ) + f" --goal-text {shell_arg(goal_text)}" + + render_optional_cli_arg("--display-name", display_name) + (" --include-command-pack-detail" if include_command_pack_detail else "") ) @@ -251,6 +255,7 @@ def _start_goal_detail_command( available_capabilities: list[str] | None, capability_route: str | None, fine_grained: bool, + display_name: str | None = None, ) -> str: return _start_goal_command( project=project, @@ -266,6 +271,7 @@ def _start_goal_detail_command( capability_route=capability_route, fine_grained=fine_grained, include_command_pack_detail=True, + display_name=display_name, ) @@ -326,6 +332,7 @@ def build_start_goal_host_surface_selection_packet( capability_route: str | None = None, fine_grained: bool = False, include_command_pack_detail: bool = False, + display_name: str | None = None, runtime_root_arg: str | None = None, ) -> dict[str, Any]: """Fail closed when the caller has not identified the current Codex host.""" @@ -378,6 +385,7 @@ def build_start_goal_host_surface_selection_packet( else "" ) + f" --goal-text {shell_arg(normalized_goal_text)}" + + render_optional_cli_arg("--display-name", display_name) + (" --include-command-pack-detail" if include_command_pack_detail else "") ) choices.append( @@ -406,6 +414,7 @@ def build_start_goal_host_surface_selection_packet( "writes_now": False, "spends_quota_now": False, "goal_text": normalized_goal_text, + "display_name": display_name, "blocked_by": "host_surface_selection", "host_surface_selection_gate": gate, "ordered_steps": [ @@ -435,6 +444,7 @@ def build_start_goal_host_surface_selection_packet( "new_peer": new_peer, "host_surface": None, "goal_text": normalized_goal_text, + "display_name": display_name, "host_surface_selection_gate": gate, "recommended_next_step": { "kind": "select_host_surface", @@ -677,33 +687,6 @@ def _project_command(project: str, command: str) -> str: return "\n".join([f"cd {shell_arg(project)}", command]) -def _goal_start_bootstrap_command( - *, - project: str, - goal_id: str, - goal_text: str | None, - cli_bin: str, - runtime_root: str | None, - fine_grained: bool, -) -> str: - objective = goal_text or "" - lines = [ - f"cd {shell_arg(project)}", - f"{render_cli_command_prefix(cli_bin=cli_bin, runtime_root=runtime_root)} bootstrap \\", - " --project . \\", - f" --goal-id {shell_arg(goal_id)} \\", - f" --objective {shell_arg(objective)} \\", - f" --adapter-kind {shell_arg(DEFAULT_HANDOFF_ADAPTER_KIND)} \\", - f" --adapter-status {shell_arg(DEFAULT_HANDOFF_ADAPTER_STATUS)} \\", - " --no-onboarding-scan \\", - " --codex-app-heartbeat ask", - ] - if fine_grained: - lines[-1] += " \\" - lines.append(" --fine-grained") - return "\n".join(lines) - - def _selected_goal_capability_route( capability_route: str | None, ) -> dict[str, Any] | None: @@ -767,6 +750,7 @@ def build_loopx_bootstrap_command_pack( capability_route: str | None = None, fine_grained: bool = False, resolve_linked_worktree_alias: bool = True, + display_name: str | None = None, runtime_root_arg: str | None = None, ) -> dict[str, Any]: inspection = inspect_bootstrap_connection( @@ -893,13 +877,14 @@ def build_loopx_bootstrap_command_pack( runtime_root=command_runtime_root, ) status_command = _project_command(resolved_project, f"{command_prefix} status") - goal_start_bootstrap_command = _goal_start_bootstrap_command( + goal_start_bootstrap_command = render_goal_start_bootstrap_command( project=resolved_project, goal_id=resolved_goal_id, goal_text=normalized_goal_text, cli_bin=cli_bin, runtime_root=command_runtime_root, fine_grained=fine_grained, + display_name=display_name, ) goal_start_plan_prompt = build_goal_start_prompt( goal_text=normalized_goal_text, @@ -980,9 +965,11 @@ def build_loopx_bootstrap_command_pack( if normalized_goal_text else "" ) + + render_optional_cli_arg("--display-name", display_name) ), "read_only": True, "goal_text": normalized_goal_text, + "display_name": display_name, "project": resolved_project, "goal_id": resolved_goal_id, "agent_id": selected_agent_id, @@ -1122,6 +1109,7 @@ def _build_multi_goal_start_selection_packet( capability_route: str | None, fine_grained: bool, include_command_pack_detail: bool, + display_name: str | None = None, ) -> dict[str, Any] | None: inspection = inspect_bootstrap_connection( project, @@ -1328,6 +1316,7 @@ def _build_multi_goal_start_selection_packet( available_capabilities=available_capabilities, capability_route=capability_route, fine_grained=fine_grained, + display_name=display_name, ) selected_command_pack = ( command_pack @@ -1396,6 +1385,7 @@ def build_start_goal_guided_packet( capability_route: str | None = None, fine_grained: bool = False, include_command_pack_detail: bool = False, + display_name: str | None = None, runtime_root_arg: str | None = None, ) -> dict[str, Any]: if goal_id is None: @@ -1412,6 +1402,7 @@ def build_start_goal_guided_packet( capability_route=capability_route, fine_grained=fine_grained, include_command_pack_detail=include_command_pack_detail, + display_name=display_name, ) if selection_packet is not None: return selection_packet @@ -1428,6 +1419,7 @@ def build_start_goal_guided_packet( capability_route=capability_route, fine_grained=fine_grained, resolve_linked_worktree_alias=False, + display_name=display_name, runtime_root_arg=runtime_root_arg, ) commands = command_pack.get("commands") @@ -1451,6 +1443,7 @@ def rerun_start_goal(selected_agent_id: str) -> str: capability_route=capability_route, fine_grained=fine_grained, include_command_pack_detail=False, + display_name=str(command_pack.get("display_name") or display_name or ""), ) fresh_registration = identity_selection_gate.get("fresh_agent_registration") @@ -1702,6 +1695,7 @@ def rerun_start_goal(selected_agent_id: str) -> str: available_capabilities=available_capabilities, capability_route=capability_route, fine_grained=fine_grained, + display_name=str(command_pack.get("display_name") or display_name or ""), ) selected_command_pack = ( command_pack diff --git a/loopx/cli_commands/bootstrap_connect.py b/loopx/cli_commands/bootstrap_connect.py index 1d854acf4..5816c9896 100644 --- a/loopx/cli_commands/bootstrap_connect.py +++ b/loopx/cli_commands/bootstrap_connect.py @@ -8,6 +8,7 @@ DEFAULT_DOMAIN, DEFAULT_OBJECTIVE, bootstrap_project, + derive_goal_display_name, render_bootstrap_markdown, ) PrintPayload = Callable[ @@ -29,6 +30,13 @@ def register_bootstrap_connect_command(subparsers: argparse._SubParsersAction) - help="Create a new forked goal id instead of reusing an existing global goal route.", ) bootstrap_parser.add_argument("--objective", default=DEFAULT_OBJECTIVE, help="Initial goal objective.") + bootstrap_parser.add_argument( + "--display-name", + help=( + "Public display title for the goal. When omitted, a public-safe title is " + "derived from the objective; the project name remains the fallback." + ), + ) bootstrap_parser.add_argument("--domain", default=DEFAULT_DOMAIN, help="Goal domain label.") bootstrap_parser.add_argument("--role", choices=["controller", "subagent"], default="controller") bootstrap_parser.add_argument("--parent-goal-id", help="Parent goal id when --role subagent.") @@ -195,6 +203,7 @@ def handle_bootstrap_connect_command( runtime_root=runtime_root, goal_id=goal_id, objective=args.objective, + display_name=args.display_name or derive_goal_display_name(args.objective), domain=args.domain, role=args.role, parent_goal_id=args.parent_goal_id, diff --git a/loopx/cli_commands/start_goal.py b/loopx/cli_commands/start_goal.py index 222398698..9655b7aeb 100644 --- a/loopx/cli_commands/start_goal.py +++ b/loopx/cli_commands/start_goal.py @@ -128,6 +128,13 @@ def register_start_goal_command(subparsers: argparse._SubParsersAction) -> None: ) start_goal_parser.add_argument("--project", default=".", help="Project directory to inspect.") start_goal_parser.add_argument("--goal-id", help="Goal id. Defaults to -goal.") + start_goal_parser.add_argument( + "--display-name", + help=( + "Public display title for the goal. When omitted, a public-safe title " + "is derived from the goal text; the project name only remains as a fallback." + ), + ) start_goal_parser.add_argument( "--agent-id", help=( @@ -296,6 +303,7 @@ def handle_start_goal_command( } print_payload(payload, args.format, _render_start_goal_markdown) return 2 + display_name = args.display_name if not args.host_surface: try: payload = build_start_goal_host_surface_selection_packet( @@ -310,6 +318,7 @@ def handle_start_goal_command( capability_route=capability_route, fine_grained=fine_grained, include_command_pack_detail=bool(args.include_command_pack_detail), + display_name=display_name, runtime_root_arg=runtime_root_arg, ) except EffectRuntimeStartupError as exc: @@ -333,6 +342,7 @@ def handle_start_goal_command( fine_grained=fine_grained, include_command_pack_detail=bool(args.include_command_pack_detail), runtime_root_arg=runtime_root_arg, + display_name=display_name, ) except EffectRuntimeStartupError as exc: payload = _effect_runtime_startup_failure_payload(exc) diff --git a/loopx/project_prompt.py b/loopx/project_prompt.py index 1528ed12c..e3f1abf7d 100644 --- a/loopx/project_prompt.py +++ b/loopx/project_prompt.py @@ -31,6 +31,12 @@ def shell_arg(value: str) -> str: return shlex.quote(value) +def render_optional_cli_arg(flag: str, value: str | None) -> str: + if not value: + return "" + return f" {flag} {shell_arg(value)}" + + def render_cli_command_prefix( *, cli_bin: str = "loopx", @@ -42,6 +48,36 @@ def render_cli_command_prefix( return prefix +def render_goal_start_bootstrap_command( + *, + project: str, + goal_id: str, + goal_text: str | None, + cli_bin: str, + runtime_root: str | None, + fine_grained: bool, + display_name: str | None = None, +) -> str: + objective = goal_text or "" + lines = [ + f"cd {shell_arg(project)}", + f"{render_cli_command_prefix(cli_bin=cli_bin, runtime_root=runtime_root)} bootstrap \\", + " --project . \\", + f" --goal-id {shell_arg(goal_id)} \\", + f" --objective {shell_arg(objective)} \\", + f" --adapter-kind {shell_arg(DEFAULT_HANDOFF_ADAPTER_KIND)} \\", + f" --adapter-status {shell_arg(DEFAULT_HANDOFF_ADAPTER_STATUS)} \\", + " --no-onboarding-scan \\", + " --codex-app-heartbeat ask", + ] + if display_name: + lines.insert(-1, f" --display-name {shell_arg(display_name)} \\") + if fine_grained: + lines[-1] += " \\" + lines.append(" --fine-grained") + return "\n".join(lines) + + def render_register_agent_command( goal_id: str, *, diff --git a/tests/control_plane/test_start_goal_compact_projection.py b/tests/control_plane/test_start_goal_compact_projection.py index dff906302..6b64e3533 100644 --- a/tests/control_plane/test_start_goal_compact_projection.py +++ b/tests/control_plane/test_start_goal_compact_projection.py @@ -2024,6 +2024,26 @@ def test_guided_takeover_with_only_open_blocker_todo_keeps_unconditional_authori assert "apply_todo_delta" not in step_ids +def test_guided_takeover_with_only_open_monitor_todo_keeps_unconditional_authoring( + tmp_path: Path, +) -> None: + project = _write_connected_project_with_todos( + tmp_path, + todos_body=( + "- [ ] [P1] observe the upstream release window\n" + " " + ), + ) + payload = _build(project, include_detail=False) + step_ids = [ + step["id"] for step in payload["guided_transaction"]["ordered_steps"] + ] + assert "write_ordered_todos" in step_ids + assert "plan_ranked_todos" in step_ids + assert "apply_todo_delta" not in step_ids + + def test_guided_takeover_with_only_deferred_advancement_todo_keeps_unconditional_authoring( tmp_path: Path, ) -> None: