multi-forge 0.5.0__tar.gz → 0.6.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.
- {multi_forge-0.5.0 → multi_forge-0.6.0}/PKG-INFO +16 -11
- {multi_forge-0.5.0 → multi_forge-0.6.0}/README.md +15 -10
- {multi_forge-0.5.0 → multi_forge-0.6.0}/pyproject.toml +1 -1
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/activity.py +10 -4
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/extensions.py +69 -8
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/_group.py +5 -4
- multi_forge-0.6.0/src/forge/cli/hooks/codex_patch.py +129 -0
- multi_forge-0.6.0/src/forge/cli/hooks/codex_policy.py +197 -0
- multi_forge-0.6.0/src/forge/cli/hooks/codex_transfer.py +113 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/commands.py +223 -19
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/direct_commands.py +113 -1
- multi_forge-0.6.0/src/forge/cli/hooks/policy.py +315 -0
- multi_forge-0.6.0/src/forge/cli/hooks/protocols.py +57 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/policy.py +247 -7
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/runtime.py +62 -1
- multi_forge-0.6.0/src/forge/cli/session_codex.py +489 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_fork.py +29 -3
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_lifecycle.py +87 -175
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_manage.py +30 -3
- multi_forge-0.6.0/src/forge/cli/session_model_pin.py +196 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/codex.py +47 -22
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/codex_stream.py +23 -6
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/types.py +5 -0
- multi_forge-0.6.0/src/forge/core/ops/codex_bridge.py +390 -0
- multi_forge-0.6.0/src/forge/core/ops/codex_enrollment.py +307 -0
- multi_forge-0.6.0/src/forge/core/ops/codex_interactive.py +560 -0
- multi_forge-0.6.0/src/forge/core/ops/codex_session.py +579 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/gc.py +18 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/usage_summary.py +45 -11
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/runtime/codex_preflight.py +61 -29
- multi_forge-0.6.0/src/forge/core/runtime/codex_rollouts.py +187 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/runtime/registry.py +61 -36
- multi_forge-0.6.0/src/forge/install/codex_hooks.py +515 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/installer.py +126 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/models.py +33 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/base.py +15 -8
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/engine.py +115 -63
- multi_forge-0.6.0/src/forge/policy/semantic/plan_check.py +561 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/promotion.py +1 -1
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/supervisor.py +4 -4
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/types.py +8 -5
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/extractor.py +1 -1
- multi_forge-0.6.0/src/forge/session/codex_handoff.py +253 -0
- multi_forge-0.6.0/src/forge/session/codex_invoke.py +81 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/exceptions.py +13 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/manager.py +17 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/models.py +51 -1
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/overrides.py +9 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/transfer.py +1 -1
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/2-extension.md +43 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist.md +13 -11
- multi_forge-0.5.0/src/forge/cli/hooks/policy.py +0 -212
- multi_forge-0.5.0/src/forge/cli/hooks/protocols.py +0 -53
- multi_forge-0.5.0/src/forge/core/ops/codex_bridge.py +0 -244
- {multi_forge-0.5.0 → multi_forge-0.6.0}/.gitignore +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/LICENSE +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/NOTICE +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/agents/.gitkeep +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/commands/.gitkeep +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/adapters/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/adapters/litellm.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/creation.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/registry.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/auth.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/backend.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/claude.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/config_cmd.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/editor.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/gc.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/guards.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/_helpers.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/install.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/read_hygiene.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/verification.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/launch_confirmation.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/logs.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/main.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/memory.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/memory_report.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/memory_writer.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/output.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/proxy.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/proxy_audit.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/proxy_costs.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/search.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_addendum.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/status_line.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/context.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/names.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/palette.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/registry.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/throttle.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/transfer.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/workflow.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/dataclass_utils.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/backends/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/backends/litellm.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/anthropic-passthrough.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-anthropic-local.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-anthropic.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini-flash-local.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini-local.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini-test.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-openai-codex-local.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-openai-local.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-openai.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-anthropic.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-deepseek.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-gemini-flash.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-gemini.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-glm.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-kimi.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-minimax.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-openai-codex.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-openai.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-qwen.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/loader.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/schema.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/capabilities.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/credentials_file.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/protocols.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/secrets.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/template_secrets.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/model_catalog.yaml +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/system_prompt_addendums/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/system_prompt_addendums/gemini.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/system_prompt_addendums/openai.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/_lifecycle.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/claude.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/base.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/litellm.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/openai_compat.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/openrouter.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/credentials.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/detection.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/errors.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/protocols.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/types.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/logging.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/models/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/models/catalog.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/models/types.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/naming.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/context.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/proxy.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/resolution.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/session.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/session_context.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/transfer.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/paths.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/process.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/cost_tracking.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/env.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/headless_json.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/proxy.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/routing.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/session_runner.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/structured_output.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/tagger.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/throttle.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/run_id.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/runtime/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/exceptions.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/io.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/lock.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/timestamps.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/transcript.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/typing_helpers.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/billing.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/correlation.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/emit.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/ledger.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/vocabulary.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/workqueue/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/workqueue/queue.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/workqueue/types.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/cli.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/exceptions.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/hooks.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/preset.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/settings_merge.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/tracking.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/version.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/coding_standards.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/registry.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/tdd.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/protocols.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/queries.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/verdict.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/store.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/config.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/handlers.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/prompts.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/branches.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/config.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/divergence.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/policy.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/stages.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/audit_logger.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/base_client.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/client_adapter.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/client_factory.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/converters.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/cost_logger.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/cost_tracker.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/data_models.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/error_hints.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/intercept.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/metrics.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/model_spec.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/passthrough.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxies.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxy_identity.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxy_orchestrator.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxy_startup.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/server.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/utils.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/adversarial.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/consensus.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/engine.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/models.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview-performance.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview-quick.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview-security.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/docreview-quick.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/docreview.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/thinkdeep.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/routing.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/synthesis.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/runtime_config.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/bm25_store.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/content_store.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/engine.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/exceptions.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/index_state.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/store.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/tokenizer.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/active.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/artifacts.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/cleanup.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/invoke.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/paths.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/relocate.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/cleanup.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/config.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/direct_model.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/effective.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/hooks/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/hooks/models.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/hooks/session_start.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/identity.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/index.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/memory_inheritance.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/memory_writer.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/passport.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/plan_resolution.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/prev_sessions.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/project_memory.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/shadow_curation.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/store.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/validation.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/cleanup.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/config_copy.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/create.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/__init__.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/container.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/docker.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/secrets.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/analyze/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/challenge/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/resources/code_consensus_evaluation.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/resources/consensus_evaluation.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/resources/synthesis.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/debate/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/debate/resources/code_debate_evaluation.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/debate/resources/debate_evaluation.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/panel/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/panel/resources/synthesis.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/0-enable.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/1-preflight.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/10-resume.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/11-config.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/12-search.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/13-policy.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/14-workflow.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/15-skills.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/16-memory.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/17-info.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/18-disable.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/19-uninstall.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/20-cleanup.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/3-authentication.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/4-proxy.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/5-session.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/6-hook.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/7-costs.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/8-status-line.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/9-direct-commands.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/report-template.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/scripts/start-container.sh +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/scripts/walkthrough-state.py +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/claude-4.6.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/claude-4.8.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/gemini-3.1.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/gpt-5.5.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/skills-writing-guide.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code-anthropic.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code-gemini.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code-openai.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs-anthropic.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs-gemini.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs-openai.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/smoke-test/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/smoke-test/scripts/smoke-test.sh +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code-anthropic.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code-gemini.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code-openai.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs-anthropic.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs-gemini.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs-openai.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/SKILL.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/resources/checklist.md +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/scripts/run-in-repo.sh +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/scripts/setup-test-repo.sh +0 -0
- {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/scripts/walkthrough-state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-forge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Multi-runtime agent toolkit: proxy routing, cost control, session management, policy enforcement, and workflow orchestration
|
|
5
5
|
Project-URL: Homepage, https://github.com/hapa1i/multi-forge
|
|
6
6
|
Project-URL: Repository, https://github.com/hapa1i/multi-forge
|
|
@@ -56,15 +56,18 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
**Multi-runtime agent toolkit: proxy routing, cost control, session management, and policy enforcement for coding
|
|
57
57
|
agents.**
|
|
58
58
|
|
|
59
|
-
Forge sits between you and your coding agent (Claude Code
|
|
60
|
-
multi-provider model routing, cost visibility with spend caps, and autonomous
|
|
61
|
-
`forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen
|
|
62
|
-
state across sessions, and enforcing policies.
|
|
59
|
+
Forge sits between you and your coding agent (Claude Code by default, with Codex as an alternate runtime and Gemini
|
|
60
|
+
next), adding persistent sessions, multi-provider model routing, cost visibility with spend caps, and autonomous
|
|
61
|
+
verification. You run `forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen
|
|
62
|
+
model provider, tracking state across sessions, and enforcing policies.
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
# Use Claude with session tracking (no proxy needed)
|
|
66
66
|
forge session start
|
|
67
67
|
|
|
68
|
+
# Or run a different runtime entirely -- Codex as an alternate frontend
|
|
69
|
+
forge session start --runtime codex # interactive TUI; hooks/policy need a one-time Codex trust enrollment
|
|
70
|
+
|
|
68
71
|
# Or route through different model providers (after creating proxies -- see Quick Start)
|
|
69
72
|
forge session start planner --proxy openrouter-openai # GPT for planning
|
|
70
73
|
forge session start --proxy openrouter-gemini # Gemini for review
|
|
@@ -223,12 +226,14 @@ Run `forge <command> --help` for details on any command.
|
|
|
223
226
|
|
|
224
227
|
## Documentation
|
|
225
228
|
|
|
226
|
-
| Audience
|
|
227
|
-
|
|
|
228
|
-
| **Users**
|
|
229
|
-
| **Developers**
|
|
230
|
-
| **Architecture**
|
|
231
|
-
| **
|
|
229
|
+
| Audience | Location | Contents |
|
|
230
|
+
| ------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
|
|
231
|
+
| **Users** | [docs/end-user/](docs/end-user/) | Tour, guides for sessions, proxies, policies, ... |
|
|
232
|
+
| **Developers** | [docs/developer/](docs/developer/) | Setup, coding standards, testing guidelines |
|
|
233
|
+
| **Architecture** | [docs/design.md](docs/design.md) | Core system narrative, data flow, invariants |
|
|
234
|
+
| **Workflow design** | [docs/design_workflows.md](docs/design_workflows.md) | Policy, skills, workflow runners, memory architecture |
|
|
235
|
+
| **CLI reference** | [docs/cli_reference.md](docs/cli_reference.md) | Terminal and direct-command inventory |
|
|
236
|
+
| **Work Board** | [docs/board/](docs/board/) | Cards, checklists, change log, implementation memory |
|
|
232
237
|
|
|
233
238
|
## Contributing
|
|
234
239
|
|
|
@@ -15,15 +15,18 @@
|
|
|
15
15
|
**Multi-runtime agent toolkit: proxy routing, cost control, session management, and policy enforcement for coding
|
|
16
16
|
agents.**
|
|
17
17
|
|
|
18
|
-
Forge sits between you and your coding agent (Claude Code
|
|
19
|
-
multi-provider model routing, cost visibility with spend caps, and autonomous
|
|
20
|
-
`forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen
|
|
21
|
-
state across sessions, and enforcing policies.
|
|
18
|
+
Forge sits between you and your coding agent (Claude Code by default, with Codex as an alternate runtime and Gemini
|
|
19
|
+
next), adding persistent sessions, multi-provider model routing, cost visibility with spend caps, and autonomous
|
|
20
|
+
verification. You run `forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen
|
|
21
|
+
model provider, tracking state across sessions, and enforcing policies.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# Use Claude with session tracking (no proxy needed)
|
|
25
25
|
forge session start
|
|
26
26
|
|
|
27
|
+
# Or run a different runtime entirely -- Codex as an alternate frontend
|
|
28
|
+
forge session start --runtime codex # interactive TUI; hooks/policy need a one-time Codex trust enrollment
|
|
29
|
+
|
|
27
30
|
# Or route through different model providers (after creating proxies -- see Quick Start)
|
|
28
31
|
forge session start planner --proxy openrouter-openai # GPT for planning
|
|
29
32
|
forge session start --proxy openrouter-gemini # Gemini for review
|
|
@@ -182,12 +185,14 @@ Run `forge <command> --help` for details on any command.
|
|
|
182
185
|
|
|
183
186
|
## Documentation
|
|
184
187
|
|
|
185
|
-
| Audience
|
|
186
|
-
|
|
|
187
|
-
| **Users**
|
|
188
|
-
| **Developers**
|
|
189
|
-
| **Architecture**
|
|
190
|
-
| **
|
|
188
|
+
| Audience | Location | Contents |
|
|
189
|
+
| ------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
|
|
190
|
+
| **Users** | [docs/end-user/](docs/end-user/) | Tour, guides for sessions, proxies, policies, ... |
|
|
191
|
+
| **Developers** | [docs/developer/](docs/developer/) | Setup, coding standards, testing guidelines |
|
|
192
|
+
| **Architecture** | [docs/design.md](docs/design.md) | Core system narrative, data flow, invariants |
|
|
193
|
+
| **Workflow design** | [docs/design_workflows.md](docs/design_workflows.md) | Policy, skills, workflow runners, memory architecture |
|
|
194
|
+
| **CLI reference** | [docs/cli_reference.md](docs/cli_reference.md) | Terminal and direct-command inventory |
|
|
195
|
+
| **Work Board** | [docs/board/](docs/board/) | Cards, checklists, change log, implementation memory |
|
|
191
196
|
|
|
192
197
|
## Contributing
|
|
193
198
|
|
|
@@ -113,10 +113,16 @@ def _render(summary: SessionActivitySummary, *, days: int | None) -> None:
|
|
|
113
113
|
|
|
114
114
|
pol = summary.policy
|
|
115
115
|
if pol and pol.has_content:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
if pol.plan_check_allow or pol.plan_check_needs_review:
|
|
117
|
+
console.print(
|
|
118
|
+
f"\n[bold]Plan check (tier-1)[/bold]: {pol.plan_check_allow} allow · "
|
|
119
|
+
f"{pol.plan_check_needs_review} needs review"
|
|
120
|
+
)
|
|
121
|
+
if pol.supervisor_allow or pol.supervisor_warn or pol.supervisor_deny or pol.total_warnings:
|
|
122
|
+
console.print(
|
|
123
|
+
f"\n[bold]Supervisor[/bold]: {pol.supervisor_allow} allow · "
|
|
124
|
+
f"{pol.supervisor_warn} warn · {pol.supervisor_deny} block"
|
|
125
|
+
)
|
|
120
126
|
for warning in pol.recent_warnings:
|
|
121
127
|
console.print(f" [yellow]•[/yellow] {warning}")
|
|
122
128
|
|
|
@@ -99,15 +99,18 @@ def _parse_modules(modules_str: str | None) -> set[InstallModule] | None:
|
|
|
99
99
|
return {InstallModule(m.strip()) for m in modules_str.split(",")}
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def _count_actions(plan: InstallPlan) -> tuple[int, int]:
|
|
102
|
+
def _count_actions(plan: InstallPlan) -> tuple[int, int, int]:
|
|
103
103
|
"""Count non-skip actions in a plan.
|
|
104
104
|
|
|
105
105
|
Returns:
|
|
106
|
-
Tuple of (file_actions, settings_actions) that
|
|
106
|
+
Tuple of (file_actions, settings_actions, codex_actions) that
|
|
107
|
+
actually change something. A codex install/update counts as an
|
|
108
|
+
action so a codex-only change never renders "Already up to date.".
|
|
107
109
|
"""
|
|
108
110
|
file_actions = sum(1 for f in plan.files if f.action != "skip")
|
|
109
111
|
settings_actions = sum(1 for s in plan.settings if s.action != "skip")
|
|
110
|
-
|
|
112
|
+
codex_actions = 1 if plan.codex is not None and plan.codex.action in ("install", "update") else 0
|
|
113
|
+
return file_actions, settings_actions, codex_actions
|
|
111
114
|
|
|
112
115
|
|
|
113
116
|
# Modules that are intentionally empty in the source tree (only .gitkeep).
|
|
@@ -165,8 +168,8 @@ def _print_completion_message(
|
|
|
165
168
|
tracking: TrackingStore,
|
|
166
169
|
) -> None:
|
|
167
170
|
"""Print appropriate completion message based on what was done."""
|
|
168
|
-
file_actions, settings_actions = _count_actions(plan)
|
|
169
|
-
total_actions = file_actions + settings_actions
|
|
171
|
+
file_actions, settings_actions, codex_actions = _count_actions(plan)
|
|
172
|
+
total_actions = file_actions + settings_actions + codex_actions
|
|
170
173
|
|
|
171
174
|
_warn_if_modules_have_no_files(plan, scope, project_root, tracking)
|
|
172
175
|
|
|
@@ -178,6 +181,8 @@ def _print_completion_message(
|
|
|
178
181
|
parts.append(f"{file_actions} file{'s' if file_actions != 1 else ''}")
|
|
179
182
|
if settings_actions > 0:
|
|
180
183
|
parts.append(f"{settings_actions} setting{'s' if settings_actions != 1 else ''}")
|
|
184
|
+
if codex_actions > 0:
|
|
185
|
+
parts.append("Codex hooks")
|
|
181
186
|
console.print(f"\n[green]Extensions enabled.[/green] ({', '.join(parts)} updated)")
|
|
182
187
|
|
|
183
188
|
print_tip(
|
|
@@ -200,6 +205,30 @@ def _print_completion_message(
|
|
|
200
205
|
required = gated[0][1].value
|
|
201
206
|
print_tip(f"Additional skills available with --profile {required}: {skill_list}", console=console)
|
|
202
207
|
|
|
208
|
+
_print_codex_completion(plan, scope)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _print_codex_completion(plan: InstallPlan, scope: InstallScope) -> None:
|
|
212
|
+
"""Print the trust-ceremony guidance (or skip notice) for the codex plan.
|
|
213
|
+
|
|
214
|
+
Registration alone is inert: Codex hooks fire only after the user's
|
|
215
|
+
one-time interactive trust ceremony, which Forge can neither perform nor
|
|
216
|
+
verify -- so a fresh registration always names the ceremony explicitly.
|
|
217
|
+
"""
|
|
218
|
+
codex = plan.codex
|
|
219
|
+
if codex is None:
|
|
220
|
+
return
|
|
221
|
+
if codex.action in ("install", "update"):
|
|
222
|
+
where = "in any project" if scope == InstallScope.USER else "in this project"
|
|
223
|
+
config = display_path(codex.config_path) if codex.config_path else "config.toml"
|
|
224
|
+
console.print("\n[dim]Next steps (Codex hooks):[/dim]")
|
|
225
|
+
console.print(f" - Forge hooks are registered in {config} but stay inert until trusted.")
|
|
226
|
+
console.print(f" - Run 'codex' interactively {where} and grant trust when prompted (one-time).")
|
|
227
|
+
elif codex.action == "conflict":
|
|
228
|
+
console.print(f"\n[yellow]Warning:[/yellow] Codex hook registration skipped: {codex.reason}")
|
|
229
|
+
elif codex.action == "unavailable":
|
|
230
|
+
console.print(f"\n[dim]Codex hooks skipped: {codex.reason}.[/dim]")
|
|
231
|
+
|
|
203
232
|
|
|
204
233
|
def _validate_anchor(anchor: Path) -> None:
|
|
205
234
|
"""Reject anchors that point inside a ``.claude/`` directory.
|
|
@@ -324,6 +353,23 @@ def _print_plan(plan: InstallPlan, dry_run: bool = False) -> None:
|
|
|
324
353
|
|
|
325
354
|
console.print(table)
|
|
326
355
|
|
|
356
|
+
if plan.codex is not None:
|
|
357
|
+
console.print(f"\n{prefix}[bold]Codex hooks (config.toml):[/bold]")
|
|
358
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
359
|
+
table.add_column("ACTION", style="dim")
|
|
360
|
+
table.add_column("TARGET")
|
|
361
|
+
table.add_column("REASON", style="dim")
|
|
362
|
+
style = {
|
|
363
|
+
"install": "green",
|
|
364
|
+
"update": "yellow",
|
|
365
|
+
"skip": "dim",
|
|
366
|
+
"conflict": "yellow", # best-effort: degrades to skip, never blocks
|
|
367
|
+
"unavailable": "dim",
|
|
368
|
+
}.get(plan.codex.action, "")
|
|
369
|
+
target = display_path(plan.codex.config_path) if plan.codex.config_path else ""
|
|
370
|
+
table.add_row(plan.codex.action, target, plan.codex.reason or "", style=style)
|
|
371
|
+
console.print(table)
|
|
372
|
+
|
|
327
373
|
if plan.has_conflicts:
|
|
328
374
|
console.print(f"\n{prefix}[bold red]Conflicts detected:[/bold red]")
|
|
329
375
|
for c in plan.conflicts:
|
|
@@ -467,7 +513,7 @@ def extensions() -> None:
|
|
|
467
513
|
"--with",
|
|
468
514
|
"-w",
|
|
469
515
|
"with_modules",
|
|
470
|
-
help="Add modules (comma-separated: commands,agents,skills,hooks,status-line,permissions)",
|
|
516
|
+
help="Add modules (comma-separated: commands,agents,skills,hooks,status-line,permissions,codex-hooks)",
|
|
471
517
|
)
|
|
472
518
|
@click.option(
|
|
473
519
|
"--without",
|
|
@@ -672,8 +718,8 @@ def sync_cmd(scope: str | None, force: bool) -> None:
|
|
|
672
718
|
console.print("\n[red]Sync failed due to conflicts.[/red]")
|
|
673
719
|
sys.exit(1)
|
|
674
720
|
else:
|
|
675
|
-
file_actions, settings_actions = _count_actions(plan)
|
|
676
|
-
total_actions = file_actions + settings_actions
|
|
721
|
+
file_actions, settings_actions, codex_actions = _count_actions(plan)
|
|
722
|
+
total_actions = file_actions + settings_actions + codex_actions
|
|
677
723
|
if total_actions == 0:
|
|
678
724
|
console.print("\n[dim]Already up to date.[/dim]")
|
|
679
725
|
else:
|
|
@@ -682,8 +728,15 @@ def sync_cmd(scope: str | None, force: bool) -> None:
|
|
|
682
728
|
parts.append(f"{file_actions} file{'s' if file_actions != 1 else ''}")
|
|
683
729
|
if settings_actions > 0:
|
|
684
730
|
parts.append(f"{settings_actions} setting{'s' if settings_actions != 1 else ''}")
|
|
731
|
+
if codex_actions > 0:
|
|
732
|
+
parts.append("Codex hooks")
|
|
685
733
|
console.print(f"\n[green]Sync complete.[/green] ({', '.join(parts)} updated)")
|
|
686
734
|
|
|
735
|
+
# A synced block can carry NEW entries whose trust is not yet
|
|
736
|
+
# granted (per-entry trusted_hash) -- the ceremony guidance
|
|
737
|
+
# matters most exactly here.
|
|
738
|
+
_print_codex_completion(plan, install_scope)
|
|
739
|
+
|
|
687
740
|
except NoForgeInstallationError as e:
|
|
688
741
|
console.print(f"[red]Error:[/red] {e}")
|
|
689
742
|
sys.exit(1)
|
|
@@ -788,6 +841,10 @@ def disable_cmd(scope: str | None, uninstall_all: bool, yes: bool) -> None:
|
|
|
788
841
|
console.print("[bold]Settings:[/bold]")
|
|
789
842
|
console.print(table)
|
|
790
843
|
|
|
844
|
+
if existing.codex_config_path:
|
|
845
|
+
console.print("\n[bold]Codex hooks:[/bold]")
|
|
846
|
+
console.print(f" [red]remove[/red] managed block in {display_path(existing.codex_config_path)}")
|
|
847
|
+
|
|
791
848
|
if not yes:
|
|
792
849
|
if not click.confirm("\nProceed with disable?"):
|
|
793
850
|
console.print("[dim]Cancelled.[/dim]")
|
|
@@ -926,6 +983,8 @@ def status_cmd(scope: str | None, path: str | None, show_all: bool, as_json: boo
|
|
|
926
983
|
"modules": list(inst.modules_enabled),
|
|
927
984
|
"files_count": len(inst.files),
|
|
928
985
|
"settings_count": len(inst.settings_entries),
|
|
986
|
+
"codex_config_path": inst.codex_config_path,
|
|
987
|
+
"codex_commands": list(inst.codex_commands),
|
|
929
988
|
"installed_at": inst.installed_at,
|
|
930
989
|
"updated_at": inst.updated_at,
|
|
931
990
|
}
|
|
@@ -966,6 +1025,8 @@ def status_cmd(scope: str | None, path: str | None, show_all: bool, as_json: boo
|
|
|
966
1025
|
console.print(f" Modules: {', '.join(installation.modules_enabled)}")
|
|
967
1026
|
console.print(f" Files: {len(installation.files)}")
|
|
968
1027
|
console.print(f" Settings: {len(installation.settings_entries)} entries")
|
|
1028
|
+
if installation.codex_config_path:
|
|
1029
|
+
console.print(f" Codex: hooks registered in {display_path(installation.codex_config_path)}")
|
|
969
1030
|
console.print(f" Installed: {installation.installed_at}")
|
|
970
1031
|
console.print(f" Updated: {installation.updated_at}")
|
|
971
1032
|
|
|
@@ -8,11 +8,12 @@ import click
|
|
|
8
8
|
@click.group(name="hook", hidden=True)
|
|
9
9
|
@click.pass_context
|
|
10
10
|
def hooks(ctx: click.Context) -> None:
|
|
11
|
-
"""Hook handlers invoked by
|
|
11
|
+
"""Hook handlers invoked by agent runtimes.
|
|
12
12
|
|
|
13
|
-
Most subcommands are invoked automatically by Claude Code
|
|
14
|
-
configured in .claude/settings.local.json
|
|
15
|
-
|
|
13
|
+
Most subcommands are invoked automatically by runtime hooks: Claude Code's
|
|
14
|
+
are configured in .claude/settings.local.json; Codex's (codex-policy-check)
|
|
15
|
+
are registered in a Codex config and require trust enrollment. The 'enable'
|
|
16
|
+
and 'disable' subcommands are user-facing.
|
|
16
17
|
"""
|
|
17
18
|
from forge.core.logging import configure_debug_logging
|
|
18
19
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Parser for Codex's apply_patch envelope (PreToolUse ``tool_input.command``).
|
|
2
|
+
|
|
3
|
+
System boundary: the patch text arrives in a Codex hook payload (external data),
|
|
4
|
+
so the parser is strict but the caller fails open -- ``None`` means "not a patch
|
|
5
|
+
Forge can reason about", and the hook allows the action rather than guessing.
|
|
6
|
+
Codex's own apply_patch rejects input outside this grammar, so failing open on
|
|
7
|
+
malformed text converges with native behavior.
|
|
8
|
+
|
|
9
|
+
Grammar (codex-cli 0.138.0; witness fixture in ``tests/fixtures/codex/hooks/``)::
|
|
10
|
+
|
|
11
|
+
*** Begin Patch
|
|
12
|
+
*** Add File: <path> | *** Update File: <path> | *** Delete File: <path>
|
|
13
|
+
*** Move to: <path> (only immediately after an Update header)
|
|
14
|
+
<body lines prefixed +, -, space, or @@; blank lines are context>
|
|
15
|
+
*** End of File (tolerated inside add/update sections)
|
|
16
|
+
*** End Patch
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
from forge.policy.types import extract_added_lines
|
|
25
|
+
|
|
26
|
+
PatchOpKind = Literal["add", "update", "delete"]
|
|
27
|
+
|
|
28
|
+
_BEGIN = "*** Begin Patch"
|
|
29
|
+
_END = "*** End Patch"
|
|
30
|
+
_EOF_MARKER = "*** End of File"
|
|
31
|
+
_MOVE_TO = "*** Move to: "
|
|
32
|
+
_HEADERS: tuple[tuple[str, PatchOpKind], ...] = (
|
|
33
|
+
("*** Add File: ", "add"),
|
|
34
|
+
("*** Update File: ", "update"),
|
|
35
|
+
("*** Delete File: ", "delete"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class PatchFileOp:
|
|
41
|
+
"""One file operation parsed from an apply_patch envelope.
|
|
42
|
+
|
|
43
|
+
``path`` is the post-op path (the "Move to" target when present) -- policies
|
|
44
|
+
judge where content lands, not where it came from. The pre-move path is
|
|
45
|
+
recoverable from ``raw_section``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
kind: PatchOpKind
|
|
49
|
+
path: str
|
|
50
|
+
move_to: str | None
|
|
51
|
+
added_content: str # introduced lines ("" for delete)
|
|
52
|
+
raw_section: str # verbatim header + body (raw_diff source)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class _Section:
|
|
57
|
+
kind: PatchOpKind
|
|
58
|
+
path: str
|
|
59
|
+
move_to: str | None = None
|
|
60
|
+
header_lines: list[str] = field(default_factory=list)
|
|
61
|
+
body: list[str] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
def finalize(self) -> PatchFileOp:
|
|
64
|
+
body = "\n".join(self.body)
|
|
65
|
+
return PatchFileOp(
|
|
66
|
+
kind=self.kind,
|
|
67
|
+
path=self.move_to or self.path,
|
|
68
|
+
move_to=self.move_to,
|
|
69
|
+
added_content="" if self.kind == "delete" else extract_added_lines(body),
|
|
70
|
+
raw_section="\n".join(self.header_lines + self.body),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_apply_patch(command: str) -> list[PatchFileOp] | None:
|
|
75
|
+
"""Parse an apply_patch envelope into per-file operations.
|
|
76
|
+
|
|
77
|
+
Returns None for anything outside the known grammar (caller fails open);
|
|
78
|
+
an empty envelope (Begin + End only) returns [].
|
|
79
|
+
"""
|
|
80
|
+
lines = [line.rstrip("\r") for line in command.split("\n")]
|
|
81
|
+
while lines and not lines[0].strip():
|
|
82
|
+
lines.pop(0)
|
|
83
|
+
while lines and not lines[-1].strip():
|
|
84
|
+
lines.pop()
|
|
85
|
+
if not lines or lines[0] != _BEGIN or lines[-1] != _END or len(lines) < 2:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
ops: list[PatchFileOp] = []
|
|
89
|
+
current: _Section | None = None
|
|
90
|
+
|
|
91
|
+
for line in lines[1:-1]:
|
|
92
|
+
header = _match_header(line)
|
|
93
|
+
if header is not None:
|
|
94
|
+
kind, path = header
|
|
95
|
+
if not path:
|
|
96
|
+
return None
|
|
97
|
+
if current is not None:
|
|
98
|
+
ops.append(current.finalize())
|
|
99
|
+
current = _Section(kind=kind, path=path, header_lines=[line])
|
|
100
|
+
elif line.startswith(_MOVE_TO):
|
|
101
|
+
# Only valid immediately after an Update header (no body yet, one move max).
|
|
102
|
+
target = line[len(_MOVE_TO) :].strip()
|
|
103
|
+
if current is None or current.kind != "update" or current.body or current.move_to or not target:
|
|
104
|
+
return None
|
|
105
|
+
current.move_to = target
|
|
106
|
+
current.header_lines.append(line)
|
|
107
|
+
elif line == _EOF_MARKER:
|
|
108
|
+
if current is None or current.kind == "delete":
|
|
109
|
+
return None
|
|
110
|
+
current.body.append(line) # kept verbatim in raw_section; extract_added_lines ignores it
|
|
111
|
+
elif current is None:
|
|
112
|
+
return None # body line before any section header
|
|
113
|
+
elif current.kind == "delete":
|
|
114
|
+
return None # Delete sections are bodyless in the grammar
|
|
115
|
+
elif line == "" or line.startswith(("+", "-", " ", "@@")):
|
|
116
|
+
current.body.append(line)
|
|
117
|
+
else:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
if current is not None:
|
|
121
|
+
ops.append(current.finalize())
|
|
122
|
+
return ops
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _match_header(line: str) -> tuple[PatchOpKind, str] | None:
|
|
126
|
+
for prefix, kind in _HEADERS:
|
|
127
|
+
if line.startswith(prefix):
|
|
128
|
+
return kind, line[len(prefix) :].strip()
|
|
129
|
+
return None
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Codex halves of the hook seam: payload -> ActionContexts, decision -> stdout JSON.
|
|
2
|
+
|
|
3
|
+
The Codex counterparts of ``ClaudeHookAdapter``/``ClaudeHookResponder`` (policy.py),
|
|
4
|
+
filling the runtime-neutral protocols in ``protocols.py``. Probe-pinned facts this
|
|
5
|
+
module encodes (codex_frontend Phase 1, codex-cli 0.138.0):
|
|
6
|
+
|
|
7
|
+
- Codex file writes arrive as ``tool_name="apply_patch"`` with the patch envelope in
|
|
8
|
+
``tool_input.command``; shell commands arrive as ``tool_name="Bash"``.
|
|
9
|
+
- Every Forge policy's ``applies_to`` gates on ``tool_name in ("Write", "Edit")``, so
|
|
10
|
+
the adapter normalizes patch operations to those names (Add File -> Write,
|
|
11
|
+
Update File -> Edit). The runtime truth stays in ``origin="codex"`` + ``tool_args``.
|
|
12
|
+
- Codex FAILS OPEN on malformed hook output, so the responder emits only
|
|
13
|
+
``json.dumps`` of literal dicts -- never hand-assembled wire strings.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from forge.cli.hooks.codex_patch import PatchFileOp, parse_apply_patch
|
|
25
|
+
from forge.cli.hooks.policy import format_deny_text, format_needs_review_text
|
|
26
|
+
from forge.policy.deterministic.base import is_under_directory
|
|
27
|
+
from forge.policy.types import ActionContext, CompositeDecision
|
|
28
|
+
|
|
29
|
+
_MAX_CONTENT_CHARS = 5000 # same truncation convention as ClaudeHookAdapter
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CodexHookAdapter:
|
|
33
|
+
"""Normalize a Codex PreToolUse payload into ``ActionContext``s (origin="codex").
|
|
34
|
+
|
|
35
|
+
Only ``apply_patch`` actions are evaluable (Codex's file-write tool); anything
|
|
36
|
+
else -- including ``Bash`` -- yields ``[]`` so the hook command fails open, the
|
|
37
|
+
same posture as Claude's hook skipping non-Write/Edit tools. A multi-file patch
|
|
38
|
+
yields one context per non-delete file operation, in patch order; deletions are
|
|
39
|
+
skipped (no policy evaluates them -- there is no introduced content).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
ORIGIN = "codex"
|
|
43
|
+
|
|
44
|
+
def build_contexts(self, payload: dict[str, Any], tool_name: str, manifest: Any) -> list[ActionContext]:
|
|
45
|
+
"""Build per-file ``ActionContext``s from a Codex PreToolUse payload ([] if unbuildable)."""
|
|
46
|
+
if tool_name != "apply_patch":
|
|
47
|
+
return []
|
|
48
|
+
tool_input = payload.get("tool_input", {})
|
|
49
|
+
if not isinstance(tool_input, dict):
|
|
50
|
+
return []
|
|
51
|
+
command = tool_input.get("command")
|
|
52
|
+
if not isinstance(command, str):
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
ops = parse_apply_patch(command)
|
|
56
|
+
if ops is None:
|
|
57
|
+
return [] # malformed patch: fail open, like Claude's unbuildable payload
|
|
58
|
+
|
|
59
|
+
# Payload cwd is where Codex resolves apply_patch paths; the hook process's
|
|
60
|
+
# own CWD is unpinned, so prefer the payload's.
|
|
61
|
+
payload_cwd = payload.get("cwd")
|
|
62
|
+
cwd = Path(payload_cwd) if isinstance(payload_cwd, str) and payload_cwd else Path(os.getcwd())
|
|
63
|
+
cwd = cwd.resolve()
|
|
64
|
+
|
|
65
|
+
return [self._context_for_op(op, cwd, manifest) for op in ops if op.kind != "delete"]
|
|
66
|
+
|
|
67
|
+
def _context_for_op(self, op: PatchFileOp, cwd: Path, manifest: Any) -> ActionContext:
|
|
68
|
+
# Normalize to the tool names every policy's applies_to expects.
|
|
69
|
+
normalized_tool = "Write" if op.kind == "add" else "Edit"
|
|
70
|
+
|
|
71
|
+
target_path = op.path
|
|
72
|
+
try:
|
|
73
|
+
p = Path(target_path)
|
|
74
|
+
if p.is_absolute():
|
|
75
|
+
target_path = str(p.relative_to(cwd))
|
|
76
|
+
except (ValueError, RuntimeError):
|
|
77
|
+
pass # Keep as-is if can't make relative
|
|
78
|
+
|
|
79
|
+
new_content: str | None = op.added_content
|
|
80
|
+
if new_content and len(new_content) > _MAX_CONTENT_CHARS:
|
|
81
|
+
new_content = new_content[:_MAX_CONTENT_CHARS] + "\n... (truncated)"
|
|
82
|
+
|
|
83
|
+
# raw_diff only for updates: an Add's full content already is new_content,
|
|
84
|
+
# while an update section genuinely is a diff (richer LLM-policy context).
|
|
85
|
+
raw_diff = op.raw_section[:_MAX_CONTENT_CHARS] if op.kind == "update" else None
|
|
86
|
+
|
|
87
|
+
return ActionContext(
|
|
88
|
+
origin=self.ORIGIN,
|
|
89
|
+
event=f"PreToolUse.{normalized_tool}",
|
|
90
|
+
tool_name=normalized_tool,
|
|
91
|
+
tool_args={
|
|
92
|
+
"codex_tool_name": "apply_patch",
|
|
93
|
+
"path": op.path,
|
|
94
|
+
"move_to": op.move_to,
|
|
95
|
+
"kind": op.kind,
|
|
96
|
+
},
|
|
97
|
+
repo_root=str(cwd),
|
|
98
|
+
session_name=manifest.name,
|
|
99
|
+
target_path=target_path,
|
|
100
|
+
new_content=new_content or None,
|
|
101
|
+
raw_diff=raw_diff,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CodexHookResponder:
|
|
106
|
+
"""Serialize a composed policy decision into Codex's PreToolUse wire contract.
|
|
107
|
+
|
|
108
|
+
Codex blocks on a strict stdout JSON ``hookSpecificOutput`` with
|
|
109
|
+
``permissionDecision: "deny"`` and exits 0 (probe-pinned; the exit-2 form also
|
|
110
|
+
blocks but the JSON form carries the reason in-band). Codex fails OPEN on
|
|
111
|
+
malformed output, so every wire string is ``json.dumps`` of a literal dict.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
BLOCK_EXIT = 0 # deny is stdout JSON, not an exit code (contrast Claude's 2)
|
|
115
|
+
ALLOW_EXIT = 0
|
|
116
|
+
|
|
117
|
+
def format_deny(self, result: CompositeDecision) -> str:
|
|
118
|
+
"""Render the deny wire JSON for a single composed decision."""
|
|
119
|
+
return self.format_deny_multi([(None, result)])
|
|
120
|
+
|
|
121
|
+
def format_needs_review(self, result: CompositeDecision) -> str:
|
|
122
|
+
"""Render the deny wire JSON for a single unresolved ``needs_review``."""
|
|
123
|
+
return self.format_needs_review_multi([(None, result)])
|
|
124
|
+
|
|
125
|
+
def format_deny_multi(self, file_results: list[tuple[str | None, CompositeDecision]]) -> str:
|
|
126
|
+
"""Render one deny wire JSON covering every denying file of a patch."""
|
|
127
|
+
reason = self._join_sections(file_results, format_deny_text)
|
|
128
|
+
return self._deny_wire(reason)
|
|
129
|
+
|
|
130
|
+
def format_needs_review_multi(self, file_results: list[tuple[str | None, CompositeDecision]]) -> str:
|
|
131
|
+
"""Render one deny wire JSON for the unresolved-review files of a patch."""
|
|
132
|
+
reason = self._join_sections(file_results, format_needs_review_text)
|
|
133
|
+
return self._deny_wire(reason)
|
|
134
|
+
|
|
135
|
+
def format_error_deny(self, reason: str) -> str:
|
|
136
|
+
"""Render the deny wire JSON for a fail-closed evaluation error (no decision)."""
|
|
137
|
+
return self._deny_wire(reason)
|
|
138
|
+
|
|
139
|
+
def allow_feedback(self, additional_context: str) -> dict[str, Any]:
|
|
140
|
+
"""Build the allow JSON (protocol conformance only).
|
|
141
|
+
|
|
142
|
+
Not emitted in Phase 3: PreToolUse ``additionalContext`` delivery on allow is
|
|
143
|
+
unprobed (only SessionStart's is confirmed), so the command allows silently.
|
|
144
|
+
"""
|
|
145
|
+
return {
|
|
146
|
+
"hookSpecificOutput": {
|
|
147
|
+
"hookEventName": "PreToolUse",
|
|
148
|
+
"permissionDecision": "allow",
|
|
149
|
+
"additionalContext": additional_context,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _deny_wire(reason: str) -> str:
|
|
155
|
+
return json.dumps(
|
|
156
|
+
{
|
|
157
|
+
"hookSpecificOutput": {
|
|
158
|
+
"hookEventName": "PreToolUse",
|
|
159
|
+
"permissionDecision": "deny",
|
|
160
|
+
"permissionDecisionReason": reason,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _join_sections(
|
|
167
|
+
file_results: list[tuple[str | None, CompositeDecision]],
|
|
168
|
+
format_text: Callable[[CompositeDecision], str],
|
|
169
|
+
) -> str:
|
|
170
|
+
sections = []
|
|
171
|
+
for path, result in file_results:
|
|
172
|
+
text = format_text(result)
|
|
173
|
+
sections.append(f"{path}:\n{text}" if path else text)
|
|
174
|
+
return "\n\n".join(sections)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def sort_contexts_tests_first(contexts: list[ActionContext]) -> list[ActionContext]:
|
|
178
|
+
"""Order contexts so tests/ paths evaluate before src/ paths.
|
|
179
|
+
|
|
180
|
+
Optimistic ordering for TDD stateful evaluation: an atomic patch adding test +
|
|
181
|
+
implementation together passes tests-before-impl (the test file populates
|
|
182
|
+
``tests_touched`` first). Uses ``is_under_directory`` -- the SAME nested-aware rule
|
|
183
|
+
the TDD policy's ``applies_to`` gates on -- so a nested ``pkg/tests`` / ``pkg/src``
|
|
184
|
+
layout is reordered too. A top-level-only prefix match would leave both nested files
|
|
185
|
+
in one bucket and false-deny an impl-first atomic patch. ``sorted`` is stable, so
|
|
186
|
+
patch order is preserved within each bucket.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def _key(ctx: ActionContext) -> int:
|
|
190
|
+
path = ctx.target_path or ""
|
|
191
|
+
if is_under_directory(path, "tests"):
|
|
192
|
+
return 0
|
|
193
|
+
if is_under_directory(path, "src"):
|
|
194
|
+
return 2
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
return sorted(contexts, key=_key)
|