From e6166f8044d904ce702d7ff9991dfef38a15244c Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 01:46:32 +0800 Subject: [PATCH 1/4] fix(connect): stop generating first-connect onboarding todos `loopx bootstrap` / `loopx connect` used to project a generated onboarding queue into a freshly connected goal: a user `onboarding_decision` gate, an agent `onboarding_todo_review` todo, optional candidate todos from a repository scan, and a `onboarding_connection_validation` todo for generic adapters. A default connect therefore landed in `state=operator_gate` with `should_run=false`, and autonomous callers had to pass `--accept-onboarding-agent-todos --begin-autonomous-advance --codex-app-heartbeat no` just to reach their own first delivery todo. An unattended benchmark run could spend its whole turn on that generated item. Connection is now a pure registration step: - `connect` / `bootstrap` write the registry entry and the active state only. The caller, or the connected domain adapter, owns the first delivery todo. - Removed the onboarding scan, candidate todos, generated user/agent todos, heartbeat opt-in gate, and `connection_validation` registry annotation, and deleted `loopx/onboarding.py`. - Removed the `--no-onboarding-scan`, `--accept-onboarding-agent-todos`, `--begin-autonomous-advance`, `--codex-app-heartbeat`, `--onboarding-connection-validation`, and `--onboarding-max-*` options, and the onboarding fields from the bootstrap payload and markdown. - `Next Action` now carries one neutral statement instead of an onboarding prompt, so a fresh goal has no hidden onboarding frontier. - Caller-facing prompts and the emitted bootstrap command pack no longer ask an agent to collect onboarding candidates; they ask for the first delivery todo instead. - Renamed the canary profile/surface from `new-user-onboarding-lifecycle` to `first-connect-contract` and regenerated the semantic inventory. Affected default-behavior lanes: fresh `connect`/`bootstrap` state projection, the quota entry gate for a newly connected goal, Codex App heartbeat opt-in, and the benchmark harnesses that worked around the gate. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- demo/auto_research/demo_e2e.py | 4 - demo/workspace/__main__.py | 2 - loopx/bootstrap.py | 580 +----------------- loopx/bootstrap_command_pack.py | 3 +- loopx/canary/planner.py | 17 +- loopx/canary/quality_surface_catalog.py | 12 +- loopx/chat_actions.py | 5 - .../claude_goal_mode/scripts/goalmode_cmd.py | 2 +- loopx/cli_commands/bootstrap_connect.py | 64 -- loopx/control_plane/projects/registry.py | 1 - loopx/demo.py | 1 - loopx/kunluncode_goal_mode/cli.py | 1 - loopx/onboarding.py | 214 ------- loopx/project_prompt.py | 29 +- loopx/semantics/inventory_v0.json | 11 +- 15 files changed, 34 insertions(+), 912 deletions(-) delete mode 100644 loopx/onboarding.py diff --git a/demo/auto_research/demo_e2e.py b/demo/auto_research/demo_e2e.py index 9df35a0e9e..7fdbaebb2c 100644 --- a/demo/auto_research/demo_e2e.py +++ b/demo/auto_research/demo_e2e.py @@ -142,10 +142,6 @@ def _seed_visible_demo_control_plane( max_children=4, allowed_domains=["auto-research-demo"], write_scope=_visible_demo_goal_write_scope(preset_context), - onboarding_scan_enabled=False, - accept_onboarding_agent_todos=False, - begin_autonomous_advance=True, - codex_app_heartbeat="no", force=False, dry_run=False, sync_global=False, diff --git a/demo/workspace/__main__.py b/demo/workspace/__main__.py index a63f1813d7..c778186d78 100644 --- a/demo/workspace/__main__.py +++ b/demo/workspace/__main__.py @@ -139,8 +139,6 @@ def seed_story(root: Path, story: dict, notice: str) -> dict: max_children=0, allowed_domains=[], write_scope=[], - onboarding_scan_enabled=False, - codex_app_heartbeat="no", force=False, dry_run=False, sync_global=False, diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py index 4085fe36e6..f3a5bed697 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -2,7 +2,6 @@ import json import re -import shlex from pathlib import Path from .file_lock import exclusive_cross_runtime_file_lock @@ -19,7 +18,6 @@ insertion_anchor, section_bounds, ) -from .control_plane.todos.next_action_runtime import bind_next_action_to_todo from .control_plane.todos.handoff_mode import ( HANDOFF_MODE_LEGACY, goal_handoff_mode, @@ -34,7 +32,6 @@ ARCHIVE_FALLBACK_INSTALL_COMMAND, DEFAULT_INSTALL_REPAIR_COMMAND, ) -from .onboarding import build_onboarding_scan from .orchestration import ( DEFAULT_ORCHESTRATION_MODE, MULTI_SUBAGENT_ORCHESTRATION_MODE, @@ -42,43 +39,11 @@ from .paths import rel_or_abs, resolve_runtime_root from .control_plane.goals.active_state_metadata import markdown_blockquote, markdown_frontmatter_string from .registry_writability import probe_registry_write_path -from .todos import add_todo_to_lines DEFAULT_OBJECTIVE = "Improve this project through bounded, verified goal segments." DEFAULT_DOMAIN = "project-goal-control-plane" -GENERIC_ONBOARDING_ADAPTER_KINDS = frozenset( - {"generic_project_goal_v0", "read_only_project_map_v0"} -) -HEARTBEAT_OPT_IN_STATUS_REQUIRED = ( - "requires explicit heartbeat=yes/no before a recurring Codex App automation is installed" -) -HEARTBEAT_OPT_IN_STATUS_PREAUTHORIZED = ( - "explicitly preauthorized; create or update the host loop before claiming recurring automation is active" -) -HEARTBEAT_OPT_IN_STATUS_DECLINED = ( - "explicitly declined; keep the goal manual or on-demand unless the user later opts in" -) -HEARTBEAT_OPT_IN_INSTRUCTION = ( - "Ask the user whether to enable the Codex App heartbeat. heartbeat=yes means create or update a recurring " - "Codex App automation from an identity-scoped `loopx heartbeat-prompt --thin` task body; heartbeat=no means " - "keep the goal manual or on-demand." -) -HEARTBEAT_OPT_IN_PREAUTHORIZED_INSTRUCTION = ( - "The user preauthorized the Codex App heartbeat. Create or update the recurring automation from an " - "identity-scoped `loopx heartbeat-prompt --thin` task body before claiming recurring automation is active." -) -HEARTBEAT_OPT_IN_DECLINED_INSTRUCTION = ( - "The user declined the Codex App heartbeat. Do not install recurring automation; keep the goal manual or " - "on-demand unless the user later opts in." -) -CODEX_APP_HEARTBEAT_CHOICES = {"ask", "yes", "no"} -ONBOARDING_CONNECTION_VALIDATION_AGENT = "agent" -ONBOARDING_CONNECTION_VALIDATION_PROVIDER_PREVALIDATED = "provider-prevalidated" -ONBOARDING_CONNECTION_VALIDATION_CHOICES = { - ONBOARDING_CONNECTION_VALIDATION_AGENT, - ONBOARDING_CONNECTION_VALIDATION_PROVIDER_PREVALIDATED, -} +DEFAULT_NEXT_ACTION = "Initial routing is owned by the connected domain adapter." def slugify_goal_id(value: str) -> str: @@ -148,324 +113,6 @@ def repair_missing_todo_source_sections(state_text: str) -> tuple[str, list[str] return "\n".join(lines) + trailing_newline, added_roles -def onboarding_candidates(onboarding_scan: dict[str, Any] | None) -> list[dict[str, Any]]: - if not isinstance(onboarding_scan, dict): - return [] - candidates = onboarding_scan.get("agent_todo_candidates") - if not isinstance(candidates, list): - return [] - return [candidate for candidate in candidates if isinstance(candidate, dict)] - - -def normalize_codex_app_heartbeat(value: str | None) -> str: - choice = (value or "ask").strip().lower() - if choice not in CODEX_APP_HEARTBEAT_CHOICES: - raise ValueError("codex_app_heartbeat must be one of: ask, yes, no") - return choice - - -def normalize_onboarding_connection_validation(value: str | None) -> str: - choice = (value or ONBOARDING_CONNECTION_VALIDATION_AGENT).strip().lower() - if choice not in ONBOARDING_CONNECTION_VALIDATION_CHOICES: - raise ValueError( - "onboarding_connection_validation must be one of: " - "agent, provider-prevalidated" - ) - return choice - - -def heartbeat_opt_in_required( - *, - onboarding_scan: dict[str, Any] | None, - codex_app_heartbeat: str, -) -> bool: - return bool(onboarding_scan and codex_app_heartbeat == "ask") - - -def host_loop_activation_required( - *, - onboarding_scan: dict[str, Any] | None, - codex_app_heartbeat: str, -) -> bool: - return bool(onboarding_scan and codex_app_heartbeat in {"ask", "yes"}) - - -def heartbeat_status(codex_app_heartbeat: str) -> str: - if codex_app_heartbeat == "yes": - return HEARTBEAT_OPT_IN_STATUS_PREAUTHORIZED - if codex_app_heartbeat == "no": - return HEARTBEAT_OPT_IN_STATUS_DECLINED - return HEARTBEAT_OPT_IN_STATUS_REQUIRED - - -def heartbeat_instruction(codex_app_heartbeat: str) -> str | None: - if codex_app_heartbeat == "yes": - return HEARTBEAT_OPT_IN_PREAUTHORIZED_INSTRUCTION - if codex_app_heartbeat == "no": - return HEARTBEAT_OPT_IN_DECLINED_INSTRUCTION - return HEARTBEAT_OPT_IN_INSTRUCTION - - -def render_onboarding_state_markdown( - *, - onboarding_scan: dict[str, Any] | None, - accept_onboarding_agent_todos: bool, - begin_autonomous_advance: bool, - codex_app_heartbeat: str, -) -> str: - if not onboarding_scan: - return "" - scan_policy = onboarding_scan.get("scan_policy") - scan_policy = scan_policy if isinstance(scan_policy, dict) else {} - recent_commits = onboarding_scan.get("recent_commits") - recent_commits = recent_commits if isinstance(recent_commits, list) else [] - signal_files = onboarding_scan.get("signal_files") - signal_files = signal_files if isinstance(signal_files, list) else [] - validation_files = onboarding_scan.get("validation_signal_files") - validation_files = validation_files if isinstance(validation_files, list) else [] - validation_paths = [ - str(item.get("path")) - for item in validation_files - if isinstance(item, dict) and item.get("path") - ] - acceptance_status = ( - "accepted and written into Agent Todo" - if accept_onboarding_agent_todos - else "requires user selection before delivery work" - ) - autonomous_status = ( - "allowed after accepted agent todos and a fresh quota guard" - if begin_autonomous_advance - else "requires an explicit user yes/no choice" - ) - lines = [ - "## Onboarding Control", - "", - "- Fast repository scan: `enabled`.", - f"- Scan read file bodies: `{bool(scan_policy.get('raw_file_bodies_read'))}`.", - f"- Git repo detected: `{bool(onboarding_scan.get('is_git_repo'))}`.", - f"- Local change count from `git status --short`: `{int(onboarding_scan.get('status_path_count') or 0)}`.", - f"- Recent commits sampled: `{len(recent_commits)}`.", - f"- Project signal files: `{', '.join(str(item) for item in signal_files) or 'none'}`.", - f"- Validation signal files: `{', '.join(validation_paths) or 'none'}`.", - f"- Candidate agent todos: `{acceptance_status}`.", - f"- Autonomous advancement: `{autonomous_status}`.", - f"- Codex App heartbeat: `{heartbeat_status(codex_app_heartbeat)}`.", - "", - "## Proposed Onboarding Candidates", - "", - ] - candidates = onboarding_candidates(onboarding_scan) - for index, candidate in enumerate(candidates, start=1): - text = str(candidate.get("text") or "").strip() - reason = str(candidate.get("reason") or "").strip() - action_kind = str(candidate.get("action_kind") or "analyze").strip() - task_class = str(candidate.get("task_class") or "advancement_task").strip() - lines.append(f"{index}. {text}") - if reason: - lines.append(f" - reason: {reason}") - lines.append(f" - metadata: `{task_class}:{action_kind}`") - if not candidates: - lines.append("No candidate agent todos were generated.") - return "\n".join(lines) - - -def onboarding_user_todo_text( - *, - acceptance_required: bool, - autonomous_choice_required: bool, - heartbeat_choice_required: bool, -) -> str | None: - decisions: list[str] = [] - reply_parts: list[str] = [] - if acceptance_required: - decisions.append("which proposed onboarding agent todos to accept") - reply_parts.append("accepted numbers") - if autonomous_choice_required: - decisions.append("whether Codex may start autonomous advancement") - reply_parts.append("autonomous=yes/no") - if heartbeat_choice_required: - decisions.append("whether to enable the Codex App heartbeat") - reply_parts.append("heartbeat=yes/no") - if not decisions: - return None - verb = "Choose" if acceptance_required else "Decide" - return f"[P1] {verb} {' and '.join(decisions)}; reply with {' plus '.join(reply_parts)}." - - -def onboarding_agent_review_todo_text( - *, - acceptance_required: bool, - autonomous_choice_required: bool, - heartbeat_choice_required: bool, -) -> str | None: - if not acceptance_required and not autonomous_choice_required and not heartbeat_choice_required: - return None - prompts: list[str] = [] - if acceptance_required: - prompts.append("which candidate agent todos to accept") - if autonomous_choice_required: - prompts.append("whether autonomous advancement may start") - if heartbeat_choice_required: - prompts.append("whether to enable the Codex App heartbeat") - prefix = "Present the onboarding scan and ask " if acceptance_required else "Ask " - text = f"[P1] {prefix}{', '.join(prompts)} before delivery work." - if heartbeat_choice_required: - text += ( - " If heartbeat=yes, create or update the Codex App heartbeat from an identity-scoped " - "`loopx heartbeat-prompt --thin` task body before claiming recurring automation is active." - ) - return text - - -def onboarding_connection_validation_action(adapter_kind: str) -> dict[str, str] | None: - if adapter_kind not in GENERIC_ONBOARDING_ADAPTER_KINDS: - return None - return { - "text": ( - "[P1] Run `loopx check` against the project registry and record the first " - "project-specific adapter signal or an explicit no-follow-up rationale." - ), - "task_class": "advancement_task", - "action_kind": "onboarding_connection_validation", - } - - -def onboarding_next_action( - *, - adapter_kind: str, - onboarding_scan: dict[str, Any] | None, - accept_onboarding_agent_todos: bool, - begin_autonomous_advance: bool, - codex_app_heartbeat: str, - include_connection_validation: bool = True, -) -> str: - if not onboarding_scan: - validation_action = ( - onboarding_connection_validation_action(adapter_kind) - if include_connection_validation - else None - ) - if validation_action: - return validation_action["text"] - return "Initial routing is owned by the connected domain adapter." - need_heartbeat_choice = codex_app_heartbeat == "ask" - if not accept_onboarding_agent_todos or not begin_autonomous_advance or need_heartbeat_choice: - asks: list[str] = [] - if not accept_onboarding_agent_todos: - asks.append("which proposed onboarding agent todos to accept") - if not begin_autonomous_advance: - asks.append("whether Codex may start autonomous advancement") - if need_heartbeat_choice: - asks.append("whether to enable the Codex App heartbeat") - follow_up = "then write accepted choices and refresh state before delivery work." - heartbeat_follow_up = "" - if need_heartbeat_choice: - heartbeat_follow_up = ( - " If heartbeat=yes, create or update the recurring automation from an identity-scoped " - "`loopx heartbeat-prompt --thin` task body." - ) - elif codex_app_heartbeat == "yes": - heartbeat_follow_up = ( - " Create or update the preauthorized recurring automation from an identity-scoped " - "`loopx heartbeat-prompt --thin` task body." - ) - autonomous_follow_up = ( - " If autonomous=yes, run the quota guard and execute the first accepted onboarding agent todo." - if not begin_autonomous_advance - else " Run the quota guard and execute the first accepted onboarding agent todo once accepted choices permit." - ) - return f"Ask the user {', '.join(asks)}, {follow_up}{heartbeat_follow_up}{autonomous_follow_up}" - if codex_app_heartbeat == "yes": - return ( - "Create or update the Codex App heartbeat from an identity-scoped `loopx heartbeat-prompt --thin` task " - "body, run the quota guard, execute the first accepted onboarding Agent Todo as a bounded segment, write " - "evidence, complete or update the todo, and refresh state." - ) - return ( - "Run the quota guard, execute the first accepted onboarding Agent Todo as a bounded segment, write evidence, " - "complete or update the todo, and refresh state." - ) - - -def apply_onboarding_todos_to_state( - text: str, - *, - adapter_kind: str, - updated_at: str, - onboarding_scan: dict[str, Any] | None, - accept_onboarding_agent_todos: bool, - begin_autonomous_advance: bool, - codex_app_heartbeat: str, - include_connection_validation: bool = True, -) -> str: - if not onboarding_scan: - lines = text.splitlines() - action = ( - onboarding_connection_validation_action(adapter_kind) - if include_connection_validation - else None - ) - if action: - added = add_todo_to_lines( - lines, - role="agent", - text=action["text"], - task_class=action["task_class"], - action_kind=action["action_kind"], - updated_at=updated_at, - ) - bind_next_action_to_todo(lines, todo_id=str(added["todo_id"])) - return "\n".join(lines) + "\n" - lines = text.splitlines() - if accept_onboarding_agent_todos: - for candidate in onboarding_candidates(onboarding_scan): - todo_text = str(candidate.get("text") or "").strip() - if not todo_text: - continue - add_todo_to_lines( - lines, - role="agent", - text=todo_text, - task_class=str(candidate.get("task_class") or "advancement_task"), - action_kind=str(candidate.get("action_kind") or "analyze"), - updated_at=updated_at, - ) - - acceptance_required = not accept_onboarding_agent_todos - autonomous_choice_required = not begin_autonomous_advance - heartbeat_choice_required = codex_app_heartbeat == "ask" - user_todo = onboarding_user_todo_text( - acceptance_required=acceptance_required, - autonomous_choice_required=autonomous_choice_required, - heartbeat_choice_required=heartbeat_choice_required, - ) - if user_todo: - add_todo_to_lines( - lines, - role="user", - text=user_todo, - task_class="user_gate", - action_kind="onboarding_decision", - updated_at=updated_at, - ) - agent_todo = onboarding_agent_review_todo_text( - acceptance_required=acceptance_required, - autonomous_choice_required=autonomous_choice_required, - heartbeat_choice_required=heartbeat_choice_required, - ) - if agent_todo: - add_todo_to_lines( - lines, - role="agent", - text=agent_todo, - task_class="advancement_task", - action_kind="onboarding_todo_review", - updated_at=updated_at, - ) - return "\n".join(lines) + "\n" - - def render_state_markdown( *, project: Path, @@ -475,30 +122,11 @@ def render_state_markdown( updated_at: str, goal_doc: Path | None, execution_profile: dict[str, Any] | None, - onboarding_scan: dict[str, Any] | None = None, - accept_onboarding_agent_todos: bool = False, - begin_autonomous_advance: bool = False, - codex_app_heartbeat: str = "ask", - include_connection_validation: bool = True, handoff_mode: str = HANDOFF_MODE_LEGACY, ) -> str: safe_objective = markdown_frontmatter_string(objective) profile_summary = execution_profile_summary(execution_profile) - onboarding_markdown = render_onboarding_state_markdown( - onboarding_scan=onboarding_scan, - accept_onboarding_agent_todos=accept_onboarding_agent_todos, - begin_autonomous_advance=begin_autonomous_advance, - codex_app_heartbeat=codex_app_heartbeat, - ) - onboarding_block = f"\n{onboarding_markdown}\n" if onboarding_markdown else "" - next_action = onboarding_next_action( - adapter_kind=adapter_kind, - onboarding_scan=onboarding_scan, - accept_onboarding_agent_todos=accept_onboarding_agent_todos, - begin_autonomous_advance=begin_autonomous_advance, - codex_app_heartbeat=codex_app_heartbeat, - include_connection_validation=include_connection_validation, - ) + next_action = DEFAULT_NEXT_ACTION # ``handoff_mode`` travels in the state front matter (RFC shared-goal # authority, Appendix B). Legacy is the absent default and is never # materialized, so an untouched goal keeps its byte-for-byte shape. @@ -542,7 +170,6 @@ def render_state_markdown( - Do not perform irreversible production operations without explicit approval. - Do not publish private project evidence. - Do not optimize for activity if no useful artifact or decision can be produced. -{onboarding_block} ## User Todo / Owner Review Reading Queue @@ -560,39 +187,13 @@ def render_state_markdown( - Created the initial goal state and registry connection. """ - return apply_onboarding_todos_to_state( - state_text, - adapter_kind=adapter_kind, - updated_at=updated_at, - onboarding_scan=onboarding_scan, - accept_onboarding_agent_todos=accept_onboarding_agent_todos, - begin_autonomous_advance=begin_autonomous_advance, - codex_app_heartbeat=codex_app_heartbeat, - include_connection_validation=include_connection_validation, - ) + return state_text def relative_state_file(project: Path, state_file: Path) -> str: return rel_or_abs(state_file, project) -def todo_add_command( - *, - project: Path, - registry_path: Path, - goal_id: str, - candidate: dict[str, Any], -) -> str: - return ( - f"loopx --registry {shlex.quote(relative_state_file(project, registry_path))} todo add " - f"--goal-id {shlex.quote(goal_id)} " - "--role agent " - f"--text {shlex.quote(str(candidate.get('text') or ''))} " - f"--task-class {shlex.quote(str(candidate.get('task_class') or 'advancement_task'))} " - f"--action-kind {shlex.quote(str(candidate.get('action_kind') or 'analyze'))}" - ) - - def build_goal_entry( *, project: Path, @@ -604,7 +205,6 @@ def build_goal_entry( goal_doc: Path | None, adapter_kind: str, adapter_status: str, - onboarding_connection_validation: str, next_probe: str | None, spawn_allowed: bool, max_children: int, @@ -626,15 +226,6 @@ def build_goal_entry( "kind": adapter_kind, "status": adapter_status, } - if ( - onboarding_connection_validation - == ONBOARDING_CONNECTION_VALIDATION_PROVIDER_PREVALIDATED - ): - adapter["connection_validation"] = { - "owner": "provider", - "status": "prevalidated", - "agent_todo_required": False, - } return { "id": goal_id, **({"display_name": display_name} if display_name else {}), @@ -726,14 +317,6 @@ def bootstrap_project( execution_surface_streak_threshold: int | None = None, execution_outcome_must_advance: list[str] | None = None, execution_turn_granularity: str | None = None, - onboarding_scan_enabled: bool = True, - onboarding_connection_validation: str = ONBOARDING_CONNECTION_VALIDATION_AGENT, - accept_onboarding_agent_todos: bool = False, - begin_autonomous_advance: bool = False, - codex_app_heartbeat: str = "ask", - onboarding_max_commits: int = 5, - onboarding_max_status_paths: int = 12, - onboarding_max_top_level_files: int = 24, preserve_todos: bool = False, display_name: str | None = None, force: bool, @@ -742,13 +325,6 @@ def bootstrap_project( allow_global_route_replacement: bool = False, ) -> dict[str, Any]: project = project.expanduser().resolve() - codex_app_heartbeat = normalize_codex_app_heartbeat(codex_app_heartbeat) - onboarding_connection_validation = normalize_onboarding_connection_validation( - onboarding_connection_validation - ) - include_connection_validation = ( - onboarding_connection_validation == ONBOARDING_CONNECTION_VALIDATION_AGENT - ) registry_path = registry_path.expanduser() if not registry_path.is_absolute(): registry_path = project / registry_path @@ -770,16 +346,6 @@ def bootstrap_project( outcome_must_advance=execution_outcome_must_advance, turn_granularity=execution_turn_granularity, ) - onboarding_scan = ( - build_onboarding_scan( - project, - max_commits=onboarding_max_commits, - max_status_paths=onboarding_max_status_paths, - max_top_level_files=onboarding_max_top_level_files, - ) - if onboarding_scan_enabled - else None - ) registry = read_json_if_exists(registry_path) registry.setdefault("schema_version", "0.1") @@ -798,7 +364,6 @@ def bootstrap_project( goal_doc=goal_doc, adapter_kind=adapter_kind, adapter_status=adapter_status, - onboarding_connection_validation=onboarding_connection_validation, next_probe=next_probe, spawn_allowed=spawn_allowed, max_children=max_children, @@ -880,25 +445,6 @@ def bootstrap_project( } ) - candidates = onboarding_candidates(onboarding_scan) - heartbeat_required = heartbeat_opt_in_required( - onboarding_scan=onboarding_scan, - codex_app_heartbeat=codex_app_heartbeat, - ) - host_loop_required = host_loop_activation_required( - onboarding_scan=onboarding_scan, - codex_app_heartbeat=codex_app_heartbeat, - ) - accept_candidate_commands = [ - todo_add_command( - project=project, - registry_path=registry_path, - goal_id=goal_id, - candidate=candidate, - ) - for candidate in candidates - ] - global_sync: dict[str, Any] | None = None global_writability: dict[str, Any] | None = None if sync_global and not dry_run: @@ -936,21 +482,6 @@ def bootstrap_project( "todo_source_migration": todo_source_migration, "force_bootstrap_warning": force_bootstrap_warning, "execution_profile": execution_profile, - "onboarding_scan": onboarding_scan, - "onboarding_agent_todo_candidates": candidates, - "accept_onboarding_agent_todos": accept_onboarding_agent_todos, - "begin_autonomous_advance": begin_autonomous_advance, - "codex_app_heartbeat": codex_app_heartbeat, - "onboarding_connection_validation": onboarding_connection_validation, - "onboarding_acceptance_required": bool(onboarding_scan and not accept_onboarding_agent_todos), - "autonomous_advance_choice_required": bool(onboarding_scan and not begin_autonomous_advance), - "heartbeat_opt_in_required": heartbeat_required, - "host_loop_activation_required": host_loop_required, - "heartbeat_opt_in_instruction": ( - heartbeat_instruction(codex_app_heartbeat) if onboarding_scan else None - ), - "onboarding_todos_written": False, - "accept_candidate_commands": accept_candidate_commands, "global_sync": global_sync, "actions": actions, "next_commands": [ @@ -997,11 +528,6 @@ def bootstrap_project( updated_at=updated_at, goal_doc=goal_doc, execution_profile=execution_profile, - onboarding_scan=onboarding_scan, - accept_onboarding_agent_todos=accept_onboarding_agent_todos, - begin_autonomous_advance=begin_autonomous_advance, - codex_app_heartbeat=codex_app_heartbeat, - include_connection_validation=include_connection_validation, handoff_mode=declared_handoff_mode, ) else: @@ -1050,21 +576,6 @@ def bootstrap_project( "todo_source_migration": todo_source_migration, "force_bootstrap_warning": force_bootstrap_warning, "execution_profile": execution_profile, - "onboarding_scan": onboarding_scan, - "onboarding_agent_todo_candidates": candidates, - "accept_onboarding_agent_todos": accept_onboarding_agent_todos, - "begin_autonomous_advance": begin_autonomous_advance, - "codex_app_heartbeat": codex_app_heartbeat, - "onboarding_connection_validation": onboarding_connection_validation, - "onboarding_acceptance_required": bool(onboarding_scan and not accept_onboarding_agent_todos), - "autonomous_advance_choice_required": bool(onboarding_scan and not begin_autonomous_advance), - "heartbeat_opt_in_required": heartbeat_required, - "host_loop_activation_required": host_loop_required, - "heartbeat_opt_in_instruction": heartbeat_instruction(codex_app_heartbeat) if onboarding_scan else None, - "onboarding_todos_written": bool( - onboarding_scan and state_action in {"created", "replaced"} and not dry_run - ), - "accept_candidate_commands": accept_candidate_commands, "global_sync": global_sync or { "enabled": sync_global, @@ -1116,16 +627,6 @@ def render_bootstrap_markdown(payload: dict[str, Any]) -> str: f"- registry_goal_action: `{payload.get('registry_goal_action')}`", f"- state_action: `{payload.get('state_action')}`", f"- execution_profile: `{execution_profile_text}`", - f"- onboarding_acceptance_required: `{payload.get('onboarding_acceptance_required')}`", - f"- autonomous_advance_choice_required: `{payload.get('autonomous_advance_choice_required')}`", - f"- codex_app_heartbeat: `{payload.get('codex_app_heartbeat')}`", - ( - "- onboarding_connection_validation: " - f"`{payload.get('onboarding_connection_validation')}`" - ), - f"- heartbeat_opt_in_required: `{payload.get('heartbeat_opt_in_required')}`", - f"- host_loop_activation_required: `{payload.get('host_loop_activation_required')}`", - f"- onboarding_todos_written: `{payload.get('onboarding_todos_written')}`", f"- global_sync: `{(payload.get('global_sync') or {}).get('wrote')}`", "", "## Actions", @@ -1147,81 +648,6 @@ def render_bootstrap_markdown(payload: dict[str, Any]) -> str: ] ) - onboarding_scan = payload.get("onboarding_scan") - if isinstance(onboarding_scan, dict): - recent_commits = onboarding_scan.get("recent_commits") - recent_commits = recent_commits if isinstance(recent_commits, list) else [] - signal_files = onboarding_scan.get("signal_files") - signal_files = signal_files if isinstance(signal_files, list) else [] - status_sample = onboarding_scan.get("status_paths_sample") - status_sample = status_sample if isinstance(status_sample, list) else [] - lines.extend( - [ - "", - "## Onboarding Scan", - f"- schema: `{onboarding_scan.get('schema_version')}`", - f"- git_repo: `{onboarding_scan.get('is_git_repo')}`", - f"- status_path_count: `{onboarding_scan.get('status_path_count')}`", - f"- recent_commits: `{len(recent_commits)}`", - f"- signal_files: `{', '.join(str(item) for item in signal_files) or 'none'}`", - "- raw_file_bodies_read: `False`", - ] - ) - if status_sample: - lines.append("- status_sample:") - for item in status_sample[:5]: - lines.append(f" - `{item}`") - if recent_commits: - lines.append("- recent_commit_sample:") - for commit in recent_commits[:5]: - if isinstance(commit, dict): - lines.append(f" - `{commit.get('hash')}` {commit.get('subject')}") - - candidates = payload.get("onboarding_agent_todo_candidates") - candidates = candidates if isinstance(candidates, list) else [] - if candidates: - lines.extend(["", "## Proposed Onboarding Candidates"]) - for index, candidate in enumerate(candidates, start=1): - if not isinstance(candidate, dict): - continue - lines.append(f"{index}. {candidate.get('text')}") - if candidate.get("reason"): - lines.append(f" - reason: {candidate.get('reason')}") - lines.append( - " - metadata: " - f"`{candidate.get('task_class') or 'advancement_task'}:{candidate.get('action_kind') or 'analyze'}`" - ) - - accept_commands = payload.get("accept_candidate_commands") - accept_commands = accept_commands if isinstance(accept_commands, list) else [] - if accept_commands: - lines.extend( - [ - "", - "## Accept Candidate Commands", - "Run only the commands for candidates the user accepts, then refresh state.", - ] - ) - for command in accept_commands: - lines.append(f"- `{command}`") - - if onboarding_scan: - lines.extend( - [ - "", - "## Autonomy And Heartbeat Choice", - ] - ) - instruction = payload.get("heartbeat_opt_in_instruction") - if instruction: - lines.append(f"- heartbeat_opt_in_instruction: {instruction}") - if payload.get("autonomous_advance_choice_required"): - lines.append( - "- Ask the user whether Codex may start autonomous advancement. If autonomous=yes, run the quota " - "guard and execute the first accepted Agent Todo; if autonomous=no, stop after writing accepted todos " - "and refresh-state." - ) - lines.extend(["", "## Next Commands"]) for command in payload.get("next_commands") or []: lines.append(f"- `{command}`") diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index 6274250498..efc9b552e7 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -675,8 +675,7 @@ def _bootstrap_command( " --project . \\", f" --goal-id {shell_arg(goal_id)} \\", f" --adapter-kind {shell_arg(DEFAULT_HANDOFF_ADAPTER_KIND)} \\", - f" --adapter-status {shell_arg(DEFAULT_HANDOFF_ADAPTER_STATUS)} \\", - " --codex-app-heartbeat ask", + f" --adapter-status {shell_arg(DEFAULT_HANDOFF_ADAPTER_STATUS)}", ] if fine_grained: lines[-1] += " \\" diff --git a/loopx/canary/planner.py b/loopx/canary/planner.py index 52071c84fd..5011b2f53f 100644 --- a/loopx/canary/planner.py +++ b/loopx/canary/planner.py @@ -977,19 +977,18 @@ ], }, { - "id": "new-user-onboarding-lifecycle", - "title": "New-user onboarding lifecycle", + "id": "first-connect-contract", + "title": "First-connect contract", "quality_risk": "high", "purpose": ( - "Check fresh no-scan connection, structured todo projection, " + "Check fresh connection state, absence of first-connect todos, " "state-gap detection, and domain-adapter routing ownership." ), "catalog_families": ["Work Routing", "State And Boundary"], "trigger_hints": ( "new user onboarding", - "onboarding lifecycle", - "no-onboarding-scan", - "onboarding_connection_validation", + "first connect", + "first-connect contract", "state projection gap", "start-goal", "loopx/bootstrap.py", @@ -1002,11 +1001,11 @@ ), "checks": [ { - "command": "python3 examples/project/onboarding-no-scan-projection-smoke.py", + "command": "python3 examples/project/first-connect-contract-smoke.py", "tier": "default", "reason": ( - "guards fresh connection-to-todo parity, state-gap warnings, " - "and domain-adapter routing ownership" + "guards fresh connection state, first-connect todo absence, " + "state-gap warnings, and domain-adapter routing ownership" ), }, ], diff --git a/loopx/canary/quality_surface_catalog.py b/loopx/canary/quality_surface_catalog.py index 9261cd440f..4f4a3ec668 100644 --- a/loopx/canary/quality_surface_catalog.py +++ b/loopx/canary/quality_surface_catalog.py @@ -421,10 +421,10 @@ def _deferred(*, owner: str, rationale: str) -> dict[str, Any]: }, }, { - "surface_id": "new-user-onboarding", - "title": "New-user goal start and host activation", + "surface_id": "first-connect", + "title": "First-connect state and host activation", "risk": "high", - "canary_profile_id": "new-user-onboarding-lifecycle", + "canary_profile_id": "first-connect-contract", "owner_paths": [ "loopx/agent_onboarding.py", "loopx/bootstrap_command_pack.py", @@ -446,9 +446,9 @@ def _deferred(*, owner: str, rationale: str) -> dict[str, Any]: "tests/control_plane/test_onboarding_model_behavior_qualification.py", ), "durable_smoke": _covered( - "examples/project/onboarding-no-scan-projection-smoke.py" + "examples/project/first-connect-contract-smoke.py" ), - "catalog_canary": _covered("new-user-onboarding-lifecycle"), + "catalog_canary": _covered("first-connect-contract"), "host_upgrade": _covered( "examples/control_plane/agent-onboard-host-loop-activation-smoke.py" ), @@ -457,7 +457,7 @@ def _deferred(*, owner: str, rationale: str) -> dict[str, Any]: "onboarding_actual_behavior_qualification_v0" ), "release_gate": _covered( - "loopx canary premerge --profile new-user-onboarding-lifecycle" + "loopx canary premerge --profile first-connect-contract" ), }, }, diff --git a/loopx/chat_actions.py b/loopx/chat_actions.py index fb248a6047..46e5472d59 100644 --- a/loopx/chat_actions.py +++ b/loopx/chat_actions.py @@ -572,16 +572,11 @@ def _apply_goal_create( ), adapter_status="connected", display_name=str(parameters.get("title") or "").strip() or None, - onboarding_connection_validation="provider-prevalidated", next_probe=None, spawn_allowed=False, max_children=0, allowed_domains=[], write_scope=[], - onboarding_scan_enabled=False, - accept_onboarding_agent_todos=False, - begin_autonomous_advance=False, - codex_app_heartbeat="no", preserve_todos=True, force=False, dry_run=False, diff --git a/loopx/claude_goal_mode/scripts/goalmode_cmd.py b/loopx/claude_goal_mode/scripts/goalmode_cmd.py index 026ff02390..faf7c30db6 100644 --- a/loopx/claude_goal_mode/scripts/goalmode_cmd.py +++ b/loopx/claude_goal_mode/scripts/goalmode_cmd.py @@ -234,7 +234,7 @@ def main(): # Claude projects keep goal state under .claude/ (not the Codex-default .codex/) state_file = f".claude/goals/{goal_id}/ACTIVE_GOAL_STATE.md" r = gh(["bootstrap", "--project", str(proj), "--goal-id", goal_id, - "--objective", task, "--state-file", state_file, "--no-onboarding-scan"]) + "--objective", task, "--state-file", state_file]) if "ok: `True`" not in r.stdout and "ok=True" not in r.stdout and r.returncode != 0: print("[loopx] bootstrap failed:\n" + (r.stdout + r.stderr)[:600]) sys.exit(1) diff --git a/loopx/cli_commands/bootstrap_connect.py b/loopx/cli_commands/bootstrap_connect.py index de9d7207d9..0dd700f97e 100644 --- a/loopx/cli_commands/bootstrap_connect.py +++ b/loopx/cli_commands/bootstrap_connect.py @@ -7,8 +7,6 @@ from ..bootstrap import ( DEFAULT_DOMAIN, DEFAULT_OBJECTIVE, - ONBOARDING_CONNECTION_VALIDATION_AGENT, - ONBOARDING_CONNECTION_VALIDATION_CHOICES, bootstrap_project, derive_goal_display_name, render_bootstrap_markdown, @@ -120,58 +118,6 @@ def register_bootstrap_connect_command(subparsers: argparse._SubParsersAction) - default=[], help="Outcome/evidence floor label that future delivery must advance. Repeatable.", ) - bootstrap_parser.add_argument( - "--no-onboarding-scan", - action="store_true", - help="Skip the fast first-connect repository scan and todo candidate proposal.", - ) - bootstrap_parser.add_argument( - "--onboarding-connection-validation", - choices=sorted(ONBOARDING_CONNECTION_VALIDATION_CHOICES), - default=ONBOARDING_CONNECTION_VALIDATION_AGENT, - help=( - "Choose who validates the project connection. The default 'agent' may create " - "a loopx-check Todo; 'provider-prevalidated' records provider ownership and " - "omits that agent Todo." - ), - ) - bootstrap_parser.add_argument( - "--accept-onboarding-agent-todos", - action="store_true", - help="Write all proposed onboarding agent todos into the initial active state.", - ) - bootstrap_parser.add_argument( - "--begin-autonomous-advance", - action="store_true", - help="Record that Codex may begin from accepted onboarding agent todos after the quota guard permits work.", - ) - bootstrap_parser.add_argument( - "--codex-app-heartbeat", - choices=["ask", "yes", "no"], - default="ask", - help=( - "Codex App recurring heartbeat choice for onboarding. Default ask creates a user gate; " - "yes/no records an explicit operator decision for headless setup." - ), - ) - bootstrap_parser.add_argument( - "--onboarding-max-commits", - type=int, - default=5, - help="Maximum recent commits sampled by the fast onboarding scan.", - ) - bootstrap_parser.add_argument( - "--onboarding-max-status-paths", - type=int, - default=12, - help="Maximum git status lines sampled by the fast onboarding scan.", - ) - bootstrap_parser.add_argument( - "--onboarding-max-top-level-files", - type=int, - default=24, - help="Maximum top-level names sampled by the fast onboarding scan.", - ) bootstrap_parser.add_argument("--force", action="store_true", help="Replace existing goal entry or state file.") bootstrap_parser.add_argument( "--preserve-todos", @@ -236,16 +182,6 @@ def handle_bootstrap_connect_command( execution_surface_streak_threshold=args.execution_surface_streak_threshold, execution_outcome_must_advance=args.execution_outcome_must_advance or None, execution_turn_granularity=("fine" if bool(args.fine_grained) else None), - onboarding_scan_enabled=not bool(args.no_onboarding_scan), - onboarding_connection_validation=str( - args.onboarding_connection_validation - ), - accept_onboarding_agent_todos=bool(args.accept_onboarding_agent_todos), - begin_autonomous_advance=bool(args.begin_autonomous_advance), - codex_app_heartbeat=str(args.codex_app_heartbeat), - onboarding_max_commits=args.onboarding_max_commits, - onboarding_max_status_paths=args.onboarding_max_status_paths, - onboarding_max_top_level_files=args.onboarding_max_top_level_files, preserve_todos=bool(args.preserve_todos), force=args.force, dry_run=args.dry_run, diff --git a/loopx/control_plane/projects/registry.py b/loopx/control_plane/projects/registry.py index 5f6d5f0cee..4e60ab75db 100644 --- a/loopx/control_plane/projects/registry.py +++ b/loopx/control_plane/projects/registry.py @@ -252,7 +252,6 @@ def register_project_goal( goal_doc=None, adapter_kind="read_only_project_map_v0", adapter_status="connected", - onboarding_connection_validation="agent", next_probe=None, spawn_allowed=False, max_children=0, diff --git a/loopx/demo.py b/loopx/demo.py index c2c49bcf66..e4b46e883b 100644 --- a/loopx/demo.py +++ b/loopx/demo.py @@ -69,7 +69,6 @@ def run_demo( max_children=3, allowed_domains=[], write_scope=[], - onboarding_scan_enabled=False, force=False, dry_run=False, sync_global=False, diff --git a/loopx/kunluncode_goal_mode/cli.py b/loopx/kunluncode_goal_mode/cli.py index 29ba7fbadd..cec051dbec 100644 --- a/loopx/kunluncode_goal_mode/cli.py +++ b/loopx/kunluncode_goal_mode/cli.py @@ -335,7 +335,6 @@ def _bootstrap_if_needed( objective, "--state-file", f".loopx/goals/{goal_id}/ACTIVE_GOAL_STATE.md", - "--no-onboarding-scan", "--no-global-sync", ] if dry_run: diff --git a/loopx/onboarding.py b/loopx/onboarding.py deleted file mode 100644 index 55170df118..0000000000 --- a/loopx/onboarding.py +++ /dev/null @@ -1,214 +0,0 @@ -from __future__ import annotations - -import subprocess -from pathlib import Path -from typing import Any - - -ONBOARDING_SCAN_SCHEMA_VERSION = "loopx_project_onboarding_v0" - -PROJECT_SIGNAL_FILES = ( - "GOAL.md", - "README.md", - "AGENTS.md", - "CONTRIBUTING.md", - "pyproject.toml", - "package.json", - "Cargo.toml", - "go.mod", - "Makefile", - "justfile", -) - -VALIDATION_SIGNAL_FILES = { - "pyproject.toml": "python", - "package.json": "node", - "Cargo.toml": "rust", - "go.mod": "go", - "Makefile": "make", - "justfile": "just", -} - - -def _git(project: Path, *args: str, timeout_seconds: float = 1.5) -> subprocess.CompletedProcess[str] | None: - try: - return subprocess.run( - ["git", "-C", str(project), *args], - check=False, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - timeout=timeout_seconds, - ) - except (OSError, subprocess.TimeoutExpired): - return None - - -def _git_lines(project: Path, *args: str, timeout_seconds: float = 1.5) -> list[str]: - result = _git(project, *args, timeout_seconds=timeout_seconds) - if result is None or result.returncode != 0: - return [] - return [line.strip() for line in result.stdout.splitlines() if line.strip()] - - -def _is_git_repo(project: Path) -> bool: - result = _git(project, "rev-parse", "--is-inside-work-tree", timeout_seconds=1) - return bool(result and result.returncode == 0 and result.stdout.strip() == "true") - - -def _top_level_files(project: Path, *, limit: int) -> list[str]: - names: list[str] = [] - try: - for child in sorted(project.iterdir(), key=lambda path: path.name.lower()): - if child.name.startswith(".git"): - continue - suffix = "/" if child.is_dir() else "" - names.append(f"{child.name}{suffix}") - if len(names) >= limit: - break - except OSError: - return [] - return names - - -def _detected_signal_files(project: Path) -> list[str]: - return [name for name in PROJECT_SIGNAL_FILES if (project / name).exists()] - - -def _recent_commits(project: Path, *, max_commits: int) -> list[dict[str, str]]: - lines = _git_lines(project, "log", f"--max-count={max(0, max_commits)}", "--pretty=format:%h%x09%s") - commits: list[dict[str, str]] = [] - for line in lines: - if "\t" in line: - short_hash, subject = line.split("\t", 1) - else: - short_hash, subject = line, "" - commits.append({"hash": short_hash, "subject": subject}) - return commits - - -def _candidate( - *, - text: str, - task_class: str = "advancement_task", - action_kind: str = "analyze", - reason: str, -) -> dict[str, str]: - return { - "text": " ".join(text.split()), - "task_class": task_class, - "action_kind": action_kind, - "reason": " ".join(reason.split()), - } - - -def build_onboarding_scan( - project: Path, - *, - max_commits: int = 5, - max_status_paths: int = 12, - max_top_level_files: int = 24, -) -> dict[str, Any]: - """Build a bounded, body-free scan for first-time project onboarding.""" - project = project.expanduser().resolve() - is_git_repo = _is_git_repo(project) - status_paths = ( - _git_lines(project, "status", "--short", "--untracked-files=normal", timeout_seconds=1.5) - if is_git_repo - else [] - ) - status_sample = status_paths[: max(0, max_status_paths)] - commits = _recent_commits(project, max_commits=max_commits) if is_git_repo else [] - signal_files = _detected_signal_files(project) - validation_signal_files = [ - {"path": path, "ecosystem": VALIDATION_SIGNAL_FILES[path]} - for path in signal_files - if path in VALIDATION_SIGNAL_FILES - ] - top_level_files = _top_level_files(project, limit=max(0, max_top_level_files)) - - candidates: list[dict[str, str]] = [] - if status_sample: - sample = ", ".join(status_sample[:5]) - candidates.append( - _candidate( - text=( - f"[P1] Inspect current uncommitted changes ({sample}) and decide what belongs " - "in the first LoopX segment before editing." - ), - action_kind="repo_status_review", - reason="The repo already has local changes, so the first safe step is ownership and scope classification.", - ) - ) - elif is_git_repo: - candidates.append( - _candidate( - text="[P1] Confirm the clean git baseline and record the first safe bounded segment before editing.", - action_kind="repo_status_review", - reason="The repo appears clean, so the agent can establish a baseline before selecting delivery work.", - ) - ) - - if commits: - count = len(commits) - candidates.append( - _candidate( - text=f"[P1] Summarize the last {count} commits and extract the safest next bounded project follow-up.", - action_kind="commit_summary", - reason="Recent commits are a fast signal of current project direction without reading private bodies.", - ) - ) - - if validation_signal_files: - files = ", ".join(item["path"] for item in validation_signal_files[:4]) - candidates.append( - _candidate( - text=f"[P1] Identify the fastest validation command from {files} and record whether it is safe to run now.", - action_kind="validation_plan", - reason="Validation entrypoints are visible from top-level project metadata.", - ) - ) - - if signal_files: - files = ", ".join(signal_files[:5]) - candidates.append( - _candidate( - text=( - f"[P2] Build a compact read-only project map from {files} and note authority sources, " - "risks, and first useful handoff." - ), - action_kind="read_only_map", - reason="Top-level project files can seed a useful map before any implementation work.", - ) - ) - - if not candidates: - candidates.append( - _candidate( - text="[P1] Do a bounded read-only repo intake and ask the user to confirm the first concrete delivery target.", - action_kind="repo_intake", - reason="No common project signals were found, so the safest first step is a narrow intake.", - ) - ) - - return { - "schema_version": ONBOARDING_SCAN_SCHEMA_VERSION, - "project_label": project.name, - "is_git_repo": is_git_repo, - "status_path_count": len(status_paths), - "status_paths_sample": status_sample, - "recent_commits": commits, - "signal_files": signal_files, - "validation_signal_files": validation_signal_files, - "top_level_files_sample": top_level_files, - "agent_todo_candidates": candidates[:4], - "user_acceptance_required": True, - "scan_policy": { - "fast": True, - "max_commits": max_commits, - "max_status_paths": max_status_paths, - "max_top_level_files": max_top_level_files, - "raw_file_bodies_read": False, - }, - } diff --git a/loopx/project_prompt.py b/loopx/project_prompt.py index 41e341da03..2fee9ad256 100644 --- a/loopx/project_prompt.py +++ b/loopx/project_prompt.py @@ -72,9 +72,7 @@ def render_goal_start_bootstrap_command( 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", + f" --adapter-status {shell_arg(DEFAULT_HANDOFF_ADAPTER_STATUS)}", ] if display_name: lines.insert(-1, f" --display-name {shell_arg(display_name)} \\") @@ -813,8 +811,9 @@ def render_codex_cli_bootstrap_message_text( {connect_command} ``` -If the connect output includes onboarding candidate todos, summarize them in -this TUI and ask me which ones to accept before starting autonomous delivery. +`connect` only registers the goal and its active state. It does not create +first-connect todos, so choose the first delivery todo explicitly (for example +with `todo add`) instead of waiting for onboarding candidates. 4. Generate the thin loop prompt after route reuse or bootstrap/connect, not before. Do not hand-write or copy an old heartbeat body: @@ -932,10 +931,8 @@ def render_prompt_text( {goal_doc} 请你按下面步骤推进,不要停在方案讨论;如果信息缺失,先从目标文档和项目结构中做保守抽取,并在最后说明假设。 -重要:`{cli_bin} connect` 默认会做一次快速 onboarding scan,基于 git status、最近 commit、顶层项目信号生成候选 agent todo。 -接入后不要直接开始 delivery;先把候选 todo 展示给我,并问我两件事: -1. 接受、编辑或拒绝哪些候选 agent todo; -2. 是否允许你从接受的 todo 开始自主推进。 +重要:`{cli_bin} connect` 只登记 goal 和 active state,不会生成首连 onboarding todo。 +接入后先只读核对状态与目标文档,把第一个交付 todo 的候选写给我确认,再开始 delivery。 0. 先确认当前 shell 能调用 LoopX CLI;如果提示 `loopx` 不在 PATH,运行本机安装脚本再继续: @@ -970,15 +967,11 @@ def render_prompt_text( ``` 4. 确认 `.loopx/registry.json` 和 `.codex/goals/{goal_id}/ACTIVE_GOAL_STATE.md` 已创建或更新。 - 阅读输出里的 `Onboarding Scan`、`Proposed Onboarding Candidates`、`Accept Candidate Commands` - 和 `Autonomy Choice`。不要让用户手动执行这些命令;你应当用中文简要解释候选 todo, - 然后询问用户: - - 接受哪些编号,是否需要改写; - - 是否 `autonomous=yes`,允许你在 quota guard 通过后开始执行第一个接受的 agent todo。 - 如果用户接受候选 todo,用输出里的 `{cli_bin} todo add ...` 命令写入 agent todo; - 如果用户允许自主推进,先运行 quota guard,再执行第一个已接受 agent todo。 - 如果用户不允许自主推进,只写入接受的 todo 并运行 `{refresh_command}`, - 然后停下来汇报。 + 接入输出里不再有 onboarding 扫描、候选 todo 或自主推进选择项;首连之后状态里 + 没有可执行的 agent todo。请只读核对目标文档和 registry 的 `execution_profile`, + 用中文给出 1-3 个第一个交付 todo 的候选,问用户确认后,用 + `{cli_bin} todo add ...` 写入被接受的条目,再运行 `{refresh_command}` 并汇报。 + 在用户确认前不要开始 delivery。 如果目标状态包含私有证据,把 `.loopx/` 和 `.codex/goals/` 加入该项目 `.gitignore`。 `{cli_bin} connect` 默认会同步到共享全局 registry;不要手动编辑其他项目的 registry。 接入后检查 registry 里的 `execution_profile`:它是本项目后续 heartbeat / adapter 的执行画像。 diff --git a/loopx/semantics/inventory_v0.json b/loopx/semantics/inventory_v0.json index 92546080d5..a0747f5e6b 100644 --- a/loopx/semantics/inventory_v0.json +++ b/loopx/semantics/inventory_v0.json @@ -115,8 +115,6 @@ {"name": "AUTHORITY_REGISTRY_CANONICAL_FIELDS", "module": "loopx/authority.py", "container": "tuple", "values": ["default_entry_docs", "topic_authority", "project_materials", "deprecated_sources"]}, {"name": "AUTHORITY_REGISTRY_SUMMARY_FIELDS", "module": "loopx/authority.py", "container": "tuple", "values": ["declared", "required", "path", "path_exists", "read_status", "default_entry_count", "default_entries_checked", "default_entries_present", "topic_authority_count", "project_material_count", "project_material_repository_count", "project_material_owner_review_required_count", "project_material_stale_count", "project_material_current_authority_count", "deprecated_source_count", "conflict_risk"]}, {"name": "AUTHORITY_SOURCE_BOUNDARIES", "module": "loopx/authority.py", "container": "set", "values": ["public", "local_private", "private_redacted"]}, - {"name": "CODEX_APP_HEARTBEAT_CHOICES", "module": "loopx/bootstrap.py", "container": "set", "values": ["ask", "yes", "no"]}, - {"name": "GENERIC_ONBOARDING_ADAPTER_KINDS", "module": "loopx/bootstrap.py", "container": "frozenset", "values": ["generic_project_goal_v0", "read_only_project_map_v0"]}, {"name": "START_GOAL_HOST_SURFACES", "module": "loopx/bootstrap_command_pack.py", "container": "tuple", "values": ["codex-app", "trae_app", "codex-app-ssh", "codex-ide-plugin", "codex-cli-tui", "claude-code", "opencode", "opencode2", "traex-cli", "pi", "gemini-cli", "cursor-agent", "zcode", "agy", "kiro-cli", "deepseek-harness", "deepseek-harness-native", "ark-managed-agent", "shell", "other-agent"]}, {"name": "ACTIVE_BOUNDARY_AUTHORITY_STATUSES", "module": "loopx/boundary_authority.py", "container": "set", "values": ["active", "approved"]}, {"name": "BOUNDARY_AUTHORITY_DECISIONS", "module": "loopx/boundary_authority.py", "container": "set", "values": ["approve", "reject", "defer"]}, @@ -542,7 +540,6 @@ {"name": "MATERIAL_PROGRESS_GRANULARITIES", "module": "loopx/long_task_cadence.py", "container": "frozenset", "values": ["multi_surface", "implementation_plus_validation", "milestone"]}, {"name": "SMALL_PROGRESS_GRANULARITIES", "module": "loopx/long_task_cadence.py", "container": "frozenset", "values": ["status_only", "single_surface"]}, {"name": "MARKDOWN_SUFFIXES", "module": "loopx/materials.py", "container": "set", "values": [".md", ".markdown"]}, - {"name": "PROJECT_SIGNAL_FILES", "module": "loopx/onboarding.py", "container": "tuple", "values": ["GOAL.md", "README.md", "AGENTS.md", "CONTRIBUTING.md", "pyproject.toml", "package.json", "Cargo.toml", "go.mod", "Makefile", "justfile"]}, {"name": "OPERATOR_GATE_DECISIONS", "module": "loopx/operator_gate.py", "container": "set", "values": ["approve", "reject", "defer"]}, {"name": "EXPLORE_HARNESS_PROFILES", "module": "loopx/orchestration.py", "container": "tuple", "values": ["generic", "adaptive-resilient", "moe-router"]}, {"name": "SUBAGENT_REASONING_EFFORTS", "module": "loopx/orchestration.py", "container": "tuple", "values": ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]}, @@ -902,13 +899,13 @@ ] }, "summary": { - "source_files": 1177, + "source_files": 1176, "python_enums": 103, - "python_closed_sets": 495, + "python_closed_sets": 492, "python_literal_aliases": 8, "typescript_const_arrays": 40, - "named_string_constants": 2033, - "schema_version_names": 756, + "named_string_constants": 2025, + "schema_version_names": 755, "schema_version_same_runtime_forks": 7, "cross_runtime_twins": 166, "same_runtime_forks": 25, From 3b0d881540c5663f59678b3840aca8e10fc441ed Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 01:46:44 +0800 Subject: [PATCH 2/4] docs(connect): document the registration-only connection contract Record the default-behavior change in the public docs: `connect`/`bootstrap` register the goal and write the active state only, and create no first-connect onboarding todo, owner-decision gate, or host-loop opt-in gate. The first delivery todo belongs to the caller or the connected domain adapter. - `docs/integration.md`: drop the removed `--no-onboarding-scan`/`--onboarding-connection-validation` provider example and state the registration-only contract. - Book chapter 05 (zh/en): replace the "next Agent Todo after connect" expectation and add the connection-contract note. - `docs/operations/new-project-codex-prompt.md`: replace the onboarding-scan and candidate-todo instructions with "collect the first delivery todo". - `docs/concepts/interaction-pattern-catalog.md`: point the state-projection pattern at the replacement first-connect smoke. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../chapters/05-connect-existing-project.md | 8 +++++- .../chapters/05-connect-existing-project.md | 8 +++++- docs/concepts/interaction-pattern-catalog.md | 2 +- docs/integration.md | 26 +++++------------ docs/operations/new-project-codex-prompt.md | 28 +++++++------------ 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/docs/book/chapters/05-connect-existing-project.md b/docs/book/chapters/05-connect-existing-project.md index ae4d4b5ee5..5ffe74573d 100644 --- a/docs/book/chapters/05-connect-existing-project.md +++ b/docs/book/chapters/05-connect-existing-project.md @@ -18,7 +18,8 @@ - `loopx doctor` 报告安装可用; - 项目存在 `.loopx/registry.json`; - 项目存在 `.codex/goals//ACTIVE_GOAL_STATE.md`; -- `loopx status` 能显示 active state、当前 Gate 和下一项 Agent Todo; +- `loopx status` 能显示 active state 和当前 frontier;首连不会生成 onboarding todo, + 第一个交付 todo 由 Agent 与你确认后写入; - `.loopx/` 与 `.codex/goals/` 不会进入 Git; - 再次连接会按精确 `goal_id` 复用已有 Goal,而不是覆盖目标; - 新接入的执行者使用 fresh `agent_id`,除非用户明确授权 takeover。 @@ -201,6 +202,11 @@ loopx start-goal \ 这个命令生成 guided transaction packet。它默认是预览,不应被理解为已经完成 Todo 写回、Host 激活和 Agent Turn。Agent 或 Host 集成需要按 packet 执行计划、状态写回与启动步骤。 +`connect` / `bootstrap` 只登记 Goal 并写入 active state:它不会生成首连 onboarding todo、 +owner 决策门禁或 Host loop opt-in 门禁。首连之后状态里没有可执行的 agent todo,第一个交付 +todo 由 Agent 或已接入的 domain adapter 写入,避免自动化从生成的 onboarding 队列而不是 +调用方自己的工作队列开始。 + ### 先选择 Goal,再选择 Agent Guided start 会把两个选择分开: diff --git a/docs/book/en/chapters/05-connect-existing-project.md b/docs/book/en/chapters/05-connect-existing-project.md index fb9311255a..30ef8b91c2 100644 --- a/docs/book/en/chapters/05-connect-existing-project.md +++ b/docs/book/en/chapters/05-connect-existing-project.md @@ -20,7 +20,8 @@ When onboarding is complete: - `loopx doctor` reports a usable installation; - `.loopx/registry.json` exists in the project; - `.codex/goals//ACTIVE_GOAL_STATE.md` exists; -- `loopx status` can show active state, current Gates, and the next Agent Todo; +- `loopx status` can show the active state and the current frontier; a first connection creates no + onboarding Todo, so the Agent writes the first delivery Todo after you confirm it; - `.loopx/` and `.codex/goals/` do not enter Git; - reconnecting reuses the exact existing `goal_id` instead of overwriting the Goal; - a new executor receives a fresh `agent_id` unless the user explicitly authorizes a takeover. @@ -213,6 +214,11 @@ This produces a guided transaction packet. It is a preview, not proof that Todo or an Agent turn has already happened. The Host integration must execute the planning, state writeback, and activation described by the packet. +`connect` / `bootstrap` register the Goal and write the active state only: they create no first-connect +onboarding Todo, owner-decision gate, or host-loop opt-in gate. A freshly connected goal therefore has no +executable Agent Todo; the Agent or the connected domain adapter writes the first delivery Todo, so +automation starts from the caller's own work queue instead of a generated onboarding queue. + ### Choose the Goal before choosing the Agent Guided start keeps two decisions separate: diff --git a/docs/concepts/interaction-pattern-catalog.md b/docs/concepts/interaction-pattern-catalog.md index 970c14c597..edf87b3ac9 100644 --- a/docs/concepts/interaction-pattern-catalog.md +++ b/docs/concepts/interaction-pattern-catalog.md @@ -1333,7 +1333,7 @@ todo and the automation drifts into monitor-only no-ops. **Validation** - `examples/state-projection-gap-smoke.py` -- `examples/project/onboarding-no-scan-projection-smoke.py` +- `examples/project/first-connect-contract-smoke.py` - `docs/project-agent-todo-contract.md` #### IP-006 Checkpointed Scope Mismatch diff --git a/docs/integration.md b/docs/integration.md index fb6cd4dbb2..886d6cd939 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -98,25 +98,13 @@ loopx bootstrap \ `loopx connect` is an alias for the same operation. The command is safe to rerun: by default it keeps an existing state file and existing registry -entry. If the goal only needs an additional write boundary after connection, -prefer the incremental migration path: - -An integration provider that already qualified the project bridge can own the -one-time connection check explicitly: - -```bash -loopx connect \ - --goal-id project-goal \ - --no-onboarding-scan \ - --onboarding-connection-validation provider-prevalidated -``` - -The default remains `agent`, which may create a `loopx check` onboarding Todo -for generic adapters. `provider-prevalidated` records provider ownership in the -registry and omits that agent Todo; it does not run validation, grant tools, or -expand the provider's authority. Use it only when the caller has already -validated the connection. Repository scanning and connection validation remain -separate controls. +entry. Connection registers the goal and writes the active state only: it does +not create first-connect onboarding Todos, owner-decision gates, or host-loop +opt-in gates. The caller — or the connected domain adapter — writes the first +delivery Todo it wants, so an autonomously driven project starts from the +caller's own work queue instead of a generated onboarding queue. If the goal +only needs an additional write boundary after connection, prefer the +incremental migration path: ```bash loopx configure-goal \ diff --git a/docs/operations/new-project-codex-prompt.md b/docs/operations/new-project-codex-prompt.md index a9a682cbf4..efecd5dc72 100644 --- a/docs/operations/new-project-codex-prompt.md +++ b/docs/operations/new-project-codex-prompt.md @@ -43,12 +43,8 @@ loopx new-project-prompt \ 请你按下面步骤推进,不要停在方案讨论: -重要:`loopx connect` 默认会做一次快速 onboarding scan,基于 git status、 -最近 commit、顶层项目信号生成候选 agent todo。接入后不要直接开始 delivery; -先把候选 todo 展示给我,并问我两件事: - -1. 接受、编辑或拒绝哪些候选 agent todo; -2. 是否允许你从接受的 todo 开始自主推进。 +重要:`loopx connect` 只登记 goal 和 active state,不会生成首连 onboarding todo。 +接入后先只读核对状态与目标文档,把第一个交付 todo 的候选写给我确认,再开始 delivery。 0. 先确认当前 shell 能调用 LoopX CLI;如果提示 `loopx` 不在 PATH,运行本机安装脚本再继续: @@ -112,18 +108,14 @@ loopx new-project-prompt \ 3. 确认 `.loopx/registry.json` 和 `.codex/goals//ACTIVE_GOAL_STATE.md` 已创建或更新。 - 阅读输出里的 `Onboarding Scan`、`Proposed Onboarding Candidates`、 - `Accept Candidate Commands` 和 `Autonomy Choice`。不要让我手动执行这些命令; - 你应当用中文简要解释候选 todo,然后询问: - - 接受哪些编号,是否需要改写; - - 是否 `autonomous=yes`,允许你在 quota guard 通过后开始执行第一个接受的 - agent todo。 - 如果我接受候选 todo,用输出里的 `loopx todo add ...` 命令写入 - agent todo;如果我允许自主推进,先运行 quota guard,再执行第一个已接受 - agent todo。如果我不允许自主推进,只写入接受的 todo 并运行 - `loopx refresh-state --goal-id `,然后停下来汇报。 - 如果目标状态包含私有证据,把 `.loopx/` 和 `.codex/goals/` - 加入该项目 `.gitignore`。 + 接入输出里不再有 onboarding 扫描、候选 todo 或自主推进选择项;首连之后状态里 + 没有可执行的 agent todo。请只读核对目标文档和 registry 的 `execution_profile`, + 用中文给出 1-3 个第一个交付 todo 的候选,问我确认后,用 + `loopx todo add ...` 写入被接受的条目,再运行 + `loopx refresh-state --goal-id ` 并汇报。在用户确认前不要开始 + delivery。 + 如果目标状态包含私有证据,把 `.loopx/` 和 `.codex/goals/` + 加入该项目 `.gitignore`。 `loopx connect` 默认会同步到共享全局 registry;不要手动编辑其他 项目的 registry。 接入后检查 registry 里的 `execution_profile`:它是本项目后续 heartbeat / From 6f779147ff84fc00023e3f8083f658aa7a724213 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 01:47:14 +0800 Subject: [PATCH 3/4] benchmark(swe-marathon): drop obsolete first-connect gate flags The SWE-marathon harnesses passed `--no-onboarding-scan` (v1/v2) or `--accept-onboarding-agent-todos --begin-autonomous-advance --codex-app-heartbeat no` (v3) to keep a native Codex goal off the generated first-connect onboarding todo. Upstream `connect`/`bootstrap` no longer writes any first-connect todo, user gate, candidate list, or opt-in gate, so those flags are gone and the harness no longer needs a bootstrap gate string. `_bootstrap_gates` now returns only the optional `--write-scope` argument, and the incident comments record that the workaround is obsolete rather than presenting it as the current recipe. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../swe-marathon/agents/codex_loopx_agent.py | 23 +++++++++++-------- .../swe-marathon/runtime/modes/run_mode.py | 2 +- .../swe-marathon/runtime/modes/session.py | 20 ++++++---------- .../runtime/turn/loopx_native_codex.py | 3 +-- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/benchmark/swe-marathon/agents/codex_loopx_agent.py b/benchmark/swe-marathon/agents/codex_loopx_agent.py index 7b7e777cc6..9922cb547d 100644 --- a/benchmark/swe-marathon/agents/codex_loopx_agent.py +++ b/benchmark/swe-marathon/agents/codex_loopx_agent.py @@ -94,7 +94,10 @@ # 而「Only user `/goal resume` reactivates it」——benchmark 里没有 user。 # v2 有 21/45 个任务、113/417 个阶段(27%)以 blocked 收尾。 # -# 三处都用官方 CLI 参数修,不碰 LoopX 渲染出的 goal body,保真度门禁照旧。 +# 修复①和②的参数已随上游删除首连 onboarding 门禁而失效:现在无论哪一版, +# bootstrap 都只登记 goal,不再写 onboarding todo,也不再要求 +# connection validation / heartbeat 选择。LOOPX_UNGATED 目前只剩写权限声明 +# 这一处差异,仍不碰 LoopX 渲染出的 goal body,保真度门禁照旧。 _UNGATED = bool(os.environ.get("LOOPX_UNGATED")) # ── 三个模式 ──────────────────────────────────────────────────────────────── @@ -157,17 +160,17 @@ class CodexLoopxAgent(CodexGoalAgent): def _bootstrap_gates(self, cwd: str) -> str: """bootstrap 末尾那串门禁/规划相关的参数。 - 基准两版(v1/v2)用的是抑制方向的组合,第三版反过来。差别只有这一处, - 其余(objective、adapter、goal-doc、goal-id)逐字不变。 + 历史记录(v1/v2 用 `--no-onboarding-scan --codex-app-heartbeat ask`, + v3 用 `--accept-onboarding-agent-todos --begin-autonomous-advance + --codex-app-heartbeat yes`)描述的是 LoopX 当时的首连门禁参数。这些参数 + 已在上游删除:bootstrap 不再写入任何首连 onboarding todo,也不再有 + connection validation / heartbeat 选择项。现在两版只剩写权限声明不同, + objective、adapter、goal-doc、goal-id 仍逐字不变。 """ if not _UNGATED: - return "--no-onboarding-scan --codex-app-heartbeat ask" - return ( - # ① 打开 LoopX 自己的首连扫描与候选 todo 提议,并允许自动推进 - "--accept-onboarding-agent-todos --begin-autonomous-advance " - # ② 预授权心跳 + 声明写权限(原来 coordination.write_scope 是空的) - f"--codex-app-heartbeat yes --write-scope {cwd}" - ) + return "" + # 声明写权限(原来 coordination.write_scope 是空的) + return f"--write-scope {cwd}" def _goal_id(self) -> str: """本 trial 的 goal id。 diff --git a/benchmark/swe-marathon/runtime/modes/run_mode.py b/benchmark/swe-marathon/runtime/modes/run_mode.py index 8ff32bb816..5c4f92d0fd 100644 --- a/benchmark/swe-marathon/runtime/modes/run_mode.py +++ b/benchmark/swe-marathon/runtime/modes/run_mode.py @@ -231,7 +231,7 @@ def main(argv: list[str] | None = None) -> int: try: session.bootstrap(objective) # 任务必须作为 todo 进 goal —— 闸门按 todo 选工作,只放进 turn 输入的话 - # 模型会去推进 onboarding todo,跑满预算却零产出且不报错。 + # 模型没有可执行 todo,会跑满预算却零产出且不报错。 session.add_task_todo(task) except SessionError as exc: out["error"] = f"bootstrap 失败: {exc}" diff --git a/benchmark/swe-marathon/runtime/modes/session.py b/benchmark/swe-marathon/runtime/modes/session.py index 95d9d8d17e..03108b272f 100644 --- a/benchmark/swe-marathon/runtime/modes/session.py +++ b/benchmark/swe-marathon/runtime/modes/session.py @@ -100,9 +100,9 @@ def bootstrap(self, objective: str) -> dict[str, Any]: "cannot be used because goal has no coordination.registered_agents list", 而它仍然退出 0,很容易被当成渲染成功。 - 两个 onboarding 开关也不能省。不加的话闸门会一直回 - "operator gate blocks gated delivery",should_run 恒为 false——无人值守 - 环境下没有 operator 去放行,整轮会静默空转出零产物却不报错。 + bootstrap 不再写入任何首连 onboarding todo(user gate / 候选 todo / + connection validation 都已删除),所以接 goal 之后闸门里不会再有 + 需要人工放行的条目,无人值守环境也不会静默空转。 """ boot = self._run([ @@ -110,12 +110,6 @@ def bootstrap(self, objective: str) -> dict[str, Any]: "--project", ".", "--goal-id", self.goal_id, "--objective", objective, - # 把 onboarding 提出的 agent todos 直接写进去,并记录允许自主推进; - # 否则等一个永远不会出现的人工放行。 - "--accept-onboarding-agent-todos", - "--begin-autonomous-advance", - # 不让 bootstrap 去问要不要建 Codex App 心跳自动化:本工作区没有真 App。 - "--codex-app-heartbeat", "no", ]) self._run([ "configure-goal", @@ -128,10 +122,10 @@ def bootstrap(self, objective: str) -> dict[str, Any]: def add_task_todo(self, task_text: str, *, todo_id: str = "wen-task") -> dict[str, Any]: """把任务正文作为一条 P0 agent todo 写进 goal。 - 这一步不能省,也不能只靠 turn/start 的输入。实测过:只把任务放进 turn - 输入、goal 里只有 onboarding todo 时,模型会老老实实按 body 的指示去推进 - **onboarding todo**,900 秒里只建了 .loopx/ 和 .codex/,任务文件一个字没改, - 而且不报错——闸门放行、Goal 活着、收据干净,看起来一切正常。 + 这一步不能省,也不能只靠 turn/start 的输入:闸门是按 todo 选工作的, + 任务不在 todo 里就不会被选中。实测过只把任务放进 turn 输入的情况,900 秒 + 里只建了 .loopx/ 和 .codex/,任务文件一个字没改,而且不报错——闸门放行、 + Goal 活着、收据干净,看起来一切正常。 闸门是按 todo 选工作的,任务不在 todo 里就不会被选中。 """ diff --git a/benchmark/swe-marathon/runtime/turn/loopx_native_codex.py b/benchmark/swe-marathon/runtime/turn/loopx_native_codex.py index 1614cae439..965ea287f4 100644 --- a/benchmark/swe-marathon/runtime/turn/loopx_native_codex.py +++ b/benchmark/swe-marathon/runtime/turn/loopx_native_codex.py @@ -330,8 +330,7 @@ def run(name, args): run("bootstrap", ["bootstrap", "--project", a.project, "--goal-id", a.goal_id, "--objective", "Complete the software engineering task described " - "in the task file and commit the finished work.", - "--no-onboarding-scan"]) + "in the task file and commit the finished work."]) run("configure_goal", ["configure-goal", "--goal-id", a.goal_id, "--registered-agent", a.agent_id, "--execute"]) prompt = run("heartbeat_prompt", From 145607e461de7b0ed2f39ed30cc5c60f010e923c Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 01:47:23 +0800 Subject: [PATCH 4/4] test(connect): replace onboarding smokes with a first-connect contract smoke Delete the two smokes that encoded the removed first-connect onboarding queue (`examples/onboarding-connect-candidates-smoke.py`, `examples/project/onboarding-no-scan-projection-smoke.py`) and guard the new contract from the outside with `examples/project/first-connect-contract-smoke.py`: a real `connect` writes no onboarding field, marker, user todo, or agent todo, a fresh goal reports `should_run=true`/`effective_action=normal_run`, and the caller's own `[P0]` todo becomes the selected `recommended_action`. Update the remaining smokes and tests that passed the removed flags, and let the fixtures that needed a runnable frontier seed their own first agent todo instead of relying on connect to generate one. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- examples/agent-diagnose-packet-smoke.py | 32 +- ...ark-native-goal-installed-profile-smoke.py | 3 - examples/bootstrap-command-pack-smoke.py | 4 +- examples/canary/catalog-planner-smoke.py | 10 +- examples/claude-goalmode-lifecycle-smoke.py | 3 +- ...out-helper-command-modularization-smoke.py | 1 - ...esh-project-onboarding-regression-smoke.py | 42 ++- .../todo-claim-lease-roadmap-smoke.py | 1 - examples/fresh-clone-quickstart-smoke.py | 2 - .../onboarding-connect-candidates-smoke.py | 285 ------------------ .../bootstrap-force-preserve-todos-smoke.py | 3 - .../project/first-connect-contract-smoke.py | 225 ++++++++++++++ .../onboarding-no-scan-projection-smoke.py | 234 -------------- .../shared-goal-authority-e2e/installed.py | 2 +- tests/control_plane/test_goal_handoff_mode.py | 1 - .../test_goal_objective_projection.py | 10 +- .../test_shadow_observable_e2e.py | 2 +- .../test_shadow_writer_boundaries.py | 2 +- .../test_start_goal_compact_projection.py | 2 - .../test_todo_next_action_settlement.py | 24 +- .../test_todo_projection_recovery.py | 2 +- tests/test_windows_install.py | 3 - 22 files changed, 313 insertions(+), 580 deletions(-) delete mode 100644 examples/onboarding-connect-candidates-smoke.py create mode 100644 examples/project/first-connect-contract-smoke.py delete mode 100644 examples/project/onboarding-no-scan-projection-smoke.py diff --git a/examples/agent-diagnose-packet-smoke.py b/examples/agent-diagnose-packet-smoke.py index 4c15da01aa..54ccea1275 100644 --- a/examples/agent-diagnose-packet-smoke.py +++ b/examples/agent-diagnose-packet-smoke.py @@ -147,7 +147,7 @@ def assert_diagnose_markdown_separates_status_and_packet_goal_counts() -> None: assert "contract_warnings_truncated: total=4" in markdown, markdown -def bootstrap_project(project: Path, runtime: Path, goal_id: str, *, onboarding: bool) -> dict: +def bootstrap_project(project: Path, runtime: Path, goal_id: str) -> dict: args = [ "--runtime-root", str(runtime), @@ -164,8 +164,6 @@ def bootstrap_project(project: Path, runtime: Path, goal_id: str, *, onboarding: "diagnose_fixture_v0", "--no-global-sync", ] - if not onboarding: - args.append("--no-onboarding-scan") return run_cli(*args) @@ -316,7 +314,7 @@ def main() -> int: runtime = root / "runtime" ready_project = write_project(root, "ready-project") - bootstrap_project(ready_project, runtime, GOAL_ID, onboarding=False) + bootstrap_project(ready_project, runtime, GOAL_ID) registry = ready_project / ".loopx" / "registry.json" added = run_cli( "--registry", @@ -380,15 +378,37 @@ def main() -> int: gated_project = write_project(root, "gated-project") gated_goal_id = "diagnose-smoke-gated" - bootstrap_project(gated_project, runtime, gated_goal_id, onboarding=True) + bootstrap_project(gated_project, runtime, gated_goal_id) gated_registry = gated_project / ".loopx" / "registry.json" + # ``connect`` no longer seeds an owner gate, so the fixture writes the + # user gate it wants to diagnose by itself. + user_gate_text = "[P1] Confirm the release window before autonomous delivery." + run_cli( + "--registry", + str(gated_registry), + "todo", + "add", + "--goal-id", + gated_goal_id, + "--role", + "user", + "--text", + user_gate_text, + "--task-class", + "user_gate", + "--action-kind", + "owner_decision", + "--global-gate", + "--execute", + ) gated_packet = run_cli("--registry", str(gated_registry), "diagnose", "--goal-id", gated_goal_id) gated_selected = gated_packet["selected"] assert gated_selected["machine_signal"] == "user_or_controller_attention", gated_selected assert gated_selected["todo_evidence"]["user_open_count"] == 1, gated_selected assert gated_selected["quota_signals"]["action_required"] is True, gated_selected assert gated_selected["quota_signals"]["open_count"] == 1, gated_selected - assert "autonomous=yes/no" in str(gated_selected["user_question"]), gated_selected + assert user_gate_text in str(gated_selected["user_question"]), gated_selected + assert gated_selected["recommended_action"] == user_gate_text, gated_selected assert "can_self_drive" not in gated_selected, gated_selected scoped_registry = write_agent_scoped_registry(root, runtime) diff --git a/examples/benchmark-native-goal-installed-profile-smoke.py b/examples/benchmark-native-goal-installed-profile-smoke.py index 99517b284b..ea6e741adb 100755 --- a/examples/benchmark-native-goal-installed-profile-smoke.py +++ b/examples/benchmark-native-goal-installed-profile-smoke.py @@ -166,9 +166,6 @@ def main() -> int: "read_only_project_map_v0", "--adapter-status", "connected-read-only", - "--no-onboarding-scan", - "--codex-app-heartbeat", - "no", "--no-global-sync", ) _run_profile_cli( diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index 52e8a07ccb..fef295914a 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -175,7 +175,6 @@ def test_missing_project_stops_before_mutation() -> None: assert isinstance(next_step, dict) assert next_step["requires_user_confirmation"] is True assert "--dry-run" in str(next_step["dry_run_command"]) - assert "--codex-app-heartbeat ask" in str(next_step["dry_run_command"]) assert "--dry-run" not in str(next_step["after_confirmation_command"]) assert "/loopx-summary-all" not in json.dumps(payload) @@ -223,7 +222,7 @@ def test_goal_text_invocation_plans_ranked_todos_before_activation() -> None: assert "--objective 'Ship the lightweight issue triage workflow'" in str( next_step["connect_command_if_needed"] ) - assert "--no-onboarding-scan" in str(next_step["connect_command_if_needed"]) + assert "--no-onboarding-scan" not in str(next_step["connect_command_if_needed"]) goal_start = payload["goal_start_contract"] assert isinstance(goal_start, dict) @@ -758,7 +757,6 @@ def test_start_goal_guided_derives_display_name_from_goal_text() -> None: "derived-display-goal", "--objective", "修复 scheduler state path 覆盖问题", - "--no-onboarding-scan", "--no-global-sync", ) registry = json.loads( diff --git a/examples/canary/catalog-planner-smoke.py b/examples/canary/catalog-planner-smoke.py index 0c5de567df..e0bac98a8f 100644 --- a/examples/canary/catalog-planner-smoke.py +++ b/examples/canary/catalog-planner-smoke.py @@ -61,7 +61,7 @@ def assert_profiles_come_from_catalog_matrix() -> None: "product-entry-workflows", "cross-runtime-impl-review-demo", "host-command-entry", - "new-user-onboarding-lifecycle", + "first-connect-contract", "runtime-connector-catalog", "frontstage-rollout", "auto-research-demo", @@ -678,16 +678,16 @@ def assert_pr_release_and_refactor_profiles_select() -> None: "loopx/contract.py", ], surfaces=[ - "new user onboarding no-onboarding-scan state projection gap start-goal" + "new user onboarding first connect contract state projection gap start-goal" ], ) onboarding_profiles = { profile["id"]: profile for profile in onboarding_payload["domain_profiles"] } - assert "new-user-onboarding-lifecycle" in onboarding_profiles, onboarding_payload - onboarding_profile = onboarding_profiles["new-user-onboarding-lifecycle"] + assert "first-connect-contract" in onboarding_profiles, onboarding_payload + onboarding_profile = onboarding_profiles["first-connect-contract"] assert [check["command"] for check in onboarding_profile["checks"]] == [ - "python3 examples/project/onboarding-no-scan-projection-smoke.py" + "python3 examples/project/first-connect-contract-smoke.py" ], onboarding_profile assert all(check["tier"] == "default" for check in onboarding_profile["checks"]) assert onboarding_profile["deep_checks_available"] is False, onboarding_profile diff --git a/examples/claude-goalmode-lifecycle-smoke.py b/examples/claude-goalmode-lifecycle-smoke.py index 0b12410276..060e3dd7d1 100644 --- a/examples/claude-goalmode-lifecycle-smoke.py +++ b/examples/claude-goalmode-lifecycle-smoke.py @@ -86,8 +86,7 @@ def test_complete_with_next_todo_uses_registered_agent(): gid = "cc-lifecycle" state_file = f".claude/goals/{gid}/ACTIVE_GOAL_STATE.md" r = loopx(["bootstrap", "--project", str(proj), "--goal-id", gid, - "--objective", "lifecycle smoke", "--state-file", state_file, - "--no-onboarding-scan"], home=home) + "--objective", "lifecycle smoke", "--state-file", state_file], home=home) assert r.returncode == 0, f"bootstrap failed:\n{r.stdout}\n{r.stderr}" registry = str(proj / ".loopx" / "registry.json") # /loopx registers only `cc` (primary + registered) diff --git a/examples/cli-bootstrap-rollout-helper-command-modularization-smoke.py b/examples/cli-bootstrap-rollout-helper-command-modularization-smoke.py index 23f8296f8b..fbf9a43c60 100644 --- a/examples/cli-bootstrap-rollout-helper-command-modularization-smoke.py +++ b/examples/cli-bootstrap-rollout-helper-command-modularization-smoke.py @@ -94,7 +94,6 @@ def assert_help_surfaces() -> None: for option in ( "--fork-goal", "--execution-minimum-scale", - "--accept-onboarding-agent-todos", "--replace-state", "--no-global-sync", ): diff --git a/examples/control_plane/fresh-project-onboarding-regression-smoke.py b/examples/control_plane/fresh-project-onboarding-regression-smoke.py index b5e7c8cc18..b71fa37aed 100644 --- a/examples/control_plane/fresh-project-onboarding-regression-smoke.py +++ b/examples/control_plane/fresh-project-onboarding-regression-smoke.py @@ -6,9 +6,11 @@ so the emitted command is accepted by ``loopx todo add``. - Defect 2: ``agent-onboard`` on a project with no ``.loopx/registry.json`` must return a typed gate, not raise ``FileNotFoundError``. - - Guided takeover: when bootstrap already provides a runnable Todo frontier, + - Guided takeover: when the goal already has a runnable Todo frontier, authoring is projected as a typed Todo delta instead of an - unconditional ``write_ordered_todos`` step. + unconditional ``write_ordered_todos`` step. `connect` no + longer seeds that frontier, so the fixture writes the first + agent Todo the way a real caller now does. See: https://github.com/huangruiteng/loopx/issues/3092 Fix: https://github.com/huangruiteng/loopx/pull/3093 @@ -181,8 +183,6 @@ def test_guided_template_acceptance(project: Path, goal_id: str) -> None: "--objective", "regression test for #3092", "--adapter-kind", "read_only_project_map_v0", "--adapter-status", "connected-read-only", - "--no-onboarding-scan", - "--codex-app-heartbeat", "ask", "--no-global-sync", check=False, ) @@ -207,6 +207,22 @@ def test_guided_template_acceptance(project: Path, goal_id: str) -> None: agent_id in registered_agents, ) + # Step 2b: write the first runnable agent Todo. connect/boot no longer seeds + # one, so the guided takeover frontier has to come from the caller. + seeded_todo = run_cli( + "--registry", str(registry_path), + "todo", "add", + "--goal-id", goal_id, + "--role", "agent", + "--text", "[P0] Implement the first bounded regression segment.", + "--task-class", "advancement_task", + "--action-kind", "implementation", + "--claimed-by", agent_id, + "--execute", + check=False, + ) + check("first agent todo exits 0", seeded_todo.returncode == 0) + # Step 3: Build the guided packet via direct Python import. guided_packet = build_start_goal_guided_packet( project=project, @@ -216,8 +232,8 @@ def test_guided_template_acceptance(project: Path, goal_id: str) -> None: host_surface="claude-code", goal_text="regression test for #3092", ) - # Fresh bootstrap seeds one runnable advancement Todo, so this fixture must - # not accept the frontier-free legacy authoring path. + # The fixture seeds one runnable advancement Todo, so this must not accept + # the frontier-free legacy authoring path. template = _require_guided_todo_delta_template( guided_packet, label="guided packet", @@ -335,14 +351,24 @@ def test_clean_second_run() -> None: "--objective", "second run regression", "--adapter-kind", "read_only_project_map_v0", "--adapter-status", "connected-read-only", - "--no-onboarding-scan", - "--codex-app-heartbeat", "ask", "--no-global-sync", check=False, ) registry_path = project / ".loopx" / "registry.json" if registry_path.exists(): _register_agent_in_registry(registry_path, goal_id, agent_id) + run_cli( + "--registry", str(registry_path), + "todo", "add", + "--goal-id", goal_id, + "--role", "agent", + "--text", "[P0] Implement the second-run bounded segment.", + "--task-class", "advancement_task", + "--action-kind", "implementation", + "--claimed-by", agent_id, + "--execute", + check=False, + ) guided = build_start_goal_guided_packet( project=project, diff --git a/examples/control_plane/todo-claim-lease-roadmap-smoke.py b/examples/control_plane/todo-claim-lease-roadmap-smoke.py index d838e1d3c8..8b90a2dec1 100644 --- a/examples/control_plane/todo-claim-lease-roadmap-smoke.py +++ b/examples/control_plane/todo-claim-lease-roadmap-smoke.py @@ -114,7 +114,6 @@ def main() -> int: "verify soft claim TTL compatibility", "--claim-ttl-minutes", "5", - "--no-onboarding-scan", "--no-global-sync", ], cwd=REPO_ROOT, diff --git a/examples/fresh-clone-quickstart-smoke.py b/examples/fresh-clone-quickstart-smoke.py index e5fe5acc97..eaa008a0bd 100644 --- a/examples/fresh-clone-quickstart-smoke.py +++ b/examples/fresh-clone-quickstart-smoke.py @@ -138,8 +138,6 @@ def main() -> int: "--goal-doc", "README.md", "--no-global-sync", - "--accept-onboarding-agent-todos", - "--begin-autonomous-advance", cwd=project, env=cli_env, ) diff --git a/examples/onboarding-connect-candidates-smoke.py b/examples/onboarding-connect-candidates-smoke.py deleted file mode 100644 index ea7e052bea..0000000000 --- a/examples/onboarding-connect-candidates-smoke.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke-test first-connect onboarding scan and todo candidate projection.""" - -from __future__ import annotations - -import json -import subprocess -import sys -import tempfile -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -if str(REPO_ROOT) not in sys.path: - sys.path.insert(0, str(REPO_ROOT)) - -from loopx.onboarding import ONBOARDING_SCAN_SCHEMA_VERSION # noqa: E402 -from loopx.status import parse_active_state_todos # noqa: E402 - - -def run(*args: str, cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [*args], - cwd=cwd, - check=check, - capture_output=True, - text=True, - ) - - -def run_cli(*args: str) -> dict: - result = subprocess.run( - [sys.executable, "-m", "loopx.cli", "--format", "json", *args], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ) - return json.loads(result.stdout) - - -def make_project(root: Path, name: str) -> Path: - project = root / name - project.mkdir() - (project / "README.md").write_text("# Fixture Project\n\nInitial goal.\n", encoding="utf-8") - (project / "pyproject.toml").write_text( - "[project]\nname = \"fixture-project\"\nversion = \"0.1.0\"\n", - encoding="utf-8", - ) - run("git", "init", cwd=project) - run("git", "add", "README.md", "pyproject.toml", cwd=project) - run( - "git", - "-c", - "user.email=loopx-smoke@example.com", - "-c", - "user.name=LoopX Smoke", - "commit", - "-m", - "Initial fixture project", - cwd=project, - ) - with (project / "README.md").open("a", encoding="utf-8") as f: - f.write("\nPending local change.\n") - return project - - -def state_text(project: Path, goal_id: str) -> str: - return (project / ".codex" / "goals" / goal_id / "ACTIVE_GOAL_STATE.md").read_text(encoding="utf-8") - - -def action_kinds(items: list[dict]) -> set[str]: - return {str(item.get("action_kind") or "") for item in items} - - -def state_updated_at(text: str) -> str: - for line in text.splitlines(): - if line.startswith("updated_at: "): - return line.split(": ", 1)[1].strip() - raise AssertionError(text) - - -def assert_todos_share_state_timestamp(text: str, items: list[dict]) -> None: - expected = state_updated_at(text) - assert items, items - for item in items: - assert item.get("updated_at") == expected, item - - -def assert_default_onboarding(project: Path, runtime: Path) -> None: - goal_id = "onboarding-smoke-default" - payload = run_cli( - "--runtime-root", - str(runtime), - "bootstrap", - "--project", - str(project), - "--goal-id", - goal_id, - "--objective", - "Exercise onboarding candidate projection.", - "--goal-doc", - "README.md", - "--no-global-sync", - ) - assert payload["ok"] is True, payload - scan = payload["onboarding_scan"] - assert scan["schema_version"] == ONBOARDING_SCAN_SCHEMA_VERSION, scan - assert scan["is_git_repo"] is True, scan - assert scan["status_path_count"] >= 1, scan - assert scan["recent_commits"], scan - assert "README.md" in scan["signal_files"], scan - assert any(item["path"] == "pyproject.toml" for item in scan["validation_signal_files"]), scan - assert scan["scan_policy"]["raw_file_bodies_read"] is False, scan - candidates = payload["onboarding_agent_todo_candidates"] - candidate_kinds = {candidate["action_kind"] for candidate in candidates} - assert "repo_status_review" in candidate_kinds, candidates - assert "commit_summary" in candidate_kinds, candidates - assert "validation_plan" in candidate_kinds, candidates - assert payload["onboarding_acceptance_required"] is True, payload - assert payload["autonomous_advance_choice_required"] is True, payload - assert payload["heartbeat_opt_in_required"] is True, payload - assert payload["host_loop_activation_required"] is True, payload - assert "heartbeat=yes" in payload["heartbeat_opt_in_instruction"], payload - assert payload["onboarding_todos_written"] is True, payload - assert len(payload["accept_candidate_commands"]) == len(candidates), payload - - registry_path = project / ".loopx" / "registry.json" - status_payload = run_cli("--registry", str(registry_path), "status") - status_item = status_payload["attention_queue"]["items"][0] - user_todo_title = status_item["user_todos"]["items"][0]["title"] - assert "Choose which proposed onboarding agent todos" in status_item["recommended_action"], status_item - assert "Codex App heartbeat" in status_item["recommended_action"], status_item - assert "heartbeat=yes/no" in user_todo_title, status_item - assert "Codex App heartbeat" in status_item["project_asset"]["next_action"], status_item - assert "Codex App heartbeat" in status_item["active_state_next_action"], status_item - assert status_item["user_todos"]["open_count"] == 1, status_item - assert status_item["agent_todos"]["open_count"] == 1, status_item - - quota_payload = run_cli("--registry", str(registry_path), "quota", "should-run", "--goal-id", goal_id) - assert quota_payload["should_run"] is False, quota_payload - assert quota_payload["effective_action"] == "operator_gate_notify", quota_payload - assert quota_payload["requires_user_action"] is True, quota_payload - assert "Codex App heartbeat" in quota_payload["recommended_action"], quota_payload - assert "heartbeat=yes/no" in quota_payload["gate_prompt"], quota_payload - assert quota_payload["user_todo_summary"]["open_count"] == 1, quota_payload - assert quota_payload["agent_todo_summary"]["open_count"] == 1, quota_payload - - text = state_text(project, goal_id) - assert "## Onboarding Control" in text, text - assert "## Proposed Onboarding Candidates" in text, text - assert "Candidate agent todos: `requires user selection before delivery work`" in text, text - assert "Autonomous advancement: `requires an explicit user yes/no choice`" in text, text - assert ( - "Codex App heartbeat: `requires explicit heartbeat=yes/no before a recurring " - "Codex App automation is installed`" in text - ), text - assert "## User Todo / Owner Review Reading Queue" in text, text - assert "## Agent Todo" in text, text - assert "accepted numbers plus autonomous=yes/no plus heartbeat=yes/no" in text, text - assert "identity-scoped `loopx heartbeat-prompt --thin`" in text, text - - todos = parse_active_state_todos(text) - user_items = todos.get("user_todos", {}).get("items", []) - agent_items = todos["agent_todos"]["items"] - assert len(user_items) == 1, user_items - assert len(agent_items) == 1, agent_items - assert action_kinds(user_items) == {"onboarding_decision"}, user_items - assert action_kinds(agent_items) == {"onboarding_todo_review"}, agent_items - assert_todos_share_state_timestamp(text, user_items + agent_items) - - -def assert_preauthorized_onboarding(project: Path, runtime: Path) -> None: - goal_id = "onboarding-smoke-preauth" - payload = run_cli( - "--runtime-root", - str(runtime), - "bootstrap", - "--project", - str(project), - "--goal-id", - goal_id, - "--objective", - "Exercise preauthorized onboarding candidate projection.", - "--goal-doc", - "README.md", - "--accept-onboarding-agent-todos", - "--begin-autonomous-advance", - "--codex-app-heartbeat", - "yes", - "--no-global-sync", - ) - assert payload["ok"] is True, payload - assert payload["codex_app_heartbeat"] == "yes", payload - assert payload["onboarding_acceptance_required"] is False, payload - assert payload["autonomous_advance_choice_required"] is False, payload - assert payload["heartbeat_opt_in_required"] is False, payload - assert payload["host_loop_activation_required"] is True, payload - assert "preauthorized" in payload["heartbeat_opt_in_instruction"], payload - text = state_text(project, goal_id) - assert "Candidate agent todos: `accepted and written into Agent Todo`" in text, text - assert "Autonomous advancement: `allowed after accepted agent todos and a fresh quota guard`" in text, text - assert "Codex App heartbeat: `explicitly preauthorized" in text, text - assert "Choose which proposed onboarding agent todos" not in text, text - - todos = parse_active_state_todos(text) - user_items = todos.get("user_todos", {}).get("items", []) - agent_items = todos["agent_todos"]["items"] - assert user_items == [], user_items - assert len(agent_items) == len(payload["onboarding_agent_todo_candidates"]), agent_items - kinds = action_kinds(agent_items) - assert "repo_status_review" in kinds, agent_items - assert "commit_summary" in kinds, agent_items - assert "validation_plan" in kinds, agent_items - assert_todos_share_state_timestamp(text, agent_items) - - -def assert_autonomy_preauth_still_requires_heartbeat_choice(project: Path, runtime: Path) -> None: - goal_id = "onboarding-smoke-autonomy-with-heartbeat-gate" - payload = run_cli( - "--runtime-root", - str(runtime), - "bootstrap", - "--project", - str(project), - "--goal-id", - goal_id, - "--objective", - "Exercise autonomous preauthorization without heartbeat preauthorization.", - "--goal-doc", - "README.md", - "--accept-onboarding-agent-todos", - "--begin-autonomous-advance", - "--no-global-sync", - ) - assert payload["ok"] is True, payload - assert payload["codex_app_heartbeat"] == "ask", payload - assert payload["onboarding_acceptance_required"] is False, payload - assert payload["autonomous_advance_choice_required"] is False, payload - assert payload["heartbeat_opt_in_required"] is True, payload - assert payload["host_loop_activation_required"] is True, payload - assert "heartbeat=yes" in payload["heartbeat_opt_in_instruction"], payload - - registry_path = project / ".loopx" / "registry.json" - quota_payload = run_cli("--registry", str(registry_path), "quota", "should-run", "--goal-id", goal_id) - assert quota_payload["effective_action"] == "operator_gate_notify", quota_payload - assert quota_payload["normal_delivery_allowed"] is False, quota_payload - assert quota_payload["requires_user_action"] is True, quota_payload - assert quota_payload["interaction_contract"]["user_channel"]["notify"] == "NOTIFY", quota_payload - assert "heartbeat=yes/no" in quota_payload["gate_prompt"], quota_payload - - text = state_text(project, goal_id) - assert "Candidate agent todos: `accepted and written into Agent Todo`" in text, text - assert "Autonomous advancement: `allowed after accepted agent todos and a fresh quota guard`" in text, text - assert ( - "Codex App heartbeat: `requires explicit heartbeat=yes/no before a recurring " - "Codex App automation is installed`" in text - ), text - assert "reply with heartbeat=yes/no" in text, text - assert "identity-scoped `loopx heartbeat-prompt --thin`" in text, text - - todos = parse_active_state_todos(text) - user_items = todos.get("user_todos", {}).get("items", []) - agent_items = todos["agent_todos"]["items"] - assert len(user_items) == 1, user_items - assert action_kinds(user_items) == {"onboarding_decision"}, user_items - assert "onboarding_todo_review" in action_kinds(agent_items), agent_items - assert_todos_share_state_timestamp(text, user_items + agent_items) - - -def main() -> int: - with tempfile.TemporaryDirectory(prefix="loopx-onboarding-smoke-") as tmp: - root = Path(tmp) - runtime = root / "runtime" - project = make_project(root, "fixture-project") - assert_default_onboarding(project, runtime) - assert_preauthorized_onboarding(project, runtime) - assert_autonomy_preauth_still_requires_heartbeat_choice(project, runtime) - - print("onboarding-connect-candidates-smoke ok") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/project/bootstrap-force-preserve-todos-smoke.py b/examples/project/bootstrap-force-preserve-todos-smoke.py index f9efe3d4e6..d1a464fe86 100644 --- a/examples/project/bootstrap-force-preserve-todos-smoke.py +++ b/examples/project/bootstrap-force-preserve-todos-smoke.py @@ -45,7 +45,6 @@ def main() -> int: GOAL_ID, "--objective", "Exercise force reconnect preservation.", - "--no-onboarding-scan", "--no-global-sync", ) assert initial["ok"] is True, initial @@ -66,7 +65,6 @@ def main() -> int: "src/**", "--force", "--preserve-todos", - "--no-onboarding-scan", "--no-global-sync", ) assert preserved["ok"] is True, preserved @@ -96,7 +94,6 @@ def main() -> int: "--write-scope", "docs/**", "--force", - "--no-onboarding-scan", "--no-global-sync", ) assert replaced["ok"] is True, replaced diff --git a/examples/project/first-connect-contract-smoke.py b/examples/project/first-connect-contract-smoke.py new file mode 100644 index 0000000000..38b2be8c76 --- /dev/null +++ b/examples/project/first-connect-contract-smoke.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Smoke-test the first-connect contract for `loopx connect`. + +`connect` registers the goal and its active state. It must not project a +first-connect onboarding todo, user gate, candidate list, or connection +validation item into the goal, because those items competed with the caller's +own first delivery todo and could park an unattended goal on an operator gate. + +This smoke guards that contract from the outside: it drives the real CLI, +reads the written active state, and checks the gate that the caller sees next. +""" + +from __future__ import annotations + +import json +import subprocess +import sys +import tempfile +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +GENERIC_GOAL_ID = "fresh-generic-connect" +DOMAIN_GOAL_ID = "fresh-domain-connect" +FIRST_TODO_ID = "fresh-first-delivery" +REMOVED_ONBOARDING_MARKERS = ( + "action_kind=onboarding_", + "## Onboarding Control", + "## Proposed Onboarding Candidates", + "## Accept Candidate Commands", +) + + +def run_cli(registry: Path, runtime: Path, *args: str) -> dict: + result = subprocess.run( + [ + sys.executable, + "-m", + "loopx.cli", + "--registry", + str(registry), + "--runtime-root", + str(runtime), + "--format", + "json", + *args, + ], + cwd=REPO_ROOT, + check=True, + text=True, + capture_output=True, + ) + return json.loads(result.stdout) + + +def initialize_project(root: Path, name: str) -> tuple[Path, Path]: + project = root / name + project.mkdir() + subprocess.run(["git", "init", "-q"], cwd=project, check=True) + readme = project / "README.md" + readme.write_text("# Synthetic first-connect fixture\n", encoding="utf-8") + (project / ".gitignore").write_text(".loopx/\n.codex/\n.local/\n", encoding="utf-8") + return project, readme + + +def state_path(project: Path, goal_id: str) -> Path: + return project / ".codex" / "goals" / goal_id / "ACTIVE_GOAL_STATE.md" + + +def assert_no_first_connect_projection(state_text: str, *, label: str) -> None: + for marker in REMOVED_ONBOARDING_MARKERS: + assert marker not in state_text, (label, marker, state_text) + user_section = state_text.split("## User Todo / Owner Review Reading Queue", 1)[1] + user_section = user_section.split("## Agent Todo", 1)[0] + assert "- [ ]" not in user_section, (label, user_section) + agent_section = state_text.split("## Agent Todo", 1)[1].split("## Next Action", 1)[0] + assert "- [ ]" not in agent_section, (label, agent_section) + assert "Initial routing is owned by the connected domain adapter." in state_text, state_text + + +def connect_generic(root: Path, runtime: Path) -> tuple[Path, Path, str]: + project, readme = initialize_project(root, "project") + registry = project / ".loopx" / "registry.json" + connected = run_cli( + registry, + runtime, + "connect", + "--project", + str(project), + "--goal-id", + GENERIC_GOAL_ID, + "--objective", + "Validate the fresh connection contract.", + "--domain", + "engineering", + "--goal-doc", + str(readme), + "--adapter-kind", + "read_only_project_map_v0", + "--adapter-status", + "connected-read-only", + "--no-global-sync", + ) + assert connected["ok"] is True, connected + for removed_field in ( + "onboarding_scan", + "onboarding_agent_todo_candidates", + "onboarding_acceptance_required", + "autonomous_advance_choice_required", + "heartbeat_opt_in_required", + "host_loop_activation_required", + "onboarding_todos_written", + "onboarding_connection_validation", + "accept_candidate_commands", + "codex_app_heartbeat", + ): + assert removed_field not in connected, (removed_field, connected) + state_text = state_path(project, GENERIC_GOAL_ID).read_text(encoding="utf-8") + assert_no_first_connect_projection(state_text, label="generic") + return project, registry, state_text + + +def main() -> int: + with tempfile.TemporaryDirectory(prefix="loopx-first-connect-") as tmp: + root = Path(tmp) + runtime = root / "runtime" + project, registry, state_text = connect_generic(root, runtime) + + mapped = run_cli( + registry, + runtime, + "read-only-map", + "--goal-id", + GENERIC_GOAL_ID, + "--no-global-sync", + ) + assert mapped["ok"] is True, mapped + + healthy_check = run_cli(registry, runtime, "check", "--scan-path", str(project / "README.md")) + assert healthy_check["ok"] is True, healthy_check + assert healthy_check["summary"]["warnings"] == 0, healthy_check + + # A fresh connection is immediately runnable: no operator gate and no + # first-connect todo stands between the caller and its own work. + ungated = run_cli(registry, runtime, "quota", "should-run", "--goal-id", GENERIC_GOAL_ID) + assert ungated["should_run"] is True, ungated + assert ungated["normal_delivery_allowed"] is True, ungated + assert ungated["effective_action"] == "normal_run", ungated + assert "user_gate" not in json.dumps(ungated.get("interaction_contract") or {}), ungated + assert ungated.get("state_projection_gap") is None, ungated + + # The caller's own first delivery todo is what the gate selects, even + # when the state file still carries the connection-time next action. + added = run_cli( + registry, + runtime, + "todo", + "add", + "--goal-id", + GENERIC_GOAL_ID, + "--role", + "agent", + "--todo-id", + FIRST_TODO_ID, + "--text", + "[P0] Land the first bounded delivery segment.", + "--task-class", + "advancement_task", + "--status", + "open", + "--execute", + ) + assert added["ok"] is True, added + gated = run_cli(registry, runtime, "quota", "should-run", "--goal-id", GENERIC_GOAL_ID) + assert gated["should_run"] is True, gated + assert gated["recommended_action"] == "[P0] Land the first bounded delivery segment.", gated + selected_state = state_path(project, GENERIC_GOAL_ID).read_text(encoding="utf-8") + assert "action_kind=onboarding_" not in selected_state, selected_state + assert (selected_state.count("- [ ]")) == state_text.count("- [ ]") + 1, selected_state + + # Domain-owned adapters keep the same contract: no gate, no injected + # first-connect work, and a clean state projection. + domain_project, domain_readme = initialize_project(root, "domain-project") + domain_registry = domain_project / ".loopx" / "registry.json" + domain_connected = run_cli( + domain_registry, + runtime, + "connect", + "--project", + str(domain_project), + "--goal-id", + DOMAIN_GOAL_ID, + "--objective", + "Validate domain-owned first connect.", + "--domain", + "engineering", + "--goal-doc", + str(domain_readme), + "--adapter-kind", + "domain_fixture_v0", + "--adapter-status", + "connected-read-only", + "--no-global-sync", + ) + assert domain_connected["ok"] is True, domain_connected + domain_state_text = state_path(domain_project, DOMAIN_GOAL_ID).read_text(encoding="utf-8") + assert_no_first_connect_projection(domain_state_text, label="domain") + domain_check = run_cli( + domain_registry, + runtime, + "check", + "--scan-path", + str(domain_readme), + ) + assert domain_check["ok"] is True, domain_check + assert domain_check["summary"]["warnings"] == 0, domain_check + domain_goal = json.loads(domain_registry.read_text(encoding="utf-8"))["goals"][0] + assert "connection_validation" not in domain_goal["adapter"], domain_goal + + print("first-connect-contract-smoke ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/project/onboarding-no-scan-projection-smoke.py b/examples/project/onboarding-no-scan-projection-smoke.py deleted file mode 100644 index f8cc1d3795..0000000000 --- a/examples/project/onboarding-no-scan-projection-smoke.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke-test fresh no-scan onboarding state and todo projection parity.""" - -from __future__ import annotations - -import json -import subprocess -import sys -import tempfile -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[2] -GOAL_ID = "fresh-no-scan-projection" -PROVIDER_GOAL_ID = "fresh-provider-prevalidated-projection" -DOMAIN_GOAL_ID = "fresh-domain-owned-projection" - - -def run_cli(registry: Path, runtime: Path, *args: str) -> dict: - result = subprocess.run( - [ - sys.executable, - "-m", - "loopx.cli", - "--registry", - str(registry), - "--runtime-root", - str(runtime), - "--format", - "json", - *args, - ], - cwd=REPO_ROOT, - check=True, - text=True, - capture_output=True, - ) - return json.loads(result.stdout) - - -def initialize_project(root: Path, name: str) -> tuple[Path, Path]: - project = root / name - project.mkdir() - subprocess.run(["git", "init", "-q"], cwd=project, check=True) - readme = project / "README.md" - readme.write_text("# Synthetic LoopX onboarding fixture\n", encoding="utf-8") - (project / ".gitignore").write_text(".loopx/\n.codex/\n.local/\n", encoding="utf-8") - return project, readme - - -def main() -> int: - with tempfile.TemporaryDirectory(prefix="loopx-no-scan-projection-") as tmp: - root = Path(tmp) - project, readme = initialize_project(root, "project") - - registry = project / ".loopx" / "registry.json" - runtime = root / "runtime" - connected = run_cli( - registry, - runtime, - "connect", - "--project", - str(project), - "--goal-id", - GOAL_ID, - "--objective", - "Validate fresh read-only onboarding.", - "--domain", - "engineering", - "--goal-doc", - str(readme), - "--adapter-kind", - "read_only_project_map_v0", - "--adapter-status", - "connected-read-only", - "--codex-app-heartbeat", - "no", - "--no-onboarding-scan", - "--no-global-sync", - ) - assert connected["ok"] is True, connected - - mapped = run_cli( - registry, - runtime, - "read-only-map", - "--goal-id", - GOAL_ID, - "--no-global-sync", - ) - assert mapped["ok"] is True, mapped - - state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" - state_text = state_file.read_text(encoding="utf-8") - assert state_text.count("action_kind=onboarding_connection_validation") == 1, state_text - assert "## Next Action" in state_text, state_text - - healthy_check = run_cli(registry, runtime, "check", "--scan-path", str(readme)) - assert healthy_check["ok"] is True, healthy_check - assert healthy_check["summary"]["warnings"] == 0, healthy_check - - quota = run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID) - assert quota["decision"] == "run", quota - assert quota["normal_delivery_allowed"] is True, quota - assert quota["effective_action"] == "normal_run", quota - assert quota.get("state_projection_gap") is None, quota - - broken_lines = [ - line - for line in state_text.splitlines() - if "action_kind=onboarding_connection_validation" not in line - and not line.startswith("- [ ] [P1] Run `loopx check` against the project registry") - ] - state_file.write_text("\n".join(broken_lines) + "\n", encoding="utf-8") - - broken_check = run_cli(registry, runtime, "check", "--scan-path", str(readme)) - assert broken_check["ok"] is True, broken_check - assert broken_check["summary"]["warnings"] == 1, broken_check - assert any("state_projection_gap" in warning for warning in broken_check["warnings"]), broken_check - - provider_project, provider_readme = initialize_project(root, "provider-project") - provider_registry = provider_project / ".loopx" / "registry.json" - provider_connected = run_cli( - provider_registry, - runtime, - "connect", - "--project", - str(provider_project), - "--goal-id", - PROVIDER_GOAL_ID, - "--objective", - "Validate provider-owned connection onboarding.", - "--domain", - "engineering", - "--goal-doc", - str(provider_readme), - "--adapter-kind", - "read_only_project_map_v0", - "--adapter-status", - "connected-read-only", - "--codex-app-heartbeat", - "no", - "--no-onboarding-scan", - "--onboarding-connection-validation", - "provider-prevalidated", - "--no-global-sync", - ) - assert provider_connected["ok"] is True, provider_connected - assert ( - provider_connected["onboarding_connection_validation"] - == "provider-prevalidated" - ), provider_connected - - provider_state_file = ( - provider_project - / ".codex" - / "goals" - / PROVIDER_GOAL_ID - / "ACTIVE_GOAL_STATE.md" - ) - provider_state_text = provider_state_file.read_text(encoding="utf-8") - assert "action_kind=onboarding_connection_validation" not in provider_state_text - assert "Run `loopx check` against the project registry" not in provider_state_text - provider_goal = json.loads(provider_registry.read_text(encoding="utf-8"))["goals"][0] - assert provider_goal["adapter"]["connection_validation"] == { - "owner": "provider", - "status": "prevalidated", - "agent_todo_required": False, - }, provider_goal - - provider_check = run_cli( - provider_registry, - runtime, - "check", - "--scan-path", - str(provider_readme), - ) - assert provider_check["ok"] is True, provider_check - assert provider_check["summary"]["warnings"] == 0, provider_check - - domain_project, domain_readme = initialize_project(root, "domain-project") - domain_registry = domain_project / ".loopx" / "registry.json" - domain_connected = run_cli( - domain_registry, - runtime, - "connect", - "--project", - str(domain_project), - "--goal-id", - DOMAIN_GOAL_ID, - "--objective", - "Validate domain-owned onboarding routing.", - "--domain", - "engineering", - "--goal-doc", - str(domain_readme), - "--adapter-kind", - "domain_fixture_v0", - "--adapter-status", - "connected-read-only", - "--codex-app-heartbeat", - "no", - "--no-onboarding-scan", - "--no-global-sync", - ) - assert domain_connected["ok"] is True, domain_connected - - domain_state_file = ( - domain_project - / ".codex" - / "goals" - / DOMAIN_GOAL_ID - / "ACTIVE_GOAL_STATE.md" - ) - domain_state_text = domain_state_file.read_text(encoding="utf-8") - assert "action_kind=onboarding_connection_validation" not in domain_state_text - assert "Initial routing is owned by the connected domain adapter." in domain_state_text - - domain_check = run_cli( - domain_registry, - runtime, - "check", - "--scan-path", - str(domain_readme), - ) - assert domain_check["ok"] is True, domain_check - assert domain_check["summary"]["warnings"] == 0, domain_check - - print("onboarding-no-scan-projection-smoke ok") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/shared-goal-authority-e2e/installed.py b/examples/shared-goal-authority-e2e/installed.py index bfca9ac213..1e0f941e43 100644 --- a/examples/shared-goal-authority-e2e/installed.py +++ b/examples/shared-goal-authority-e2e/installed.py @@ -143,7 +143,7 @@ def run(self) -> None: self.cli("console_project_bootstrap", "bootstrap", "--project", str(self.project), "--goal-id", GOAL, "--objective", "Qualify installed authority transactions.", - "--no-onboarding-scan", "--onboarding-connection-validation", "provider-prevalidated", "--no-global-sync") + "--no-global-sync") # Set configuration only, before shadow bootstrap creates the real binding. registry = json.loads(self.registry.read_text()) goal = next(item for item in registry["goals"] if item["id"] == GOAL) diff --git a/tests/control_plane/test_goal_handoff_mode.py b/tests/control_plane/test_goal_handoff_mode.py index aeefc5358a..a2aa064f17 100644 --- a/tests/control_plane/test_goal_handoff_mode.py +++ b/tests/control_plane/test_goal_handoff_mode.py @@ -1819,7 +1819,6 @@ def _force_bootstrap( max_children=0, allowed_domains=None, write_scope=None, - onboarding_scan_enabled=False, preserve_todos=False, force=True, dry_run=False, diff --git a/tests/control_plane/test_goal_objective_projection.py b/tests/control_plane/test_goal_objective_projection.py index 40caad24e4..beecd5e7ad 100644 --- a/tests/control_plane/test_goal_objective_projection.py +++ b/tests/control_plane/test_goal_objective_projection.py @@ -76,9 +76,11 @@ def test_bootstrap_keeps_objective_separate_from_todo_sources( } assert items["user"] == [] assert archive == [] - assert len(items["agent"]) == 1 - assert items["agent"][0]["action_kind"] == "onboarding_connection_validation" - assert active_state_next_action_entries(state_text) == [items["agent"][0]["text"]] + assert items["agent"] == [] + assert active_state_next_action_entries(state_text) == [ + "Initial routing is owned by the connected domain adapter." + ] + assert "action_kind=onboarding_" not in state_text assert active_state_section_text(state_text, "Objective") == " ".join(objective.split()) objective_line = next(line for line in state_text.splitlines() if line.startswith("objective: ")) assert json.loads(objective_line.removeprefix("objective: ")) == objective @@ -113,7 +115,7 @@ def test_start_goal_bootstrap_todo_and_chat_readback(tmp_path, capsys, monkeypat assert main(["--format", "json", "--registry", str(registry_path), "todo", "list", "--goal-id", GOAL_ID]) == 0 todos = json.loads(capsys.readouterr().out) - assert todos["agent_todos"]["open_count"] == 1 + assert todos["agent_todos"]["open_count"] == 0 registry = json.loads(registry_path.read_text()) goal = next(goal for goal in registry["goals"] if goal["id"] == GOAL_ID) assert _goal_public_context(registry, goal)["objective"] == " ".join(objective.split())[:600] diff --git a/tests/control_plane/test_shadow_observable_e2e.py b/tests/control_plane/test_shadow_observable_e2e.py index b0f16fc8fb..9f010f3bcf 100644 --- a/tests/control_plane/test_shadow_observable_e2e.py +++ b/tests/control_plane/test_shadow_observable_e2e.py @@ -214,7 +214,7 @@ def test_bootstrap_replacement_preserves_existing_authority(caller: Caller, repl w = caller w.add('Existing canonical state') args = ('bootstrap', '--project', str(w.path), '--state-file', 'STATE.md', - '--objective', 'Replacement objective', '--no-onboarding-scan', '--no-global-sync') + '--objective', 'Replacement objective', '--no-global-sync') assert w.call(*args, '--dry-run')['ok'] is True if replacement == 'missing': w.state.unlink() diff --git a/tests/control_plane/test_shadow_writer_boundaries.py b/tests/control_plane/test_shadow_writer_boundaries.py index 294da6027d..bfdc48b11e 100644 --- a/tests/control_plane/test_shadow_writer_boundaries.py +++ b/tests/control_plane/test_shadow_writer_boundaries.py @@ -201,7 +201,7 @@ def test_force_bootstrap_cannot_erase_an_active_shadow_binding(tmp_path: Path, p goal_id=GOAL, objective="Rebuild safely.", domain="test", role="primary", parent_goal_id=None, state_file=state, goal_doc=None, adapter_kind="generic_project_goal_v0", adapter_status="connected", next_probe=None, spawn_allowed=False, max_children=0, - allowed_domains=[], write_scope=[], onboarding_scan_enabled=False, + allowed_domains=[], write_scope=[], force=True, preserve_todos=preserve, dry_run=False, sync_global=False) assert (registry.read_bytes(), state.read_bytes()) == before diff --git a/tests/control_plane/test_start_goal_compact_projection.py b/tests/control_plane/test_start_goal_compact_projection.py index 4b36f896ee..8bea202e50 100644 --- a/tests/control_plane/test_start_goal_compact_projection.py +++ b/tests/control_plane/test_start_goal_compact_projection.py @@ -1933,8 +1933,6 @@ def test_ark_managed_agent_plans_todos_before_one_shot_goal_activation( f" --objective {shlex.quote(GOAL_TEXT)}" " --adapter-kind read_only_project_map_v0" " --adapter-status connected-read-only" - " --no-onboarding-scan" - " --codex-app-heartbeat ask" ) assert actionable_connect_command in payload["message"] assert "preview the issue-fix route before todo writeback" not in payload["message"] diff --git a/tests/control_plane/test_todo_next_action_settlement.py b/tests/control_plane/test_todo_next_action_settlement.py index 744fb3726f..d16ad26750 100644 --- a/tests/control_plane/test_todo_next_action_settlement.py +++ b/tests/control_plane/test_todo_next_action_settlement.py @@ -89,7 +89,7 @@ def _write_fixture(tmp_path: Path, *, next_action: str) -> tuple[Path, Path]: return registry, state -def test_bootstrap_binds_generated_connection_validation_next_action( +def test_bootstrap_renders_no_first_connect_todo_or_next_action_binding( tmp_path: Path, ) -> None: state_text = render_state_markdown( @@ -102,13 +102,10 @@ def test_bootstrap_binds_generated_connection_validation_next_action( execution_profile=None, ) - assert "" in state_text + assert "loopx:next-action" not in state_text + assert f"todo_id={added['todo_id']}" in state_text def test_agent_todo_add_preserves_same_priority_and_manual_next_actions( @@ -378,7 +376,7 @@ def test_complete_reprojects_typed_next_action_to_open_successor( role="agent", text=completed_text, task_class="advancement_task", - action_kind="onboarding_connection_validation", + action_kind="project_connection_review", claimed_by=AGENT_ID, ) successor = add_goal_todo( @@ -524,7 +522,7 @@ def test_complete_preserves_unrelated_owner_next_action(tmp_path: Path) -> None: role="agent", text="[P1] Validate the project connection.", task_class="advancement_task", - action_kind="onboarding_connection_validation", + action_kind="project_connection_review", claimed_by=AGENT_ID, ) @@ -596,7 +594,7 @@ def test_complete_migrates_legacy_exact_text_next_action(tmp_path: Path) -> None role="agent", text=completed_text, task_class="advancement_task", - action_kind="onboarding_connection_validation", + action_kind="project_connection_review", claimed_by=AGENT_ID, ) diff --git a/tests/control_plane/test_todo_projection_recovery.py b/tests/control_plane/test_todo_projection_recovery.py index 73441e902d..fc7a81c7ec 100644 --- a/tests/control_plane/test_todo_projection_recovery.py +++ b/tests/control_plane/test_todo_projection_recovery.py @@ -337,7 +337,7 @@ def test_objective_display_never_changes_canonical_authority(canonical_display, state.write_text(render_state_markdown( project=state.parent, goal_id="goal-a", adapter_kind="read_only_project_map_v0", objective=objective, updated_at="2026-09-15T00:00:00Z", - goal_doc=None, execution_profile=None, include_connection_validation=False, + goal_doc=None, execution_profile=None, )) code, delivered = _run(registry, before["provider_revision"], "--execute") assert code == 0 and delivered["status"] == "delivered", delivered diff --git a/tests/test_windows_install.py b/tests/test_windows_install.py index 14cef33250..3cb1ddbf9a 100644 --- a/tests/test_windows_install.py +++ b/tests/test_windows_install.py @@ -142,10 +142,7 @@ def test_windows_installer_promotes_release_and_runs_doctor(tmp_path: Path) -> N "windows-probe", "--objective", "Verify the native Windows PowerShell lifecycle", - "--no-onboarding-scan", "--no-global-sync", - "--codex-app-heartbeat", - "no", ], env=launch_env, )