claude-mpm 4.15.6__py3-none-any.whl ā 4.21.3__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +286 -0
- claude_mpm/agents/BASE_PM.md +272 -23
- claude_mpm/agents/PM_INSTRUCTIONS.md +49 -0
- claude_mpm/agents/agent_loader.py +4 -4
- claude_mpm/agents/templates/engineer.json +5 -1
- claude_mpm/agents/templates/php-engineer.json +10 -4
- claude_mpm/agents/templates/python_engineer.json +8 -3
- claude_mpm/agents/templates/rust_engineer.json +12 -7
- claude_mpm/agents/templates/svelte-engineer.json +225 -0
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/mpm_init/__init__.py +73 -0
- claude_mpm/cli/commands/mpm_init/core.py +525 -0
- claude_mpm/cli/commands/mpm_init/display.py +341 -0
- claude_mpm/cli/commands/mpm_init/git_activity.py +427 -0
- claude_mpm/cli/commands/mpm_init/modes.py +397 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +442 -0
- claude_mpm/cli/commands/mpm_init_cli.py +396 -0
- claude_mpm/cli/commands/mpm_init_handler.py +67 -1
- claude_mpm/cli/commands/skills.py +488 -0
- claude_mpm/cli/executor.py +2 -0
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +42 -0
- claude_mpm/cli/parsers/skills_parser.py +137 -0
- claude_mpm/cli/startup.py +57 -0
- claude_mpm/commands/mpm-auto-configure.md +52 -0
- claude_mpm/commands/mpm-help.md +6 -0
- claude_mpm/commands/mpm-init.md +112 -6
- claude_mpm/commands/mpm-resume.md +372 -0
- claude_mpm/commands/mpm-version.md +113 -0
- claude_mpm/commands/mpm.md +2 -0
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/constants.py +12 -0
- claude_mpm/core/config.py +42 -0
- claude_mpm/core/factories.py +1 -1
- claude_mpm/core/interfaces.py +56 -1
- claude_mpm/core/optimized_agent_loader.py +3 -3
- claude_mpm/hooks/__init__.py +8 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
- claude_mpm/hooks/session_resume_hook.py +121 -0
- claude_mpm/models/resume_log.py +340 -0
- claude_mpm/services/agents/auto_config_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
- claude_mpm/services/agents/deployment/agent_validator.py +17 -1
- claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
- claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
- claude_mpm/services/agents/local_template_manager.py +1 -1
- claude_mpm/services/agents/recommender.py +47 -0
- claude_mpm/services/cli/resume_service.py +617 -0
- claude_mpm/services/cli/session_manager.py +87 -0
- claude_mpm/services/cli/session_pause_manager.py +504 -0
- claude_mpm/services/cli/session_resume_helper.py +372 -0
- claude_mpm/services/core/base.py +26 -11
- claude_mpm/services/core/interfaces.py +56 -1
- claude_mpm/services/core/models/agent_config.py +3 -0
- claude_mpm/services/core/models/process.py +4 -0
- claude_mpm/services/core/path_resolver.py +1 -1
- claude_mpm/services/diagnostics/models.py +21 -0
- claude_mpm/services/event_bus/relay.py +23 -7
- claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
- claude_mpm/services/local_ops/__init__.py +2 -0
- claude_mpm/services/mcp_config_manager.py +7 -131
- claude_mpm/services/mcp_gateway/auto_configure.py +31 -25
- claude_mpm/services/mcp_gateway/core/process_pool.py +19 -10
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +26 -21
- claude_mpm/services/memory/failure_tracker.py +19 -4
- claude_mpm/services/session_manager.py +205 -1
- claude_mpm/services/unified/deployment_strategies/local.py +1 -1
- claude_mpm/services/version_service.py +104 -1
- claude_mpm/skills/__init__.py +21 -0
- claude_mpm/skills/agent_skills_injector.py +324 -0
- claude_mpm/skills/bundled/LICENSE_ATTRIBUTIONS.md +79 -0
- claude_mpm/skills/bundled/api-documentation.md +393 -0
- claude_mpm/skills/bundled/async-testing.md +571 -0
- claude_mpm/skills/bundled/code-review.md +143 -0
- claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +79 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +178 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +112 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +146 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +81 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +152 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +131 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +325 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +499 -0
- claude_mpm/skills/bundled/docker-containerization.md +194 -0
- claude_mpm/skills/bundled/express-local-dev.md +1429 -0
- claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
- claude_mpm/skills/bundled/git-workflow.md +414 -0
- claude_mpm/skills/bundled/imagemagick.md +204 -0
- claude_mpm/skills/bundled/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +86 -0
- claude_mpm/skills/bundled/main/internal-comms/SKILL.md +43 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
- claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +160 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +157 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +425 -0
- claude_mpm/skills/bundled/main/skill-creator/SKILL.md +189 -0
- claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
- claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
- claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
- claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
- claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +303 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +113 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +72 -0
- claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
- claude_mpm/skills/bundled/pdf.md +141 -0
- claude_mpm/skills/bundled/performance-profiling.md +567 -0
- claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
- claude_mpm/skills/bundled/security-scanning.md +327 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +119 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
- claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +140 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
- claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +184 -0
- claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +35 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +44 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +34 -0
- claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
- claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
- claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +129 -0
- claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
- claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
- claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
- claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
- claude_mpm/skills/bundled/xlsx.md +157 -0
- claude_mpm/skills/registry.py +97 -9
- claude_mpm/skills/skills_registry.py +348 -0
- claude_mpm/skills/skills_service.py +739 -0
- claude_mpm/tools/code_tree_analyzer/__init__.py +45 -0
- claude_mpm/tools/code_tree_analyzer/analysis.py +299 -0
- claude_mpm/tools/code_tree_analyzer/cache.py +131 -0
- claude_mpm/tools/code_tree_analyzer/core.py +380 -0
- claude_mpm/tools/code_tree_analyzer/discovery.py +403 -0
- claude_mpm/tools/code_tree_analyzer/events.py +168 -0
- claude_mpm/tools/code_tree_analyzer/gitignore.py +308 -0
- claude_mpm/tools/code_tree_analyzer/models.py +39 -0
- claude_mpm/tools/code_tree_analyzer/multilang_analyzer.py +224 -0
- claude_mpm/tools/code_tree_analyzer/python_analyzer.py +284 -0
- claude_mpm/utils/agent_dependency_loader.py +2 -2
- {claude_mpm-4.15.6.dist-info ā claude_mpm-4.21.3.dist-info}/METADATA +211 -33
- {claude_mpm-4.15.6.dist-info ā claude_mpm-4.21.3.dist-info}/RECORD +206 -64
- claude_mpm/agents/INSTRUCTIONS_OLD_DEPRECATED.md +0 -602
- claude_mpm/cli/commands/mpm_init.py +0 -2008
- claude_mpm/tools/code_tree_analyzer.py +0 -1825
- {claude_mpm-4.15.6.dist-info ā claude_mpm-4.21.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.15.6.dist-info ā claude_mpm-4.21.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.15.6.dist-info ā claude_mpm-4.21.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.15.6.dist-info ā claude_mpm-4.21.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Resume Log Generator Service.
|
|
2
|
+
|
|
3
|
+
Automatically generates session resume logs when approaching or hitting token limits.
|
|
4
|
+
Integrates with session management and response tracking infrastructure.
|
|
5
|
+
|
|
6
|
+
Triggers:
|
|
7
|
+
- model_context_window_exceeded (stop_reason)
|
|
8
|
+
- Manual pause command
|
|
9
|
+
- 95% token threshold reached
|
|
10
|
+
- Session end with high token usage (>85%)
|
|
11
|
+
|
|
12
|
+
Design Principles:
|
|
13
|
+
- Atomic file operations (via state_storage)
|
|
14
|
+
- Non-blocking generation
|
|
15
|
+
- Graceful degradation if generation fails
|
|
16
|
+
- Integration with existing session state
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from claude_mpm.core.logging_utils import get_logger
|
|
24
|
+
from claude_mpm.models.resume_log import ContextMetrics, ResumeLog
|
|
25
|
+
from claude_mpm.storage.state_storage import StateStorage
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResumeLogGenerator:
|
|
31
|
+
"""Service for generating session resume logs."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
storage_dir: Optional[Path] = None,
|
|
36
|
+
config: Optional[Dict[str, Any]] = None,
|
|
37
|
+
):
|
|
38
|
+
"""Initialize resume log generator.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
storage_dir: Directory for resume logs (default: .claude-mpm/resume-logs)
|
|
42
|
+
config: Configuration dictionary
|
|
43
|
+
"""
|
|
44
|
+
self.storage_dir = storage_dir or Path.home() / ".claude-mpm" / "resume-logs"
|
|
45
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# State storage for atomic writes
|
|
48
|
+
self.state_storage = StateStorage(
|
|
49
|
+
storage_dir=self.storage_dir.parent / "storage"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Configuration
|
|
53
|
+
self.config = config or {}
|
|
54
|
+
self.enabled = (
|
|
55
|
+
self.config.get("context_management", {})
|
|
56
|
+
.get("resume_logs", {})
|
|
57
|
+
.get("enabled", True)
|
|
58
|
+
)
|
|
59
|
+
self.auto_generate = (
|
|
60
|
+
self.config.get("context_management", {})
|
|
61
|
+
.get("resume_logs", {})
|
|
62
|
+
.get("auto_generate", True)
|
|
63
|
+
)
|
|
64
|
+
self.max_tokens = (
|
|
65
|
+
self.config.get("context_management", {})
|
|
66
|
+
.get("resume_logs", {})
|
|
67
|
+
.get("max_tokens", 10000)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Trigger thresholds
|
|
71
|
+
thresholds = self.config.get("context_management", {}).get("thresholds", {})
|
|
72
|
+
self.threshold_caution = thresholds.get("caution", 0.70)
|
|
73
|
+
self.threshold_warning = thresholds.get("warning", 0.85)
|
|
74
|
+
self.threshold_critical = thresholds.get("critical", 0.95)
|
|
75
|
+
|
|
76
|
+
logger.info(
|
|
77
|
+
f"ResumeLogGenerator initialized (enabled={self.enabled}, auto_generate={self.auto_generate})"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def should_generate(
|
|
81
|
+
self,
|
|
82
|
+
stop_reason: Optional[str] = None,
|
|
83
|
+
token_usage_pct: Optional[float] = None,
|
|
84
|
+
manual_trigger: bool = False,
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""Determine if resume log should be generated.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
stop_reason: Claude API stop_reason
|
|
90
|
+
token_usage_pct: Current token usage percentage (0.0-1.0)
|
|
91
|
+
manual_trigger: Manual pause/stop command
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if resume log should be generated
|
|
95
|
+
"""
|
|
96
|
+
if not self.enabled or not self.auto_generate:
|
|
97
|
+
return manual_trigger # Only generate on manual trigger if auto is disabled
|
|
98
|
+
|
|
99
|
+
# Trigger conditions
|
|
100
|
+
triggers = [
|
|
101
|
+
stop_reason == "max_tokens",
|
|
102
|
+
stop_reason == "model_context_window_exceeded",
|
|
103
|
+
manual_trigger,
|
|
104
|
+
token_usage_pct and token_usage_pct >= self.threshold_critical,
|
|
105
|
+
token_usage_pct
|
|
106
|
+
and token_usage_pct >= self.threshold_warning, # Generate at 85% too
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
should_gen = any(triggers)
|
|
110
|
+
|
|
111
|
+
if should_gen:
|
|
112
|
+
reason = "unknown"
|
|
113
|
+
if stop_reason:
|
|
114
|
+
reason = f"stop_reason={stop_reason}"
|
|
115
|
+
elif manual_trigger:
|
|
116
|
+
reason = "manual_trigger"
|
|
117
|
+
elif token_usage_pct:
|
|
118
|
+
reason = f"token_usage={token_usage_pct:.1%}"
|
|
119
|
+
|
|
120
|
+
logger.info(f"Resume log generation triggered: {reason}")
|
|
121
|
+
|
|
122
|
+
return should_gen
|
|
123
|
+
|
|
124
|
+
def generate_from_session_state(
|
|
125
|
+
self,
|
|
126
|
+
session_id: str,
|
|
127
|
+
session_state: Dict[str, Any],
|
|
128
|
+
stop_reason: Optional[str] = None,
|
|
129
|
+
) -> Optional[ResumeLog]:
|
|
130
|
+
"""Generate resume log from session state data.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
session_id: Current session ID
|
|
134
|
+
session_state: Session state dictionary
|
|
135
|
+
stop_reason: Claude API stop_reason
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Generated ResumeLog or None if generation failed
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
# Extract context metrics
|
|
142
|
+
context_data = session_state.get("context_metrics", {})
|
|
143
|
+
context_metrics = ContextMetrics(
|
|
144
|
+
total_budget=context_data.get("total_budget", 200000),
|
|
145
|
+
used_tokens=context_data.get("used_tokens", 0),
|
|
146
|
+
remaining_tokens=context_data.get("remaining_tokens", 0),
|
|
147
|
+
percentage_used=context_data.get("percentage_used", 0.0),
|
|
148
|
+
stop_reason=stop_reason or context_data.get("stop_reason"),
|
|
149
|
+
model=context_data.get("model", "claude-sonnet-4.5"),
|
|
150
|
+
session_id=session_id,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Extract content from session state
|
|
154
|
+
mission_summary = session_state.get("mission_summary", "")
|
|
155
|
+
accomplishments = session_state.get("accomplishments", [])
|
|
156
|
+
key_findings = session_state.get("key_findings", [])
|
|
157
|
+
decisions_made = session_state.get("decisions_made", [])
|
|
158
|
+
next_steps = session_state.get("next_steps", [])
|
|
159
|
+
critical_context = session_state.get("critical_context", {})
|
|
160
|
+
|
|
161
|
+
# Extract metadata
|
|
162
|
+
files_modified = session_state.get("files_modified", [])
|
|
163
|
+
agents_used = session_state.get("agents_used", {})
|
|
164
|
+
errors_encountered = session_state.get("errors_encountered", [])
|
|
165
|
+
warnings = session_state.get("warnings", [])
|
|
166
|
+
|
|
167
|
+
# Create resume log
|
|
168
|
+
resume_log = ResumeLog(
|
|
169
|
+
session_id=session_id,
|
|
170
|
+
previous_session_id=session_state.get("previous_session_id"),
|
|
171
|
+
context_metrics=context_metrics,
|
|
172
|
+
mission_summary=mission_summary,
|
|
173
|
+
accomplishments=accomplishments,
|
|
174
|
+
key_findings=key_findings,
|
|
175
|
+
decisions_made=decisions_made,
|
|
176
|
+
next_steps=next_steps,
|
|
177
|
+
critical_context=critical_context,
|
|
178
|
+
files_modified=files_modified,
|
|
179
|
+
agents_used=agents_used,
|
|
180
|
+
errors_encountered=errors_encountered,
|
|
181
|
+
warnings=warnings,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
logger.info(f"Generated resume log for session {session_id}")
|
|
185
|
+
return resume_log
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(
|
|
189
|
+
f"Failed to generate resume log from session state: {e}", exc_info=True
|
|
190
|
+
)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def generate_from_todo_list(
|
|
194
|
+
self,
|
|
195
|
+
session_id: str,
|
|
196
|
+
todos: List[Dict[str, Any]],
|
|
197
|
+
context_metrics: Optional[ContextMetrics] = None,
|
|
198
|
+
) -> Optional[ResumeLog]:
|
|
199
|
+
"""Generate resume log from TODO list.
|
|
200
|
+
|
|
201
|
+
Useful when session state is minimal but TODO list has rich information.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
session_id: Current session ID
|
|
205
|
+
todos: TODO list items
|
|
206
|
+
context_metrics: Context metrics (optional)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Generated ResumeLog or None if generation failed
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
# Categorize todos
|
|
213
|
+
completed = [t for t in todos if t.get("status") == "completed"]
|
|
214
|
+
in_progress = [t for t in todos if t.get("status") == "in_progress"]
|
|
215
|
+
pending = [t for t in todos if t.get("status") == "pending"]
|
|
216
|
+
|
|
217
|
+
# Build accomplishments from completed tasks
|
|
218
|
+
accomplishments = [f"ā {task['content']}" for task in completed]
|
|
219
|
+
|
|
220
|
+
# Build next steps from in-progress and pending
|
|
221
|
+
next_steps = []
|
|
222
|
+
for task in in_progress:
|
|
223
|
+
next_steps.append(f"[IN PROGRESS] {task['content']}")
|
|
224
|
+
for task in pending:
|
|
225
|
+
next_steps.append(f"[PENDING] {task['content']}")
|
|
226
|
+
|
|
227
|
+
# Create mission summary
|
|
228
|
+
mission_summary = f"Working on {len(todos)} tasks: {len(completed)} completed, {len(in_progress)} in progress, {len(pending)} pending."
|
|
229
|
+
|
|
230
|
+
# Use provided context metrics or create default
|
|
231
|
+
if context_metrics is None:
|
|
232
|
+
context_metrics = ContextMetrics(session_id=session_id)
|
|
233
|
+
|
|
234
|
+
# Create resume log
|
|
235
|
+
resume_log = ResumeLog(
|
|
236
|
+
session_id=session_id,
|
|
237
|
+
context_metrics=context_metrics,
|
|
238
|
+
mission_summary=mission_summary,
|
|
239
|
+
accomplishments=accomplishments,
|
|
240
|
+
next_steps=next_steps,
|
|
241
|
+
critical_context={
|
|
242
|
+
"total_tasks": len(todos),
|
|
243
|
+
"completed_tasks": len(completed),
|
|
244
|
+
"in_progress_tasks": len(in_progress),
|
|
245
|
+
"pending_tasks": len(pending),
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
logger.info(f"Generated resume log from TODO list for session {session_id}")
|
|
250
|
+
return resume_log
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(
|
|
254
|
+
f"Failed to generate resume log from TODO list: {e}", exc_info=True
|
|
255
|
+
)
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def save_resume_log(self, resume_log: ResumeLog) -> Optional[Path]:
|
|
259
|
+
"""Save resume log to storage.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
resume_log: ResumeLog instance to save
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Path to saved file or None if save failed
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
# Save as markdown (primary format)
|
|
269
|
+
md_path = resume_log.save(storage_dir=self.storage_dir)
|
|
270
|
+
|
|
271
|
+
# Also save as JSON for programmatic access
|
|
272
|
+
json_path = self.storage_dir / f"session-{resume_log.session_id}.json"
|
|
273
|
+
self.state_storage.write_json(
|
|
274
|
+
data=resume_log.to_dict(),
|
|
275
|
+
file_path=json_path,
|
|
276
|
+
atomic=True,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
logger.info(f"Resume log saved: {md_path}")
|
|
280
|
+
return md_path
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(f"Failed to save resume log: {e}", exc_info=True)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
def load_resume_log(self, session_id: str) -> Optional[str]:
|
|
287
|
+
"""Load resume log markdown content.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
session_id: Session ID to load
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Markdown content or None if not found
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
md_path = self.storage_dir / f"session-{session_id}.md"
|
|
297
|
+
|
|
298
|
+
if not md_path.exists():
|
|
299
|
+
logger.debug(f"Resume log not found for session {session_id}")
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
content = md_path.read_text(encoding="utf-8")
|
|
303
|
+
logger.info(f"Loaded resume log for session {session_id}")
|
|
304
|
+
return content
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Failed to load resume log: {e}", exc_info=True)
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
def list_resume_logs(self) -> List[Dict[str, Any]]:
|
|
311
|
+
"""List all available resume logs.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
List of resume log metadata
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
logs = []
|
|
318
|
+
|
|
319
|
+
for md_file in self.storage_dir.glob("session-*.md"):
|
|
320
|
+
# Extract session ID from filename
|
|
321
|
+
session_id = md_file.stem.replace("session-", "")
|
|
322
|
+
|
|
323
|
+
# Check if JSON metadata exists
|
|
324
|
+
json_file = md_file.with_suffix(".json")
|
|
325
|
+
metadata = {}
|
|
326
|
+
if json_file.exists():
|
|
327
|
+
json_data = self.state_storage.read_json(json_file)
|
|
328
|
+
if json_data:
|
|
329
|
+
metadata = {
|
|
330
|
+
"session_id": session_id,
|
|
331
|
+
"created_at": json_data.get("created_at"),
|
|
332
|
+
"previous_session_id": json_data.get("previous_session_id"),
|
|
333
|
+
"context_metrics": json_data.get("context_metrics", {}),
|
|
334
|
+
"file_path": str(md_file),
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if metadata:
|
|
338
|
+
logs.append(metadata)
|
|
339
|
+
else:
|
|
340
|
+
# Fallback to file metadata
|
|
341
|
+
logs.append(
|
|
342
|
+
{
|
|
343
|
+
"session_id": session_id,
|
|
344
|
+
"file_path": str(md_file),
|
|
345
|
+
"modified_at": datetime.fromtimestamp(
|
|
346
|
+
md_file.stat().st_mtime, tz=timezone.utc
|
|
347
|
+
).isoformat(),
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Sort by creation time (newest first)
|
|
352
|
+
logs.sort(
|
|
353
|
+
key=lambda x: x.get("created_at", x.get("modified_at", "")),
|
|
354
|
+
reverse=True,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
logger.debug(f"Found {len(logs)} resume logs")
|
|
358
|
+
return logs
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Failed to list resume logs: {e}", exc_info=True)
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
def cleanup_old_logs(self, keep_count: int = 10) -> int:
|
|
365
|
+
"""Clean up old resume logs, keeping only the most recent.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
keep_count: Number of logs to keep
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Number of logs deleted
|
|
372
|
+
"""
|
|
373
|
+
try:
|
|
374
|
+
logs = self.list_resume_logs()
|
|
375
|
+
|
|
376
|
+
if len(logs) <= keep_count:
|
|
377
|
+
logger.debug(
|
|
378
|
+
f"No cleanup needed ({len(logs)} logs <= {keep_count} keep)"
|
|
379
|
+
)
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
# Delete old logs
|
|
383
|
+
deleted = 0
|
|
384
|
+
for log in logs[keep_count:]:
|
|
385
|
+
try:
|
|
386
|
+
md_path = Path(log["file_path"])
|
|
387
|
+
json_path = md_path.with_suffix(".json")
|
|
388
|
+
|
|
389
|
+
if md_path.exists():
|
|
390
|
+
md_path.unlink()
|
|
391
|
+
deleted += 1
|
|
392
|
+
|
|
393
|
+
if json_path.exists():
|
|
394
|
+
json_path.unlink()
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.warning(f"Failed to delete log {log['session_id']}: {e}")
|
|
398
|
+
|
|
399
|
+
logger.info(f"Cleaned up {deleted} old resume logs (kept {keep_count})")
|
|
400
|
+
return deleted
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"Failed to cleanup old logs: {e}", exc_info=True)
|
|
404
|
+
return 0
|
|
405
|
+
|
|
406
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
407
|
+
"""Get resume log statistics.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary with statistics
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
logs = self.list_resume_logs()
|
|
414
|
+
|
|
415
|
+
total_size = 0
|
|
416
|
+
for log in logs:
|
|
417
|
+
path = Path(log["file_path"])
|
|
418
|
+
if path.exists():
|
|
419
|
+
total_size += path.stat().st_size
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
"enabled": self.enabled,
|
|
423
|
+
"auto_generate": self.auto_generate,
|
|
424
|
+
"total_logs": len(logs),
|
|
425
|
+
"storage_dir": str(self.storage_dir),
|
|
426
|
+
"total_size_kb": round(total_size / 1024, 2),
|
|
427
|
+
"thresholds": {
|
|
428
|
+
"caution": f"{self.threshold_caution:.0%}",
|
|
429
|
+
"warning": f"{self.threshold_warning:.0%}",
|
|
430
|
+
"critical": f"{self.threshold_critical:.0%}",
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.error(f"Failed to get stats: {e}", exc_info=True)
|
|
436
|
+
return {
|
|
437
|
+
"enabled": self.enabled,
|
|
438
|
+
"error": str(e),
|
|
439
|
+
}
|
|
@@ -86,6 +86,7 @@ from claude_mpm.services.core.models.process import (
|
|
|
86
86
|
PROTECTED_PORT_RANGES,
|
|
87
87
|
DeploymentState,
|
|
88
88
|
ProcessInfo,
|
|
89
|
+
ProcessStatus,
|
|
89
90
|
StartConfig,
|
|
90
91
|
is_port_protected,
|
|
91
92
|
)
|
|
@@ -147,6 +148,7 @@ __all__ = [
|
|
|
147
148
|
"PortConflictError",
|
|
148
149
|
"ProcessInfo",
|
|
149
150
|
"ProcessSpawnError",
|
|
151
|
+
"ProcessStatus",
|
|
150
152
|
# Data models - Process
|
|
151
153
|
"ResourceMonitor",
|
|
152
154
|
"ResourceUsage",
|
|
@@ -1130,139 +1130,15 @@ class MCPConfigManager:
|
|
|
1130
1130
|
"""
|
|
1131
1131
|
Detect and fix corrupted MCP service installations.
|
|
1132
1132
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
3. Fixes missing dependencies (like mcp-ticketer's gql)
|
|
1137
|
-
4. Validates fixes worked
|
|
1133
|
+
NOTE: Proactive health checking has been disabled.
|
|
1134
|
+
Each MCP service should stand on its own and handle its own issues.
|
|
1135
|
+
This function now only returns success without checking services.
|
|
1138
1136
|
|
|
1139
1137
|
Returns:
|
|
1140
1138
|
Tuple of (success, message)
|
|
1141
1139
|
"""
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
failed_services = []
|
|
1145
|
-
|
|
1146
|
-
# Check each service for issues
|
|
1147
|
-
for service_name in self.PIPX_SERVICES:
|
|
1148
|
-
# Check if service is enabled in config
|
|
1149
|
-
if not self.should_enable_service(service_name):
|
|
1150
|
-
self.logger.debug(f"Skipping {service_name} (disabled in config)")
|
|
1151
|
-
continue
|
|
1152
|
-
|
|
1153
|
-
self.logger.debug(f"š Checking {service_name} for issues...")
|
|
1154
|
-
issue_type = self._detect_service_issue(service_name)
|
|
1155
|
-
if issue_type:
|
|
1156
|
-
services_to_fix.append((service_name, issue_type))
|
|
1157
|
-
self.logger.debug(f" ā ļø Found issue with {service_name}: {issue_type}")
|
|
1158
|
-
else:
|
|
1159
|
-
self.logger.debug(f" ā
{service_name} is functioning correctly")
|
|
1160
|
-
|
|
1161
|
-
if not services_to_fix:
|
|
1162
|
-
return True, "All MCP services are functioning correctly"
|
|
1163
|
-
|
|
1164
|
-
# Fix each problematic service
|
|
1165
|
-
for service_name, issue_type in services_to_fix:
|
|
1166
|
-
self.logger.info(f"š§ Fixing {service_name}: {issue_type}")
|
|
1167
|
-
|
|
1168
|
-
if issue_type == "not_installed":
|
|
1169
|
-
# Install the service
|
|
1170
|
-
success, method = self._install_service_with_fallback(service_name)
|
|
1171
|
-
if success:
|
|
1172
|
-
fixed_services.append(f"{service_name} (installed via {method})")
|
|
1173
|
-
else:
|
|
1174
|
-
failed_services.append(f"{service_name} (installation failed)")
|
|
1175
|
-
|
|
1176
|
-
elif issue_type == "import_error":
|
|
1177
|
-
# Reinstall to fix corrupted installation
|
|
1178
|
-
self.logger.info(
|
|
1179
|
-
f" Reinstalling {service_name} to fix import errors..."
|
|
1180
|
-
)
|
|
1181
|
-
success = self._reinstall_service(service_name)
|
|
1182
|
-
if success:
|
|
1183
|
-
# NOTE: Removed automatic dependency injection workaround
|
|
1184
|
-
# Package maintainers should fix dependency declarations
|
|
1185
|
-
fixed_services.append(f"{service_name} (reinstalled)")
|
|
1186
|
-
else:
|
|
1187
|
-
failed_services.append(f"{service_name} (reinstall failed)")
|
|
1188
|
-
|
|
1189
|
-
elif issue_type == "missing_dependency":
|
|
1190
|
-
# Fix missing dependencies - try injection first, then reinstall if needed
|
|
1191
|
-
self.logger.info(
|
|
1192
|
-
f" {service_name} has missing dependencies - attempting fix..."
|
|
1193
|
-
)
|
|
1194
|
-
|
|
1195
|
-
# First try to inject dependencies without reinstalling
|
|
1196
|
-
injection_success = self._inject_missing_dependencies(service_name)
|
|
1197
|
-
|
|
1198
|
-
if injection_success:
|
|
1199
|
-
# Verify the fix worked
|
|
1200
|
-
issue_after_injection = self._detect_service_issue(service_name)
|
|
1201
|
-
if issue_after_injection is None:
|
|
1202
|
-
fixed_services.append(f"{service_name} (dependencies injected)")
|
|
1203
|
-
self.logger.info(
|
|
1204
|
-
f" ā
Fixed {service_name} with dependency injection"
|
|
1205
|
-
)
|
|
1206
|
-
continue # Move to next service
|
|
1207
|
-
|
|
1208
|
-
# If injection alone didn't work, try full reinstall
|
|
1209
|
-
self.logger.info(
|
|
1210
|
-
" Dependency injection insufficient, trying full reinstall..."
|
|
1211
|
-
)
|
|
1212
|
-
success = self._auto_reinstall_mcp_service(service_name)
|
|
1213
|
-
if success:
|
|
1214
|
-
fixed_services.append(
|
|
1215
|
-
f"{service_name} (auto-reinstalled with dependencies)"
|
|
1216
|
-
)
|
|
1217
|
-
else:
|
|
1218
|
-
# Provide specific manual fix for known services
|
|
1219
|
-
if service_name == "mcp-ticketer":
|
|
1220
|
-
self.logger.warning(
|
|
1221
|
-
f" Auto-fix failed for {service_name}. Manual fix: "
|
|
1222
|
-
f"pipx uninstall {service_name} && pipx install {service_name} && pipx inject {service_name} gql"
|
|
1223
|
-
)
|
|
1224
|
-
else:
|
|
1225
|
-
self.logger.warning(
|
|
1226
|
-
f" Auto-reinstall failed for {service_name}. Manual fix: "
|
|
1227
|
-
f"pipx uninstall {service_name} && pipx install {service_name}"
|
|
1228
|
-
)
|
|
1229
|
-
failed_services.append(f"{service_name} (auto-reinstall failed)")
|
|
1230
|
-
|
|
1231
|
-
elif issue_type == "path_issue":
|
|
1232
|
-
# Path issues are handled by config updates
|
|
1233
|
-
self.logger.info(
|
|
1234
|
-
f" Path issue for {service_name} will be fixed by config update"
|
|
1235
|
-
)
|
|
1236
|
-
fixed_services.append(f"{service_name} (config updated)")
|
|
1237
|
-
|
|
1238
|
-
# Build result message
|
|
1239
|
-
messages = []
|
|
1240
|
-
if fixed_services:
|
|
1241
|
-
messages.append(f"ā
Fixed: {', '.join(fixed_services)}")
|
|
1242
|
-
if failed_services:
|
|
1243
|
-
messages.append(f"ā Failed: {', '.join(failed_services)}")
|
|
1244
|
-
|
|
1245
|
-
# Return success if at least some services were fixed
|
|
1246
|
-
success = len(fixed_services) > 0 or len(failed_services) == 0
|
|
1247
|
-
message = " | ".join(messages) if messages else "No services needed fixing"
|
|
1248
|
-
|
|
1249
|
-
# Provide manual fix instructions if auto-fix failed
|
|
1250
|
-
if failed_services:
|
|
1251
|
-
message += "\n\nš” Manual fix instructions:"
|
|
1252
|
-
for failed in failed_services:
|
|
1253
|
-
service = failed.split(" ")[0]
|
|
1254
|
-
if service in self.SERVICE_MISSING_DEPENDENCIES:
|
|
1255
|
-
deps = " ".join(
|
|
1256
|
-
[
|
|
1257
|
-
f"&& pipx inject {service} {dep}"
|
|
1258
|
-
for dep in self.SERVICE_MISSING_DEPENDENCIES[service]
|
|
1259
|
-
]
|
|
1260
|
-
)
|
|
1261
|
-
message += f"\n ⢠{service}: pipx uninstall {service} && pipx install {service} {deps}"
|
|
1262
|
-
else:
|
|
1263
|
-
message += f"\n ⢠{service}: pipx uninstall {service} && pipx install {service}"
|
|
1264
|
-
|
|
1265
|
-
return success, message
|
|
1140
|
+
# Services should stand on their own - no proactive health checking
|
|
1141
|
+
return True, "MCP services managing their own health"
|
|
1266
1142
|
|
|
1267
1143
|
def _detect_service_issue(self, service_name: str) -> Optional[str]:
|
|
1268
1144
|
"""
|
|
@@ -1497,7 +1373,7 @@ class MCPConfigManager:
|
|
|
1497
1373
|
)
|
|
1498
1374
|
|
|
1499
1375
|
if result.returncode == 0:
|
|
1500
|
-
self.logger.
|
|
1376
|
+
self.logger.debug(f" ā
Successfully injected {dep}")
|
|
1501
1377
|
# Check if already injected (pipx will complain if package already exists)
|
|
1502
1378
|
elif (
|
|
1503
1379
|
"already satisfied" in result.stderr.lower()
|
|
@@ -1582,7 +1458,7 @@ class MCPConfigManager:
|
|
|
1582
1458
|
)
|
|
1583
1459
|
|
|
1584
1460
|
# Verify the reinstall worked
|
|
1585
|
-
self.logger.
|
|
1461
|
+
self.logger.debug(f" ā Verifying {service_name} installation...")
|
|
1586
1462
|
issue = self._detect_service_issue(service_name)
|
|
1587
1463
|
|
|
1588
1464
|
if issue is None:
|