memstack-skill-loader 4.0.6__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.6/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.1.0}/PKG-INFO +1 -1
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/pyproject.toml +1 -1
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/agent_runner.py +176 -33
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/dashboard.html +498 -69
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/dashboard.py +495 -76
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/stats.py +57 -1
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/README.md +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/setup.cfg +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/categories.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/memory_db.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.0.6 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.0.6 → 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
|
# ---------------------------------------------------------------------------
|
|
@@ -190,6 +271,24 @@ def _extract_task_text(raw: str) -> str:
|
|
|
190
271
|
return result[:72]
|
|
191
272
|
|
|
192
273
|
|
|
274
|
+
def _clean_task_description(task: str) -> str:
|
|
275
|
+
"""Strip Working directory/Branch boilerplate, returning the user's task description."""
|
|
276
|
+
import re
|
|
277
|
+
_BOILERPLATE_LINE = re.compile(
|
|
278
|
+
r'^(working\s+directory:|branch:|read\s+all\s+files\s+before\s+modifying\.?)',
|
|
279
|
+
re.IGNORECASE
|
|
280
|
+
)
|
|
281
|
+
lines = task.splitlines()
|
|
282
|
+
start = 0
|
|
283
|
+
for i, line in enumerate(lines):
|
|
284
|
+
stripped = line.strip()
|
|
285
|
+
if not stripped or _BOILERPLATE_LINE.match(stripped):
|
|
286
|
+
start = i + 1
|
|
287
|
+
else:
|
|
288
|
+
break
|
|
289
|
+
return "\n".join(lines[start:]).strip()
|
|
290
|
+
|
|
291
|
+
|
|
193
292
|
def _extract_commit_from_reviewer(reviewer_output: str) -> str:
|
|
194
293
|
"""Extract a commit-friendly summary from the reviewer's APPROVED message."""
|
|
195
294
|
import re
|
|
@@ -334,6 +433,7 @@ def _extract_text_from_stream_line(line: str) -> Optional[str]:
|
|
|
334
433
|
def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
|
|
335
434
|
log_path: Optional[Path] = None, timeout: int = 600,
|
|
336
435
|
model: str = "", session_id: Optional[str] = None,
|
|
436
|
+
working_dir: str = "",
|
|
337
437
|
) -> tuple[str, int, int]:
|
|
338
438
|
"""Call the Anthropic Messages API directly via httpx.
|
|
339
439
|
|
|
@@ -411,7 +511,7 @@ def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
|
|
|
411
511
|
|
|
412
512
|
try:
|
|
413
513
|
log_agent_invocation(
|
|
414
|
-
name, len(prompt), len(output), session_id,
|
|
514
|
+
name, len(prompt), len(output), session_id, working_dir,
|
|
415
515
|
input_tokens=input_tokens, output_tokens=output_tokens, cost_usd=0.0,
|
|
416
516
|
)
|
|
417
517
|
except Exception:
|
|
@@ -646,6 +746,7 @@ class Session:
|
|
|
646
746
|
meta = {
|
|
647
747
|
"session_id": self.session_id,
|
|
648
748
|
"task": self.task,
|
|
749
|
+
"task_description": _clean_task_description(self.task),
|
|
649
750
|
"status": self.status,
|
|
650
751
|
"started_at": self.started_at,
|
|
651
752
|
"iteration": self.iteration,
|
|
@@ -707,6 +808,14 @@ def _orchestrate(session: Session) -> None:
|
|
|
707
808
|
session_log_dir = STATE_DIR / "sessions" / session.session_id
|
|
708
809
|
|
|
709
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
|
+
|
|
710
819
|
# Step 1: Manager analyzes the task
|
|
711
820
|
session.agents["manager"]["status"] = "busy"
|
|
712
821
|
session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
@@ -721,14 +830,25 @@ def _orchestrate(session: Session) -> None:
|
|
|
721
830
|
if session.user_name:
|
|
722
831
|
manager_prompt = f"The user's name is {session.user_name}.\n\n" + manager_prompt
|
|
723
832
|
try:
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
)
|
|
732
852
|
except subprocess.TimeoutExpired:
|
|
733
853
|
session.agents["manager"]["status"] = "timeout"
|
|
734
854
|
session.status = "error"
|
|
@@ -743,7 +863,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
743
863
|
return
|
|
744
864
|
session.agents["manager"]["input_tokens"] += m_in
|
|
745
865
|
session.agents["manager"]["output_tokens"] += m_out
|
|
746
|
-
session.agents["manager"]["context_tokens"] =
|
|
866
|
+
session.agents["manager"]["context_tokens"] = m_ctx
|
|
747
867
|
session.agents["manager"]["last_output"] = (manager_output or "")[:500]
|
|
748
868
|
|
|
749
869
|
session.agents["manager"]["status"] = "done"
|
|
@@ -783,15 +903,27 @@ def _orchestrate(session: Session) -> None:
|
|
|
783
903
|
if session.user_name:
|
|
784
904
|
builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
|
|
785
905
|
try:
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
|
795
927
|
except subprocess.TimeoutExpired:
|
|
796
928
|
session.agents["builder"]["status"] = "timeout"
|
|
797
929
|
session.status = "error"
|
|
@@ -849,14 +981,25 @@ def _orchestrate(session: Session) -> None:
|
|
|
849
981
|
+ f"\n\nBuilder output (iteration {iteration}):\n{builder_output}"
|
|
850
982
|
)
|
|
851
983
|
try:
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
+
)
|
|
860
1003
|
except subprocess.TimeoutExpired:
|
|
861
1004
|
session.agents["reviewer"]["status"] = "timeout"
|
|
862
1005
|
session.status = "error"
|
|
@@ -871,7 +1014,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
871
1014
|
return
|
|
872
1015
|
session.agents["reviewer"]["input_tokens"] += r_in
|
|
873
1016
|
session.agents["reviewer"]["output_tokens"] += r_out
|
|
874
|
-
session.agents["reviewer"]["context_tokens"] =
|
|
1017
|
+
session.agents["reviewer"]["context_tokens"] = r_ctx
|
|
875
1018
|
session.agents["reviewer"]["last_output"] = (reviewer_output or "")[:500]
|
|
876
1019
|
|
|
877
1020
|
session.agents["reviewer"]["status"] = "done"
|