Coverage for cli / commands / autopilot_cmd.py: 100.00%

313 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Autopilot command: one command from a plain terminal to a working agent.""" 

2 

3import json 

4import sys 

5import tomllib 

6from pathlib import Path 

7from typing import Any 

8 

9import click 

10 

11from ..autopilot import ( 

12 CLAUDE_CODE_PACKAGE, 

13 CLAUDE_CODE_VERSION, 

14 CODEX_BEDROCK_PROVIDER, 

15 CODEX_PACKAGE, 

16 CODEX_VERSION, 

17 AutopilotEngine, 

18 build_claude_env, 

19 build_codex_config_toml, 

20 build_codex_env, 

21 build_codex_launch_argv, 

22 build_codex_owned_args, 

23 build_launch_argv, 

24 build_mcp_config, 

25 build_plugin_args, 

26 claude_install_command, 

27 codex_config_path, 

28 codex_install_command, 

29 config_path, 

30 effective_aws_region, 

31 exec_claude, 

32 exec_codex, 

33 find_claude_binary, 

34 find_codex_binary, 

35 has_resumable_session, 

36 install_claude_code, 

37 install_codex, 

38 plugin_paths_requested, 

39 resolve_codex_model, 

40 resolve_codex_reasoning_effort, 

41 resolve_engine, 

42 resolve_mcp_flags, 

43 resolve_model, 

44 resolve_plugin_paths, 

45 resolve_small_fast_model, 

46 stage_codex_skills, 

47 stage_imports, 

48 validate_imports, 

49 write_codex_config, 

50 write_mcp_config, 

51) 

52from ..config import GCOConfig 

53from ..output import confirm, emit_structured_document, get_output_formatter 

54 

55# <pyflowchart-code-diagram> BEGIN - auto-inserted, do not edit 

56# Generated at (UTC): 2026-09-08T04:02:10Z 

57# Generated from Git commit: 90f6f6b1fc98467cbe695cbef78b92ccfc8ee8c4 

58# Flowchart(s) generated from this file: 

59# * ``_plan`` -> ``diagrams/code_diagrams/cli/commands/autopilot_cmd._plan.html`` 

60# (PNG: ``diagrams/code_diagrams/cli/commands/autopilot_cmd._plan.png``) 

61# Regenerate with ``SOURCE_DATE_EPOCH=<unix-seconds> GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/generate.py --code-only``. 

62# <pyflowchart-code-diagram> END 

63 

64 

65#: claude's own session-resumption flags. When one of these appears in the 

66#: passthrough args (after ``--``), the caller has already made a resume 

67#: choice and autopilot neither prompts nor injects its own flags. 

68_CLAUDE_RESUME_FLAGS = frozenset({"-c", "--continue", "-r", "--resume"}) 

69 

70pass_config = click.make_pass_decorator(GCOConfig, ensure=True) 

71 

72 

73def _stdin_is_interactive() -> bool: 

74 """Whether a human is on the other end (gates the resume prompt).""" 

75 return sys.stdin.isatty() 

76 

77 

78def _parse_mcp_env(pairs: tuple[str, ...]) -> dict[str, str]: 

79 """Parse ``--mcp-env KEY=VALUE`` pairs, rejecting malformed input.""" 

80 env: dict[str, str] = {} 

81 for pair in pairs: 

82 key, separator, value = pair.partition("=") 

83 if not separator or not key.strip(): 

84 raise ValueError(f"--mcp-env expects KEY=VALUE, got {pair!r}.") 

85 env[key.strip()] = value 

86 return env 

87 

88 

89def _plan( 

90 config: Any, 

91 model: str | None, 

92 small_fast_model: str | None, 

93 companions: bool, 

94 enable: tuple[str, ...] = (), 

95 mcp_env: tuple[str, ...] = (), 

96 plugins: tuple[str, ...] = (), 

97 skills: tuple[str, ...] = (), 

98 agents: tuple[str, ...] = (), 

99 engine: str | AutopilotEngine | None = None, 

100) -> tuple[dict[str, Any], list[str]]: 

101 """Resolve an engine launch plan without installing or writing files.""" 

102 resolved_engine = resolve_engine(engine) 

103 gco_mcp_env = resolve_mcp_flags(enable) 

104 gco_mcp_env.update(_parse_mcp_env(mcp_env)) 

105 workspace = Path.cwd() 

106 mcp_config = build_mcp_config( 

107 workspace, 

108 include_companions=companions, 

109 gco_mcp_env=gco_mcp_env, 

110 ) 

111 resolved_region = effective_aws_region(config.default_region) 

112 

113 if resolved_engine is AutopilotEngine.CODEX: 

114 resolved_small = resolve_small_fast_model(small_fast_model) 

115 if resolved_small is not None: 

116 raise ValueError( 

117 "--small-fast-model and GCO_AUTOPILOT_SMALL_FAST_MODEL are " 

118 "supported only by the claude-code engine" 

119 ) 

120 if plugin_paths_requested(plugins): 

121 raise ValueError( 

122 "--plugin and GCO_AUTOPILOT_PLUGIN_DIRS are Claude Code plugin " 

123 "inputs and are not supported by the codex engine" 

124 ) 

125 plugin_paths: list[Path] = [] 

126 if agents: 

127 raise ValueError( 

128 "--agents imports Claude Code agent files and is not supported by the codex engine" 

129 ) 

130 validate_imports(skills, ()) 

131 resolved_model, warnings = resolve_codex_model(model) 

132 reasoning_effort = resolve_codex_reasoning_effort(model) 

133 codex_config = build_codex_config_toml( 

134 mcp_config, 

135 model=resolved_model, 

136 region=resolved_region, 

137 reasoning_effort=reasoning_effort, 

138 ) 

139 binary = find_codex_binary() 

140 pin = f"{CODEX_PACKAGE}@{CODEX_VERSION}" 

141 install_command = codex_install_command() 

142 config_file = codex_config_path() 

143 display_name = "Codex" 

144 resumable = False 

145 else: 

146 resolved_model, warnings = resolve_model(model) 

147 resolved_small = resolve_small_fast_model(small_fast_model) 

148 plugin_paths = resolve_plugin_paths(plugins) 

149 validate_imports(skills, agents) 

150 reasoning_effort = None 

151 codex_config = None 

152 binary = find_claude_binary() 

153 pin = f"{CLAUDE_CODE_PACKAGE}@{CLAUDE_CODE_VERSION}" 

154 install_command = claude_install_command() 

155 config_file = config_path() 

156 display_name = "Claude Code" 

157 resumable = has_resumable_session(workspace) 

158 

159 plan = { 

160 "engine": resolved_engine.value, 

161 "engine_display_name": display_name, 

162 "engine_binary": binary, 

163 "engine_pin": pin, 

164 "model": resolved_model, 

165 "small_fast_model": resolved_small, 

166 "reasoning_effort": reasoning_effort, 

167 "region": resolved_region, 

168 "workspace": str(workspace), 

169 "mcp_config_path": str(config_file), 

170 "mcp_servers": sorted(mcp_config["mcpServers"]), 

171 "gco_mcp_env": dict(sorted(gco_mcp_env.items())), 

172 "plugins": [str(path) for path in plugin_paths], 

173 "import_skills": [str(Path(item).expanduser()) for item in skills], 

174 "import_agents": [str(Path(item).expanduser()) for item in agents], 

175 "claude_binary": binary if resolved_engine is AutopilotEngine.CLAUDE_CODE else None, 

176 "claude_code_pin": (pin if resolved_engine is AutopilotEngine.CLAUDE_CODE else None), 

177 "codex_binary": binary if resolved_engine is AutopilotEngine.CODEX else None, 

178 "codex_pin": pin if resolved_engine is AutopilotEngine.CODEX else None, 

179 "install_command": " ".join(install_command), 

180 "resumable_session": resumable, 

181 "mcp_config": mcp_config, 

182 "codex_config": codex_config, 

183 } 

184 return plan, warnings 

185 

186 

187def _resolve_resume_args( 

188 plan: dict[str, Any], 

189 continue_session: bool, 

190 resume: str | None, 

191 engine_args: tuple[str, ...], 

192 yes: bool, 

193) -> tuple[str, ...]: 

194 """Decide which claude resume flags this launch carries. 

195 

196 Explicit wins: ``--continue`` / ``--resume`` (ours, or claude's own in 

197 the passthrough args) are honored as given. Otherwise, when Claude Code 

198 already has a session for this workspace and we're on an interactive 

199 terminal, offer to pick it up — one keypress instead of retyping 

200 context. The prompt is skipped for ``--yes`` and non-TTY runs so 

201 scripted invocations never hang, and a fresh session stays the default. 

202 """ 

203 if continue_session: 

204 return ("--continue",) 

205 if resume is not None: 

206 return ("--resume",) if resume == "" else ("--resume", resume) 

207 if _CLAUDE_RESUME_FLAGS & set(engine_args): 

208 return () 

209 if ( 

210 not yes 

211 and plan["resumable_session"] 

212 and _stdin_is_interactive() 

213 and confirm( 

214 "Resume your previous Claude Code session in this workspace?", 

215 default=False, 

216 ) 

217 ): 

218 return ("--continue",) 

219 return () 

220 

221 

222def _resolve_codex_resume_args( 

223 continue_session: bool, 

224 resume: str | None, 

225) -> tuple[str, ...]: 

226 """Map generic resume options onto Codex's resume subcommand.""" 

227 if continue_session: 

228 return ("resume", "--last") 

229 if resume is not None: 

230 if resume == "": 

231 return ("resume",) 

232 if resume.startswith("-"): 

233 return ("resume", "--", resume) 

234 return ("resume", resume) 

235 return () 

236 

237 

238def _validate_codex_engine_args(engine_args: tuple[str, ...]) -> None: 

239 """Reject native overrides that would invalidate Autopilot's launch plan. 

240 

241 GCO owns the Bedrock provider/model, project-layer isolation, update policy, 

242 and generated MCP process definitions. Native config may still tighten the 

243 GCO server to the two documentation tools used by the reviewed live demo. 

244 """ 

245 direct_overrides = { 

246 "-m", 

247 "--model", 

248 "-p", 

249 "--profile", 

250 "--oss", 

251 "--local-provider", 

252 "-C", 

253 "--cd", 

254 "--remote", 

255 "--remote-auth-token-env", 

256 "update", 

257 } 

258 direct_prefixes = ( 

259 "--model=", 

260 "--profile=", 

261 "--local-provider=", 

262 "--cd=", 

263 "--remote=", 

264 "--remote-auth-token-env=", 

265 ) 

266 config_options = {"-c", "--config"} 

267 reserved_config_roots = { 

268 "check_for_update_on_startup", 

269 "mcp_servers", 

270 "model", 

271 "model_provider", 

272 "model_providers", 

273 "model_reasoning_effort", 

274 "profile", 

275 "profiles", 

276 "project_root_markers", 

277 "projects", 

278 } 

279 recorder_tools = {"find_docs", "read_resource"} 

280 

281 def config_root(assignment: str) -> str | None: 

282 key_expression, separator, _value = assignment.partition("=") 

283 if not separator: 

284 return None 

285 try: 

286 parsed = tomllib.loads(f"{key_expression}=0") 

287 except tomllib.TOMLDecodeError: 

288 # Invalid TOML will fail in Codex itself, but retain a conservative 

289 # lexical fallback so malformed quoting cannot bypass an owned root. 

290 normalized = key_expression.strip().lstrip("\"'") 

291 return normalized.split(".", 1)[0].strip().rstrip("\"'") or None 

292 return next(iter(parsed), None) 

293 

294 def is_safe_gco_tool_narrowing(assignment: str) -> bool: 

295 """Allow only the recorder's fail-closed GCO documentation overlay.""" 

296 try: 

297 parsed = tomllib.loads(assignment) 

298 except tomllib.TOMLDecodeError: 

299 return False 

300 if set(parsed) != {"mcp_servers"}: 

301 return False 

302 servers = parsed["mcp_servers"] 

303 if not isinstance(servers, dict) or set(servers) != {"gco"}: 

304 return False 

305 gco = servers["gco"] 

306 if not isinstance(gco, dict) or not set(gco) <= { 

307 "enabled_tools", 

308 "required", 

309 "tools", 

310 }: 

311 return False 

312 if "required" in gco and gco["required"] is not True: 

313 return False 

314 if "enabled_tools" in gco: 

315 enabled = gco["enabled_tools"] 

316 if ( 

317 not isinstance(enabled, list) 

318 or not enabled 

319 or not all(isinstance(tool, str) for tool in enabled) 

320 or not set(enabled) <= recorder_tools 

321 ): 

322 return False 

323 if "tools" in gco: 

324 tools = gco["tools"] 

325 if not isinstance(tools, dict) or not set(tools) <= recorder_tools: 

326 return False 

327 for policy in tools.values(): 

328 if not isinstance(policy, dict) or policy != {"approval_mode": "approve"}: 

329 return False 

330 return True 

331 

332 index = 0 

333 while index < len(engine_args): 

334 argument = engine_args[index] 

335 if argument == "--": 

336 # Codex treats its own separator as the end of native options; the 

337 # remaining tokens are prompt text and must not be option-scanned. 

338 break 

339 

340 attached_short_override = ( 

341 (argument.startswith("-m") and argument != "-m") 

342 or (argument.startswith("-p") and argument != "-p") 

343 or (argument.startswith("-C") and argument != "-C") 

344 ) and not argument.startswith("--") 

345 if ( 

346 argument in direct_overrides 

347 or argument.startswith(direct_prefixes) 

348 or attached_short_override 

349 ): 

350 raise ValueError( 

351 f"Codex passthrough option {argument!r} would override Autopilot's " 

352 "isolated Bedrock launch plan. Use the top-level `gco autopilot " 

353 "--model` option for model overrides; alternate projects, " 

354 "profiles, providers, remote sessions, and in-place updates are " 

355 "incompatible with this engine." 

356 ) 

357 

358 assignment: str | None = None 

359 option = argument 

360 if argument in config_options: 

361 if index + 1 < len(engine_args): 

362 assignment = engine_args[index + 1] 

363 index += 1 

364 elif argument.startswith("--config="): 

365 assignment = argument.removeprefix("--config=") 

366 option = "--config" 

367 elif argument.startswith("-c") and argument != "-c": 

368 assignment = argument[2:].removeprefix("=") 

369 option = "-c" 

370 

371 root = config_root(assignment) if assignment is not None else None 

372 if ( 

373 root == "mcp_servers" 

374 and assignment is not None 

375 and is_safe_gco_tool_narrowing(assignment) 

376 ): 

377 index += 1 

378 continue 

379 if root in reserved_config_roots and assignment is not None: 

380 key = assignment.partition("=")[0] 

381 raise ValueError( 

382 f"Codex passthrough option {option!r} cannot override {key!r}; " 

383 "that setting is owned by Autopilot's isolated Bedrock plan. " 

384 "Use top-level `--model` or context.bedrock.codex in cdk.json." 

385 ) 

386 index += 1 

387 

388 

389def _print_dry_run(formatter: Any, plan: dict[str, Any]) -> None: 

390 """Render the launch plan as the table-format summary.""" 

391 print() 

392 print(" GCO Autopilot — launch plan") 

393 print(" " + "-" * 68) 

394 print(f" Engine: {plan['engine_display_name']}") 

395 print(f" Model (Bedrock): {plan['model']}") 

396 if plan["reasoning_effort"]: 

397 print(f" Reasoning effort: {plan['reasoning_effort']}") 

398 if plan["small_fast_model"]: 

399 print(f" Fast model: {plan['small_fast_model']}") 

400 print(f" AWS region: {plan['region']}") 

401 print(f" Workspace: {plan['workspace']}") 

402 isolation = "--strict-mcp-config" if plan["engine"] == "claude-code" else "isolated CODEX_HOME" 

403 print(f" MCP config: {plan['mcp_config_path']} ({isolation})") 

404 print(f" MCP servers ({len(plan['mcp_servers'])}): " + ", ".join(plan["mcp_servers"])) 

405 if plan["gco_mcp_env"]: 

406 rendered = ", ".join(f"{k}={v}" for k, v in plan["gco_mcp_env"].items()) 

407 print(f" GCO MCP env: {rendered}") 

408 else: 

409 print(" GCO MCP env: (none — default read-only toolset)") 

410 if plan["plugins"]: 

411 print(f" Plugins: {', '.join(plan['plugins'])}") 

412 if plan["engine"] == AutopilotEngine.CODEX.value and plan["import_skills"]: 

413 skills = ", ".join(plan["import_skills"]) 

414 print(f" Skills: {skills} (copied into isolated CODEX_HOME)") 

415 else: 

416 imports = [f"skills:{path}" for path in plan["import_skills"]] + [ 

417 f"agents:{path}" for path in plan["import_agents"] 

418 ] 

419 if imports: 

420 print(f" Imports: {', '.join(imports)} (staged as a session plugin)") 

421 if plan["engine_binary"]: 

422 print(f" {plan['engine_display_name']}: {plan['engine_binary']}") 

423 else: 

424 print( 

425 f" {plan['engine_display_name']}: not installed — will offer: " 

426 f"{plan['install_command']}" 

427 ) 

428 if plan["resumable_session"]: 

429 print(" Previous session: found — launch will offer to resume (or pass --continue)") 

430 elif plan["engine"] == "claude-code": 

431 print(" Previous session: none for this workspace") 

432 else: 

433 print(" Previous session: use --continue or --resume to reopen a Codex session") 

434 print(" " + "-" * 68) 

435 print(" Dry run only — nothing was written or launched.") 

436 print() 

437 

438 

439@click.command("autopilot") 

440@click.option( 

441 "--engine", 

442 type=click.Choice([engine.value for engine in AutopilotEngine], case_sensitive=False), 

443 default=None, 

444 help=( 

445 "Agent runtime (default: claude-code; env override: GCO_AUTOPILOT_ENGINE). " 

446 "Codex uses Amazon Bedrock and an isolated CODEX_HOME." 

447 ), 

448) 

449@click.option( 

450 "--model", 

451 "-m", 

452 default=None, 

453 help=( 

454 "Bedrock model or inference-profile id for the selected engine. " 

455 "Defaults: context.bedrock.claude_code_default_model_id or " 

456 "context.bedrock.codex_default_model_id. Codex env override: " 

457 "GCO_AUTOPILOT_CODEX_MODEL; shared fallback: GCO_AUTOPILOT_MODEL." 

458 ), 

459) 

460@click.option( 

461 "--small-fast-model", 

462 default=None, 

463 help=( 

464 "Optional Bedrock model for Claude Code's background/fast tasks " 

465 "(env override: GCO_AUTOPILOT_SMALL_FAST_MODEL; unset by default). " 

466 "Claude-only; Codex rejects fast-model configuration." 

467 ), 

468) 

469@click.option( 

470 "--companions/--no-companions", 

471 "companions", 

472 default=True, 

473 help="Include the recommended companion MCP servers (default: yes).", 

474) 

475@click.option( 

476 "--enable", 

477 "-e", 

478 "enable", 

479 multiple=True, 

480 metavar="FLAG", 

481 help=( 

482 "Enable a GCO MCP feature flag for the session (repeatable). " 

483 "Accepts the short form (mission, all-tools, infrastructure-deploy) " 

484 "or the full GCO_ENABLE_* name; unknown flags fail with the valid list." 

485 ), 

486) 

487@click.option( 

488 "--mcp-env", 

489 "mcp_env", 

490 multiple=True, 

491 metavar="KEY=VALUE", 

492 help=( 

493 "Set an arbitrary environment variable on the GCO MCP server " 

494 "(repeatable), e.g. GCO_MCP_TOOL_SEARCH=bm25. Wins over --enable " 

495 "for the same key." 

496 ), 

497) 

498@click.option( 

499 "--plugin", 

500 "plugins", 

501 multiple=True, 

502 metavar="PATH", 

503 help=( 

504 "Load a Claude Code plugin directory or .zip into the session " 

505 "(repeatable; env: GCO_AUTOPILOT_PLUGIN_DIRS, colon-separated). " 

506 "Claude-only; Codex rejects plugins." 

507 ), 

508) 

509@click.option( 

510 "--skills", 

511 "skills", 

512 multiple=True, 

513 metavar="DIR", 

514 help=( 

515 "Import a directory of skills (one subdirectory per skill, each " 

516 "with a SKILL.md) into the session (repeatable). Claude stages a " 

517 "session plugin; Codex copies skills into GCO's isolated CODEX_HOME." 

518 ), 

519) 

520@click.option( 

521 "--agents", 

522 "agents", 

523 multiple=True, 

524 metavar="DIR", 

525 help=( 

526 "Import a directory of Claude Code agent files (*.md subagent " 

527 "definitions) into the session (repeatable). Claude-only; Codex " 

528 "rejects agent imports." 

529 ), 

530) 

531@click.option( 

532 "--continue", 

533 "-c", 

534 "continue_session", 

535 is_flag=True, 

536 help="Resume the most recent session for the selected engine and workspace.", 

537) 

538@click.option( 

539 "--resume", 

540 "resume", 

541 is_flag=False, 

542 flag_value="", 

543 default=None, 

544 metavar="[SESSION_ID]", 

545 help=( 

546 "Resume a specific session by id, or open the selected engine's " 

547 "interactive session picker when no id is given." 

548 ), 

549) 

550@click.option( 

551 "--dry-run", 

552 is_flag=True, 

553 help="Show the resolved launch plan without installing, writing, or launching.", 

554) 

555@click.option( 

556 "--print-config", 

557 "print_config", 

558 is_flag=True, 

559 help="Print the selected engine's generated MCP/agent config and exit.", 

560) 

561@click.option("--yes", "-y", is_flag=True, help="Install the selected engine without prompting.") 

562@click.argument("engine_args", nargs=-1, type=click.UNPROCESSED) 

563@pass_config 

564def autopilot( 

565 config: Any, 

566 engine: Any, 

567 model: Any, 

568 small_fast_model: Any, 

569 companions: Any, 

570 enable: Any, 

571 mcp_env: Any, 

572 plugins: Any, 

573 skills: Any, 

574 agents: Any, 

575 continue_session: Any, 

576 resume: Any, 

577 dry_run: Any, 

578 print_config: Any, 

579 yes: Any, 

580 engine_args: Any, 

581) -> None: 

582 """Launch a fully configured Claude Code or Codex session for GCO. 

583 

584 Claude Code remains the default engine. Select Codex with ``--engine 

585 codex`` or ``GCO_AUTOPILOT_ENGINE=codex``. Both engines use Amazon 

586 Bedrock through your AWS credentials and receive the GCO MCP server plus 

587 the recommended companion servers. 

588 

589 Each engine has an independent model default in ``cdk.json`` and supports 

590 ``--model`` / environment overrides. Codex uses GCO's isolated 

591 ``~/.gco/autopilot/codex`` home and official Amazon Bedrock provider 

592 configuration; Claude preserves its JSON config and strict MCP mode. 

593 

594 If the selected CLI is absent, Autopilot offers to install its exact npm 

595 pin. Arguments after ``--`` pass through unchanged to that CLI. 

596 ``--continue`` and ``--resume`` map to the selected engine's native 

597 resume syntax; Claude also keeps its interactive previous-session prompt. 

598 

599 GCO MCP feature flags gate opt-in tool groups. By default the session gets 

600 the read-only toolset; pass ``--enable`` per flag or ``--enable all-tools``. 

601 ``--mcp-env`` sets any other GCO server variable. 

602 

603 ``--skills`` works with either engine. ``--plugin`` and ``--agents`` are 

604 Claude-only because they use Claude Code plugin and agent formats. 

605 

606 \b 

607 Examples: 

608 gco autopilot 

609 gco autopilot --engine codex 

610 GCO_AUTOPILOT_ENGINE=codex gco autopilot --continue 

611 gco autopilot --resume 

612 gco autopilot -e mission -e infrastructure-deploy 

613 gco autopilot -e all-tools 

614 gco autopilot --mcp-env GCO_MCP_TOOL_SEARCH=bm25 

615 gco autopilot --skills ~/team-skills 

616 gco autopilot --skills ~/team-skills --agents ~/my-agents 

617 gco autopilot --plugin ~/plugins/incident-response 

618 gco autopilot -m global.anthropic.claude-sonnet-4-6 

619 gco autopilot --engine codex -m global.openai.gpt-5.6-terra 

620 gco autopilot --engine codex --print-config 

621 gco autopilot --dry-run 

622 gco autopilot -y -- --permission-mode plan 

623 

624 \b 

625 Requirements: 

626 - AWS credentials with Bedrock model invocation access 

627 - The selected model enabled in the resolved AWS Region 

628 - npm only when the selected CLI is not already installed 

629 """ 

630 formatter = get_output_formatter(config) 

631 

632 if continue_session and resume is not None: 

633 formatter.print_error("Pass either --continue or --resume, not both.") 

634 sys.exit(1) 

635 

636 if config.output_format != "table" and not (dry_run or print_config): 

637 formatter.print_error( 

638 "Live Autopilot sessions require terminal output. Use `--output table`, " 

639 "or combine machine output with `--dry-run` or `--print-config`." 

640 ) 

641 sys.exit(2) 

642 

643 try: 

644 resolved_engine = resolve_engine(engine) 

645 if resolved_engine is AutopilotEngine.CODEX: 

646 _validate_codex_engine_args(tuple(engine_args)) 

647 plan, warnings = _plan( 

648 config, 

649 model, 

650 small_fast_model, 

651 companions, 

652 tuple(enable), 

653 tuple(mcp_env), 

654 tuple(plugins), 

655 tuple(skills), 

656 tuple(agents), 

657 engine=resolved_engine, 

658 ) 

659 except ValueError as e: 

660 formatter.print_error(str(e)) 

661 sys.exit(1) 

662 except Exception as e: 

663 formatter.print_error(f"Failed to resolve the autopilot launch plan: {e}") 

664 sys.exit(1) 

665 

666 for warning in warnings: 

667 formatter.print_warning(warning) 

668 

669 if print_config: 

670 if plan["engine"] == AutopilotEngine.CODEX.value: 

671 click.echo(plan["codex_config"], nl=False) 

672 else: 

673 # Preserve Claude's raw JSON machine-readable surface. 

674 emit_structured_document( 

675 plan["mcp_config"], 

676 output_format="json", 

677 rendered=json.dumps(plan["mcp_config"], indent=2), 

678 ) 

679 return 

680 

681 if dry_run: 

682 if config.output_format == "table": 

683 _print_dry_run(formatter, plan) 

684 else: 

685 formatter.print( 

686 { 

687 key: value 

688 for key, value in plan.items() 

689 if key not in {"mcp_config", "codex_config"} 

690 } 

691 ) 

692 return 

693 

694 if plan["engine"] == AutopilotEngine.CODEX.value: 

695 codex_binary = plan["codex_binary"] 

696 if codex_binary is None: 

697 formatter.print_info(f"Codex is not installed (pinned: {plan['codex_pin']}).") 

698 if not yes and not confirm(f"Install it now with `{plan['install_command']}`?"): 

699 formatter.print_error( 

700 "Codex is required for this engine. Install it manually with " 

701 f"`{plan['install_command']}` and re-run `gco autopilot --engine codex`." 

702 ) 

703 sys.exit(1) 

704 rc = install_codex() 

705 if rc == 127: 

706 formatter.print_error( 

707 "npm was not found on PATH. The GCO dev container ships the required " 

708 "Node.js/npm toolchain; rebuild it or install npm and re-run." 

709 ) 

710 sys.exit(1) 

711 if rc != 0: 

712 formatter.print_error(f"`{plan['install_command']}` failed with exit code {rc}.") 

713 sys.exit(1) 

714 codex_binary = find_codex_binary() 

715 if codex_binary is None: 

716 formatter.print_error( 

717 "Codex installed but the `codex` binary is not on PATH. Open a new " 

718 "shell (or fix your npm global bin path) and re-run." 

719 ) 

720 sys.exit(1) 

721 try: 

722 write_codex_config(str(plan["codex_config"])) 

723 stage_codex_skills(tuple(skills)) 

724 except (OSError, ValueError) as e: 

725 formatter.print_error(f"Failed to prepare the Codex session: {e}") 

726 sys.exit(1) 

727 resume_args = _resolve_codex_resume_args(continue_session, resume) 

728 env = build_codex_env(plan["region"]) 

729 owned_args = build_codex_owned_args( 

730 model=plan["model"], 

731 region=plan["region"], 

732 reasoning_effort=plan["reasoning_effort"], 

733 workspace=Path(plan["workspace"]), 

734 ) 

735 argv = build_codex_launch_argv( 

736 codex_binary, 

737 root_args=owned_args, 

738 resume_args=resume_args, 

739 extra_args=tuple(engine_args), 

740 ) 

741 details = f", provider={CODEX_BEDROCK_PROVIDER}" 

742 if plan["reasoning_effort"] is not None: 

743 details = f", reasoning={plan['reasoning_effort']}{details}" 

744 formatter.print_info(f"Launching Codex on Bedrock ({plan['model']}{details})...") 

745 try: 

746 rc = exec_codex(argv, env) 

747 except OSError as e: 

748 formatter.print_error( 

749 f"Failed to launch Codex at {argv[0]}: {e}. Reinstall with " 

750 f"`{' '.join(codex_install_command())}` and re-run." 

751 ) 

752 sys.exit(1) 

753 sys.exit(rc) 

754 

755 claude_binary = plan["claude_binary"] 

756 if claude_binary is None: 

757 formatter.print_info(f"Claude Code is not installed (pinned: {plan['claude_code_pin']}).") 

758 if not yes and not confirm(f"Install it now with `{plan['install_command']}`?"): 

759 formatter.print_error( 

760 "Claude Code is required. Install it manually with " 

761 f"`{plan['install_command']}` and re-run `gco autopilot`." 

762 ) 

763 sys.exit(1) 

764 rc = install_claude_code() 

765 if rc == 127: 

766 formatter.print_error( 

767 "npm was not found on PATH. Install Node.js/npm (the GCO dev " 

768 "container ships both), or install Claude Code another way, " 

769 "then re-run `gco autopilot`." 

770 ) 

771 sys.exit(1) 

772 if rc != 0: 

773 formatter.print_error(f"`{plan['install_command']}` failed with exit code {rc}.") 

774 sys.exit(1) 

775 claude_binary = find_claude_binary() 

776 if claude_binary is None: 

777 formatter.print_error( 

778 "Claude Code installed but the `claude` binary is not on PATH. " 

779 "Open a new shell (or fix your npm global bin path) and re-run." 

780 ) 

781 sys.exit(1) 

782 

783 try: 

784 written = write_mcp_config(plan["mcp_config"]) 

785 staged_plugin = stage_imports(tuple(skills), tuple(agents)) 

786 except (OSError, ValueError) as e: 

787 formatter.print_error(f"Failed to prepare the session: {e}") 

788 sys.exit(1) 

789 

790 plugin_paths = [Path(path) for path in plan["plugins"]] 

791 if staged_plugin is not None: 

792 plugin_paths.append(staged_plugin) 

793 plugin_args = build_plugin_args(plugin_paths) 

794 

795 resume_args = _resolve_resume_args(plan, continue_session, resume, tuple(engine_args), yes) 

796 

797 env = build_claude_env(plan["model"], plan["region"], plan["small_fast_model"]) 

798 argv = build_launch_argv(claude_binary, written, tuple(engine_args), resume_args, plugin_args) 

799 

800 formatter.print_info(f"Launching Claude Code on Bedrock ({plan['model']})...") 

801 try: 

802 rc = exec_claude(argv, env) # returns only on Windows 

803 except OSError as e: 

804 # A claude on PATH that cannot exec (for example a shim whose 

805 # blocked postinstall never fetched the native binary) must fail 

806 # with a remediation, not a traceback. 

807 formatter.print_error( 

808 f"Failed to launch Claude Code at {argv[0]}: {e}. " 

809 "The install may be incomplete — reinstall with " 

810 f"`{' '.join(claude_install_command())}` and re-run `gco autopilot`." 

811 ) 

812 sys.exit(1) 

813 sys.exit(rc)