specfact-cli 0.4.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.

Potentially problematic release.


This version of specfact-cli might be problematic. Click here for more details.

Files changed (60) hide show
  1. specfact_cli/__init__.py +14 -0
  2. specfact_cli/agents/__init__.py +23 -0
  3. specfact_cli/agents/analyze_agent.py +392 -0
  4. specfact_cli/agents/base.py +95 -0
  5. specfact_cli/agents/plan_agent.py +202 -0
  6. specfact_cli/agents/registry.py +176 -0
  7. specfact_cli/agents/sync_agent.py +133 -0
  8. specfact_cli/analyzers/__init__.py +10 -0
  9. specfact_cli/analyzers/code_analyzer.py +775 -0
  10. specfact_cli/cli.py +397 -0
  11. specfact_cli/commands/__init__.py +7 -0
  12. specfact_cli/commands/enforce.py +87 -0
  13. specfact_cli/commands/import_cmd.py +355 -0
  14. specfact_cli/commands/init.py +119 -0
  15. specfact_cli/commands/plan.py +1090 -0
  16. specfact_cli/commands/repro.py +172 -0
  17. specfact_cli/commands/sync.py +408 -0
  18. specfact_cli/common/__init__.py +24 -0
  19. specfact_cli/common/logger_setup.py +673 -0
  20. specfact_cli/common/logging_utils.py +41 -0
  21. specfact_cli/common/text_utils.py +52 -0
  22. specfact_cli/common/utils.py +48 -0
  23. specfact_cli/comparators/__init__.py +10 -0
  24. specfact_cli/comparators/plan_comparator.py +391 -0
  25. specfact_cli/generators/__init__.py +13 -0
  26. specfact_cli/generators/plan_generator.py +105 -0
  27. specfact_cli/generators/protocol_generator.py +115 -0
  28. specfact_cli/generators/report_generator.py +200 -0
  29. specfact_cli/generators/workflow_generator.py +111 -0
  30. specfact_cli/importers/__init__.py +6 -0
  31. specfact_cli/importers/speckit_converter.py +773 -0
  32. specfact_cli/importers/speckit_scanner.py +704 -0
  33. specfact_cli/models/__init__.py +32 -0
  34. specfact_cli/models/deviation.py +105 -0
  35. specfact_cli/models/enforcement.py +150 -0
  36. specfact_cli/models/plan.py +97 -0
  37. specfact_cli/models/protocol.py +28 -0
  38. specfact_cli/modes/__init__.py +18 -0
  39. specfact_cli/modes/detector.py +126 -0
  40. specfact_cli/modes/router.py +153 -0
  41. specfact_cli/sync/__init__.py +11 -0
  42. specfact_cli/sync/repository_sync.py +279 -0
  43. specfact_cli/sync/speckit_sync.py +388 -0
  44. specfact_cli/utils/__init__.py +57 -0
  45. specfact_cli/utils/console.py +69 -0
  46. specfact_cli/utils/feature_keys.py +213 -0
  47. specfact_cli/utils/git.py +241 -0
  48. specfact_cli/utils/ide_setup.py +381 -0
  49. specfact_cli/utils/prompts.py +179 -0
  50. specfact_cli/utils/structure.py +496 -0
  51. specfact_cli/utils/yaml_utils.py +200 -0
  52. specfact_cli/validators/__init__.py +19 -0
  53. specfact_cli/validators/fsm.py +260 -0
  54. specfact_cli/validators/repro_checker.py +320 -0
  55. specfact_cli/validators/schema.py +200 -0
  56. specfact_cli-0.4.0.dist-info/METADATA +332 -0
  57. specfact_cli-0.4.0.dist-info/RECORD +60 -0
  58. specfact_cli-0.4.0.dist-info/WHEEL +4 -0
  59. specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
  60. specfact_cli-0.4.0.dist-info/licenses/LICENSE.md +55 -0
@@ -0,0 +1,202 @@
1
+ """
2
+ Plan Agent - Plan management with business logic understanding.
3
+
4
+ This module provides the PlanAgent for plan management operations with
5
+ enhanced prompts and context injection for CoPilot integration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from beartype import beartype
14
+ from icontract import ensure, require
15
+
16
+ from specfact_cli.agents.base import AgentMode
17
+
18
+
19
+ class PlanAgent(AgentMode):
20
+ """
21
+ Plan management agent with business logic understanding.
22
+
23
+ Provides enhanced prompts for plan management operations with
24
+ context injection from IDE and repository analysis.
25
+ """
26
+
27
+ @beartype
28
+ @require(lambda command: bool(command), "Command must be non-empty")
29
+ @ensure(lambda result: isinstance(result, str) and bool(result), "Prompt must be non-empty string")
30
+ def generate_prompt(self, command: str, context: dict[str, Any] | None = None) -> str:
31
+ """
32
+ Generate enhanced prompt for plan management.
33
+
34
+ Args:
35
+ command: CLI command being executed (e.g., "plan init", "plan compare")
36
+ context: Context dictionary with current file, selection, workspace
37
+
38
+ Returns:
39
+ Enhanced prompt optimized for CoPilot
40
+
41
+ Examples:
42
+ >>> agent = PlanAgent()
43
+ >>> prompt = agent.generate_prompt("plan init", {"current_file": "idea.yaml"})
44
+ >>> "interactive" in prompt.lower()
45
+ True
46
+ """
47
+ if context is None:
48
+ context = {}
49
+
50
+ current_file = context.get("current_file", "")
51
+ selection = context.get("selection", "")
52
+ workspace = context.get("workspace", "")
53
+
54
+ if command in ("plan promote", "plan adopt"):
55
+ auto_plan_path = context.get("auto_plan_path", "")
56
+ auto_plan_content = ""
57
+ if auto_plan_path and Path(auto_plan_path).exists():
58
+ auto_plan_content = Path(auto_plan_path).read_text()[:500] # Limit length
59
+
60
+ prompt = f"""
61
+ Analyze the repository and cross-validate identified features/stories.
62
+
63
+ Repository Context:
64
+ - Workspace: {workspace or "None"}
65
+ - Current file: {current_file or "None"}
66
+ - Selection: {selection or "None"}
67
+
68
+ Auto-Derived Plan (from AST analysis):
69
+ {auto_plan_content[:500] if auto_plan_content else "None"}
70
+
71
+ Tasks:
72
+ 1. Validate each identified feature exists in the codebase
73
+ 2. Identify missing features that AST analysis missed
74
+ 3. Identify false positives (classes/components that aren't features)
75
+ 4. Cross-validate stories against actual code
76
+ 5. Refine confidence scores based on code quality and documentation
77
+ 6. Suggest theme categorization improvements
78
+ 7. Extract business context from code comments/docs
79
+
80
+ Output Format:
81
+ - Validated features list with refined confidence scores
82
+ - Missing features (AI discovered, not in AST analysis)
83
+ - False positives (features to remove from plan)
84
+ - Story mapping validation (corrections needed)
85
+ - Theme categorization suggestions
86
+ - Business context extraction (idea, target users, value hypothesis)
87
+ """
88
+ elif command == "plan init":
89
+ prompt = f"""
90
+ Initialize plan bundle with interactive wizard.
91
+
92
+ Context:
93
+ - Current file: {current_file or "None"}
94
+ - Selection: {selection or "None"}
95
+ - Workspace: {workspace or "None"}
96
+
97
+ Extract from context:
98
+ 1. Idea from selection or current file
99
+ 2. Business plan structure
100
+ 3. Product plan themes
101
+ 4. Features from workspace structure
102
+
103
+ Generate interactive prompts for missing information.
104
+ """
105
+ elif command == "plan compare":
106
+ prompt = f"""
107
+ Compare manual vs auto-derived plans.
108
+
109
+ Context:
110
+ - Current file: {current_file or "None"}
111
+ - Selection: {selection or "None"}
112
+ - Workspace: {workspace or "None"}
113
+
114
+ Focus on:
115
+ 1. Deviation explanations
116
+ 2. Fix suggestions
117
+ 3. Interactive deviation review
118
+
119
+ Generate rich console output with explanations.
120
+ """
121
+ else:
122
+ prompt = f"Execute plan command: {command}"
123
+
124
+ return prompt.strip()
125
+
126
+ @beartype
127
+ @require(lambda command: bool(command), "Command must be non-empty")
128
+ @ensure(lambda result: isinstance(result, dict), "Result must be a dictionary")
129
+ def execute(
130
+ self, command: str, args: dict[str, Any] | None = None, context: dict[str, Any] | None = None
131
+ ) -> dict[str, Any]:
132
+ """
133
+ Execute plan command with enhanced prompts.
134
+
135
+ Args:
136
+ command: CLI command being executed (e.g., "plan init")
137
+ args: Command arguments (e.g., {"idea": "idea.yaml"})
138
+ context: Context dictionary with current file, selection, workspace
139
+
140
+ Returns:
141
+ Command result with enhanced output
142
+
143
+ Examples:
144
+ >>> agent = PlanAgent()
145
+ >>> result = agent.execute("plan init", {"idea": "idea.yaml"}, {"current_file": "idea.yaml"})
146
+ >>> isinstance(result, dict)
147
+ True
148
+ """
149
+ if args is None:
150
+ args = {}
151
+ if context is None:
152
+ context = {}
153
+
154
+ # Generate enhanced prompt
155
+ prompt = self.generate_prompt(command, context)
156
+
157
+ # For Phase 4.1, return structured result with prompt
158
+ # In Phase 4.2+, this will route to actual command execution with agent mode
159
+ return {
160
+ "type": "plan",
161
+ "command": command,
162
+ "prompt": prompt,
163
+ "args": args,
164
+ "context": context,
165
+ "enhanced": True,
166
+ }
167
+
168
+ @beartype
169
+ @ensure(lambda result: isinstance(result, dict), "Result must be a dictionary")
170
+ def inject_context(self, context: dict[str, Any] | None = None) -> dict[str, Any]:
171
+ """
172
+ Inject context information specific to plan operations.
173
+
174
+ Args:
175
+ context: Basic context dictionary (can be None)
176
+
177
+ Returns:
178
+ Enhanced context with plan-specific information
179
+
180
+ Examples:
181
+ >>> agent = PlanAgent()
182
+ >>> enhanced = agent.inject_context({"current_file": "idea.yaml"})
183
+ >>> isinstance(enhanced, dict)
184
+ True
185
+ """
186
+ enhanced = super().inject_context(context)
187
+
188
+ # Add plan artifacts if workspace is available
189
+ if enhanced.get("workspace"):
190
+ workspace_path = Path(enhanced["workspace"])
191
+ if workspace_path.exists() and workspace_path.is_dir():
192
+ # Find plan artifacts
193
+ plan_artifacts = {}
194
+ specfact_dir = workspace_path / ".specfact"
195
+ if specfact_dir.exists():
196
+ plans_dir = specfact_dir / "plans"
197
+ if plans_dir.exists():
198
+ plan_files = list(plans_dir.glob("*.yaml")) + list(plans_dir.glob("*.yml"))
199
+ plan_artifacts["plan_files"] = [str(f) for f in plan_files[:5]] # Limit to first 5
200
+ enhanced["plan_artifacts"] = plan_artifacts
201
+
202
+ return enhanced
@@ -0,0 +1,176 @@
1
+ """
2
+ Agent Registry - Registry for agent mode instances.
3
+
4
+ This module provides a registry for managing agent mode instances and
5
+ routing commands to appropriate agents based on command type.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from beartype import beartype
11
+ from icontract import ensure, require
12
+
13
+ from specfact_cli.agents.analyze_agent import AnalyzeAgent
14
+ from specfact_cli.agents.base import AgentMode
15
+ from specfact_cli.agents.plan_agent import PlanAgent
16
+ from specfact_cli.agents.sync_agent import SyncAgent
17
+
18
+
19
+ class AgentRegistry:
20
+ """
21
+ Registry for agent mode instances.
22
+
23
+ Provides centralized management of agent instances and routing
24
+ based on command type.
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ """Initialize agent registry with default agents."""
29
+ self._agents: dict[str, AgentMode] = {}
30
+ self._register_default_agents()
31
+
32
+ @beartype
33
+ def _register_default_agents(self) -> None:
34
+ """Register default agent instances."""
35
+ self._agents["analyze"] = AnalyzeAgent()
36
+ self._agents["plan"] = PlanAgent()
37
+ self._agents["sync"] = SyncAgent()
38
+
39
+ @beartype
40
+ @require(lambda name: bool(name), "Agent name must be non-empty")
41
+ @require(lambda agent: isinstance(agent, AgentMode), "Agent must be AgentMode instance")
42
+ def register(self, name: str, agent: AgentMode) -> None:
43
+ """
44
+ Register an agent instance.
45
+
46
+ Args:
47
+ name: Agent name (e.g., "analyze", "plan", "sync")
48
+ agent: Agent mode instance
49
+
50
+ Examples:
51
+ >>> registry = AgentRegistry()
52
+ >>> registry.register("custom", AnalyzeAgent())
53
+ """
54
+ self._agents[name] = agent
55
+
56
+ @beartype
57
+ @require(lambda name: bool(name), "Agent name must be non-empty")
58
+ @ensure(lambda result: result is None or isinstance(result, AgentMode), "Result must be AgentMode or None")
59
+ def get(self, name: str) -> AgentMode | None:
60
+ """
61
+ Get an agent instance by name.
62
+
63
+ Args:
64
+ name: Agent name (e.g., "analyze", "plan", "sync")
65
+
66
+ Returns:
67
+ Agent mode instance or None if not found
68
+
69
+ Examples:
70
+ >>> registry = AgentRegistry()
71
+ >>> agent = registry.get("analyze")
72
+ >>> isinstance(agent, AnalyzeAgent)
73
+ True
74
+ """
75
+ return self._agents.get(name)
76
+
77
+ @beartype
78
+ @require(lambda command: bool(command), "Command must be non-empty")
79
+ @ensure(lambda result: result is None or isinstance(result, AgentMode), "Result must be AgentMode or None")
80
+ def get_agent_for_command(self, command: str) -> AgentMode | None:
81
+ """
82
+ Get agent instance for a command.
83
+
84
+ Args:
85
+ command: CLI command (e.g., "import from-code", "plan init", "sync spec-kit")
86
+
87
+ Returns:
88
+ Agent mode instance or None if not found
89
+
90
+ Examples:
91
+ >>> registry = AgentRegistry()
92
+ >>> agent = registry.get_agent_for_command("import from-code")
93
+ >>> isinstance(agent, AnalyzeAgent)
94
+ True
95
+ """
96
+ # Extract command type from command string
97
+ command_parts = command.split()
98
+ if not command_parts:
99
+ return None
100
+
101
+ command_type = command_parts[0]
102
+
103
+ # Map command types to agent names
104
+ agent_map: dict[str, str] = {
105
+ "import": "analyze", # import from-code uses AnalyzeAgent
106
+ "plan": "plan",
107
+ "sync": "sync",
108
+ }
109
+
110
+ agent_name = agent_map.get(command_type)
111
+ if agent_name:
112
+ return self.get(agent_name)
113
+
114
+ return None
115
+
116
+ @beartype
117
+ def list_agents(self) -> list[str]:
118
+ """
119
+ List all registered agent names.
120
+
121
+ Returns:
122
+ List of agent names
123
+
124
+ Examples:
125
+ >>> registry = AgentRegistry()
126
+ >>> names = registry.list_agents()
127
+ >>> "analyze" in names
128
+ True
129
+ """
130
+ return list(self._agents.keys())
131
+
132
+
133
+ # Global registry instance
134
+ _registry: AgentRegistry | None = None
135
+
136
+
137
+ @beartype
138
+ @ensure(lambda result: isinstance(result, AgentRegistry), "Result must be AgentRegistry")
139
+ def get_registry() -> AgentRegistry:
140
+ """
141
+ Get the global agent registry instance.
142
+
143
+ Returns:
144
+ AgentRegistry instance
145
+
146
+ Examples:
147
+ >>> registry = get_registry()
148
+ >>> isinstance(registry, AgentRegistry)
149
+ True
150
+ """
151
+ global _registry
152
+ if _registry is None:
153
+ _registry = AgentRegistry()
154
+ return _registry
155
+
156
+
157
+ @beartype
158
+ @require(lambda command: bool(command), "Command must be non-empty")
159
+ @ensure(lambda result: result is None or isinstance(result, AgentMode), "Result must be AgentMode or None")
160
+ def get_agent(command: str) -> AgentMode | None:
161
+ """
162
+ Get agent instance for a command (convenience function).
163
+
164
+ Args:
165
+ command: CLI command (e.g., "import from-code")
166
+
167
+ Returns:
168
+ Agent mode instance or None if not found
169
+
170
+ Examples:
171
+ >>> agent = get_agent("import from-code")
172
+ >>> isinstance(agent, AnalyzeAgent)
173
+ True
174
+ """
175
+ registry = get_registry()
176
+ return registry.get_agent_for_command(command)
@@ -0,0 +1,133 @@
1
+ """
2
+ Sync Agent - Bidirectional sync with conflict resolution.
3
+
4
+ This module provides the SyncAgent for sync operations with enhanced
5
+ prompts and conflict resolution assistance for CoPilot integration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from beartype import beartype
13
+ from icontract import ensure, require
14
+
15
+ from specfact_cli.agents.base import AgentMode
16
+
17
+
18
+ class SyncAgent(AgentMode):
19
+ """
20
+ Bidirectional sync agent with conflict resolution.
21
+
22
+ Provides enhanced prompts for sync operations with conflict
23
+ resolution assistance and change explanation.
24
+ """
25
+
26
+ @beartype
27
+ @require(lambda command: bool(command), "Command must be non-empty")
28
+ @ensure(lambda result: isinstance(result, str) and bool(result), "Prompt must be non-empty string")
29
+ def generate_prompt(self, command: str, context: dict[str, Any] | None = None) -> str:
30
+ """
31
+ Generate enhanced prompt for sync operation.
32
+
33
+ Args:
34
+ command: CLI command being executed (e.g., "sync spec-kit", "sync repository")
35
+ context: Context dictionary with current file, selection, workspace
36
+
37
+ Returns:
38
+ Enhanced prompt optimized for CoPilot
39
+
40
+ Examples:
41
+ >>> agent = SyncAgent()
42
+ >>> prompt = agent.generate_prompt("sync spec-kit", {"workspace": "."})
43
+ >>> "conflict" in prompt.lower()
44
+ True
45
+ """
46
+ if context is None:
47
+ context = {}
48
+
49
+ current_file = context.get("current_file", "")
50
+ selection = context.get("selection", "")
51
+ workspace = context.get("workspace", "")
52
+
53
+ prompt = f"""
54
+ Perform bidirectional sync with conflict resolution.
55
+
56
+ Context:
57
+ - Current file: {current_file or "None"}
58
+ - Selection: {selection or "None"}
59
+ - Workspace: {workspace or "None"}
60
+
61
+ Sync strategy:
62
+ 1. Detect changes in Spec-Kit artifacts and .specfact/ artifacts
63
+ 2. Automatic source detection
64
+ 3. Conflict resolution assistance
65
+ 4. Change explanation and preview
66
+
67
+ Generate interactive conflict resolution prompts.
68
+ """
69
+ return prompt.strip()
70
+
71
+ @beartype
72
+ @require(lambda command: bool(command), "Command must be non-empty")
73
+ @ensure(lambda result: isinstance(result, dict), "Result must be a dictionary")
74
+ def execute(
75
+ self, command: str, args: dict[str, Any] | None = None, context: dict[str, Any] | None = None
76
+ ) -> dict[str, Any]:
77
+ """
78
+ Execute sync command with enhanced prompts.
79
+
80
+ Args:
81
+ command: CLI command being executed (e.g., "sync spec-kit")
82
+ args: Command arguments (e.g., {"source": "spec-kit", "target": ".specfact"})
83
+ context: Context dictionary with current file, selection, workspace
84
+
85
+ Returns:
86
+ Command result with enhanced output
87
+
88
+ Examples:
89
+ >>> agent = SyncAgent()
90
+ >>> result = agent.execute("sync spec-kit", {"source": "spec-kit"}, {"workspace": "."})
91
+ >>> isinstance(result, dict)
92
+ True
93
+ """
94
+ if args is None:
95
+ args = {}
96
+ if context is None:
97
+ context = {}
98
+
99
+ # Generate enhanced prompt
100
+ prompt = self.generate_prompt(command, context)
101
+
102
+ # For Phase 4.1, return structured result with prompt
103
+ # In Phase 4.2+, this will route to actual command execution with agent mode
104
+ return {
105
+ "type": "sync",
106
+ "command": command,
107
+ "prompt": prompt,
108
+ "args": args,
109
+ "context": context,
110
+ "enhanced": True,
111
+ }
112
+
113
+ @beartype
114
+ @ensure(lambda result: isinstance(result, dict), "Result must be a dictionary")
115
+ def inject_context(self, context: dict[str, Any] | None = None) -> dict[str, Any]:
116
+ """
117
+ Inject context information specific to sync operations.
118
+
119
+ Args:
120
+ context: Basic context dictionary (can be None)
121
+
122
+ Returns:
123
+ Enhanced context with sync-specific information
124
+
125
+ Examples:
126
+ >>> agent = SyncAgent()
127
+ >>> enhanced = agent.inject_context({"workspace": "."})
128
+ >>> isinstance(enhanced, dict)
129
+ True
130
+ """
131
+ # Sync-specific context injection will be enhanced in Phase 4.2+
132
+ # For now, just return the enhanced context from base class
133
+ return super().inject_context(context)
@@ -0,0 +1,10 @@
1
+ """
2
+ Analyzers module for SpecFact CLI.
3
+
4
+ This module provides classes for analyzing code to extract features,
5
+ stories, and generate plan bundles from brownfield codebases.
6
+ """
7
+
8
+ from specfact_cli.analyzers.code_analyzer import CodeAnalyzer
9
+
10
+ __all__ = ["CodeAnalyzer"]