codexloop 0.2.0__tar.gz → 0.3.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.
- {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/ci.yml +24 -1
- {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/release-please.yml +5 -1
- codexloop-0.3.0/.release-please-manifest.json +3 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/CHANGELOG.md +30 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/PKG-INFO +1 -1
- {codexloop-0.2.0 → codexloop-0.3.0}/pyproject.toml +14 -1
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/__init__.py +1 -1
- codexloop-0.3.0/src/codexloop/application/interfaces/__init__.py +75 -0
- codexloop-0.3.0/src/codexloop/application/interfaces/agent.py +46 -0
- codexloop-0.3.0/src/codexloop/application/interfaces/api.py +10 -0
- codexloop-0.3.0/src/codexloop/application/interfaces/control.py +21 -0
- {codexloop-0.2.0/src/codexloop/application/usecases → codexloop-0.3.0/src/codexloop/application/interfaces}/doctor.py +7 -9
- codexloop-0.3.0/src/codexloop/application/interfaces/observability.py +42 -0
- codexloop-0.3.0/src/codexloop/application/interfaces/permissions.py +14 -0
- codexloop-0.3.0/src/codexloop/application/interfaces/storage.py +31 -0
- codexloop-0.3.0/src/codexloop/application/interfaces/system.py +16 -0
- codexloop-0.3.0/src/codexloop/application/ports.py +61 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/runner.py +115 -1
- codexloop-0.3.0/src/codexloop/application/usecases/doctor.py +18 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/run_control.py +1 -5
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/bootstrap.py +15 -2
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/app.py +24 -1
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/run.py +10 -1
- codexloop-0.3.0/src/codexloop/domain/forecast.py +271 -0
- codexloop-0.3.0/src/codexloop/domain/handoff_marker.py +107 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/loop.py +31 -2
- codexloop-0.3.0/src/codexloop/domain/verbosity.py +80 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/logging.py +21 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/rundir.py +27 -2
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/fakes.py +3 -0
- codexloop-0.3.0/tests/application/test_interfaces_convention.py +100 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_runner.py +66 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_app.py +12 -0
- codexloop-0.3.0/tests/cli/test_verbosity_flags.py +48 -0
- codexloop-0.3.0/tests/domain/test_forecast.py +209 -0
- codexloop-0.3.0/tests/domain/test_handoff_marker.py +83 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_loop.py +73 -1
- codexloop-0.3.0/tests/domain/test_verbosity.py +60 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_providers_gateway_extra.py +2 -1
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_appserver_gateway.py +9 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_coverage_boost.py +5 -1
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_doctor_env.py +36 -0
- codexloop-0.3.0/tests/infrastructure/test_git_savepoints_units.py +69 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_logging_redact.py +41 -0
- codexloop-0.3.0/tests/infrastructure/test_rollout_scan.py +91 -0
- codexloop-0.3.0/tests/infrastructure/test_rundir_run_id.py +48 -0
- codexloop-0.3.0/uv.lock +2277 -0
- codexloop-0.2.0/.release-please-manifest.json +0 -3
- codexloop-0.2.0/src/codexloop/application/ports.py +0 -158
- {codexloop-0.2.0 → codexloop-0.3.0}/.agents/skills/codexloop-run/SKILL.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.claude/skills/codexloop-run/SKILL.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.cursor/rules/codexloop-run.mdc +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.editorconfig +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.github/CODEOWNERS +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/docs.yml +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/publish-testpypi.yml +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.gitignore +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/.pre-commit-config.yaml +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/AGENTS.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/CLAUDE.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/CONTRIBUTING.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/LICENSE +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/README.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/SECURITY.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0001-onion-import-linter.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0002-subprocess-codex-exec.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0003-credits-exhausted.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0004-adaptive-probe-loop.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0005-layered-capacity-probe.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0006-generated-openai-rest.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0007-default-approval-sandbox.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0008-policy-via-c-flags.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0009-optional-app-server.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0010-rollout-best-effort.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0011-never-consume-resets.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0012-minimum-codex-version.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/index.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/index.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/contributing.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/getting-started.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/guides/rest-api-surface.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/index.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/plans/_shared-transplant-outline.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/plans/architecture-and-roadmap.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/plans/research-notes.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/publishing.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/reference/cli.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/docs/superpowers/plans/2026-08-13-codexloop-implementation.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/mkdocs.yml +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/release-please-config.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/dto.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/list_threads.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/preflight.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/resume_thread.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/run_plan.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/asyncio.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/approval_cmd.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/capacity.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/cwd_cmd.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/doctor.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/effort_cmd.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/logs.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/model_cmd.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/prompt.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/reset.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/resume.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/runs.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/sandbox_cmd.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/savepoints.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/snapshot.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/status.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/stop.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/threads.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/unwind.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/watch.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/render.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/approval.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/backoff.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/budget.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/capacity.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/classify.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/completion.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/control.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/error_codes.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/errors.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/model_profile.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/plan.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/savepoint.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/savepoint_message.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/session.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/signals.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/waiting.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/argv.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/events.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/gateway.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/probe.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/process.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/schema.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/scripted.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/translate.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/api_baseline.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/binder.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/gateway.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/introspect.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/json_io.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/params.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/providers.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/registry.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/client.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/gateway.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/ratelimits.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/audit.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/capacity_probe.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/clock.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/config.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/control.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/doctor_env.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/events.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/git_savepoints.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/lock.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/notify.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/progress.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/redact.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/rollout.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/snapshot.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/state.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/state_bus.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/stream_ui.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/py.typed +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_doctor.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_ports.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_usecases.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_waiting_runner.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_api.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_asyncio.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_e2e_shim.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_ops_commands.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/conftest.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/strategies.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_approval.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_backoff.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_budget.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_capacity.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_classify.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_completion.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_control.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_error_codes.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_model_profile.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_plan.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_savepoint.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_session.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_waiting.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/README.md +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/clean_completion.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/done_with_marker.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/huge_line.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/malformed_line.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/tool_heavy.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/truncated_stream.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/turn_failed_429_quota.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/turn_failed_429_window.jsonl +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_binder.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_drift.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_gateway.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_introspect.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_json_io.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_params.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_params_extra.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_providers.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_appserver.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_argv.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_capacity_probe.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_config.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_control_and_savepoints.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_events.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_gateway.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_lock.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_probe.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_process.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_ratelimits_extra.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_rollout.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_rundir_state.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_scripted.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_snapshot.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_state_bus.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_stream_ui.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_translate.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/done.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/quota_then_topup.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/unknown_429.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/wait_long.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/window_then_done.json +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/system/conftest.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/system/test_matrix_inprocess.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/system/test_subprocess_smoke.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/__init__.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/fake_appserver.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/fake_codex.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/test_fake_codex.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/test_bootstrap.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/test_no_vendor_leak.py +0 -0
- {codexloop-0.2.0 → codexloop-0.3.0}/tests/test_packaging.py +0 -0
|
@@ -35,7 +35,17 @@ jobs:
|
|
|
35
35
|
- run: lint-imports
|
|
36
36
|
- run: bandit -q -r src/codexloop
|
|
37
37
|
- run: pip-audit
|
|
38
|
-
|
|
38
|
+
# Per layer, not in aggregate: a blanket floor lets a well-tested domain
|
|
39
|
+
# hide an untested adapter. pytest-cov unions --cov scopes within one
|
|
40
|
+
# invocation, so each floor needs its own run.
|
|
41
|
+
- name: Coverage floor - domain (100%)
|
|
42
|
+
run: pytest -q --cov=codexloop.domain --cov-fail-under=100
|
|
43
|
+
- name: Coverage floor - application (100%)
|
|
44
|
+
run: pytest -q --cov=codexloop.application --cov-fail-under=100
|
|
45
|
+
- name: Coverage floor - infrastructure (100%)
|
|
46
|
+
run: pytest -q --cov=codexloop.infrastructure --cov-fail-under=100
|
|
47
|
+
- name: Coverage floor - cli (100%)
|
|
48
|
+
run: pytest -q --cov=codexloop.cli --cov-fail-under=100
|
|
39
49
|
|
|
40
50
|
docs:
|
|
41
51
|
name: MkDocs builds strictly
|
|
@@ -49,6 +59,19 @@ jobs:
|
|
|
49
59
|
- run: pip install -e ".[docs]"
|
|
50
60
|
- run: mkdocs build --strict
|
|
51
61
|
|
|
62
|
+
uv-lock:
|
|
63
|
+
name: uv.lock is in sync with pyproject.toml
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v7.0.1
|
|
67
|
+
- uses: actions/setup-python@v7.0.0
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.12"
|
|
70
|
+
- uses: astral-sh/setup-uv@v3
|
|
71
|
+
with:
|
|
72
|
+
enable-cache: true
|
|
73
|
+
- run: uv lock --check
|
|
74
|
+
|
|
52
75
|
build:
|
|
53
76
|
name: Package builds and is installable
|
|
54
77
|
runs-on: ubuntu-latest
|
|
@@ -9,6 +9,10 @@ on:
|
|
|
9
9
|
description: "Build and publish ref to PyPI (Trusted Publishing). Use after tagging or to retry a release."
|
|
10
10
|
type: boolean
|
|
11
11
|
default: false
|
|
12
|
+
publish_tag:
|
|
13
|
+
description: "Git tag to build/publish when publish_to_pypi=true"
|
|
14
|
+
type: string
|
|
15
|
+
default: "v0.2.0"
|
|
12
16
|
|
|
13
17
|
permissions:
|
|
14
18
|
contents: write
|
|
@@ -65,7 +69,7 @@ jobs:
|
|
|
65
69
|
steps:
|
|
66
70
|
- uses: actions/checkout@v4
|
|
67
71
|
with:
|
|
68
|
-
ref:
|
|
72
|
+
ref: ${{ inputs.publish_tag }}
|
|
69
73
|
- uses: actions/setup-python@v5
|
|
70
74
|
with:
|
|
71
75
|
python-version: "3.12"
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/adammatthewsteinberger/codexloop/compare/v0.2.0...v0.3.0) (2026-08-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add a -v/-q verbosity ladder and third-party log control ([b58dfc0](https://github.com/adammatthewsteinberger/codexloop/commit/b58dfc017ce9af34af07cb390ea9a8efd67435d3))
|
|
9
|
+
* add capacity forecasting (measurement only, disabled by default) ([c2a362b](https://github.com/adammatthewsteinberger/codexloop/commit/c2a362bcbd111e7f87e5fb2534eb1687e50dc42d))
|
|
10
|
+
* add the wind-down decision to the run loop state machine ([033b5b2](https://github.com/adammatthewsteinberger/codexloop/commit/033b5b2ecece721832f31cd08e103738c193c812))
|
|
11
|
+
* let the caller name a run with --run-id ([e22c2d0](https://github.com/adammatthewsteinberger/codexloop/commit/e22c2d05c03db5be84ecbbd544da418130c1023c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **tests:** remove residual SHA-collision flake in savepoints test ([#20](https://github.com/adammatthewsteinberger/codexloop/issues/20)) ([b375f71](https://github.com/adammatthewsteinberger/codexloop/commit/b375f71ba5ebda62f178bdc39bb619418932dedf))
|
|
17
|
+
|
|
18
|
+
## [0.3.0](https://github.com/adammatthewsteinberger/codexloop/compare/v0.2.0...v0.3.0) (2026-08-16)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* add a -v/-q verbosity ladder and third-party log control ([b58dfc0](https://github.com/adammatthewsteinberger/codexloop/commit/b58dfc017ce9af34af07cb390ea9a8efd67435d3))
|
|
24
|
+
* add capacity forecasting (measurement only, disabled by default) ([c2a362b](https://github.com/adammatthewsteinberger/codexloop/commit/c2a362bcbd111e7f87e5fb2534eb1687e50dc42d))
|
|
25
|
+
* add the wind-down decision to the run loop state machine ([033b5b2](https://github.com/adammatthewsteinberger/codexloop/commit/033b5b2ecece721832f31cd08e103738c193c812))
|
|
26
|
+
* let the caller name a run with --run-id ([e22c2d0](https://github.com/adammatthewsteinberger/codexloop/commit/e22c2d05c03db5be84ecbbd544da418130c1023c))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* **tests:** remove residual SHA-collision flake in savepoints test ([#20](https://github.com/adammatthewsteinberger/codexloop/issues/20)) ([b375f71](https://github.com/adammatthewsteinberger/codexloop/commit/b375f71ba5ebda62f178bdc39bb619418932dedf))
|
|
32
|
+
|
|
3
33
|
## [0.2.0](https://github.com/adammatthewsteinberger/codexloop/compare/v0.1.1...v0.2.0) (2026-08-14)
|
|
4
34
|
|
|
5
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codexloop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Onion-architected, autonomous OpenAI Codex session runner — never blocks on a human, distinguishes rate limits from exhausted credits, and resumes safely across usage windows.
|
|
5
5
|
Project-URL: Homepage, https://github.com/adammatthewsteinberger/codexloop
|
|
6
6
|
Project-URL: Repository, https://github.com/adammatthewsteinberger/codexloop
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codexloop"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Onion-architected, autonomous OpenAI Codex session runner — never blocks on a human, distinguishes rate limits from exhausted credits, and resumes safely across usage windows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -173,3 +173,16 @@ forbidden_modules = [
|
|
|
173
173
|
"docker",
|
|
174
174
|
"sqlalchemy",
|
|
175
175
|
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
[[tool.importlinter.contracts]]
|
|
179
|
+
name = "Interfaces declare seams, never consume them"
|
|
180
|
+
type = "forbidden"
|
|
181
|
+
source_modules = ["codexloop.application.interfaces"]
|
|
182
|
+
forbidden_modules = [
|
|
183
|
+
"codexloop.application.runner",
|
|
184
|
+
"codexloop.application.usecases",
|
|
185
|
+
"codexloop.infrastructure",
|
|
186
|
+
"codexloop.cli",
|
|
187
|
+
"codexloop.bootstrap",
|
|
188
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Application interfaces -- every seam implemented by infrastructure/ and
|
|
2
|
+
never imported from it.
|
|
3
|
+
|
|
4
|
+
One module per collaborator family so a reader finds a seam by what it does
|
|
5
|
+
rather than by scrolling one long file. `application/ports.py` re-exports
|
|
6
|
+
this package unchanged, so existing imports keep working.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from codexloop.application.interfaces.agent import (
|
|
12
|
+
AgentGateway,
|
|
13
|
+
CapacityProbe,
|
|
14
|
+
RunResources,
|
|
15
|
+
ThreadCatalog,
|
|
16
|
+
)
|
|
17
|
+
from codexloop.application.interfaces.api import (
|
|
18
|
+
ApiGateway,
|
|
19
|
+
)
|
|
20
|
+
from codexloop.application.interfaces.control import (
|
|
21
|
+
ControlInbox,
|
|
22
|
+
RunControl,
|
|
23
|
+
)
|
|
24
|
+
from codexloop.application.interfaces.doctor import (
|
|
25
|
+
DoctorCheck,
|
|
26
|
+
DoctorEnvironment,
|
|
27
|
+
DoctorReport,
|
|
28
|
+
)
|
|
29
|
+
from codexloop.application.interfaces.observability import (
|
|
30
|
+
AuditLog,
|
|
31
|
+
Logger,
|
|
32
|
+
Notifier,
|
|
33
|
+
ProgressReporter,
|
|
34
|
+
RunEventSink,
|
|
35
|
+
StateBus,
|
|
36
|
+
)
|
|
37
|
+
from codexloop.application.interfaces.permissions import (
|
|
38
|
+
PermissionMode,
|
|
39
|
+
)
|
|
40
|
+
from codexloop.application.interfaces.storage import (
|
|
41
|
+
RunSnapshotSink,
|
|
42
|
+
RunStateStore,
|
|
43
|
+
SavePointStore,
|
|
44
|
+
SessionLock,
|
|
45
|
+
)
|
|
46
|
+
from codexloop.application.interfaces.system import (
|
|
47
|
+
Clock,
|
|
48
|
+
Sleeper,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"AgentGateway",
|
|
53
|
+
"ApiGateway",
|
|
54
|
+
"AuditLog",
|
|
55
|
+
"CapacityProbe",
|
|
56
|
+
"Clock",
|
|
57
|
+
"ControlInbox",
|
|
58
|
+
"DoctorCheck",
|
|
59
|
+
"DoctorEnvironment",
|
|
60
|
+
"DoctorReport",
|
|
61
|
+
"Logger",
|
|
62
|
+
"Notifier",
|
|
63
|
+
"PermissionMode",
|
|
64
|
+
"ProgressReporter",
|
|
65
|
+
"RunControl",
|
|
66
|
+
"RunEventSink",
|
|
67
|
+
"RunResources",
|
|
68
|
+
"RunSnapshotSink",
|
|
69
|
+
"RunStateStore",
|
|
70
|
+
"SavePointStore",
|
|
71
|
+
"SessionLock",
|
|
72
|
+
"Sleeper",
|
|
73
|
+
"StateBus",
|
|
74
|
+
"ThreadCatalog",
|
|
75
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The vendor session seam: sending turns, probing capacity, enumerating
|
|
2
|
+
sessions, and the run-scoped resources attached to one."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from codexloop.application.dto import ProbeResult, TurnOutcome
|
|
10
|
+
from codexloop.application.interfaces.permissions import PermissionMode
|
|
11
|
+
from codexloop.domain.model_profile import ModelEffortProfile
|
|
12
|
+
from codexloop.domain.session import ThreadRef
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class AgentGateway(Protocol):
|
|
17
|
+
"""Vendor-agnostic agent session (exec and app-server adapters implement this)."""
|
|
18
|
+
|
|
19
|
+
async def send_turn(self, prompt: str) -> TurnOutcome: ...
|
|
20
|
+
async def close(self) -> None: ...
|
|
21
|
+
async def set_profile(self, profile: ModelEffortProfile) -> None: ...
|
|
22
|
+
async def set_permission_mode(self, mode: PermissionMode) -> None: ...
|
|
23
|
+
async def set_cwd(self, path: str) -> None: ...
|
|
24
|
+
async def set_session_resources(self, resources: Mapping[str, object]) -> None: ...
|
|
25
|
+
def resolve_tool_approval(self, request_id: str, *, allow: bool, reason: str = "") -> bool: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class CapacityProbe(Protocol):
|
|
30
|
+
async def probe(self) -> ProbeResult: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class ThreadCatalog(Protocol):
|
|
35
|
+
"""Our run registry keyed by ``thread_id``, not vendor session discovery."""
|
|
36
|
+
|
|
37
|
+
def list_threads(self) -> Sequence[ThreadRef]: ...
|
|
38
|
+
def get(self, thread_id: str) -> ThreadRef | None: ...
|
|
39
|
+
def record(self, ref: ThreadRef) -> None: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class RunResources(Protocol):
|
|
44
|
+
"""Placeholder for run-scoped attachments applied mid-run."""
|
|
45
|
+
|
|
46
|
+
def as_payload(self) -> Mapping[str, object]: ...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""The out-of-band control plane: commands an operator drops for a live run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from codexloop.domain.control import ControlCommand
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class RunControl(Protocol):
|
|
14
|
+
def poll(self) -> Sequence[ControlCommand]: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class ControlInbox(Protocol):
|
|
19
|
+
"""Where an out-of-band command is dropped for a live run to pick up."""
|
|
20
|
+
|
|
21
|
+
def enqueue(self, command: ControlCommand) -> Path: ...
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""What `doctor` needs from the outside world, and the vocabulary it answers in.
|
|
2
|
+
|
|
3
|
+
The result types live here alongside the Protocol: they are part of the seam's
|
|
4
|
+
vocabulary, not of the use case that consumes it.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
9
|
from collections.abc import Mapping
|
|
6
10
|
from dataclasses import dataclass, field
|
|
7
11
|
from pathlib import Path
|
|
8
|
-
from typing import Protocol
|
|
12
|
+
from typing import Protocol, runtime_checkable
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
@dataclass(frozen=True, slots=True)
|
|
@@ -26,12 +30,6 @@ class DoctorReport:
|
|
|
26
30
|
return all(check.passed for check in self.checks)
|
|
27
31
|
|
|
28
32
|
|
|
33
|
+
@runtime_checkable
|
|
29
34
|
class DoctorEnvironment(Protocol):
|
|
30
35
|
def diagnose(self, *, cwd: Path) -> DoctorReport: ...
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def run_doctor(env: DoctorEnvironment, *, cwd: Path) -> DoctorReport:
|
|
34
|
-
return env.diagnose(cwd=cwd)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
__all__ = ["DoctorCheck", "DoctorEnvironment", "DoctorReport", "run_doctor"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Everything the run emits outward: logs, audit records, progress, events,
|
|
2
|
+
state publications, usage reads, and operator notifications."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class ProgressReporter(Protocol):
|
|
12
|
+
def report(self, event: str, **detail: object) -> None: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class AuditLog(Protocol):
|
|
17
|
+
def append(self, event_type: str, payload: Mapping[str, object]) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class Notifier(Protocol):
|
|
22
|
+
def notify(self, title: str, body: str) -> None: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class Logger(Protocol):
|
|
27
|
+
def bind(self, **kwargs: object) -> Logger: ...
|
|
28
|
+
def debug(self, event: str, **kwargs: object) -> None: ...
|
|
29
|
+
def info(self, event: str, **kwargs: object) -> None: ...
|
|
30
|
+
def warning(self, event: str, **kwargs: object) -> None: ...
|
|
31
|
+
def error(self, event: str, **kwargs: object) -> None: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@runtime_checkable
|
|
35
|
+
class RunEventSink(Protocol):
|
|
36
|
+
def emit(self, event: Mapping[str, object]) -> None: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class StateBus(Protocol):
|
|
41
|
+
def publish(self, event_type: str, payload: Mapping[str, object]) -> None: ...
|
|
42
|
+
def subscribe(self, callback: Callable[[str, Mapping[str, object]], None]) -> None: ...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Permission vocabulary shared across the seam, so vendor CLI flag strings
|
|
2
|
+
never leak up into the application layer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PermissionMode(StrEnum):
|
|
10
|
+
"""Autonomy posture at the application boundary — not Codex CLI flag strings."""
|
|
11
|
+
|
|
12
|
+
AUTONOMOUS = "autonomous"
|
|
13
|
+
READ_ONLY = "read_only"
|
|
14
|
+
FULL_ACCESS = "full_access"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Durable run state: the state store, the run lock, git save points, and the
|
|
2
|
+
snapshot sink."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class RunStateStore(Protocol):
|
|
12
|
+
def load(self, run_id: str) -> dict[str, object] | None: ...
|
|
13
|
+
def save(self, run_id: str, state: Mapping[str, object]) -> None: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class SessionLock(Protocol):
|
|
18
|
+
def acquire(self, thread_id: str) -> bool: ...
|
|
19
|
+
def release(self, thread_id: str) -> None: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class SavePointStore(Protocol):
|
|
24
|
+
def create(self, run_id: str, label: str) -> str: ...
|
|
25
|
+
def list(self, run_id: str) -> Sequence[str]: ...
|
|
26
|
+
def unwind(self, run_id: str, to: str) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class RunSnapshotSink(Protocol):
|
|
31
|
+
def write(self, snapshot: Mapping[str, object]) -> None: ...
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Time and sleeping -- the two ambient effects the run loop needs faked."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class Clock(Protocol):
|
|
11
|
+
def now(self) -> datetime: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class Sleeper(Protocol):
|
|
16
|
+
async def sleep_until(self, when: datetime) -> None: ...
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Backwards-compatible re-export of `application.interfaces`.
|
|
2
|
+
|
|
3
|
+
The Protocols moved into `application/interfaces/` so every seam lives in
|
|
4
|
+
one discoverable place, one module per collaborator family. This shim keeps
|
|
5
|
+
the old `from codexloop.application.ports import X` path working; new code
|
|
6
|
+
should import from `codexloop.application.interfaces`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from codexloop.application.interfaces import (
|
|
12
|
+
AgentGateway,
|
|
13
|
+
ApiGateway,
|
|
14
|
+
AuditLog,
|
|
15
|
+
CapacityProbe,
|
|
16
|
+
Clock,
|
|
17
|
+
ControlInbox,
|
|
18
|
+
DoctorCheck,
|
|
19
|
+
DoctorEnvironment,
|
|
20
|
+
DoctorReport,
|
|
21
|
+
Logger,
|
|
22
|
+
Notifier,
|
|
23
|
+
PermissionMode,
|
|
24
|
+
ProgressReporter,
|
|
25
|
+
RunControl,
|
|
26
|
+
RunEventSink,
|
|
27
|
+
RunResources,
|
|
28
|
+
RunSnapshotSink,
|
|
29
|
+
RunStateStore,
|
|
30
|
+
SavePointStore,
|
|
31
|
+
SessionLock,
|
|
32
|
+
Sleeper,
|
|
33
|
+
StateBus,
|
|
34
|
+
ThreadCatalog,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AgentGateway",
|
|
39
|
+
"ApiGateway",
|
|
40
|
+
"AuditLog",
|
|
41
|
+
"CapacityProbe",
|
|
42
|
+
"Clock",
|
|
43
|
+
"ControlInbox",
|
|
44
|
+
"DoctorCheck",
|
|
45
|
+
"DoctorEnvironment",
|
|
46
|
+
"DoctorReport",
|
|
47
|
+
"Logger",
|
|
48
|
+
"Notifier",
|
|
49
|
+
"PermissionMode",
|
|
50
|
+
"ProgressReporter",
|
|
51
|
+
"RunControl",
|
|
52
|
+
"RunEventSink",
|
|
53
|
+
"RunResources",
|
|
54
|
+
"RunSnapshotSink",
|
|
55
|
+
"RunStateStore",
|
|
56
|
+
"SavePointStore",
|
|
57
|
+
"SessionLock",
|
|
58
|
+
"Sleeper",
|
|
59
|
+
"StateBus",
|
|
60
|
+
"ThreadCatalog",
|
|
61
|
+
]
|
|
@@ -12,6 +12,7 @@ from codexloop.application.ports import (
|
|
|
12
12
|
AgentGateway,
|
|
13
13
|
CapacityProbe,
|
|
14
14
|
Clock,
|
|
15
|
+
Logger,
|
|
15
16
|
Notifier,
|
|
16
17
|
PermissionMode,
|
|
17
18
|
ProgressReporter,
|
|
@@ -45,6 +46,14 @@ from codexloop.domain.control import (
|
|
|
45
46
|
Stop,
|
|
46
47
|
)
|
|
47
48
|
from codexloop.domain.error_codes import ErrorClass, classify_code
|
|
49
|
+
from codexloop.domain.forecast import (
|
|
50
|
+
BurnRate,
|
|
51
|
+
WindDown,
|
|
52
|
+
WindDownPolicy,
|
|
53
|
+
forecast,
|
|
54
|
+
should_wind_down,
|
|
55
|
+
)
|
|
56
|
+
from codexloop.domain.handoff_marker import HandoffMarker
|
|
48
57
|
from codexloop.domain.loop import (
|
|
49
58
|
BackoffUntil,
|
|
50
59
|
Drain,
|
|
@@ -55,6 +64,7 @@ from codexloop.domain.loop import (
|
|
|
55
64
|
RunState,
|
|
56
65
|
SendTurn,
|
|
57
66
|
WaitUntil,
|
|
67
|
+
WindDownAndFinish,
|
|
58
68
|
)
|
|
59
69
|
from codexloop.domain.model_profile import Effort, ModelEffortProfile
|
|
60
70
|
from codexloop.domain.plan import WorkPlan
|
|
@@ -66,6 +76,26 @@ _FAR_FUTURE = timedelta(days=3650)
|
|
|
66
76
|
_ZERO = timedelta(0)
|
|
67
77
|
|
|
68
78
|
|
|
79
|
+
class _NullLogger:
|
|
80
|
+
"""Diagnostics are optional; a run must not require them to be wired."""
|
|
81
|
+
|
|
82
|
+
def bind(self, **kwargs: object) -> _NullLogger:
|
|
83
|
+
del kwargs
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def debug(self, event: str, **kwargs: object) -> None:
|
|
87
|
+
del event, kwargs
|
|
88
|
+
|
|
89
|
+
def info(self, event: str, **kwargs: object) -> None:
|
|
90
|
+
del event, kwargs
|
|
91
|
+
|
|
92
|
+
def warning(self, event: str, **kwargs: object) -> None:
|
|
93
|
+
del event, kwargs
|
|
94
|
+
|
|
95
|
+
def error(self, event: str, **kwargs: object) -> None:
|
|
96
|
+
del event, kwargs
|
|
97
|
+
|
|
98
|
+
|
|
69
99
|
def _discard_artifact(name: str, content: str) -> None:
|
|
70
100
|
del name, content
|
|
71
101
|
|
|
@@ -90,6 +120,9 @@ class RunnerContext:
|
|
|
90
120
|
)
|
|
91
121
|
wait_policy: AdaptiveWaitPolicy | None = None
|
|
92
122
|
max_wait: timedelta | None = None
|
|
123
|
+
logger: Logger | None = None
|
|
124
|
+
handoff_marker_writer: Callable[[HandoffMarker], None] | None = None
|
|
125
|
+
wind_down_policy: WindDownPolicy = field(default_factory=WindDownPolicy)
|
|
93
126
|
run_id: str = "anonymous"
|
|
94
127
|
cwd: str = "."
|
|
95
128
|
model: str = "unknown"
|
|
@@ -110,6 +143,9 @@ class AutonomousRunner:
|
|
|
110
143
|
self._notifier = ctx.notifier
|
|
111
144
|
self._reporter = ctx.reporter
|
|
112
145
|
self._budget = ctx.budget
|
|
146
|
+
self._log = ctx.logger or _NullLogger()
|
|
147
|
+
self._handoff_marker_writer = ctx.handoff_marker_writer
|
|
148
|
+
self._wind_down_policy = ctx.wind_down_policy
|
|
113
149
|
self._wait_policy = ctx.wait_policy or AdaptiveWaitPolicy(WaitConfig(), rand=lambda: 0.0)
|
|
114
150
|
self._max_wait = ctx.max_wait
|
|
115
151
|
self._run_id = ctx.run_id
|
|
@@ -188,7 +224,11 @@ class AutonomousRunner:
|
|
|
188
224
|
self._record_thread(thread_id, started)
|
|
189
225
|
wait_attempt = 0
|
|
190
226
|
self._quota_notified = False
|
|
191
|
-
outcome = LoopOutcome(
|
|
227
|
+
outcome = LoopOutcome(
|
|
228
|
+
capacity=capacity,
|
|
229
|
+
completion=completion,
|
|
230
|
+
wind_down=self._project_wind_down(capacity),
|
|
231
|
+
)
|
|
192
232
|
case Probe():
|
|
193
233
|
probed = await self._probe.probe()
|
|
194
234
|
outcome = LoopOutcome(capacity=probed.outcome)
|
|
@@ -207,6 +247,22 @@ class AutonomousRunner:
|
|
|
207
247
|
first_turn_done=not first_turn,
|
|
208
248
|
plan_text=plan_text,
|
|
209
249
|
)
|
|
250
|
+
case WindDownAndFinish() as wound_down:
|
|
251
|
+
self._write_stop_summary(thread_id, remaining)
|
|
252
|
+
self._persist(
|
|
253
|
+
key=thread_id,
|
|
254
|
+
remaining=remaining,
|
|
255
|
+
first_turn_done=not first_turn,
|
|
256
|
+
plan_text=plan_text,
|
|
257
|
+
reason=f"wind-down: {wound_down.reason}",
|
|
258
|
+
)
|
|
259
|
+
self._write_handoff_marker(wound_down, thread_id, remaining)
|
|
260
|
+
return RunResult(
|
|
261
|
+
success=False,
|
|
262
|
+
reason=f"wind-down: {wound_down.reason}",
|
|
263
|
+
turns=self._ledger.turns,
|
|
264
|
+
thread_id=thread_id,
|
|
265
|
+
)
|
|
210
266
|
case Finish() as finish:
|
|
211
267
|
self._persist(
|
|
212
268
|
key=thread_id,
|
|
@@ -401,6 +457,64 @@ class AutonomousRunner:
|
|
|
401
457
|
)
|
|
402
458
|
self._write_artifact("stop-summary.md", body)
|
|
403
459
|
|
|
460
|
+
def _project_wind_down(self, capacity: CapacityState) -> WindDown | None:
|
|
461
|
+
"""Forecast remaining capacity, but only while the vendor says we are
|
|
462
|
+
not already blocked.
|
|
463
|
+
|
|
464
|
+
Returning None for every non-Available state is what keeps the plan
|
|
465
|
+
windows from ever influencing whether a turn is *sent*: once a real
|
|
466
|
+
rejection lands, the state machine's capacity routing owns it.
|
|
467
|
+
"""
|
|
468
|
+
if not isinstance(capacity, Available):
|
|
469
|
+
return None
|
|
470
|
+
now = self._clock.now()
|
|
471
|
+
turns = self._ledger.turns
|
|
472
|
+
projection = forecast(
|
|
473
|
+
capacity,
|
|
474
|
+
turns_spent=turns,
|
|
475
|
+
max_turns=self._budget.max_turns,
|
|
476
|
+
dollars_spent=self._ledger.dollars,
|
|
477
|
+
max_dollars=self._budget.max_dollars,
|
|
478
|
+
observed=BurnRate(
|
|
479
|
+
turns=turns,
|
|
480
|
+
elapsed_seconds=self._ledger.elapsed.total_seconds(),
|
|
481
|
+
dollars=self._ledger.dollars,
|
|
482
|
+
),
|
|
483
|
+
capacity_as_of=now,
|
|
484
|
+
now=now,
|
|
485
|
+
policy=self._wind_down_policy,
|
|
486
|
+
)
|
|
487
|
+
self._log.debug(
|
|
488
|
+
"capacity.forecast",
|
|
489
|
+
headroom=projection.binding.fraction,
|
|
490
|
+
source=projection.binding.source,
|
|
491
|
+
turns_until_exhaustion=projection.turns_until_exhaustion,
|
|
492
|
+
)
|
|
493
|
+
return should_wind_down(projection, self._wind_down_policy, turns_spent=turns)
|
|
494
|
+
|
|
495
|
+
def _write_handoff_marker(
|
|
496
|
+
self, decision: WindDownAndFinish, thread_id: str | None, remaining: list[str]
|
|
497
|
+
) -> None:
|
|
498
|
+
"""Written last, after the summary and state it names, so that a marker
|
|
499
|
+
on disk means everything it points at is on disk."""
|
|
500
|
+
if self._handoff_marker_writer is None:
|
|
501
|
+
return
|
|
502
|
+
binding = decision.forecast.binding
|
|
503
|
+
self._handoff_marker_writer(
|
|
504
|
+
HandoffMarker(
|
|
505
|
+
run_id=self._run_id,
|
|
506
|
+
reason=decision.reason,
|
|
507
|
+
produced_at=self._clock.now(),
|
|
508
|
+
headroom=binding.fraction,
|
|
509
|
+
headroom_source=binding.source,
|
|
510
|
+
resets_at=binding.resets_at,
|
|
511
|
+
session_id=thread_id,
|
|
512
|
+
turns_spent=self._ledger.turns,
|
|
513
|
+
dollars_spent=self._ledger.dollars,
|
|
514
|
+
remaining_work=tuple(remaining),
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
|
|
404
518
|
|
|
405
519
|
def _permission_mode(sandbox: SandboxMode) -> PermissionMode:
|
|
406
520
|
match sandbox:
|