multi-forge 0.2.0__py3-none-any.whl
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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Claude binary invocation utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for invoking the Claude Code CLI binary
|
|
4
|
+
with proper argument handling and environment setup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def invoke_claude(
|
|
14
|
+
*,
|
|
15
|
+
session_id: str | None = None,
|
|
16
|
+
resume_id: str | None = None,
|
|
17
|
+
fork_session: bool = False,
|
|
18
|
+
name: str | None = None,
|
|
19
|
+
model: str | None = None,
|
|
20
|
+
system_prompt_file: str | None = None,
|
|
21
|
+
env_vars: dict[str, str] | None = None,
|
|
22
|
+
unset_env_vars: list[str] | None = None,
|
|
23
|
+
cwd: str | None = None,
|
|
24
|
+
extra_args: list[str] | None = None,
|
|
25
|
+
) -> int:
|
|
26
|
+
"""Invoke the Claude Code CLI binary.
|
|
27
|
+
|
|
28
|
+
Builds the command line arguments, sets up environment variables,
|
|
29
|
+
and runs Claude as a subprocess.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
session_id: UUID for new session (--session-id flag).
|
|
33
|
+
resume_id: UUID to resume (--resume flag).
|
|
34
|
+
fork_session: Whether to fork (--fork-session flag).
|
|
35
|
+
name: Display name for Claude's session (--name flag).
|
|
36
|
+
model: Model tier to use (--model flag).
|
|
37
|
+
system_prompt_file: Path to system prompt file (--append-system-prompt-file flag).
|
|
38
|
+
env_vars: Additional environment variables to set.
|
|
39
|
+
unset_env_vars: Environment variables to remove from the child process.
|
|
40
|
+
cwd: Working directory for Claude process.
|
|
41
|
+
extra_args: Additional CLI arguments to pass through to Claude.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Claude's exit code.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
FileNotFoundError: If claude binary is not found.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> # Start new session
|
|
51
|
+
>>> exit_code = invoke_claude(
|
|
52
|
+
... session_id="abc-123",
|
|
53
|
+
... model="opus",
|
|
54
|
+
... env_vars={"FORGE_SESSION": "my-session"},
|
|
55
|
+
... )
|
|
56
|
+
|
|
57
|
+
>>> # Resume session
|
|
58
|
+
>>> exit_code = invoke_claude(
|
|
59
|
+
... resume_id="abc-123",
|
|
60
|
+
... env_vars={"FORGE_SESSION": "my-session"},
|
|
61
|
+
... )
|
|
62
|
+
|
|
63
|
+
>>> # Fork session
|
|
64
|
+
>>> exit_code = invoke_claude(
|
|
65
|
+
... resume_id="parent-uuid",
|
|
66
|
+
... fork_session=True,
|
|
67
|
+
... env_vars={
|
|
68
|
+
... "FORGE_SESSION": "fork-name",
|
|
69
|
+
... "FORGE_FORK_NAME": "fork-name",
|
|
70
|
+
... "FORGE_PARENT_SESSION": "parent-session",
|
|
71
|
+
... },
|
|
72
|
+
... )
|
|
73
|
+
"""
|
|
74
|
+
cmd = _build_command(
|
|
75
|
+
session_id=session_id,
|
|
76
|
+
resume_id=resume_id,
|
|
77
|
+
fork_session=fork_session,
|
|
78
|
+
name=name,
|
|
79
|
+
model=model,
|
|
80
|
+
system_prompt_file=system_prompt_file,
|
|
81
|
+
extra_args=extra_args,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
env = _build_environment(env_vars, unset_env_vars)
|
|
85
|
+
|
|
86
|
+
return _run_claude(cmd, env=env, cwd=cwd)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_claude_args(
|
|
90
|
+
*,
|
|
91
|
+
session_id: str | None = None,
|
|
92
|
+
resume_id: str | None = None,
|
|
93
|
+
fork_session: bool = False,
|
|
94
|
+
name: str | None = None,
|
|
95
|
+
model: str | None = None,
|
|
96
|
+
system_prompt_file: str | None = None,
|
|
97
|
+
extra_args: list[str] | None = None,
|
|
98
|
+
) -> list[str]:
|
|
99
|
+
"""Build Claude CLI arguments without the executable prefix."""
|
|
100
|
+
args: list[str] = []
|
|
101
|
+
|
|
102
|
+
if session_id:
|
|
103
|
+
args.extend(["--session-id", session_id])
|
|
104
|
+
elif resume_id:
|
|
105
|
+
args.extend(["--resume", resume_id])
|
|
106
|
+
if fork_session:
|
|
107
|
+
args.append("--fork-session")
|
|
108
|
+
|
|
109
|
+
# --name works with both --session-id and --resume
|
|
110
|
+
if name:
|
|
111
|
+
args.extend(["--name", name])
|
|
112
|
+
|
|
113
|
+
if model:
|
|
114
|
+
args.extend(["--model", model])
|
|
115
|
+
|
|
116
|
+
if system_prompt_file:
|
|
117
|
+
args.extend(["--append-system-prompt-file", system_prompt_file])
|
|
118
|
+
|
|
119
|
+
if extra_args: # e.g. -- --debug
|
|
120
|
+
args.extend(extra_args)
|
|
121
|
+
return args
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _build_command(
|
|
125
|
+
*,
|
|
126
|
+
session_id: str | None = None,
|
|
127
|
+
resume_id: str | None = None,
|
|
128
|
+
fork_session: bool = False,
|
|
129
|
+
name: str | None = None,
|
|
130
|
+
model: str | None = None,
|
|
131
|
+
system_prompt_file: str | None = None,
|
|
132
|
+
extra_args: list[str] | None = None,
|
|
133
|
+
) -> list[str]:
|
|
134
|
+
"""Build the command line arguments for Claude.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
session_id: UUID for new session.
|
|
138
|
+
resume_id: UUID to resume.
|
|
139
|
+
fork_session: Whether to fork.
|
|
140
|
+
model: Model tier.
|
|
141
|
+
system_prompt_file: Path to system prompt file.
|
|
142
|
+
extra_args: Additional CLI arguments appended after all other flags.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of command line arguments.
|
|
146
|
+
"""
|
|
147
|
+
# No --bare: interactive sessions need hooks, LSP, and plugin sync
|
|
148
|
+
return [
|
|
149
|
+
"claude",
|
|
150
|
+
*build_claude_args(
|
|
151
|
+
session_id=session_id,
|
|
152
|
+
resume_id=resume_id,
|
|
153
|
+
fork_session=fork_session,
|
|
154
|
+
name=name,
|
|
155
|
+
model=model,
|
|
156
|
+
system_prompt_file=system_prompt_file,
|
|
157
|
+
extra_args=extra_args,
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _build_environment(
|
|
163
|
+
extra_vars: dict[str, str] | None = None,
|
|
164
|
+
unset_vars: list[str] | None = None,
|
|
165
|
+
) -> dict[str, str]:
|
|
166
|
+
"""Build the environment for Claude process.
|
|
167
|
+
|
|
168
|
+
Delegates to the shared ``build_claude_env`` utility.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
extra_vars: Additional environment variables to set.
|
|
172
|
+
unset_vars: Environment variables to remove from the child process.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Complete environment dictionary.
|
|
176
|
+
"""
|
|
177
|
+
from forge.core.reactive.env import build_claude_env
|
|
178
|
+
|
|
179
|
+
env = build_claude_env(extra_vars=extra_vars)
|
|
180
|
+
for key in unset_vars or ():
|
|
181
|
+
env.pop(key, None)
|
|
182
|
+
return env
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _run_claude(
|
|
186
|
+
cmd: list[str],
|
|
187
|
+
env: dict[str, str] | None = None,
|
|
188
|
+
cwd: str | None = None,
|
|
189
|
+
) -> int:
|
|
190
|
+
"""Run the Claude binary as a subprocess.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
cmd: Command line arguments.
|
|
194
|
+
env: Environment variables.
|
|
195
|
+
cwd: Working directory.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Claude's exit code.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
FileNotFoundError: If claude binary is not found.
|
|
202
|
+
"""
|
|
203
|
+
if cwd:
|
|
204
|
+
cwd = str(Path(cwd).resolve())
|
|
205
|
+
|
|
206
|
+
result = subprocess.run(
|
|
207
|
+
cmd,
|
|
208
|
+
env=env,
|
|
209
|
+
cwd=cwd,
|
|
210
|
+
# Let Claude take over the terminal
|
|
211
|
+
stdin=None,
|
|
212
|
+
stdout=None,
|
|
213
|
+
stderr=None,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return result.returncode
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def find_claude_binary() -> str | None:
|
|
220
|
+
"""Find the claude binary in PATH.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Path to claude binary, or None if not found.
|
|
224
|
+
"""
|
|
225
|
+
import shutil
|
|
226
|
+
|
|
227
|
+
return shutil.which("claude")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def is_claude_available() -> bool:
|
|
231
|
+
"""Check if claude binary is available.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if claude is in PATH, False otherwise.
|
|
235
|
+
"""
|
|
236
|
+
return find_claude_binary() is not None
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Path utilities for Claude Code integration.
|
|
2
|
+
|
|
3
|
+
Claude stores session data at: ~/.claude/projects/<encoded-path>/
|
|
4
|
+
- <session_id>.jsonl - Transcript
|
|
5
|
+
- agent-<uuid>.jsonl - Agent logs
|
|
6
|
+
|
|
7
|
+
This module provides utilities for:
|
|
8
|
+
- Encoding project paths for Claude's directory structure
|
|
9
|
+
- Resolving transcript and agent log paths
|
|
10
|
+
- Finding project roots (handles git worktrees)
|
|
11
|
+
- Computing Claude's effective project root for a session
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from forge.session.models import SessionState
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_claude_home() -> Path:
|
|
23
|
+
"""Get the Claude home directory (~/.claude).
|
|
24
|
+
|
|
25
|
+
Respects CLAUDE_HOME environment variable if set (for testing isolation).
|
|
26
|
+
Note: We expand a leading "~" so values like "~/.claude" work correctly,
|
|
27
|
+
even though that's the default.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to ~/.claude directory (or CLAUDE_HOME override).
|
|
31
|
+
"""
|
|
32
|
+
claude_home = os.environ.get("CLAUDE_HOME")
|
|
33
|
+
if claude_home:
|
|
34
|
+
return Path(claude_home).expanduser()
|
|
35
|
+
return Path.home() / ".claude"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_claude_projects_dir() -> Path:
|
|
39
|
+
"""Get the Claude projects directory (~/.claude/projects).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Path to ~/.claude/projects directory.
|
|
43
|
+
"""
|
|
44
|
+
return get_claude_home() / "projects"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def encode_project_path(project_root: str) -> str:
|
|
48
|
+
"""Encode project path for Claude's directory structure.
|
|
49
|
+
|
|
50
|
+
Claude stores session data in directories named after the project path,
|
|
51
|
+
with path separators and dots replaced by hyphens.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
project_root: Absolute path to project root.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Encoded path string (e.g., '/home/user/project' -> '-home-user-project').
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> encode_project_path("/home/user/my.project")
|
|
61
|
+
'-home-user-my-project'
|
|
62
|
+
"""
|
|
63
|
+
normalized = str(Path(project_root).resolve())
|
|
64
|
+
encoded = normalized.replace("/", "-").replace(".", "-")
|
|
65
|
+
|
|
66
|
+
return encoded
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_transcript_path(project_root: str, session_id: str) -> Path:
|
|
70
|
+
"""Get the path to a session transcript file.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
project_root: Absolute path to project root.
|
|
74
|
+
session_id: Claude session UUID.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Path to the transcript file (may not exist).
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> get_transcript_path("/home/user/project", "abc-123")
|
|
81
|
+
PosixPath('/home/user/.claude/projects/-home-user-project/abc-123.jsonl')
|
|
82
|
+
"""
|
|
83
|
+
encoded_path = encode_project_path(project_root)
|
|
84
|
+
return get_claude_projects_dir() / encoded_path / f"{session_id}.jsonl"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def find_agent_logs(project_root: str, session_id: str) -> list[Path]:
|
|
88
|
+
"""Find agent log files containing a specific session ID.
|
|
89
|
+
|
|
90
|
+
Agent logs don't use session UUID in filename, only in content.
|
|
91
|
+
This function searches log file contents to find matching logs.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
project_root: Absolute path to project root.
|
|
95
|
+
session_id: Claude session UUID to search for.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of paths to agent log files containing the session ID.
|
|
99
|
+
Returns empty list if directory doesn't exist or no matches found.
|
|
100
|
+
"""
|
|
101
|
+
encoded_path = encode_project_path(project_root)
|
|
102
|
+
project_dir = get_claude_projects_dir() / encoded_path
|
|
103
|
+
|
|
104
|
+
if not project_dir.exists():
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
matching_logs: list[Path] = []
|
|
108
|
+
|
|
109
|
+
for log_file in project_dir.glob("agent-*.jsonl"):
|
|
110
|
+
try:
|
|
111
|
+
content = log_file.read_text(encoding="utf-8")
|
|
112
|
+
if session_id in content:
|
|
113
|
+
matching_logs.append(log_file)
|
|
114
|
+
except (OSError, UnicodeDecodeError):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
return matching_logs
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_claude_project_root(state: SessionState) -> str:
|
|
121
|
+
"""Claude Code project root for a session.
|
|
122
|
+
|
|
123
|
+
Claude Code scopes .claude/ settings, conversations, and transcripts
|
|
124
|
+
to its launch CWD. This computes the correct CWD for any session
|
|
125
|
+
topology so that hooks, transcripts, and --resume all resolve correctly.
|
|
126
|
+
|
|
127
|
+
Rules:
|
|
128
|
+
- Non-worktree sessions: use forge_root (always correct).
|
|
129
|
+
- Nested projects (forge_root inside checkout): use forge_root so
|
|
130
|
+
Claude finds .claude/ at the nested path.
|
|
131
|
+
- Root-level worktrees (forge_root anchored at parent repo): use
|
|
132
|
+
worktree.path because extensions are installed at the checkout root.
|
|
133
|
+
"""
|
|
134
|
+
if not state.worktree:
|
|
135
|
+
return state.forge_root or str(Path.cwd())
|
|
136
|
+
|
|
137
|
+
worktree_root = Path(state.worktree.path)
|
|
138
|
+
if state.forge_root:
|
|
139
|
+
try:
|
|
140
|
+
Path(state.forge_root).relative_to(worktree_root)
|
|
141
|
+
return state.forge_root # Nested: forge_root is inside checkout
|
|
142
|
+
except ValueError:
|
|
143
|
+
pass
|
|
144
|
+
return str(worktree_root) # Root-level: forge_root is at parent repo
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def find_project_root(start_path: str | None = None) -> Path:
|
|
148
|
+
"""Find the git repository root by walking up the directory tree.
|
|
149
|
+
|
|
150
|
+
Handles both regular git repositories (where .git is a directory)
|
|
151
|
+
and git worktrees (where .git is a file pointing to the main repo).
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
start_path: Starting directory to search from. Defaults to cwd.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Path to the git repository root.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
FileNotFoundError: If no git repository found.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> find_project_root("/home/user/project/src/module")
|
|
164
|
+
PosixPath('/home/user/project')
|
|
165
|
+
"""
|
|
166
|
+
if start_path is None:
|
|
167
|
+
current = Path.cwd().resolve()
|
|
168
|
+
else:
|
|
169
|
+
current = Path(start_path).resolve()
|
|
170
|
+
|
|
171
|
+
while current != current.parent:
|
|
172
|
+
git_path = current / ".git"
|
|
173
|
+
|
|
174
|
+
# In worktrees, .git is a FILE; in main checkout, it's a DIRECTORY
|
|
175
|
+
if git_path.exists():
|
|
176
|
+
return current
|
|
177
|
+
|
|
178
|
+
current = current.parent
|
|
179
|
+
|
|
180
|
+
if (current / ".git").exists():
|
|
181
|
+
return current
|
|
182
|
+
|
|
183
|
+
raise FileNotFoundError(f"No git repository found at or above '{start_path or os.getcwd()}'")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_project_encoded_dir(project_root: str) -> Path:
|
|
187
|
+
"""Get the Claude projects subdirectory for a project.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
project_root: Absolute path to project root.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Path to the project's Claude data directory.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> get_project_encoded_dir("/home/user/project")
|
|
197
|
+
PosixPath('/home/user/.claude/projects/-home-user-project')
|
|
198
|
+
"""
|
|
199
|
+
encoded_path = encode_project_path(project_root)
|
|
200
|
+
return get_claude_projects_dir() / encoded_path
|
forge/session/cleanup.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Session age-based cleanup.
|
|
2
|
+
|
|
3
|
+
Mirrors the log management cleanup pattern (forge.cli.logs):
|
|
4
|
+
- auto_clean_old_sessions(): called on CLI startup (best-effort)
|
|
5
|
+
- clean_old_sessions(): core logic for both auto and manual cleanup
|
|
6
|
+
|
|
7
|
+
Uses SessionManager.delete_session() for all actual deletion — it already
|
|
8
|
+
handles manifests, index, transcripts, worktrees, co-resident sessions,
|
|
9
|
+
and active-registry cleanup.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
|
|
18
|
+
from forge.core.state import parse_iso
|
|
19
|
+
from forge.runtime_config import get_runtime_config
|
|
20
|
+
from forge.session import SessionManager
|
|
21
|
+
from forge.session.active import ActiveSessionStore
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SessionCleanupResult:
|
|
28
|
+
"""Result of a session cleanup operation.
|
|
29
|
+
|
|
30
|
+
All skip categories are surfaced so --dry-run and CLI output can
|
|
31
|
+
report every case. No silent drops.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
deleted: list[str] = field(default_factory=list)
|
|
35
|
+
skipped_active: list[str] = field(default_factory=list)
|
|
36
|
+
skipped_unparseable: list[str] = field(default_factory=list)
|
|
37
|
+
failed: list[tuple[str, str]] = field(default_factory=list)
|
|
38
|
+
aborted_error: str | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def aborted(self) -> bool:
|
|
42
|
+
"""Return True when cleanup stopped before evaluating sessions."""
|
|
43
|
+
return self.aborted_error is not None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_empty(self) -> bool:
|
|
47
|
+
"""Return True when cleanup found nothing actionable and did not abort."""
|
|
48
|
+
return (
|
|
49
|
+
not self.deleted
|
|
50
|
+
and not self.skipped_active
|
|
51
|
+
and not self.skipped_unparseable
|
|
52
|
+
and not self.failed
|
|
53
|
+
and self.aborted_error is None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def has_failures(self) -> bool:
|
|
58
|
+
"""Return True when cleanup aborted or any deletion failed."""
|
|
59
|
+
return self.aborted_error is not None or bool(self.failed)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def failure_count(self) -> int:
|
|
63
|
+
"""Return the number of surfaced cleanup failures."""
|
|
64
|
+
return len(self.failed) + (1 if self.aborted_error is not None else 0)
|
|
65
|
+
|
|
66
|
+
def failure_items(self) -> list[tuple[str, str]]:
|
|
67
|
+
"""Return cleanup failures as display-ready (name, error) pairs."""
|
|
68
|
+
items = list(self.failed)
|
|
69
|
+
if self.aborted_error is not None:
|
|
70
|
+
items.insert(0, ("active session registry", self.aborted_error))
|
|
71
|
+
return items
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def should_exit_nonzero(self) -> bool:
|
|
75
|
+
"""Return True when CLI cleanup should exit with an error."""
|
|
76
|
+
return self.has_failures
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def has_partial_success(self) -> bool:
|
|
80
|
+
"""Return True when cleanup deleted sessions before later failures."""
|
|
81
|
+
return bool(self.deleted)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def has_only_skips(self) -> bool:
|
|
85
|
+
"""Return True when cleanup evaluated sessions but only skipped them."""
|
|
86
|
+
return (
|
|
87
|
+
not self.deleted
|
|
88
|
+
and not self.failed
|
|
89
|
+
and self.aborted_error is None
|
|
90
|
+
and bool(self.skipped_active or self.skipped_unparseable)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def has_results(self) -> bool:
|
|
95
|
+
"""Return True when cleanup produced any visible outcome."""
|
|
96
|
+
return not self.is_empty
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def summary_failed_count(self) -> int:
|
|
100
|
+
"""Return the number of failures for user-facing summaries."""
|
|
101
|
+
return self.failure_count
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def summary_failed_label(self) -> str:
|
|
105
|
+
"""Return singular/plural label for failure summaries."""
|
|
106
|
+
return "failure" if self.failure_count == 1 else "failures"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def clean_old_sessions(
|
|
110
|
+
older_than_days: int,
|
|
111
|
+
*,
|
|
112
|
+
delete_transcripts: bool = True,
|
|
113
|
+
delete_worktree: bool = False,
|
|
114
|
+
delete_branch: bool = False,
|
|
115
|
+
force: bool = False,
|
|
116
|
+
) -> SessionCleanupResult:
|
|
117
|
+
"""Delete sessions whose last_accessed_at is older than the threshold.
|
|
118
|
+
|
|
119
|
+
Active sessions (per ActiveSessionStore) are always skipped. Sessions
|
|
120
|
+
with unparseable timestamps are skipped and reported.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
older_than_days: Age threshold in days.
|
|
124
|
+
delete_transcripts: Delete Claude transcript files (~/.claude/projects/*.jsonl).
|
|
125
|
+
Forge artifact snapshots (.forge/artifacts/) are never removed.
|
|
126
|
+
delete_worktree: Delete git worktree directories (default False for safety).
|
|
127
|
+
delete_branch: Delete git branches (requires delete_worktree=True).
|
|
128
|
+
force: Bypass dirty-worktree protection (only relevant when delete_worktree=True).
|
|
129
|
+
"""
|
|
130
|
+
result = SessionCleanupResult()
|
|
131
|
+
manager = SessionManager()
|
|
132
|
+
|
|
133
|
+
all_sessions = manager.list_sessions(include_incognito=True)
|
|
134
|
+
|
|
135
|
+
# One-pass active session lookup (single lock/read/probe cycle).
|
|
136
|
+
# Fail-closed: if we can't determine liveness, abort cleanup entirely.
|
|
137
|
+
# Sessions are high-value objects — deleting one whose Claude process is
|
|
138
|
+
# still running would destroy state.
|
|
139
|
+
active_store = ActiveSessionStore()
|
|
140
|
+
try:
|
|
141
|
+
active_entries = active_store.list_sessions()
|
|
142
|
+
# Use (name, forge_root) tuples to avoid cross-project false positives
|
|
143
|
+
active_identities = {(name, ae.forge_root or ae.worktree_path) for name, ae in active_entries}
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.debug("Cannot read active session registry, aborting cleanup: %s", e)
|
|
146
|
+
result.aborted_error = str(e)
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
for name, entry in all_sessions:
|
|
150
|
+
# Check age
|
|
151
|
+
try:
|
|
152
|
+
dt = parse_iso(entry.last_accessed_at)
|
|
153
|
+
age_days = (datetime.now(UTC) - dt).total_seconds() / 86400
|
|
154
|
+
except (ValueError, TypeError, AttributeError):
|
|
155
|
+
result.skipped_unparseable.append(name)
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
if age_days <= older_than_days:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# Check active status (scoped by forge_root to avoid cross-project false positives)
|
|
162
|
+
entry_identity = (name, entry.forge_root or entry.worktree_path)
|
|
163
|
+
if entry_identity in active_identities:
|
|
164
|
+
result.skipped_active.append(name)
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# Delete (scoped by forge_root to avoid cross-project collisions)
|
|
168
|
+
try:
|
|
169
|
+
manager.delete_session(
|
|
170
|
+
name,
|
|
171
|
+
delete_transcripts=delete_transcripts,
|
|
172
|
+
delete_worktree=delete_worktree,
|
|
173
|
+
delete_branch=delete_branch,
|
|
174
|
+
force=force,
|
|
175
|
+
forge_root=entry.forge_root or entry.worktree_path,
|
|
176
|
+
)
|
|
177
|
+
result.deleted.append(name)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
result.failed.append((name, str(e)))
|
|
180
|
+
logger.debug("Failed to clean session '%s': %s", name, e, exc_info=True)
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def auto_clean_old_sessions() -> None:
|
|
186
|
+
"""Auto-prune old sessions based on session_retention_days config.
|
|
187
|
+
|
|
188
|
+
Called opportunistically on CLI startup. Best-effort: swallows all
|
|
189
|
+
exceptions to avoid breaking CLI commands.
|
|
190
|
+
|
|
191
|
+
Auto-cleanup uses safe defaults:
|
|
192
|
+
- delete_transcripts=True (transcripts are useless without sessions)
|
|
193
|
+
- delete_worktree=False (too destructive for automatic operation)
|
|
194
|
+
- delete_branch=False (branches are lightweight, keep them)
|
|
195
|
+
- force=True (safe because delete_worktree=False means dirty-check is never reached)
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
rc = get_runtime_config()
|
|
199
|
+
if rc.session_retention_days <= 0:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
cleanup_result = clean_old_sessions(
|
|
203
|
+
older_than_days=rc.session_retention_days,
|
|
204
|
+
delete_transcripts=True,
|
|
205
|
+
delete_worktree=False,
|
|
206
|
+
delete_branch=False,
|
|
207
|
+
force=True,
|
|
208
|
+
)
|
|
209
|
+
if cleanup_result.deleted:
|
|
210
|
+
logger.debug(
|
|
211
|
+
"Auto-cleaned %d session(s) older than %d days",
|
|
212
|
+
len(cleanup_result.deleted),
|
|
213
|
+
rc.session_retention_days,
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.debug("Session auto-cleanup error (non-fatal): %s", e)
|
forge/session/config.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Default configuration values for forge session.
|
|
2
|
+
|
|
3
|
+
These defaults can be overridden via environment variables:
|
|
4
|
+
- FORGE_DEFAULT_PROXY_TEMPLATE
|
|
5
|
+
- FORGE_DEFAULT_PROXY_BASE_URL
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Proxy defaults (configurable via env vars)
|
|
14
|
+
DEFAULT_PROXY_TEMPLATE = os.environ.get("FORGE_DEFAULT_PROXY_TEMPLATE", "litellm-openai")
|
|
15
|
+
DEFAULT_PROXY_BASE_URL = os.environ.get("FORGE_DEFAULT_PROXY_BASE_URL", "http://localhost:8085")
|
|
16
|
+
|
|
17
|
+
# Launch mode constants
|
|
18
|
+
LAUNCH_MODE_HOST = "host"
|
|
19
|
+
LAUNCH_MODE_SIDECAR = "sidecar"
|
|
20
|
+
|
|
21
|
+
# Sidecar sessions always talk to the proxy on the container-local loopback.
|
|
22
|
+
SIDECAR_RUNTIME_BASE_URL = "http://localhost:8085"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _discover_templates() -> tuple[str, ...]:
|
|
26
|
+
"""Derive valid proxy templates from the templates directory."""
|
|
27
|
+
templates_dir = Path(__file__).parent.parent / "config" / "defaults" / "templates"
|
|
28
|
+
if not templates_dir.is_dir():
|
|
29
|
+
return ()
|
|
30
|
+
return tuple(sorted(p.stem for p in templates_dir.glob("*.yaml")))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Valid proxy templates (derived from templates directory)
|
|
34
|
+
VALID_PROXY_TEMPLATES: tuple[str, ...] = _discover_templates()
|