copper-pilot-cli 0.1.11__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/HISTORY.md +99 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/PKG-INFO +1 -1
- copper_pilot_cli-1.0.0/VERSION +1 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_acp.py +5 -1
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_config.py +42 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_graph.py +10 -2
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_preferences.py +2 -5
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_protocol.py +74 -11
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_workspace.py +24 -15
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/design.py +16 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/reviewer.py +7 -4
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_acp.py +18 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_durability.py +113 -1
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_protocol.py +79 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_validate.py +72 -0
- copper_pilot_cli-1.0.0/tests/test_workspace.py +103 -0
- copper_pilot_cli-0.1.11/VERSION +0 -1
- copper_pilot_cli-0.1.11/tests/test_workspace.py +0 -32
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/.claude-plugin/marketplace.json +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/.gitignore +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/CODE_OF_CONDUCT.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/CONTRIBUTING.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/LICENSE +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/NOTICE +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/README.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/SECURITY.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/SUPPORT.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/UPSTREAM.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/assets/esp32-mini-1-h4.gif +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/assets/logo-dark.png +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/assets/logo-light.png +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/assets/logo.png +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/cli.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/development.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/harness.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/docs/validate.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/.gitignore +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/.copperpilot/PLANS.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/.copperpilot/README.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/.copperpilot/TOOLS.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/.copperpilot/plans/esp32-dev-board.plan.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/.gitignore +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/README.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/esp32/3d-models/SW-SMD_L3.9-W2.9-H2.0-LS4.8.step +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/esp32/3d-models/TYPE-C-31-M-12.step +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/esp32/esp32.kicad_pcb +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/esp32/esp32.kicad_pro +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/esp32/esp32.kicad_sch +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/esp32/esp32.png +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/helper.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/001_esp32/main.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/.copperpilot/PLANS.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/.copperpilot/README.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/.copperpilot/plans/cm5-camera-carrier.plan.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/.gitignore +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/README.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/assembly_spec.json +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/3dmodels/CM4.step +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/3dmodels/CM5.step +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/3dmodels/Camera_IMX219-D160.step +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/3dmodels/USB_A_Molex_105057-0001_Vertical.wrl +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/3dmodels/USB_A_vertical.step +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/CM5.3dshapes/5033981892.stp +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/CM5.3dshapes/DF40C-100DS.stp +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/CM5.3dshapes/JTHDA-19F08.STEP +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/CM5.3dshapes/SRP5030CC.stp +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.kicad_dru +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.kicad_pcb +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.kicad_pro +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.kicad_sch +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.kicad_sym +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.png +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/C_0402_1005Metric.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/C_0805_2012Metric.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/Camera_Module_IMX219-D160.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/Crystal_SMD_3225-4Pin_3.2x2.5mm.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/HDMI_Mini-C_Molex_47151-0001.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/LED_0603_1608Metric.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/L_Bourns_SRP5030CC.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/L_CommonModeChoke_Coilank_ACM1210.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/MOLEX_USB_67298-4090.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/Molex_200528-0150_1x15-1MP_P1.00mm_Horizontal.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/PinHeader_1x03_P2.54mm_Vertical.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/PinHeader_2x20_P2.54mm_Vertical.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/R_0402_1005Metric.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/R_0603_1608Metric.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/Raspberry-Pi-4-Compute-Module.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/SDCARD_MOLEX_503398-1892.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/SOT-23-5.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/SW_SPST_PTS645Sx43SMTR92.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/TSOT-23-6.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/Texas_DRT-3.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/UQFN-10_1.4x1.8mm_P0.4mm.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera.pretty/USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal.kicad_mod +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/cm5_camera_bottom.png +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/fp-lib-table +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/cm5_camera/sym-lib-table +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/generate_3d_models.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/helper.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/examples/002_cm5_camera/main.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/plugins/copper-pilot/.claude-plugin/plugin.json +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/plugins/copper-pilot/.mcp.json +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/plugins/copper-pilot/agents/copper-pilot.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/plugins/copper-pilot/skills/copper-pilot-review/SKILL.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/pyproject.toml +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/skills/copper-pilot-review/SKILL.md +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/__init__.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/__main__.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/_upstream/__init__.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/_upstream/dcode_0_1_69/PROVENANCE.json +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/_upstream/dcode_0_1_69/__init__.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/_upstream/dcode_0_1_69/diff_utils.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/_version.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/clipboard.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_api.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_app.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_auth.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_background.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_chrome.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_features.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_hooks.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_logo.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_main.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_mcp.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_models.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_presentation.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_status_events.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_theme.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_tools.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_update.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_widgets.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/deepagents_tools.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/diagnostics.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/langchain.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/media_utils.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/py.typed +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/sessions.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/textual_patches.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/tool_batch.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/__init__.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/builtins.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/cli.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/config.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/default.yaml +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/kicad.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/report.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/runner.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/conftest.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/approval.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-120-ascii.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-120-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-120-nocolor.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-50-ascii.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-50-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-50-nocolor.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-80-ascii.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-80-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/idle-input-80-nocolor.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/slash-menu.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/status-agent-balanced.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/status-agent-intelligence.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/status-ask-balanced.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/status-ask-intelligence.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/status-plan-balanced.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/status-plan-intelligence.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-120-ascii.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-120-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-120-nocolor.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-50-ascii.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-50-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-50-nocolor.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-80-ascii.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-80-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/welcome-80-nocolor.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/snapshots/working-color.txt +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_auth.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_chrome_snapshots.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_clipboard.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_cm5_camera_example.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_copper_api.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_e2e.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_esp32_example.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_features.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_graph_batch.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_langchain.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_live_chat_e2e.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_logo.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_main.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_mcp.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_models.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_plugin_manifest.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_preferences.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_presentation.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_protocol_batch.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_release_gates.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_sessions.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_status_events.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_themes_and_speed.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_tool_batch.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_tools.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_tui.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_upstream_provenance.py +0 -0
- {copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/tests/test_validate_live.py +0 -0
|
@@ -5,6 +5,104 @@ Changelog
|
|
|
5
5
|
(unreleased)
|
|
6
6
|
------------
|
|
7
7
|
|
|
8
|
+
Fix
|
|
9
|
+
~~~
|
|
10
|
+
- Do not count echoed request fields toward the stream byte budget.
|
|
11
|
+
[AdityaNG, Claude Opus 5.5]
|
|
12
|
+
|
|
13
|
+
The hosted service echoes the request `query` (and, in the final frame,
|
|
14
|
+
`context`) in every chat_response. A live probe showed the echo is ~97% of
|
|
15
|
+
each frame. A validate arch.review prompt is ~76 KB, so a long review on a
|
|
16
|
+
large board crossed the 64 MiB client budget after ~815 frames (2601 s)
|
|
17
|
+
although the hosted output itself was ~4.4 MB, and the check ERRORed with
|
|
18
|
+
"Chat exceeded the client stream byte budget."
|
|
19
|
+
|
|
20
|
+
The budget now counts frame bytes minus the echoed request fields the
|
|
21
|
+
client never reads, and the error names the counted size and frame count.
|
|
22
|
+
- Stop cutting long hosted answers at the 16 KB status limit. [AdityaNG,
|
|
23
|
+
Claude Opus 5.5]
|
|
24
|
+
|
|
25
|
+
Every hosted response frame went through sanitize_status, which caps
|
|
26
|
+
status lines at 16 KB. The hosted side can send a whole answer as one
|
|
27
|
+
response frame, so a validate review longer than 16 KB lost the end of
|
|
28
|
+
its findings JSON. The reviewer then found no json block, sent a repair
|
|
29
|
+
turn into the already huge thread, and the server refused it ("This
|
|
30
|
+
conversation is too large to continue"), so arch.review ERRORed. Chat
|
|
31
|
+
and ACP answers over 16 KB were cut the same way.
|
|
32
|
+
|
|
33
|
+
Response text now gets a 1 MiB bound; status and reasoning lines keep
|
|
34
|
+
16 KB. A validate check that fails with a hosted failed final also
|
|
35
|
+
records the hosted turn id, as chat does.
|
|
36
|
+
- Surface stalled or failed hosted turns instead of hanging. [AdityaNG,
|
|
37
|
+
Claude Opus 5.5]
|
|
38
|
+
|
|
39
|
+
Three ways a long ACP turn could hang or hide its cause:
|
|
40
|
+
|
|
41
|
+
- idle_frame_timeout was None, so a hosted server that kept the socket
|
|
42
|
+
(and keepalive) up but stopped sending hung the prompt forever. It now
|
|
43
|
+
defaults to 10 minutes (COPPER_PILOT_IDLE_TIMEOUT_SECONDS, 0 disables)
|
|
44
|
+
and raises the existing retryable CHAT_IDLE_TIMEOUT with the silence
|
|
45
|
+
length in the message. The hosted side heartbeats every 25 s.
|
|
46
|
+
- A failed hosted final (success false) still waited, unbounded, for
|
|
47
|
+
background shells and then ran a continuation turn. A runaway background
|
|
48
|
+
command held the hosted error back until the caller timed out, and a
|
|
49
|
+
continuation that succeeded still reported the stale error. A failed
|
|
50
|
+
final now ends the turn and kills leftover background processes.
|
|
51
|
+
- The hosted copy "CopperPilot ran into an error." carries no detail. The
|
|
52
|
+
ACP error now names the hosted turn id, and the client logs the failing
|
|
53
|
+
final frame's keys, so the server-side cause can be found.
|
|
54
|
+
- Guard the state lock with sys.platform so ty passes on Windows.
|
|
55
|
+
[AdityaNG, Claude Opus 5.5]
|
|
56
|
+
- Stop concurrent sessions racing on recent-projects state. [AdityaNG,
|
|
57
|
+
Claude Opus 5.5]
|
|
58
|
+
|
|
59
|
+
Every process wrote recent-projects.json (and config.json) through the
|
|
60
|
+
same fixed temp name, so parallel `copper-pilot acp` session starts moved
|
|
61
|
+
or clobbered each other's temp file and session/new failed with
|
|
62
|
+
FileNotFoundError on os.replace.
|
|
63
|
+
|
|
64
|
+
Writes now go through a per-write mkstemp temp file, the recent-projects
|
|
65
|
+
read-modify-write holds a POSIX flock so no entry is dropped, and the
|
|
66
|
+
bookkeeping logs instead of raising so it can never fail a session.
|
|
67
|
+
|
|
68
|
+
Other
|
|
69
|
+
~~~~~
|
|
70
|
+
- Merge pull request #37 from CopperPilot/fix/stream-byte-budget.
|
|
71
|
+
[Aditya]
|
|
72
|
+
|
|
73
|
+
fix: do not count echoed request fields toward the stream byte budget
|
|
74
|
+
- Merge pull request #35 from CopperPilot/fix/long-response-truncation.
|
|
75
|
+
[Aditya]
|
|
76
|
+
|
|
77
|
+
fix: stop cutting long hosted answers at the 16 KB status limit
|
|
78
|
+
- Merge pull request #33 from CopperPilot/fix/hosted-stream-stall.
|
|
79
|
+
[Aditya]
|
|
80
|
+
|
|
81
|
+
fix: surface stalled or failed hosted turns instead of hanging
|
|
82
|
+
- Merge pull request #34 from CopperPilot/fix/validate-hierarchical-
|
|
83
|
+
nets. [Aditya]
|
|
84
|
+
|
|
85
|
+
fix(validate): ground findings that cite label names of hierarchical nets
|
|
86
|
+
- Fix(validate): ground findings that cite label names of hierarchical
|
|
87
|
+
nets. [AdityaNG, Claude Opus 5.5]
|
|
88
|
+
|
|
89
|
+
KiCad names local and hierarchical label nets with their sheet path
|
|
90
|
+
("/RAIL_IO_3", "/power/RAIL_IO_3"), but reviewers cite the label
|
|
91
|
+
("RAIL_IO_3"). Grounding compared the cited name to the full net name
|
|
92
|
+
only, so every real finding on such a board was marked UNGROUNDED and
|
|
93
|
+
could never fail a blocking check (WARN / exit 3 instead of FAIL).
|
|
94
|
+
|
|
95
|
+
A cited net now grounds when it equals a design net name or is a
|
|
96
|
+
"/"-boundary suffix of one; invented names still do not.
|
|
97
|
+
- Merge pull request #32 from CopperPilot/fix/recent-projects-race.
|
|
98
|
+
[Aditya]
|
|
99
|
+
|
|
100
|
+
fix: stop concurrent ACP sessions racing on recent-projects state
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
0.1.11 (2026-10-25)
|
|
104
|
+
-------------------
|
|
105
|
+
|
|
8
106
|
Fix
|
|
9
107
|
~~~
|
|
10
108
|
- Do not block turn cancellation on loading-widget removal. [Cursor,
|
|
@@ -40,6 +138,7 @@ Fix
|
|
|
40
138
|
|
|
41
139
|
Other
|
|
42
140
|
~~~~~
|
|
141
|
+
- Release: version 0.1.11 🚀 [AdityaNG]
|
|
43
142
|
- Merge pull request #31 from naveen-
|
|
44
143
|
forkd/feat/cli_chat_chrome_2026_10_24. [Aditya]
|
|
45
144
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: copper-pilot-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: CopperPilot hosted agent for the terminal and LangChain
|
|
5
5
|
Project-URL: Homepage, https://copperpilot.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/CopperPilot/copper-pilot-cli
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
|
@@ -1252,7 +1252,11 @@ class CopperAcpAgent:
|
|
|
1252
1252
|
await send(session_id=session_id, update=update_agent_thought_text(f"{label}\n"))
|
|
1253
1253
|
elif kind == EventKind.FINAL.value and isinstance(payload, dict):
|
|
1254
1254
|
if payload.get("success") is False or payload.get("error"):
|
|
1255
|
-
|
|
1255
|
+
message = str(payload.get("error") or "hosted failure")
|
|
1256
|
+
# The hosted copy is generic; the turn id lets the service
|
|
1257
|
+
# owners find the server-side cause.
|
|
1258
|
+
turn_id = event.get("turn_id")
|
|
1259
|
+
return f"{message} (hosted turn {turn_id})" if turn_id else message
|
|
1256
1260
|
return None
|
|
1257
1261
|
|
|
1258
1262
|
# -------------------------------------------------------- broker handlers
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
5
6
|
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Generator
|
|
6
10
|
from dataclasses import dataclass
|
|
7
11
|
from pathlib import Path
|
|
8
12
|
from urllib.parse import urlparse
|
|
@@ -60,3 +64,41 @@ def paths() -> AppPaths:
|
|
|
60
64
|
logs=state / "logs",
|
|
61
65
|
recent_projects=state / "recent-projects.json",
|
|
62
66
|
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def atomic_write_text(destination: Path, text: str, *, mode: int | None = None) -> None:
|
|
70
|
+
"""Replace ``destination`` via a temp file unique to this write.
|
|
71
|
+
|
|
72
|
+
A shared temp name lets concurrent processes move or clobber each other's
|
|
73
|
+
half-written file, so every write gets its own name in the same directory.
|
|
74
|
+
"""
|
|
75
|
+
fd, name = tempfile.mkstemp(
|
|
76
|
+
prefix=f".{destination.name}.", suffix=".tmp", dir=destination.parent
|
|
77
|
+
)
|
|
78
|
+
temporary = Path(name)
|
|
79
|
+
try:
|
|
80
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
81
|
+
handle.write(text)
|
|
82
|
+
if mode is not None:
|
|
83
|
+
temporary.chmod(mode)
|
|
84
|
+
temporary.replace(destination)
|
|
85
|
+
except BaseException:
|
|
86
|
+
with contextlib.suppress(OSError):
|
|
87
|
+
temporary.unlink()
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@contextlib.contextmanager
|
|
92
|
+
def state_lock(destination: Path) -> Generator[None]:
|
|
93
|
+
"""Serialize read-modify-write of a state file across processes (POSIX only)."""
|
|
94
|
+
if sys.platform == "win32":
|
|
95
|
+
yield
|
|
96
|
+
return
|
|
97
|
+
import fcntl
|
|
98
|
+
|
|
99
|
+
with open(destination.with_name(f".{destination.name}.lock"), "a") as handle:
|
|
100
|
+
fcntl.flock(handle, fcntl.LOCK_EX)
|
|
101
|
+
try:
|
|
102
|
+
yield
|
|
103
|
+
finally:
|
|
104
|
+
fcntl.flock(handle, fcntl.LOCK_UN)
|
|
@@ -216,6 +216,8 @@ class _TurnRun:
|
|
|
216
216
|
self.context_parts: list[str] = []
|
|
217
217
|
self.failure: ErrorInfo | None = None
|
|
218
218
|
self.cancelled = False
|
|
219
|
+
# The hosted service ended the turn with ``success: false``.
|
|
220
|
+
self.final_failed = False
|
|
219
221
|
|
|
220
222
|
# ------------------------------------------------------------ streaming
|
|
221
223
|
|
|
@@ -244,6 +246,8 @@ class _TurnRun:
|
|
|
244
246
|
self._track(event)
|
|
245
247
|
|
|
246
248
|
def _track(self, event: CopperEvent) -> None:
|
|
249
|
+
if event.kind is EventKind.FINAL and isinstance(event.data, dict):
|
|
250
|
+
self.final_failed = event.data.get("success") is False or bool(event.data.get("error"))
|
|
247
251
|
if event.kind is EventKind.TEXT:
|
|
248
252
|
self.text.append(str(event.data))
|
|
249
253
|
elif event.kind is EventKind.STATUS_EVENT and isinstance(event.data, dict):
|
|
@@ -426,6 +430,8 @@ class _TurnRun:
|
|
|
426
430
|
]
|
|
427
431
|
if not await self.run_with_retry(continuation, first_messages=messages):
|
|
428
432
|
return False
|
|
433
|
+
if self.final_failed:
|
|
434
|
+
return False
|
|
429
435
|
return True
|
|
430
436
|
|
|
431
437
|
# ---------------------------------------------------------------- close
|
|
@@ -526,10 +532,12 @@ async def create_copper_graph(agent: CopperPilotAgent) -> Any:
|
|
|
526
532
|
query if isinstance(query, str) else json.dumps(query),
|
|
527
533
|
first_messages=original,
|
|
528
534
|
)
|
|
529
|
-
|
|
535
|
+
# A failed final ends the turn: waiting on background shells first
|
|
536
|
+
# would hold the hosted error back for as long as they run.
|
|
537
|
+
if completed and not run.final_failed:
|
|
530
538
|
await run.wait_for_background()
|
|
531
539
|
finally:
|
|
532
|
-
if run.failure is not None or run.cancelled:
|
|
540
|
+
if run.failure is not None or run.cancelled or run.final_failed:
|
|
533
541
|
manager = _background_manager(agent)
|
|
534
542
|
if manager is not None:
|
|
535
543
|
await manager.kill_all()
|
{copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/copper_preferences.py
RENAMED
|
@@ -7,7 +7,7 @@ import stat
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from copper_pilot_cli.copper_config import paths
|
|
10
|
+
from copper_pilot_cli.copper_config import atomic_write_text, paths
|
|
11
11
|
from copper_pilot_cli.copper_tools import ApprovalMode
|
|
12
12
|
|
|
13
13
|
YOLO_ACK_VERSION = 1
|
|
@@ -30,10 +30,7 @@ def save_preferences(value: dict[str, Any]) -> None:
|
|
|
30
30
|
destination = _path()
|
|
31
31
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
32
32
|
destination.parent.chmod(stat.S_IRWXU)
|
|
33
|
-
|
|
34
|
-
temporary.write_text(json.dumps(value, indent=2), encoding="utf-8")
|
|
35
|
-
temporary.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
36
|
-
temporary.replace(destination)
|
|
33
|
+
atomic_write_text(destination, json.dumps(value, indent=2), mode=stat.S_IRUSR | stat.S_IWUSR)
|
|
37
34
|
|
|
38
35
|
|
|
39
36
|
def saved_approval_mode() -> ApprovalMode:
|
|
@@ -6,6 +6,7 @@ import asyncio
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
|
+
import os
|
|
9
10
|
import re
|
|
10
11
|
import ssl
|
|
11
12
|
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Sequence
|
|
@@ -37,6 +38,34 @@ MAX_CONTEXT_MESSAGE_BYTES = 64 * 1024
|
|
|
37
38
|
MAX_CONTEXT_TOTAL_BYTES = 4 * 1024 * 1024
|
|
38
39
|
MAX_CONTEXT_ENTRIES = 2_000
|
|
39
40
|
MAX_STREAM_BYTES = 64 * 1024 * 1024
|
|
41
|
+
# Request fields the hosted service echoes back in every chat_response. The
|
|
42
|
+
# client never reads them, and the echoed ``query`` alone (a whole validate
|
|
43
|
+
# prompt) can be most of each frame, so they do not count toward the budget.
|
|
44
|
+
ECHOED_REQUEST_FIELDS = (
|
|
45
|
+
"query",
|
|
46
|
+
"context",
|
|
47
|
+
"document_context",
|
|
48
|
+
"pcb_data",
|
|
49
|
+
"pcb_info",
|
|
50
|
+
"schematic_data",
|
|
51
|
+
"schematic_info",
|
|
52
|
+
)
|
|
53
|
+
MAX_STATUS_BYTES = 16 * 1024
|
|
54
|
+
# The answer itself can be long (a validate review's findings JSON easily
|
|
55
|
+
# passes 16 KB) and the hosted side may send it as one frame.
|
|
56
|
+
MAX_RESPONSE_TEXT_BYTES = 1024 * 1024
|
|
57
|
+
IDLE_FRAME_TIMEOUT_ENV = "COPPER_PILOT_IDLE_TIMEOUT_SECONDS"
|
|
58
|
+
DEFAULT_IDLE_FRAME_TIMEOUT = 10 * 60.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def idle_frame_timeout() -> float | None:
|
|
62
|
+
"""Seconds a streaming turn may stay silent; ``None`` (env ``0``) waits forever."""
|
|
63
|
+
raw = os.getenv(IDLE_FRAME_TIMEOUT_ENV, "").strip()
|
|
64
|
+
try:
|
|
65
|
+
value = float(raw) if raw else DEFAULT_IDLE_FRAME_TIMEOUT
|
|
66
|
+
except ValueError:
|
|
67
|
+
value = DEFAULT_IDLE_FRAME_TIMEOUT
|
|
68
|
+
return value if value > 0 else None
|
|
40
69
|
|
|
41
70
|
|
|
42
71
|
class ProtocolError(RuntimeError):
|
|
@@ -414,7 +443,7 @@ def websocket_url(base_url: str, *, agentic: bool = True) -> str:
|
|
|
414
443
|
return urlunparse((scheme, parsed.netloc, path, "", "", ""))
|
|
415
444
|
|
|
416
445
|
|
|
417
|
-
def sanitize_status(value: str) -> str:
|
|
446
|
+
def sanitize_status(value: str, limit: int = MAX_STATUS_BYTES) -> str:
|
|
418
447
|
"""Convert hosted status HTML to bounded terminal-safe plain markdown."""
|
|
419
448
|
safe = re.sub(
|
|
420
449
|
r"<(?:script|style)\b[^>]*>[\s\S]*?</(?:script|style)>",
|
|
@@ -429,7 +458,7 @@ def sanitize_status(value: str) -> str:
|
|
|
429
458
|
)
|
|
430
459
|
text = _TERMINAL_ESCAPE.sub("", text)
|
|
431
460
|
text = "".join(character for character in text if character in "\n\t" or ord(character) >= 32)
|
|
432
|
-
return _truncate_utf8(text,
|
|
461
|
+
return _truncate_utf8(text, limit)
|
|
433
462
|
|
|
434
463
|
|
|
435
464
|
def usage_reset_time(frame: dict[str, Any]) -> str | None:
|
|
@@ -457,6 +486,20 @@ def error_message(value: Any) -> str:
|
|
|
457
486
|
ToolHandler = Callable[[ToolRequest], Awaitable[Any]]
|
|
458
487
|
|
|
459
488
|
|
|
489
|
+
def _counted_frame_bytes(raw: str | bytes, frame: dict[str, Any]) -> int:
|
|
490
|
+
"""Bytes of one frame that count toward the stream budget, minus echoed request fields."""
|
|
491
|
+
size = len(raw.encode() if isinstance(raw, str) else raw)
|
|
492
|
+
response = frame.get("chat_response")
|
|
493
|
+
if isinstance(response, dict):
|
|
494
|
+
echoed = sum(
|
|
495
|
+
len(json.dumps(response[key]).encode())
|
|
496
|
+
for key in ECHOED_REQUEST_FIELDS
|
|
497
|
+
if key in response
|
|
498
|
+
)
|
|
499
|
+
size -= echoed
|
|
500
|
+
return max(size, 0)
|
|
501
|
+
|
|
502
|
+
|
|
460
503
|
def tool_error_result(exc: BaseException) -> dict[str, Any]:
|
|
461
504
|
"""The tool_result payload reported when a local tool raises."""
|
|
462
505
|
return {
|
|
@@ -489,10 +532,11 @@ class HostedChatClient:
|
|
|
489
532
|
self._socket: Any = None
|
|
490
533
|
self._owner: str | None = None
|
|
491
534
|
self.first_frame_timeout = 60.0
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
|
|
535
|
+
# Long hosted reasoning or subagent runs can stay silent for minutes,
|
|
536
|
+
# and websocket keepalive only detects a dead peer. A server that keeps
|
|
537
|
+
# the socket open but stops sending would otherwise hang the turn
|
|
538
|
+
# forever, so a generous idle bound turns that stall into an error.
|
|
539
|
+
self.idle_frame_timeout: float | None = idle_frame_timeout()
|
|
496
540
|
self.frame_budget = 10_000
|
|
497
541
|
|
|
498
542
|
async def cancel(self) -> None:
|
|
@@ -582,7 +626,10 @@ class HostedChatClient:
|
|
|
582
626
|
raw = await asyncio.wait_for(socket.recv(), timeout)
|
|
583
627
|
except TimeoutError as exc:
|
|
584
628
|
phase = "first frame" if frame_count == 0 else "next frame"
|
|
585
|
-
message =
|
|
629
|
+
message = (
|
|
630
|
+
f"Timed out waiting for the hosted chat {phase} "
|
|
631
|
+
f"(no data for {timeout:g} s)."
|
|
632
|
+
)
|
|
586
633
|
raise ProtocolError(
|
|
587
634
|
message,
|
|
588
635
|
code="CHAT_FIRST_FRAME_TIMEOUT"
|
|
@@ -594,17 +641,22 @@ class HostedChatClient:
|
|
|
594
641
|
closed = exc
|
|
595
642
|
break
|
|
596
643
|
frame_count += 1
|
|
597
|
-
stream_bytes += len(raw.encode() if isinstance(raw, str) else raw)
|
|
598
644
|
if frame_count > self.frame_budget:
|
|
599
645
|
raise ProtocolError("Chat exceeded the client frame budget.")
|
|
600
|
-
if stream_bytes > MAX_STREAM_BYTES:
|
|
601
|
-
raise ProtocolError("Chat exceeded the client stream byte budget.")
|
|
602
646
|
try:
|
|
603
647
|
frame = json.loads(raw)
|
|
604
648
|
except (json.JSONDecodeError, TypeError) as exc:
|
|
605
649
|
raise ProtocolError("Hosted chat returned invalid JSON.") from exc
|
|
606
650
|
if not isinstance(frame, dict):
|
|
607
651
|
raise ProtocolError("Hosted chat frame must be a JSON object.")
|
|
652
|
+
stream_bytes += _counted_frame_bytes(raw, frame)
|
|
653
|
+
if stream_bytes > MAX_STREAM_BYTES:
|
|
654
|
+
raise ProtocolError(
|
|
655
|
+
"Chat exceeded the client stream byte budget "
|
|
656
|
+
f"({stream_bytes // (1024 * 1024)} MiB of hosted output "
|
|
657
|
+
f"in {frame_count} frames, limit "
|
|
658
|
+
f"{MAX_STREAM_BYTES // (1024 * 1024)} MiB)."
|
|
659
|
+
)
|
|
608
660
|
frame_type = frame.get("type")
|
|
609
661
|
if frame_type == "error":
|
|
610
662
|
message = frame.get("public_message") or frame.get("message")
|
|
@@ -647,7 +699,9 @@ class HostedChatClient:
|
|
|
647
699
|
}
|
|
648
700
|
if response.response:
|
|
649
701
|
yield CopperEvent(
|
|
650
|
-
EventKind.TEXT,
|
|
702
|
+
EventKind.TEXT,
|
|
703
|
+
sanitize_status(response.response, MAX_RESPONSE_TEXT_BYTES),
|
|
704
|
+
**metadata,
|
|
651
705
|
)
|
|
652
706
|
if response.reasoning_update:
|
|
653
707
|
yield CopperEvent(
|
|
@@ -741,6 +795,15 @@ class HostedChatClient:
|
|
|
741
795
|
break
|
|
742
796
|
if frame_type == "final" or response.is_final:
|
|
743
797
|
saw_final = True
|
|
798
|
+
if response.success is False or response.error:
|
|
799
|
+
logger.warning(
|
|
800
|
+
"Hosted turn ended unsuccessfully (turn_id=%s, error=%r, "
|
|
801
|
+
"frame keys=%s, extra fields=%s)",
|
|
802
|
+
response.turn_id,
|
|
803
|
+
response.error,
|
|
804
|
+
sorted(frame),
|
|
805
|
+
sorted(response.model_extra or {}),
|
|
806
|
+
)
|
|
744
807
|
yield CopperEvent(
|
|
745
808
|
EventKind.FINAL,
|
|
746
809
|
{
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import os
|
|
7
8
|
from dataclasses import asdict, dataclass
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
|
-
from copper_pilot_cli.copper_config import paths
|
|
11
|
+
from copper_pilot_cli.copper_config import atomic_write_text, paths, state_lock
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
11
14
|
|
|
12
15
|
IGNORED_NAMES = {
|
|
13
16
|
".git",
|
|
@@ -117,22 +120,28 @@ def build_initial_file_tree(root: Path, limit: int = 2_000) -> str:
|
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
def remember_workspace(context: WorkspaceContext, limit: int = 20) -> None:
|
|
120
|
-
"""Persist most-recently-used folders atomically.
|
|
123
|
+
"""Persist most-recently-used folders atomically.
|
|
124
|
+
|
|
125
|
+
Best-effort bookkeeping: a failure here is logged, never raised, so it
|
|
126
|
+
cannot fail the session that triggered it.
|
|
127
|
+
"""
|
|
121
128
|
destination = paths().recent_projects
|
|
122
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
-
existing: list[dict[str, object]] = []
|
|
124
129
|
try:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
existing
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
with state_lock(destination):
|
|
132
|
+
existing: list[dict[str, object]] = []
|
|
133
|
+
try:
|
|
134
|
+
loaded = json.loads(destination.read_text())
|
|
135
|
+
if isinstance(loaded, list):
|
|
136
|
+
existing = [row for row in loaded if isinstance(row, dict)]
|
|
137
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
138
|
+
pass
|
|
139
|
+
root = str(context.root)
|
|
140
|
+
rows = [{"root": root, **asdict(context)}]
|
|
141
|
+
rows.extend(row for row in existing if row.get("root") != root)
|
|
142
|
+
atomic_write_text(destination, json.dumps(rows[:limit], default=str, indent=2))
|
|
143
|
+
except OSError:
|
|
144
|
+
logger.warning("Could not update recent projects at %s", destination, exc_info=True)
|
|
136
145
|
|
|
137
146
|
|
|
138
147
|
def recent_workspaces() -> list[Path]:
|
|
@@ -283,6 +283,22 @@ class DesignIndex:
|
|
|
283
283
|
def net_names(self) -> set[str]:
|
|
284
284
|
return {name.upper() for name in self.nets}
|
|
285
285
|
|
|
286
|
+
def has_net(self, name: str) -> bool:
|
|
287
|
+
"""Whether ``name`` cites a design net, by full path or hierarchical leaf.
|
|
288
|
+
|
|
289
|
+
KiCad names a local or hierarchical label's net with its sheet path
|
|
290
|
+
(``/RAIL_IO_3``, ``/power/RAIL_IO_3``) while people cite the label
|
|
291
|
+
(``RAIL_IO_3``). Accept the full name or any ``/``-boundary suffix of it.
|
|
292
|
+
"""
|
|
293
|
+
cited = name.strip().upper()
|
|
294
|
+
if not cited:
|
|
295
|
+
return False
|
|
296
|
+
for net in self.nets:
|
|
297
|
+
full = net.upper()
|
|
298
|
+
if full == cited or full.endswith("/" + cited.lstrip("/")):
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
286
302
|
@property
|
|
287
303
|
def layer_names(self) -> set[str]:
|
|
288
304
|
return {name.lower() for name in self.layers}
|
{copper_pilot_cli-0.1.11 → copper_pilot_cli-1.0.0}/src/copper_pilot_cli/validate/reviewer.py
RENAMED
|
@@ -205,9 +205,12 @@ class HostedTurnRunner:
|
|
|
205
205
|
)
|
|
206
206
|
elif kind == EventKind.FINAL.value and isinstance(payload, dict):
|
|
207
207
|
if payload.get("success") is False or payload.get("error"):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
message = str(payload.get("error") or "hosted turn failed")
|
|
209
|
+
# The hosted copy is generic; the turn id lets the
|
|
210
|
+
# service owners find the server-side cause.
|
|
211
|
+
if data.get("turn_id"):
|
|
212
|
+
message = f"{message} (hosted turn {data['turn_id']})"
|
|
213
|
+
result.error = result.error or {"message": message}
|
|
211
214
|
except BaseException:
|
|
212
215
|
await runtime.acancel()
|
|
213
216
|
raise
|
|
@@ -460,7 +463,7 @@ def ground_findings(findings: list[Finding], index: DesignIndex | None) -> list[
|
|
|
460
463
|
problems.append("design index unavailable")
|
|
461
464
|
else:
|
|
462
465
|
missing_refs = [ref for ref in loc.refs if ref.upper() not in index.refs]
|
|
463
|
-
missing_nets = [net for net in loc.nets if
|
|
466
|
+
missing_nets = [net for net in loc.nets if not index.has_net(net)]
|
|
464
467
|
if missing_refs:
|
|
465
468
|
problems.append(f"unknown refs {', '.join(missing_refs)}")
|
|
466
469
|
if missing_nets:
|
|
@@ -826,6 +826,24 @@ async def test_prompt_reports_failed_final_event(tmp_path, monkeypatch) -> None:
|
|
|
826
826
|
await agent.prompt(session_id=session_id, prompt=[TextContentBlock(type="text", text="x")])
|
|
827
827
|
|
|
828
828
|
|
|
829
|
+
async def test_failed_final_event_names_the_hosted_turn(tmp_path, monkeypatch) -> None:
|
|
830
|
+
# The hosted copy is the generic "CopperPilot ran into an error."; the turn
|
|
831
|
+
# id is what lets the service owners find the server-side cause.
|
|
832
|
+
runtime = FakeRuntime(
|
|
833
|
+
[
|
|
834
|
+
{
|
|
835
|
+
"type": "copper.final",
|
|
836
|
+
"data": {"success": False, "error": "CopperPilot ran into an error."},
|
|
837
|
+
"turn_id": "turn-42",
|
|
838
|
+
}
|
|
839
|
+
]
|
|
840
|
+
)
|
|
841
|
+
agent, _conn, _runtime, session_id = await _connected_agent(tmp_path, monkeypatch, runtime)
|
|
842
|
+
with pytest.raises(RequestError) as caught:
|
|
843
|
+
await agent.prompt(session_id=session_id, prompt=[TextContentBlock(type="text", text="x")])
|
|
844
|
+
assert str(caught.value) == "CopperPilot ran into an error. (hosted turn turn-42)"
|
|
845
|
+
|
|
846
|
+
|
|
829
847
|
@pytest.mark.parametrize(
|
|
830
848
|
("error", "code"),
|
|
831
849
|
[
|