minder-cli 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.
- minder/__init__.py +12 -0
- minder/api/routers/prompts.py +177 -0
- minder/application/__init__.py +1 -0
- minder/application/admin/__init__.py +11 -0
- minder/application/admin/dto.py +453 -0
- minder/application/admin/jobs.py +327 -0
- minder/application/admin/use_cases.py +1895 -0
- minder/auth/__init__.py +12 -0
- minder/auth/context.py +26 -0
- minder/auth/middleware.py +70 -0
- minder/auth/principal.py +59 -0
- minder/auth/rate_limiter.py +89 -0
- minder/auth/rbac.py +60 -0
- minder/auth/service.py +541 -0
- minder/bootstrap/__init__.py +9 -0
- minder/bootstrap/providers.py +109 -0
- minder/bootstrap/transport.py +807 -0
- minder/cache/__init__.py +10 -0
- minder/cache/providers.py +140 -0
- minder/chunking/__init__.py +4 -0
- minder/chunking/code_splitter.py +184 -0
- minder/chunking/splitter.py +136 -0
- minder/cli.py +1542 -0
- minder/config.py +179 -0
- minder/continuity.py +363 -0
- minder/dev.py +160 -0
- minder/embedding/__init__.py +9 -0
- minder/embedding/base.py +7 -0
- minder/embedding/local.py +65 -0
- minder/embedding/openai.py +7 -0
- minder/graph/__init__.py +11 -0
- minder/graph/edges.py +13 -0
- minder/graph/executor.py +127 -0
- minder/graph/graph.py +263 -0
- minder/graph/nodes/__init__.py +27 -0
- minder/graph/nodes/evaluator.py +21 -0
- minder/graph/nodes/guard.py +64 -0
- minder/graph/nodes/llm.py +59 -0
- minder/graph/nodes/planning.py +30 -0
- minder/graph/nodes/reasoning.py +87 -0
- minder/graph/nodes/reranker.py +141 -0
- minder/graph/nodes/retriever.py +86 -0
- minder/graph/nodes/verification.py +230 -0
- minder/graph/nodes/workflow_planner.py +250 -0
- minder/graph/runtime.py +15 -0
- minder/graph/state.py +26 -0
- minder/llm/__init__.py +5 -0
- minder/llm/base.py +14 -0
- minder/llm/local.py +381 -0
- minder/llm/openai.py +89 -0
- minder/models/__init__.py +109 -0
- minder/models/base.py +10 -0
- minder/models/client.py +137 -0
- minder/models/document.py +34 -0
- minder/models/error.py +32 -0
- minder/models/graph.py +114 -0
- minder/models/history.py +32 -0
- minder/models/job.py +62 -0
- minder/models/prompt.py +41 -0
- minder/models/repository.py +62 -0
- minder/models/rule.py +68 -0
- minder/models/session.py +51 -0
- minder/models/skill.py +52 -0
- minder/models/user.py +41 -0
- minder/models/workflow.py +35 -0
- minder/observability/__init__.py +57 -0
- minder/observability/audit.py +243 -0
- minder/observability/logging.py +253 -0
- minder/observability/metrics.py +448 -0
- minder/observability/tracing.py +215 -0
- minder/presentation/__init__.py +1 -0
- minder/presentation/http/__init__.py +1 -0
- minder/presentation/http/admin/__init__.py +3 -0
- minder/presentation/http/admin/api.py +1309 -0
- minder/presentation/http/admin/context.py +94 -0
- minder/presentation/http/admin/dashboard.py +111 -0
- minder/presentation/http/admin/jobs.py +208 -0
- minder/presentation/http/admin/memories.py +185 -0
- minder/presentation/http/admin/prompts.py +219 -0
- minder/presentation/http/admin/routes.py +127 -0
- minder/presentation/http/admin/runtime.py +650 -0
- minder/presentation/http/admin/search.py +368 -0
- minder/presentation/http/admin/skills.py +230 -0
- minder/prompts/__init__.py +646 -0
- minder/prompts/formatter.py +142 -0
- minder/resources/__init__.py +318 -0
- minder/retrieval/__init__.py +5 -0
- minder/retrieval/hybrid.py +178 -0
- minder/retrieval/mmr.py +116 -0
- minder/retrieval/multi_hop.py +115 -0
- minder/runtime.py +15 -0
- minder/server.py +145 -0
- minder/store/__init__.py +64 -0
- minder/store/document.py +115 -0
- minder/store/error.py +82 -0
- minder/store/feedback.py +114 -0
- minder/store/graph.py +588 -0
- minder/store/history.py +57 -0
- minder/store/interfaces.py +512 -0
- minder/store/milvus/__init__.py +11 -0
- minder/store/milvus/client.py +26 -0
- minder/store/milvus/collections.py +15 -0
- minder/store/milvus/vector_store.py +232 -0
- minder/store/mongodb/__init__.py +11 -0
- minder/store/mongodb/client.py +49 -0
- minder/store/mongodb/indexes.py +90 -0
- minder/store/mongodb/operational_store.py +993 -0
- minder/store/relational.py +1087 -0
- minder/store/repo_state.py +58 -0
- minder/store/rule.py +93 -0
- minder/store/vector.py +79 -0
- minder/tools/__init__.py +47 -0
- minder/tools/auth.py +94 -0
- minder/tools/graph.py +839 -0
- minder/tools/ingest.py +353 -0
- minder/tools/memory.py +381 -0
- minder/tools/query.py +307 -0
- minder/tools/registry.py +269 -0
- minder/tools/repo_scanner.py +1266 -0
- minder/tools/search.py +15 -0
- minder/tools/session.py +316 -0
- minder/tools/skills.py +899 -0
- minder/tools/workflow.py +215 -0
- minder/transport/__init__.py +4 -0
- minder/transport/base.py +286 -0
- minder/transport/sse.py +252 -0
- minder/transport/stdio.py +29 -0
- minder_cli-0.2.0.dist-info/METADATA +318 -0
- minder_cli-0.2.0.dist-info/RECORD +132 -0
- minder_cli-0.2.0.dist-info/WHEEL +4 -0
- minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
- minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
minder/config.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict, TomlConfigSettingsSource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ServerConfig(BaseModel):
|
|
8
|
+
name: str = "minder"
|
|
9
|
+
version: str = "0.1.0"
|
|
10
|
+
transport: str = "sse"
|
|
11
|
+
host: str = "0.0.0.0"
|
|
12
|
+
port: int = 8800
|
|
13
|
+
log_level: str = "info"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DashboardConfig(BaseModel):
|
|
17
|
+
base_path: str = "/dashboard"
|
|
18
|
+
static_dir: str = "src/dashboard/dist"
|
|
19
|
+
dev_server_url: str | None = None
|
|
20
|
+
api_url: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthConfig(BaseModel):
|
|
24
|
+
enabled: bool = True
|
|
25
|
+
jwt_secret: str = "dev-secret-key-change-me-in-prod"
|
|
26
|
+
jwt_expiry_hours: int = 24
|
|
27
|
+
api_key_prefix: str = "mk_"
|
|
28
|
+
client_api_key_prefix: str = "mkc_"
|
|
29
|
+
client_token_expiry_minutes: int = 60
|
|
30
|
+
default_admin_email: str = "admin@example.com"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmbeddingConfig(BaseModel):
|
|
34
|
+
provider: str = "llamacpp"
|
|
35
|
+
model_name: str = "ggml-org/embeddinggemma-300M-GGUF"
|
|
36
|
+
model_path: str = "~/.minder/models/embeddinggemma-300M-Q8_0.gguf"
|
|
37
|
+
dimensions: int = 768
|
|
38
|
+
openai_api_key: Optional[str] = None
|
|
39
|
+
openai_model: str = "text-embedding-3-small"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LLMConfig(BaseModel):
|
|
43
|
+
provider: str = "llamacpp"
|
|
44
|
+
model_name: str = "ggml-org/gemma-4-E2B-it-GGUF"
|
|
45
|
+
model_path: str = "~/.minder/models/gemma-4-e2b-it-Q8_0.gguf"
|
|
46
|
+
context_length: int = 131072
|
|
47
|
+
temperature: float = 0.1
|
|
48
|
+
openai_api_key: Optional[str] = None
|
|
49
|
+
openai_model: str = "gpt-4o-mini"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VectorStoreConfig(BaseModel):
|
|
53
|
+
provider: str = "milvus_lite" # "milvus" (standalone) | "milvus_lite" | "memory"
|
|
54
|
+
db_path: str = "~/.minder/data/milvus.db" # used by milvus_lite only
|
|
55
|
+
uri: str = "http://localhost:19530" # used by milvus standalone
|
|
56
|
+
collection_prefix: str = "minder_"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RelationalStoreConfig(BaseModel):
|
|
60
|
+
provider: str = "mongodb" # "mongodb" | "sqlite" | "postgresql"
|
|
61
|
+
db_path: str = "minder.db" # used by sqlite
|
|
62
|
+
uri: str = "postgresql+asyncpg://localhost/minder" # used by postgresql
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GraphStoreConfig(BaseModel):
|
|
66
|
+
enabled: bool = True
|
|
67
|
+
provider: str = "auto" # "auto" | "sqlite" | "postgresql"
|
|
68
|
+
db_path: str = "~/.minder/data/graph.db" # used by sqlite
|
|
69
|
+
uri: str = "postgresql+asyncpg://localhost/minder_graph" # used by postgresql
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MongoDBConfig(BaseModel):
|
|
73
|
+
uri: str = "mongodb://localhost:27017"
|
|
74
|
+
database: str = "minder"
|
|
75
|
+
min_pool_size: int = 5
|
|
76
|
+
max_pool_size: int = 50
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RedisConfig(BaseModel):
|
|
80
|
+
uri: str = "redis://localhost:6379/0"
|
|
81
|
+
prefix: str = "minder:"
|
|
82
|
+
session_ttl: int = 86400
|
|
83
|
+
cache_ttl: int = 3600
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RetrievalConfig(BaseModel):
|
|
87
|
+
top_k: int = 10
|
|
88
|
+
rerank_top_n: int = 5
|
|
89
|
+
similarity_threshold: float = 0.7
|
|
90
|
+
hybrid_alpha: float = 0.7
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CacheConfig(BaseModel):
|
|
94
|
+
enabled: bool = True
|
|
95
|
+
provider: str = "redis" # "redis" is the only supported runtime backend
|
|
96
|
+
max_size: int = (
|
|
97
|
+
1000 # unused; kept for backwards-compat with any existing .env files
|
|
98
|
+
)
|
|
99
|
+
ttl_seconds: int = 3600
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RateLimitConfig(BaseModel):
|
|
103
|
+
enabled: bool = False
|
|
104
|
+
window_seconds: int = 60
|
|
105
|
+
admin_limit: int = 120
|
|
106
|
+
member_limit: int = 60
|
|
107
|
+
readonly_limit: int = 20
|
|
108
|
+
client_limit: int = 90
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class VerificationConfig(BaseModel):
|
|
112
|
+
enabled: bool = True
|
|
113
|
+
sandbox: str = "docker"
|
|
114
|
+
timeout_seconds: int = 30
|
|
115
|
+
docker_image: str = "minder-sandbox:latest"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class WorkflowConfig(BaseModel):
|
|
119
|
+
enforcement: str = "strict"
|
|
120
|
+
default_workflow: str = "tdd"
|
|
121
|
+
repo_state_dir: str = ".minder"
|
|
122
|
+
block_step_skips: bool = True
|
|
123
|
+
orchestration_runtime: str = "internal"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SeedingConfig(BaseModel):
|
|
127
|
+
skills_repo: str = ""
|
|
128
|
+
skills_branch: str = "main"
|
|
129
|
+
skills_path: str = "skills/"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Settings(BaseSettings):
|
|
133
|
+
server: ServerConfig = Field(default_factory=ServerConfig)
|
|
134
|
+
dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
|
|
135
|
+
auth: AuthConfig = Field(default_factory=AuthConfig)
|
|
136
|
+
embedding: EmbeddingConfig = Field(default_factory=EmbeddingConfig)
|
|
137
|
+
llm: LLMConfig = Field(default_factory=LLMConfig)
|
|
138
|
+
vector_store: VectorStoreConfig = Field(default_factory=VectorStoreConfig)
|
|
139
|
+
relational_store: RelationalStoreConfig = Field(
|
|
140
|
+
default_factory=RelationalStoreConfig
|
|
141
|
+
)
|
|
142
|
+
graph_store: GraphStoreConfig = Field(default_factory=GraphStoreConfig)
|
|
143
|
+
mongodb: MongoDBConfig = Field(default_factory=MongoDBConfig)
|
|
144
|
+
redis: RedisConfig = Field(default_factory=RedisConfig)
|
|
145
|
+
retrieval: RetrievalConfig = Field(default_factory=RetrievalConfig)
|
|
146
|
+
cache: CacheConfig = Field(default_factory=CacheConfig)
|
|
147
|
+
rate_limit: RateLimitConfig = Field(default_factory=RateLimitConfig)
|
|
148
|
+
verification: VerificationConfig = Field(default_factory=VerificationConfig)
|
|
149
|
+
workflow: WorkflowConfig = Field(default_factory=WorkflowConfig)
|
|
150
|
+
seeding: SeedingConfig = Field(default_factory=SeedingConfig)
|
|
151
|
+
|
|
152
|
+
model_config = SettingsConfigDict(
|
|
153
|
+
env_prefix="MINDER_",
|
|
154
|
+
env_nested_delimiter="__",
|
|
155
|
+
env_file=".env",
|
|
156
|
+
env_file_encoding="utf-8",
|
|
157
|
+
toml_file="minder.toml",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def settings_customise_sources(
|
|
162
|
+
cls,
|
|
163
|
+
settings_cls,
|
|
164
|
+
init_settings,
|
|
165
|
+
env_settings,
|
|
166
|
+
dotenv_settings,
|
|
167
|
+
file_secret_settings,
|
|
168
|
+
):
|
|
169
|
+
return (
|
|
170
|
+
init_settings,
|
|
171
|
+
env_settings,
|
|
172
|
+
dotenv_settings,
|
|
173
|
+
TomlConfigSettingsSource(settings_cls),
|
|
174
|
+
file_secret_settings,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
MinderConfig = Settings
|
|
179
|
+
settings = Settings()
|
minder/continuity.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from minder.config import MinderConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_step_name(value: str | None) -> str:
|
|
11
|
+
return str(value or "").strip().lower()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def step_keywords(step_name: str | None) -> set[str]:
|
|
15
|
+
normalized = normalize_step_name(step_name)
|
|
16
|
+
return {
|
|
17
|
+
token
|
|
18
|
+
for token in normalized.replace("/", " ").replace("_", " ").split()
|
|
19
|
+
if token
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def required_artifacts_for_step(step_name: str | None) -> list[str]:
|
|
24
|
+
normalized = normalize_step_name(step_name)
|
|
25
|
+
if "problem" in normalized or "intake" in normalized:
|
|
26
|
+
return ["problem_statement", "acceptance_criteria"]
|
|
27
|
+
if "analysis" in normalized or "use case" in normalized:
|
|
28
|
+
return ["analysis_notes", "use_cases"]
|
|
29
|
+
if "test" in normalized:
|
|
30
|
+
return ["test_plan", "failing_tests"]
|
|
31
|
+
if "implement" in normalized:
|
|
32
|
+
return ["implementation_notes", "changed_files"]
|
|
33
|
+
if "verif" in normalized:
|
|
34
|
+
return ["verification_report", "test_results"]
|
|
35
|
+
if "review" in normalized:
|
|
36
|
+
return ["review_notes", "approval_summary"]
|
|
37
|
+
if "release" in normalized or "deploy" in normalized:
|
|
38
|
+
return ["release_notes", "rollback_plan"]
|
|
39
|
+
return ["step_notes"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def allowed_tools_for_step(step_name: str | None) -> list[str]:
|
|
43
|
+
normalized = normalize_step_name(step_name)
|
|
44
|
+
base_tools = [
|
|
45
|
+
"minder_session_restore",
|
|
46
|
+
"minder_session_save",
|
|
47
|
+
"minder_session_context",
|
|
48
|
+
"minder_memory_recall",
|
|
49
|
+
"minder_workflow_step",
|
|
50
|
+
"minder_workflow_guard",
|
|
51
|
+
]
|
|
52
|
+
if "test" in normalized:
|
|
53
|
+
return base_tools + ["minder_search_code", "minder_search_errors"]
|
|
54
|
+
if "implement" in normalized:
|
|
55
|
+
return base_tools + ["minder_search_code", "minder_query"]
|
|
56
|
+
if "review" in normalized:
|
|
57
|
+
return base_tools + ["minder_query", "minder_search_code"]
|
|
58
|
+
return base_tools + ["minder_search", "minder_search_code"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def forbidden_actions_for_step(
|
|
62
|
+
step_name: str | None,
|
|
63
|
+
*,
|
|
64
|
+
blocked_by: list[str],
|
|
65
|
+
current_step: str | None,
|
|
66
|
+
) -> list[str]:
|
|
67
|
+
forbidden = ["skip_required_steps", "ignore_workflow_state"]
|
|
68
|
+
if blocked_by:
|
|
69
|
+
forbidden.append("advance_while_blocked")
|
|
70
|
+
if normalize_step_name(step_name) != normalize_step_name(current_step):
|
|
71
|
+
forbidden.append("jump_to_unapproved_step")
|
|
72
|
+
return forbidden
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def output_contract_for_step(step_name: str | None) -> dict[str, Any]:
|
|
76
|
+
normalized = normalize_step_name(step_name)
|
|
77
|
+
if "test" in normalized:
|
|
78
|
+
return {
|
|
79
|
+
"type": "test_spec",
|
|
80
|
+
"must_include": ["target_behaviour", "failing_assertions", "scope_limit"],
|
|
81
|
+
}
|
|
82
|
+
if "implement" in normalized:
|
|
83
|
+
return {
|
|
84
|
+
"type": "implementation_update",
|
|
85
|
+
"must_include": ["changed_files", "minimal_fix", "verification_plan"],
|
|
86
|
+
}
|
|
87
|
+
if "review" in normalized:
|
|
88
|
+
return {
|
|
89
|
+
"type": "review_report",
|
|
90
|
+
"must_include": [
|
|
91
|
+
"blocking_issues",
|
|
92
|
+
"recommended_changes",
|
|
93
|
+
"residual_risks",
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
"type": "step_update",
|
|
98
|
+
"must_include": ["summary", "next_actions"],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_instruction_envelope(
|
|
103
|
+
*,
|
|
104
|
+
workflow: Any,
|
|
105
|
+
workflow_state: Any,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
current_step = getattr(workflow_state, "current_step", None)
|
|
108
|
+
blocked_by = list(getattr(workflow_state, "blocked_by", []) or [])
|
|
109
|
+
artifacts = dict(getattr(workflow_state, "artifacts", {}) or {})
|
|
110
|
+
required_artifacts = required_artifacts_for_step(current_step)
|
|
111
|
+
return {
|
|
112
|
+
"workflow_id": str(getattr(workflow, "id", "")),
|
|
113
|
+
"workflow_version": getattr(workflow, "version", 1),
|
|
114
|
+
"workflow_name": getattr(workflow, "name", ""),
|
|
115
|
+
"current_step": current_step,
|
|
116
|
+
"next_step": getattr(workflow_state, "next_step", None),
|
|
117
|
+
"blocked_by": blocked_by,
|
|
118
|
+
"required_artifacts": required_artifacts,
|
|
119
|
+
"required_artifact_status": {
|
|
120
|
+
artifact_name: artifact_name in artifacts and bool(artifacts[artifact_name])
|
|
121
|
+
for artifact_name in required_artifacts
|
|
122
|
+
},
|
|
123
|
+
"forbidden_actions": forbidden_actions_for_step(
|
|
124
|
+
current_step,
|
|
125
|
+
blocked_by=blocked_by,
|
|
126
|
+
current_step=current_step,
|
|
127
|
+
),
|
|
128
|
+
"allowed_tools": allowed_tools_for_step(current_step),
|
|
129
|
+
"output_contract": output_contract_for_step(current_step),
|
|
130
|
+
"policies": dict(getattr(workflow, "policies", {}) or {}),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_continuity_brief(
|
|
135
|
+
*,
|
|
136
|
+
session: Any,
|
|
137
|
+
workflow_state: Any | None = None,
|
|
138
|
+
workflow: Any | None = None,
|
|
139
|
+
recalled_memories: list[dict[str, Any]] | None = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
state = dict(getattr(session, "state", {}) or {})
|
|
142
|
+
project_context = dict(getattr(session, "project_context", {}) or {})
|
|
143
|
+
active_skills = dict(getattr(session, "active_skills", {}) or {})
|
|
144
|
+
blocked_by = (
|
|
145
|
+
list(getattr(workflow_state, "blocked_by", []) or []) if workflow_state else []
|
|
146
|
+
)
|
|
147
|
+
completed_steps = (
|
|
148
|
+
list(getattr(workflow_state, "completed_steps", []) or [])
|
|
149
|
+
if workflow_state
|
|
150
|
+
else []
|
|
151
|
+
)
|
|
152
|
+
current_step = (
|
|
153
|
+
getattr(workflow_state, "current_step", None) if workflow_state else None
|
|
154
|
+
)
|
|
155
|
+
next_step = getattr(workflow_state, "next_step", None) if workflow_state else None
|
|
156
|
+
|
|
157
|
+
task = (
|
|
158
|
+
state.get("task")
|
|
159
|
+
or state.get("checkpoint")
|
|
160
|
+
or state.get("phase")
|
|
161
|
+
or "Active engineering task"
|
|
162
|
+
)
|
|
163
|
+
repo_path = project_context.get("repo_path") or project_context.get("repo")
|
|
164
|
+
branch = project_context.get("branch")
|
|
165
|
+
open_files = list(project_context.get("open_files", []) or [])
|
|
166
|
+
state_blockers = state.get("blockers") or state.get("blocked_by") or []
|
|
167
|
+
blockers = [
|
|
168
|
+
str(item) for item in [*blocked_by, *state_blockers] if str(item).strip()
|
|
169
|
+
]
|
|
170
|
+
next_actions = list(state.get("next_steps", []) or [])
|
|
171
|
+
if not next_actions and next_step:
|
|
172
|
+
next_actions.append(
|
|
173
|
+
f"Advance to {next_step} once current step requirements are satisfied."
|
|
174
|
+
)
|
|
175
|
+
if current_step and not next_actions:
|
|
176
|
+
next_actions.append(f"Complete the remaining artifacts for {current_step}.")
|
|
177
|
+
|
|
178
|
+
confirmed_progress: list[str] = []
|
|
179
|
+
if completed_steps:
|
|
180
|
+
confirmed_progress.append(
|
|
181
|
+
f"Completed workflow steps: {', '.join(completed_steps)}"
|
|
182
|
+
)
|
|
183
|
+
if open_files:
|
|
184
|
+
confirmed_progress.append(f"Open files in focus: {', '.join(open_files)}")
|
|
185
|
+
if active_skills:
|
|
186
|
+
confirmed_progress.append(
|
|
187
|
+
f"Active skills: {', '.join(sorted(str(key) for key in active_skills.keys()))}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
risk_signals: list[str] = []
|
|
191
|
+
if blockers:
|
|
192
|
+
risk_signals.append("workflow_blocked")
|
|
193
|
+
if workflow and getattr(workflow, "enforcement", "") == "strict":
|
|
194
|
+
risk_signals.append("strict_workflow_enforcement")
|
|
195
|
+
if not open_files:
|
|
196
|
+
risk_signals.append("low_editor_context")
|
|
197
|
+
|
|
198
|
+
source_refs = [f"session:{getattr(session, 'id', '')}"]
|
|
199
|
+
if workflow_state is not None:
|
|
200
|
+
source_refs.append(f"workflow_state:{getattr(workflow_state, 'id', '')}")
|
|
201
|
+
if recalled_memories:
|
|
202
|
+
source_refs.extend(
|
|
203
|
+
f"memory:{item['id']}" for item in recalled_memories[:3] if item.get("id")
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"problem_framing": {
|
|
208
|
+
"task": task,
|
|
209
|
+
"repo_path": repo_path,
|
|
210
|
+
"branch": branch,
|
|
211
|
+
"workflow_step": current_step,
|
|
212
|
+
},
|
|
213
|
+
"confirmed_progress": confirmed_progress,
|
|
214
|
+
"unresolved_blockers": blockers,
|
|
215
|
+
"risk_signals": risk_signals,
|
|
216
|
+
"next_valid_actions": next_actions,
|
|
217
|
+
"source_references": source_refs,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def compatibility_score_for_memory(
|
|
222
|
+
*,
|
|
223
|
+
tags: list[str],
|
|
224
|
+
title: str,
|
|
225
|
+
content: str,
|
|
226
|
+
current_step: str | None,
|
|
227
|
+
artifact_type: str | None,
|
|
228
|
+
) -> tuple[float, list[str]]:
|
|
229
|
+
reasons: list[str] = []
|
|
230
|
+
score = 0.0
|
|
231
|
+
normalized_tags = {str(tag).strip().lower() for tag in tags if str(tag).strip()}
|
|
232
|
+
text = f"{title} {content}".lower()
|
|
233
|
+
|
|
234
|
+
if current_step:
|
|
235
|
+
step_words = step_keywords(current_step)
|
|
236
|
+
if step_words & normalized_tags:
|
|
237
|
+
score += 1.0
|
|
238
|
+
reasons.append("tag_matches_workflow_step")
|
|
239
|
+
elif any(word in text for word in step_words):
|
|
240
|
+
score += 0.6
|
|
241
|
+
reasons.append("content_mentions_workflow_step")
|
|
242
|
+
|
|
243
|
+
if artifact_type:
|
|
244
|
+
normalized_artifact = artifact_type.strip().lower()
|
|
245
|
+
if normalized_artifact in normalized_tags:
|
|
246
|
+
score += 0.5
|
|
247
|
+
reasons.append("tag_matches_artifact_type")
|
|
248
|
+
elif normalized_artifact in text:
|
|
249
|
+
score += 0.3
|
|
250
|
+
reasons.append("content_mentions_artifact_type")
|
|
251
|
+
|
|
252
|
+
return min(score, 1.5), reasons
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _extract_json_object(raw: str) -> dict[str, Any] | None:
|
|
256
|
+
if not raw.strip():
|
|
257
|
+
return None
|
|
258
|
+
candidates = re.findall(r"\{.*\}", raw, flags=re.DOTALL)
|
|
259
|
+
for candidate in candidates:
|
|
260
|
+
try:
|
|
261
|
+
value = json.loads(candidate)
|
|
262
|
+
except json.JSONDecodeError:
|
|
263
|
+
continue
|
|
264
|
+
if isinstance(value, dict):
|
|
265
|
+
return value
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ContinuitySynthesizer:
|
|
270
|
+
def __init__(self, config: MinderConfig) -> None:
|
|
271
|
+
from minder.llm.local import LocalModelLLM
|
|
272
|
+
|
|
273
|
+
self._config = config
|
|
274
|
+
self._llm = LocalModelLLM(
|
|
275
|
+
config.llm.model_path,
|
|
276
|
+
runtime="auto",
|
|
277
|
+
context_length=config.llm.context_length,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def synthesize_memory_hits(
|
|
281
|
+
self,
|
|
282
|
+
*,
|
|
283
|
+
query: str,
|
|
284
|
+
hits: list[dict[str, Any]],
|
|
285
|
+
current_step: str | None,
|
|
286
|
+
artifact_type: str | None,
|
|
287
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
288
|
+
fallback = self._memory_hits_fallback(
|
|
289
|
+
query=query,
|
|
290
|
+
hits=hits,
|
|
291
|
+
current_step=current_step,
|
|
292
|
+
artifact_type=artifact_type,
|
|
293
|
+
)
|
|
294
|
+
prompt = "\n\n".join(
|
|
295
|
+
[
|
|
296
|
+
"You are synthesizing recalled engineering memories for a workflow-aware assistant.",
|
|
297
|
+
"Return only valid JSON with keys: summary, focus, recommended_hit_ids, hit_summaries.",
|
|
298
|
+
"Keep hit_summaries as an object keyed by hit id.",
|
|
299
|
+
f"Current workflow step: {current_step or 'unknown'}",
|
|
300
|
+
f"Artifact type: {artifact_type or 'unknown'}",
|
|
301
|
+
f"User recall query: {query}",
|
|
302
|
+
f"Hits: {json.dumps(hits[:5], ensure_ascii=True, indent=2)}",
|
|
303
|
+
]
|
|
304
|
+
)
|
|
305
|
+
raw = self._llm.complete_text(
|
|
306
|
+
prompt,
|
|
307
|
+
max_tokens=700,
|
|
308
|
+
temperature=min(max(self._config.llm.temperature, 0.05), 0.3),
|
|
309
|
+
fallback="",
|
|
310
|
+
)
|
|
311
|
+
parsed = _extract_json_object(raw)
|
|
312
|
+
if not parsed:
|
|
313
|
+
return fallback, {
|
|
314
|
+
"provider": "heuristic",
|
|
315
|
+
"model": self._config.llm.model_name,
|
|
316
|
+
"runtime": self._llm.runtime,
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
"summary": str(parsed.get("summary", fallback["summary"]))
|
|
320
|
+
or fallback["summary"],
|
|
321
|
+
"focus": str(parsed.get("focus", fallback["focus"])) or fallback["focus"],
|
|
322
|
+
"recommended_hit_ids": list(
|
|
323
|
+
parsed.get("recommended_hit_ids", fallback["recommended_hit_ids"])
|
|
324
|
+
),
|
|
325
|
+
"hit_summaries": {
|
|
326
|
+
str(key): str(value)
|
|
327
|
+
for key, value in dict(
|
|
328
|
+
parsed.get("hit_summaries", fallback["hit_summaries"]) or {}
|
|
329
|
+
).items()
|
|
330
|
+
},
|
|
331
|
+
}, {
|
|
332
|
+
"provider": "local_llm",
|
|
333
|
+
"model": "gemma-4-e2b-it",
|
|
334
|
+
"runtime": self._llm.runtime,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def _memory_hits_fallback(
|
|
338
|
+
self,
|
|
339
|
+
*,
|
|
340
|
+
query: str,
|
|
341
|
+
hits: list[dict[str, Any]],
|
|
342
|
+
current_step: str | None,
|
|
343
|
+
artifact_type: str | None,
|
|
344
|
+
) -> dict[str, Any]:
|
|
345
|
+
focus = current_step or artifact_type or "general retrieval"
|
|
346
|
+
recommended_ids = [str(hit.get("id", "")) for hit in hits[:2] if hit.get("id")]
|
|
347
|
+
hit_summaries = {
|
|
348
|
+
str(hit.get("id", "")): (
|
|
349
|
+
f"Use {hit.get('title', 'this memory')} for {focus}; reasons: {', '.join(hit.get('continuity_reasons', [])) or 'semantic match'}"
|
|
350
|
+
)
|
|
351
|
+
for hit in hits[:5]
|
|
352
|
+
if hit.get("id")
|
|
353
|
+
}
|
|
354
|
+
top_titles = (
|
|
355
|
+
", ".join(str(hit.get("title", "")) for hit in hits[:2] if hit.get("title"))
|
|
356
|
+
or "no strong hits"
|
|
357
|
+
)
|
|
358
|
+
return {
|
|
359
|
+
"summary": f"Top recalled memories for '{query}' focus on {focus}: {top_titles}.",
|
|
360
|
+
"focus": focus,
|
|
361
|
+
"recommended_hit_ids": recommended_ids,
|
|
362
|
+
"hit_summaries": hit_summaries,
|
|
363
|
+
}
|
minder/dev.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
DEFAULT_WATCH_INTERVAL_SECONDS = 0.75
|
|
11
|
+
WATCHED_CONFIG_FILES = (".env", "minder.toml")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def repo_root() -> Path:
|
|
15
|
+
return Path(__file__).resolve().parents[2]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_dev_command() -> list[str]:
|
|
19
|
+
return [sys.executable, "-m", "minder.server"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_dev_env(
|
|
23
|
+
root: Path,
|
|
24
|
+
*,
|
|
25
|
+
transport: str = "sse",
|
|
26
|
+
port: int | None = None,
|
|
27
|
+
) -> dict[str, str]:
|
|
28
|
+
env = os.environ.copy()
|
|
29
|
+
src_path = str(root / "src")
|
|
30
|
+
existing_pythonpath = env.get("PYTHONPATH", "")
|
|
31
|
+
pythonpath_parts = [part for part in existing_pythonpath.split(os.pathsep) if part]
|
|
32
|
+
if src_path not in pythonpath_parts:
|
|
33
|
+
pythonpath_parts.insert(0, src_path)
|
|
34
|
+
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
|
35
|
+
env.setdefault("UV_CACHE_DIR", ".uv-cache")
|
|
36
|
+
env["MINDER_SERVER__TRANSPORT"] = transport
|
|
37
|
+
if port is not None:
|
|
38
|
+
env["MINDER_SERVER__PORT"] = str(port)
|
|
39
|
+
return env
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def collect_watch_files(root: Path) -> list[Path]:
|
|
43
|
+
watched_files: list[Path] = []
|
|
44
|
+
src_root = root / "src"
|
|
45
|
+
if src_root.exists():
|
|
46
|
+
watched_files.extend(sorted(path for path in src_root.rglob("*.py") if path.is_file()))
|
|
47
|
+
for config_name in WATCHED_CONFIG_FILES:
|
|
48
|
+
config_path = root / config_name
|
|
49
|
+
if config_path.is_file():
|
|
50
|
+
watched_files.append(config_path)
|
|
51
|
+
return watched_files
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def snapshot_mtimes(paths: list[Path]) -> dict[str, float]:
|
|
55
|
+
snapshot: dict[str, float] = {}
|
|
56
|
+
for path in paths:
|
|
57
|
+
if path.exists():
|
|
58
|
+
snapshot[str(path)] = path.stat().st_mtime
|
|
59
|
+
return snapshot
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def start_server_process(root: Path, env: dict[str, str]) -> subprocess.Popen[bytes]:
|
|
63
|
+
return subprocess.Popen(build_dev_command(), cwd=root, env=env)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def stop_server_process(process: subprocess.Popen[bytes]) -> None:
|
|
67
|
+
if process.poll() is not None:
|
|
68
|
+
return
|
|
69
|
+
process.terminate()
|
|
70
|
+
try:
|
|
71
|
+
process.wait(timeout=5)
|
|
72
|
+
except subprocess.TimeoutExpired:
|
|
73
|
+
process.kill()
|
|
74
|
+
process.wait(timeout=5)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_dev_server(
|
|
78
|
+
*,
|
|
79
|
+
transport: str = "sse",
|
|
80
|
+
port: int | None = None,
|
|
81
|
+
interval_seconds: float = DEFAULT_WATCH_INTERVAL_SECONDS,
|
|
82
|
+
) -> int:
|
|
83
|
+
root = repo_root()
|
|
84
|
+
env = build_dev_env(root, transport=transport, port=port)
|
|
85
|
+
print(
|
|
86
|
+
"Starting Minder dev server with hot reload "
|
|
87
|
+
f"(transport={transport}, port={env.get('MINDER_SERVER__PORT', 'default')}).",
|
|
88
|
+
flush=True,
|
|
89
|
+
)
|
|
90
|
+
print(f"Watching {root / 'src'} plus {', '.join(WATCHED_CONFIG_FILES)} for changes.", flush=True)
|
|
91
|
+
print("Run with uv run python scripts/dev_server.py", flush=True)
|
|
92
|
+
|
|
93
|
+
process = start_server_process(root, env)
|
|
94
|
+
previous_snapshot = snapshot_mtimes(collect_watch_files(root))
|
|
95
|
+
exit_code: int = 0
|
|
96
|
+
exit_reported = False
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
while True:
|
|
100
|
+
time.sleep(interval_seconds)
|
|
101
|
+
current_snapshot = snapshot_mtimes(collect_watch_files(root))
|
|
102
|
+
if current_snapshot != previous_snapshot:
|
|
103
|
+
previous_snapshot = current_snapshot
|
|
104
|
+
print("Source change detected. Restarting Minder...", flush=True)
|
|
105
|
+
stop_server_process(process)
|
|
106
|
+
process = start_server_process(root, env)
|
|
107
|
+
exit_reported = False
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
current_return_code = process.poll()
|
|
111
|
+
if current_return_code is not None:
|
|
112
|
+
exit_code = current_return_code
|
|
113
|
+
if not exit_reported:
|
|
114
|
+
print(
|
|
115
|
+
f"Minder dev server exited with code {current_return_code}. "
|
|
116
|
+
"Waiting for the next file change to restart.",
|
|
117
|
+
flush=True,
|
|
118
|
+
)
|
|
119
|
+
exit_reported = True
|
|
120
|
+
except KeyboardInterrupt:
|
|
121
|
+
print("Stopping Minder dev server...", flush=True)
|
|
122
|
+
finally:
|
|
123
|
+
stop_server_process(process)
|
|
124
|
+
return exit_code
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
128
|
+
parser = argparse.ArgumentParser(description="Run Minder in dev mode with hot reload.")
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--transport",
|
|
131
|
+
default="sse",
|
|
132
|
+
choices=("sse", "stdio"),
|
|
133
|
+
help="Transport to run during development. Defaults to sse.",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--port",
|
|
137
|
+
type=int,
|
|
138
|
+
default=None,
|
|
139
|
+
help="Override the Minder server port for the dev process.",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--interval",
|
|
143
|
+
type=float,
|
|
144
|
+
default=DEFAULT_WATCH_INTERVAL_SECONDS,
|
|
145
|
+
help="Polling interval in seconds for file watching.",
|
|
146
|
+
)
|
|
147
|
+
return parser.parse_args(argv)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main(argv: list[str] | None = None) -> int:
|
|
151
|
+
args = parse_args(argv)
|
|
152
|
+
return run_dev_server(
|
|
153
|
+
transport=args.transport,
|
|
154
|
+
port=args.port,
|
|
155
|
+
interval_seconds=args.interval,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
raise SystemExit(main())
|