memstack-skill-loader 4.0.7__tar.gz → 4.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memstack_skill_loader-4.0.7/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.1.0}/PKG-INFO +1 -1
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/pyproject.toml +1 -1
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/agent_runner.py +155 -34
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/dashboard.html +410 -15
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/dashboard.py +479 -86
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/README.md +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/setup.cfg +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/categories.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/memory_db.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
|
@@ -28,9 +28,15 @@ AGENT_TIMEOUT = 3600 # seconds per --print invocation (default 60 minutes)
|
|
|
28
28
|
MAX_ITERATIONS = 2
|
|
29
29
|
|
|
30
30
|
ANTHROPIC_BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "")
|
|
31
|
+
API_KEY_FILE = Path.home() / ".memstack" / "api_key"
|
|
31
32
|
API_DEFAULT_MODEL = "claude-sonnet-4-20250514"
|
|
32
33
|
API_MAX_TOKENS = 16000
|
|
33
34
|
|
|
35
|
+
if not os.environ.get("ANTHROPIC_API_KEY") and API_KEY_FILE.is_file():
|
|
36
|
+
_stored_key = API_KEY_FILE.read_text(encoding="utf-8").strip()
|
|
37
|
+
if _stored_key:
|
|
38
|
+
os.environ["ANTHROPIC_API_KEY"] = _stored_key
|
|
39
|
+
|
|
34
40
|
SYSTEM_PROMPTS = {
|
|
35
41
|
"manager": (
|
|
36
42
|
"You are a Manager agent. You receive a task and project context (directory listing "
|
|
@@ -109,30 +115,105 @@ def discover_mcp_servers(workdir: str) -> list[str]:
|
|
|
109
115
|
return []
|
|
110
116
|
|
|
111
117
|
|
|
118
|
+
_VALID_GIT_WORKFLOWS = ("auto", "single_branch", "dev_to_default", "feature_branch")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _migrate_config(cfg: dict) -> dict:
|
|
122
|
+
"""Migrate per-project values from plain arrays to objects with blocked_mcp key."""
|
|
123
|
+
changed = False
|
|
124
|
+
for key, val in list(cfg.items()):
|
|
125
|
+
if key.startswith("_"):
|
|
126
|
+
continue
|
|
127
|
+
if isinstance(val, list):
|
|
128
|
+
cfg[key] = {"blocked_mcp": val}
|
|
129
|
+
changed = True
|
|
130
|
+
if changed:
|
|
131
|
+
save_builder_tools_config(cfg)
|
|
132
|
+
return cfg
|
|
133
|
+
|
|
134
|
+
|
|
112
135
|
def load_builder_tools_config() -> dict:
|
|
113
|
-
"""Load per-project
|
|
136
|
+
"""Load per-project config from ~/.memstack/builder-tools-config.json, migrating if needed."""
|
|
114
137
|
try:
|
|
115
138
|
if BUILDER_TOOLS_CONFIG_FILE.is_file():
|
|
116
|
-
|
|
139
|
+
cfg = json.loads(BUILDER_TOOLS_CONFIG_FILE.read_text(encoding="utf-8"))
|
|
140
|
+
return _migrate_config(cfg)
|
|
117
141
|
except Exception:
|
|
118
142
|
pass
|
|
119
143
|
return {}
|
|
120
144
|
|
|
121
145
|
|
|
122
146
|
def save_builder_tools_config(config: dict) -> None:
|
|
123
|
-
"""Save per-project
|
|
147
|
+
"""Save per-project config."""
|
|
124
148
|
BUILDER_TOOLS_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
125
149
|
BUILDER_TOOLS_CONFIG_FILE.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
126
150
|
|
|
127
151
|
|
|
152
|
+
def _get_project_config(cfg: dict, workdir: str) -> dict:
|
|
153
|
+
"""Return the project config dict for workdir, or empty dict."""
|
|
154
|
+
val = cfg.get(workdir)
|
|
155
|
+
if isinstance(val, dict):
|
|
156
|
+
return val
|
|
157
|
+
return {}
|
|
158
|
+
|
|
159
|
+
|
|
128
160
|
def get_blocked_servers_for_project(workdir: str) -> list[str]:
|
|
129
161
|
"""Return blocked MCP servers for a project, falling back to global defaults."""
|
|
130
162
|
cfg = load_builder_tools_config()
|
|
131
|
-
|
|
132
|
-
|
|
163
|
+
proj = _get_project_config(cfg, workdir)
|
|
164
|
+
if proj:
|
|
165
|
+
return proj.get("blocked_mcp", [])
|
|
133
166
|
return cfg.get("_global_defaults", [])
|
|
134
167
|
|
|
135
168
|
|
|
169
|
+
def get_git_workflow_for_project(workdir: str) -> tuple[str, bool]:
|
|
170
|
+
"""Return (workflow, is_project_config) for a working directory."""
|
|
171
|
+
cfg = load_builder_tools_config()
|
|
172
|
+
proj = _get_project_config(cfg, workdir)
|
|
173
|
+
if proj and "git_workflow" in proj:
|
|
174
|
+
wf = proj["git_workflow"]
|
|
175
|
+
if wf in _VALID_GIT_WORKFLOWS:
|
|
176
|
+
return wf, True
|
|
177
|
+
global_wf = cfg.get("_git_workflow", "auto")
|
|
178
|
+
if global_wf not in _VALID_GIT_WORKFLOWS:
|
|
179
|
+
global_wf = "auto"
|
|
180
|
+
return global_wf, False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_VALID_AGENT_MODES = ("api", "subscription")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _default_agent_modes() -> dict:
|
|
187
|
+
"""Return default execution modes based on whether an API key is available."""
|
|
188
|
+
has_key = bool(os.environ.get("ANTHROPIC_API_KEY") or ANTHROPIC_BASE_URL)
|
|
189
|
+
if has_key:
|
|
190
|
+
return {"manager": "api", "builder": "subscription", "reviewer": "api"}
|
|
191
|
+
return {"manager": "subscription", "builder": "subscription", "reviewer": "subscription"}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_agent_modes() -> dict:
|
|
195
|
+
"""Load agent execution modes from config, falling back to defaults."""
|
|
196
|
+
cfg = load_builder_tools_config()
|
|
197
|
+
stored = cfg.get("_agent_modes", {})
|
|
198
|
+
defaults = _default_agent_modes()
|
|
199
|
+
modes = {}
|
|
200
|
+
for agent in ("manager", "builder", "reviewer"):
|
|
201
|
+
val = stored.get(agent, "")
|
|
202
|
+
modes[agent] = val if val in _VALID_AGENT_MODES else defaults[agent]
|
|
203
|
+
return modes
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def save_agent_modes(modes: dict) -> None:
|
|
207
|
+
"""Save agent execution modes to config."""
|
|
208
|
+
cfg = load_builder_tools_config()
|
|
209
|
+
clean = {}
|
|
210
|
+
for agent in ("manager", "builder", "reviewer"):
|
|
211
|
+
val = modes.get(agent, "")
|
|
212
|
+
clean[agent] = val if val in _VALID_AGENT_MODES else "subscription"
|
|
213
|
+
cfg["_agent_modes"] = clean
|
|
214
|
+
save_builder_tools_config(cfg)
|
|
215
|
+
|
|
216
|
+
|
|
136
217
|
# ---------------------------------------------------------------------------
|
|
137
218
|
# Project context gathering
|
|
138
219
|
# ---------------------------------------------------------------------------
|
|
@@ -727,6 +808,14 @@ def _orchestrate(session: Session) -> None:
|
|
|
727
808
|
session_log_dir = STATE_DIR / "sessions" / session.session_id
|
|
728
809
|
|
|
729
810
|
try:
|
|
811
|
+
# Load execution modes for this run
|
|
812
|
+
agent_modes = load_agent_modes()
|
|
813
|
+
has_api_key = bool(os.environ.get("ANTHROPIC_API_KEY") or ANTHROPIC_BASE_URL)
|
|
814
|
+
for _ag in ("manager", "builder", "reviewer"):
|
|
815
|
+
if agent_modes[_ag] == "api" and not has_api_key:
|
|
816
|
+
agent_modes[_ag] = "subscription"
|
|
817
|
+
print(f"[agent-runner] {_ag}: mode=api but no API key set, falling back to subscription", file=sys.stderr)
|
|
818
|
+
|
|
730
819
|
# Step 1: Manager analyzes the task
|
|
731
820
|
session.agents["manager"]["status"] = "busy"
|
|
732
821
|
session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
@@ -741,15 +830,25 @@ def _orchestrate(session: Session) -> None:
|
|
|
741
830
|
if session.user_name:
|
|
742
831
|
manager_prompt = f"The user's name is {session.user_name}.\n\n" + manager_prompt
|
|
743
832
|
try:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
833
|
+
if agent_modes["manager"] == "api":
|
|
834
|
+
manager_output, m_in, m_out = _invoke_api_agent(
|
|
835
|
+
"manager", manager_prompt,
|
|
836
|
+
system_prompt=SYSTEM_PROMPTS["manager"],
|
|
837
|
+
log_path=session_log_dir / "manager.log",
|
|
838
|
+
timeout=min(600, session.timeout),
|
|
839
|
+
model=session.models.get("manager", ""),
|
|
840
|
+
session_id=session.session_id,
|
|
841
|
+
working_dir=session.working_dir,
|
|
842
|
+
)
|
|
843
|
+
m_ctx = m_in
|
|
844
|
+
else:
|
|
845
|
+
manager_output, m_in, m_out, m_ctx = _invoke_agent(
|
|
846
|
+
"manager", manager_prompt, session.working_dir,
|
|
847
|
+
log_path=session_log_dir / "manager.log",
|
|
848
|
+
skip_permissions=True, session_id=session.session_id,
|
|
849
|
+
timeout=min(600, session.timeout),
|
|
850
|
+
model=session.models.get("manager", ""),
|
|
851
|
+
)
|
|
753
852
|
except subprocess.TimeoutExpired:
|
|
754
853
|
session.agents["manager"]["status"] = "timeout"
|
|
755
854
|
session.status = "error"
|
|
@@ -764,7 +863,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
764
863
|
return
|
|
765
864
|
session.agents["manager"]["input_tokens"] += m_in
|
|
766
865
|
session.agents["manager"]["output_tokens"] += m_out
|
|
767
|
-
session.agents["manager"]["context_tokens"] =
|
|
866
|
+
session.agents["manager"]["context_tokens"] = m_ctx
|
|
768
867
|
session.agents["manager"]["last_output"] = (manager_output or "")[:500]
|
|
769
868
|
|
|
770
869
|
session.agents["manager"]["status"] = "done"
|
|
@@ -804,15 +903,27 @@ def _orchestrate(session: Session) -> None:
|
|
|
804
903
|
if session.user_name:
|
|
805
904
|
builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
|
|
806
905
|
try:
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
906
|
+
if agent_modes["builder"] == "subscription":
|
|
907
|
+
builder_output, b_in, b_out, b_ctx = _invoke_agent(
|
|
908
|
+
"builder", builder_prompt, session.working_dir,
|
|
909
|
+
log_path=session_log_dir / "builder.log",
|
|
910
|
+
skip_permissions=True, session_id=session.session_id,
|
|
911
|
+
timeout=session.timeout,
|
|
912
|
+
model=session.models.get("builder", ""),
|
|
913
|
+
disallowed_tools=session.blocked_mcp_servers,
|
|
914
|
+
session=session,
|
|
915
|
+
)
|
|
916
|
+
else:
|
|
917
|
+
builder_output, b_in, b_out = _invoke_api_agent(
|
|
918
|
+
"builder", builder_prompt,
|
|
919
|
+
system_prompt=SYSTEM_PROMPTS["builder"].format(working_dir=session.working_dir),
|
|
920
|
+
log_path=session_log_dir / "builder.log",
|
|
921
|
+
timeout=session.timeout,
|
|
922
|
+
model=session.models.get("builder", ""),
|
|
923
|
+
session_id=session.session_id,
|
|
924
|
+
working_dir=session.working_dir,
|
|
925
|
+
)
|
|
926
|
+
b_ctx = b_in
|
|
816
927
|
except subprocess.TimeoutExpired:
|
|
817
928
|
session.agents["builder"]["status"] = "timeout"
|
|
818
929
|
session.status = "error"
|
|
@@ -870,15 +981,25 @@ def _orchestrate(session: Session) -> None:
|
|
|
870
981
|
+ f"\n\nBuilder output (iteration {iteration}):\n{builder_output}"
|
|
871
982
|
)
|
|
872
983
|
try:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
984
|
+
if agent_modes["reviewer"] == "api":
|
|
985
|
+
reviewer_output, r_in, r_out = _invoke_api_agent(
|
|
986
|
+
"reviewer", reviewer_prompt,
|
|
987
|
+
system_prompt=SYSTEM_PROMPTS["reviewer"],
|
|
988
|
+
log_path=session_log_dir / "reviewer.log",
|
|
989
|
+
timeout=session.timeout,
|
|
990
|
+
model=session.models.get("reviewer", ""),
|
|
991
|
+
session_id=session.session_id,
|
|
992
|
+
working_dir=session.working_dir,
|
|
993
|
+
)
|
|
994
|
+
r_ctx = r_in
|
|
995
|
+
else:
|
|
996
|
+
reviewer_output, r_in, r_out, r_ctx = _invoke_agent(
|
|
997
|
+
"reviewer", reviewer_prompt, session.working_dir,
|
|
998
|
+
log_path=session_log_dir / "reviewer.log",
|
|
999
|
+
skip_permissions=True, session_id=session.session_id,
|
|
1000
|
+
timeout=session.timeout,
|
|
1001
|
+
model=session.models.get("reviewer", ""),
|
|
1002
|
+
)
|
|
882
1003
|
except subprocess.TimeoutExpired:
|
|
883
1004
|
session.agents["reviewer"]["status"] = "timeout"
|
|
884
1005
|
session.status = "error"
|
|
@@ -893,7 +1014,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
893
1014
|
return
|
|
894
1015
|
session.agents["reviewer"]["input_tokens"] += r_in
|
|
895
1016
|
session.agents["reviewer"]["output_tokens"] += r_out
|
|
896
|
-
session.agents["reviewer"]["context_tokens"] =
|
|
1017
|
+
session.agents["reviewer"]["context_tokens"] = r_ctx
|
|
897
1018
|
session.agents["reviewer"]["last_output"] = (reviewer_output or "")[:500]
|
|
898
1019
|
|
|
899
1020
|
session.agents["reviewer"]["status"] = "done"
|