memstack-skill-loader 4.10.0__tar.gz → 4.11.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.10.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.11.0}/PKG-INFO +1 -1
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/pyproject.toml +2 -2
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/agent_runner.py +214 -3
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/dashboard.html +37 -18
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/memory_db.py +161 -16
- memstack_skill_loader-4.11.0/src/memstack_skill_loader/skill_descriptions.json +522 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +4 -0
- memstack_skill_loader-4.11.0/tests/test_advisor_strategy.py +195 -0
- memstack_skill_loader-4.11.0/tests/test_failed_approaches.py +83 -0
- memstack_skill_loader-4.11.0/tests/test_search_memory.py +161 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/README.md +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/setup.cfg +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/categories.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/dashboard.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/goal.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/goal_engine.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/hooks/__init__.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/hooks/deny_git.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/server.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_body_parser_metric.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_code_read_savings.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_license_environment.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_license_grace.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_pro_compressor.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_pro_skills_update.py +0 -0
- {memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/tests/test_skill_drift.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "memstack-skill-loader"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.11.0"
|
|
8
8
|
description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
@@ -23,4 +23,4 @@ memstack-index = "memstack_skill_loader.indexer:main"
|
|
|
23
23
|
where = ["src"]
|
|
24
24
|
|
|
25
25
|
[tool.setuptools.package-data]
|
|
26
|
-
memstack_skill_loader = ["dashboard.html"]
|
|
26
|
+
memstack_skill_loader = ["dashboard.html", "skill_descriptions.json"]
|
|
@@ -156,6 +156,35 @@ SYSTEM_PROMPTS = {
|
|
|
156
156
|
"or update a .env.example file with the variable name and a placeholder value.\n"
|
|
157
157
|
"- Do NOT run git add, git commit, or any git commands. Only make file changes."
|
|
158
158
|
),
|
|
159
|
+
"advisor": (
|
|
160
|
+
"You are an Advisor agent. You are called in ONLY when a Builder agent has failed "
|
|
161
|
+
"three consecutive review cycles on the SAME subtask. The approach being used is not "
|
|
162
|
+
"working and will not start working by trying harder — it needs to change.\n\n"
|
|
163
|
+
"You receive:\n"
|
|
164
|
+
"- The original subtask specification.\n"
|
|
165
|
+
"- The full history of Builder attempts and the Reviewer's REVISE feedback for each.\n\n"
|
|
166
|
+
"Your job:\n"
|
|
167
|
+
"1. DIAGNOSE the root cause. Identify the single recurring reason the attempts keep "
|
|
168
|
+
"failing review — look for the pattern across all attempts, not just the last one.\n"
|
|
169
|
+
"2. PRESCRIBE a DIFFERENT fundamental approach. It MUST be materially different from "
|
|
170
|
+
"what has already been tried — a different file, a different mechanism, a different "
|
|
171
|
+
"sequence, or a different technique. Do NOT restate, rephrase, or lightly tweak the "
|
|
172
|
+
"approach that already failed.\n\n"
|
|
173
|
+
"Hard rules:\n"
|
|
174
|
+
"- Do NOT use tools. Reason only from the text provided.\n"
|
|
175
|
+
"- Do NOT repeat the failed approach. If your recommendation resembles what was already "
|
|
176
|
+
"tried, it is wrong — find another angle.\n"
|
|
177
|
+
"- Be concrete and file-specific. The Builder will act on this directly.\n"
|
|
178
|
+
"- Be concise. This output is injected into the Builder's next prompt.\n\n"
|
|
179
|
+
"Output EXACTLY this structure:\n"
|
|
180
|
+
"ROOT CAUSE: <one or two sentences naming why every attempt failed>\n"
|
|
181
|
+
"DIFFERENT APPROACH: <the new approach, explicitly different from what failed>\n"
|
|
182
|
+
"CONCRETE STEPS:\n"
|
|
183
|
+
"- <step 1>\n"
|
|
184
|
+
"- <step 2>\n"
|
|
185
|
+
"- <step 3 (add more only if needed)>\n"
|
|
186
|
+
"AVOID: <the specific thing the Builder must stop doing>"
|
|
187
|
+
),
|
|
159
188
|
"reviewer": (
|
|
160
189
|
"You are a Reviewer agent. You evaluate the Builder's changes against the original task "
|
|
161
190
|
"specification using a structured 4-perspective review.\n\n"
|
|
@@ -1016,6 +1045,119 @@ def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
|
|
|
1016
1045
|
return output, total_input_tokens, output_tokens
|
|
1017
1046
|
|
|
1018
1047
|
|
|
1048
|
+
# ---------------------------------------------------------------------------
|
|
1049
|
+
# Advisor consult (direct API — no file tools; fires after 3 REVISE strikes)
|
|
1050
|
+
# ---------------------------------------------------------------------------
|
|
1051
|
+
|
|
1052
|
+
def _consult_advisor(session: "Session", spec: str, attempts_history: str,
|
|
1053
|
+
log_path: Optional[Path] = None, model: str = "",
|
|
1054
|
+
display_name: str = "Advisor") -> str:
|
|
1055
|
+
"""Consult the Advisor after 3 consecutive Reviewer-REVISE strikes on one subtask.
|
|
1056
|
+
|
|
1057
|
+
Runs via the direct Anthropic API (_invoke_api_agent) — no file tools. Returns a
|
|
1058
|
+
formatted intervention block to fold into the next Builder prompt, or "" on any
|
|
1059
|
+
failure so the caller degrades gracefully to normal revision. Advisor token usage
|
|
1060
|
+
is written to advisor.log only; it is intentionally NOT tracked in session.agents.
|
|
1061
|
+
"""
|
|
1062
|
+
advisor_input = (
|
|
1063
|
+
f"Subtask specification:\n{spec}\n\n"
|
|
1064
|
+
"The Builder has now failed three consecutive review cycles on this subtask. "
|
|
1065
|
+
"Here is the full history of attempts and the Reviewer's REVISE feedback:\n\n"
|
|
1066
|
+
f"{attempts_history}\n\n"
|
|
1067
|
+
"Diagnose the recurring root cause and prescribe a DIFFERENT fundamental approach."
|
|
1068
|
+
)
|
|
1069
|
+
try:
|
|
1070
|
+
advisor_output, _a_in, _a_out = _invoke_api_agent(
|
|
1071
|
+
"advisor", advisor_input,
|
|
1072
|
+
system_prompt=SYSTEM_PROMPTS["advisor"],
|
|
1073
|
+
log_path=log_path,
|
|
1074
|
+
timeout=session.timeout,
|
|
1075
|
+
model=model,
|
|
1076
|
+
session_id=session.session_id,
|
|
1077
|
+
working_dir=session.working_dir,
|
|
1078
|
+
display_name=display_name,
|
|
1079
|
+
)
|
|
1080
|
+
except Exception as exc:
|
|
1081
|
+
session.add_message("system", "all", f"Advisor consult failed ({exc}); continuing with normal revision")
|
|
1082
|
+
return ""
|
|
1083
|
+
if not (advisor_output or "").strip():
|
|
1084
|
+
return ""
|
|
1085
|
+
# Decision 3: post under the already-registered "system" role, not a new author.
|
|
1086
|
+
session.add_message("system", "builder", advisor_output)
|
|
1087
|
+
return (
|
|
1088
|
+
"=== ADVISOR INTERVENTION (after 3 failed attempts) ===\n"
|
|
1089
|
+
"The previous approach has failed repeatedly. Do NOT repeat it. "
|
|
1090
|
+
"Follow this alternative approach instead:\n"
|
|
1091
|
+
f"{advisor_output}\n"
|
|
1092
|
+
"=== END ADVISOR INTERVENTION ==="
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
# ---------------------------------------------------------------------------
|
|
1097
|
+
# Failed-approaches memory (best-effort; never interrupts a run)
|
|
1098
|
+
# ---------------------------------------------------------------------------
|
|
1099
|
+
|
|
1100
|
+
_FA_STORE_SLICE = 1200 # max chars persisted per approach / feedback
|
|
1101
|
+
_FA_SHOW_SLICE = 500 # max chars shown per field in the read-back block
|
|
1102
|
+
_FA_TOPK = 3
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def _record_failed_approach(session: "Session", task_text: str,
|
|
1106
|
+
title: Optional[str], spec: Optional[str],
|
|
1107
|
+
builder_output: str, reviewer_output: str,
|
|
1108
|
+
iteration: int) -> None:
|
|
1109
|
+
"""Persist one Reviewer-REVISE failure. Best-effort; never raises."""
|
|
1110
|
+
try:
|
|
1111
|
+
from .memory_db import record_failed_approach
|
|
1112
|
+
record_failed_approach(
|
|
1113
|
+
project_dir=session.working_dir,
|
|
1114
|
+
task_text=task_text or "",
|
|
1115
|
+
approach_summary=(builder_output or "")[:_FA_STORE_SLICE],
|
|
1116
|
+
reviewer_feedback=(reviewer_output or "")[:_FA_STORE_SLICE],
|
|
1117
|
+
iteration=iteration,
|
|
1118
|
+
subtask_title=title,
|
|
1119
|
+
subtask_spec=spec,
|
|
1120
|
+
session_id=session.session_id,
|
|
1121
|
+
)
|
|
1122
|
+
except Exception as exc:
|
|
1123
|
+
print(f"[agent-runner] record_failed_approach failed: {exc}", file=sys.stderr)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _failed_approaches_block(session: "Session", task_text: str,
|
|
1127
|
+
title: Optional[str] = None,
|
|
1128
|
+
spec: Optional[str] = None) -> str:
|
|
1129
|
+
"""Return a 'previously failed approaches' block, or '' if none / on error."""
|
|
1130
|
+
try:
|
|
1131
|
+
from .memory_db import query_failed_approaches
|
|
1132
|
+
key = " ".join(p for p in (task_text, title, spec) if p)
|
|
1133
|
+
rows = query_failed_approaches(
|
|
1134
|
+
key, project_dir=session.working_dir, top_k=_FA_TOPK
|
|
1135
|
+
)
|
|
1136
|
+
if not rows:
|
|
1137
|
+
return ""
|
|
1138
|
+
lines = [
|
|
1139
|
+
"=== PREVIOUSLY FAILED APPROACHES ON SIMILAR TASKS ===",
|
|
1140
|
+
"These were tried on similar work and REJECTED by the reviewer. "
|
|
1141
|
+
"Do NOT repeat them; learn from why they failed.",
|
|
1142
|
+
"",
|
|
1143
|
+
]
|
|
1144
|
+
for i, r in enumerate(rows, 1):
|
|
1145
|
+
t = r.get("subtask_title") or "(whole task)"
|
|
1146
|
+
lines.append(f"[{i}] (attempt {r.get('iteration')}) {t}")
|
|
1147
|
+
lines.append(
|
|
1148
|
+
f" Approach tried: {(r.get('approach_summary') or '')[:_FA_SHOW_SLICE]}"
|
|
1149
|
+
)
|
|
1150
|
+
lines.append(
|
|
1151
|
+
f" Why it failed: {(r.get('reviewer_feedback') or '')[:_FA_SHOW_SLICE]}"
|
|
1152
|
+
)
|
|
1153
|
+
lines.append("")
|
|
1154
|
+
lines.append("=== END PREVIOUSLY FAILED APPROACHES ===")
|
|
1155
|
+
return "\n".join(lines)
|
|
1156
|
+
except Exception as exc:
|
|
1157
|
+
print(f"[agent-runner] failed_approaches lookup failed: {exc}", file=sys.stderr)
|
|
1158
|
+
return ""
|
|
1159
|
+
|
|
1160
|
+
|
|
1019
1161
|
# ---------------------------------------------------------------------------
|
|
1020
1162
|
# CC subprocess invocation (Builder — needs file tools)
|
|
1021
1163
|
# ---------------------------------------------------------------------------
|
|
@@ -1532,6 +1674,9 @@ def _orchestrate(session: Session) -> None:
|
|
|
1532
1674
|
session.add_message("system", "all", f"Starting subtask {idx} of {len(subtasks)}: {title}")
|
|
1533
1675
|
builder_input = spec
|
|
1534
1676
|
subtask_passed = False
|
|
1677
|
+
fail_streak = 0 # consecutive Reviewer-REVISE strikes on THIS subtask
|
|
1678
|
+
advisor_used = False # cap: at most one Advisor consult per subtask
|
|
1679
|
+
attempts_history: list[str] = []
|
|
1535
1680
|
|
|
1536
1681
|
for iteration in range(1, MAX_ITERATIONS + 1):
|
|
1537
1682
|
if session.status == "stopped":
|
|
@@ -1551,9 +1696,15 @@ def _orchestrate(session: Session) -> None:
|
|
|
1551
1696
|
session.agents["builder"]["last_output"] = ""
|
|
1552
1697
|
session._save_state()
|
|
1553
1698
|
|
|
1699
|
+
fa_block = ""
|
|
1700
|
+
if iteration == 1:
|
|
1701
|
+
fa_block = _failed_approaches_block(
|
|
1702
|
+
session, session.task, title, spec
|
|
1703
|
+
)
|
|
1554
1704
|
builder_prompt = (
|
|
1555
1705
|
SYSTEM_PROMPTS["builder"].format(working_dir=session.working_dir)
|
|
1556
1706
|
+ "\n\n" + context
|
|
1707
|
+
+ (("\n\n" + fa_block) if fa_block else "")
|
|
1557
1708
|
+ f"\n\nOriginal task:\n{session.task}"
|
|
1558
1709
|
+ (f"\n\nAdditional context from user:\n{session.context}" if session.context else "")
|
|
1559
1710
|
+ f"\n\nBuild specification:\n{builder_input}"
|
|
@@ -1741,10 +1892,37 @@ def _orchestrate(session: Session) -> None:
|
|
|
1741
1892
|
session.add_message("system", "all", f"Subtask {idx} of {len(subtasks)} approved: {title}")
|
|
1742
1893
|
break
|
|
1743
1894
|
|
|
1895
|
+
# Reviewer requested changes (REVISE) — a retryable strike. Hard-fails
|
|
1896
|
+
# never reach here; they break out of the iteration loop above.
|
|
1897
|
+
fail_streak += 1
|
|
1898
|
+
attempts_history.append(
|
|
1899
|
+
f"--- Attempt {iteration} ---\n"
|
|
1900
|
+
f"Builder did:\n{(builder_output or '')[:1500]}\n"
|
|
1901
|
+
f"Reviewer REVISE feedback:\n{(reviewer_output or '')[:1500]}"
|
|
1902
|
+
)
|
|
1903
|
+
_record_failed_approach(
|
|
1904
|
+
session, session.task, title, spec,
|
|
1905
|
+
builder_output, reviewer_output, iteration,
|
|
1906
|
+
)
|
|
1907
|
+
advisor_block = ""
|
|
1908
|
+
if fail_streak >= 3 and not advisor_used:
|
|
1909
|
+
advisor_used = True
|
|
1910
|
+
session.add_message(
|
|
1911
|
+
"system", "all",
|
|
1912
|
+
f"Subtask {idx} of {len(subtasks)}: Builder stuck after "
|
|
1913
|
+
f"{fail_streak} attempts — consulting Advisor for a new approach",
|
|
1914
|
+
)
|
|
1915
|
+
advisor_block = _consult_advisor(
|
|
1916
|
+
session, spec, "\n\n".join(attempts_history),
|
|
1917
|
+
log_path=session_log_dir / "advisor.log",
|
|
1918
|
+
model=session.models.get("advisor", ""),
|
|
1919
|
+
display_name=display_names.get("advisor", "Advisor"),
|
|
1920
|
+
)
|
|
1744
1921
|
builder_input = (
|
|
1745
1922
|
f"The reviewer requested changes:\n{reviewer_output}\n\n"
|
|
1746
1923
|
f"Original subtask specification:\n{spec}\n\n"
|
|
1747
|
-
"
|
|
1924
|
+
+ (advisor_block + "\n\n" if advisor_block else "")
|
|
1925
|
+
+ "Please address the reviewer's feedback."
|
|
1748
1926
|
)
|
|
1749
1927
|
else:
|
|
1750
1928
|
if not subtask_passed:
|
|
@@ -1826,6 +2004,9 @@ def _orchestrate(session: Session) -> None:
|
|
|
1826
2004
|
|
|
1827
2005
|
# Step 2-3: Builder/Reviewer loop
|
|
1828
2006
|
builder_input = manager_output
|
|
2007
|
+
fail_streak = 0 # consecutive Reviewer-REVISE strikes on this task
|
|
2008
|
+
advisor_used = False # cap: at most one Advisor consult per task
|
|
2009
|
+
attempts_history: list[str] = []
|
|
1829
2010
|
|
|
1830
2011
|
for iteration in range(1, MAX_ITERATIONS + 1):
|
|
1831
2012
|
if session.status == "stopped":
|
|
@@ -1845,9 +2026,13 @@ def _orchestrate(session: Session) -> None:
|
|
|
1845
2026
|
session.agents["builder"]["last_output"] = ""
|
|
1846
2027
|
session._save_state()
|
|
1847
2028
|
|
|
2029
|
+
fa_block = ""
|
|
2030
|
+
if iteration == 1:
|
|
2031
|
+
fa_block = _failed_approaches_block(session, session.task)
|
|
1848
2032
|
builder_prompt = (
|
|
1849
2033
|
SYSTEM_PROMPTS["builder"].format(working_dir=session.working_dir)
|
|
1850
2034
|
+ "\n\n" + context
|
|
2035
|
+
+ (("\n\n" + fa_block) if fa_block else "")
|
|
1851
2036
|
+ f"\n\nOriginal task:\n{session.task}"
|
|
1852
2037
|
+ (f"\n\nAdditional context from user:\n{session.context}" if session.context else "")
|
|
1853
2038
|
+ f"\n\nBuild specification:\n{builder_input}"
|
|
@@ -2088,11 +2273,37 @@ def _orchestrate(session: Session) -> None:
|
|
|
2088
2273
|
session._save_state()
|
|
2089
2274
|
return
|
|
2090
2275
|
|
|
2091
|
-
# Changes requested — feed back to builder
|
|
2276
|
+
# Changes requested — feed back to builder. Retryable REVISE strike;
|
|
2277
|
+
# hard-fails return out above and never reach here.
|
|
2278
|
+
fail_streak += 1
|
|
2279
|
+
attempts_history.append(
|
|
2280
|
+
f"--- Attempt {iteration} ---\n"
|
|
2281
|
+
f"Builder did:\n{(builder_output or '')[:1500]}\n"
|
|
2282
|
+
f"Reviewer REVISE feedback:\n{(reviewer_output or '')[:1500]}"
|
|
2283
|
+
)
|
|
2284
|
+
_record_failed_approach(
|
|
2285
|
+
session, session.task, None, None,
|
|
2286
|
+
builder_output, reviewer_output, iteration,
|
|
2287
|
+
)
|
|
2288
|
+
advisor_block = ""
|
|
2289
|
+
if fail_streak >= 3 and not advisor_used:
|
|
2290
|
+
advisor_used = True
|
|
2291
|
+
session.add_message(
|
|
2292
|
+
"system", "all",
|
|
2293
|
+
f"Builder stuck after {fail_streak} attempts — "
|
|
2294
|
+
"consulting Advisor for a new approach",
|
|
2295
|
+
)
|
|
2296
|
+
advisor_block = _consult_advisor(
|
|
2297
|
+
session, manager_output, "\n\n".join(attempts_history),
|
|
2298
|
+
log_path=session_log_dir / "advisor.log",
|
|
2299
|
+
model=session.models.get("advisor", ""),
|
|
2300
|
+
display_name=display_names.get("advisor", "Advisor"),
|
|
2301
|
+
)
|
|
2092
2302
|
builder_input = (
|
|
2093
2303
|
f"The reviewer requested changes:\n{reviewer_output}\n\n"
|
|
2094
2304
|
f"Original manager specification:\n{manager_output}\n\n"
|
|
2095
|
-
"
|
|
2305
|
+
+ (advisor_block + "\n\n" if advisor_block else "")
|
|
2306
|
+
+ "Please address the reviewer's feedback."
|
|
2096
2307
|
)
|
|
2097
2308
|
|
|
2098
2309
|
session.status = "completed"
|
|
@@ -3599,7 +3599,7 @@ async function resetAgentUI() {
|
|
|
3599
3599
|
// Restore working directory from localStorage (user likely wants same project)
|
|
3600
3600
|
const cachedWd = localStorage.getItem('memstack-last-workdir');
|
|
3601
3601
|
const wdInput = document.getElementById('agent-workdir-input');
|
|
3602
|
-
if (cachedWd) { wdInput.value = cachedWd;
|
|
3602
|
+
if (cachedWd) { wdInput.value = cachedWd; refreshWorkdir(cachedWd); }
|
|
3603
3603
|
}
|
|
3604
3604
|
|
|
3605
3605
|
async function startAgentTask() {
|
|
@@ -3636,12 +3636,16 @@ async function startAgentTask() {
|
|
|
3636
3636
|
await fetchBranches(workDir);
|
|
3637
3637
|
}
|
|
3638
3638
|
}
|
|
3639
|
+
// Repopulate the branch dropdown from the ACTUAL working dir on every run
|
|
3640
|
+
// start (not only the git-init path above), so the options, pre-selected
|
|
3641
|
+
// branch, and dataset.currentBranch reflect this repo — never a stale
|
|
3642
|
+
// dropdown left over from a previously-loaded directory.
|
|
3643
|
+
if (workDir) await fetchBranches(workDir);
|
|
3639
3644
|
const branchSel = document.getElementById('agent-branch-select');
|
|
3640
3645
|
const branchRow = document.getElementById('agent-branch-row');
|
|
3641
3646
|
const selectedBranch = (branchRow && branchRow.style.display !== 'none' && branchSel && branchSel.value) ? branchSel.value : '';
|
|
3642
3647
|
if (selectedBranch && workDir) {
|
|
3643
|
-
const
|
|
3644
|
-
const currentBranch = curOpt ? curOpt.value : '';
|
|
3648
|
+
const currentBranch = branchSel.dataset.currentBranch || '';
|
|
3645
3649
|
if (selectedBranch !== currentBranch) {
|
|
3646
3650
|
btn.textContent = 'Checking out branch...';
|
|
3647
3651
|
const coRes = await fetch('/api/git-checkout', {method:'POST', headers: AUTH_HEADERS, body: JSON.stringify({workdir: workDir, branch: selectedBranch})});
|
|
@@ -3720,9 +3724,17 @@ async function loadAgentMonitor() {
|
|
|
3720
3724
|
wdInput._mcpBound = true;
|
|
3721
3725
|
wdInput.addEventListener('blur', () => {
|
|
3722
3726
|
const wd = wdInput.value.trim();
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3727
|
+
refreshWorkdir(wd);
|
|
3728
|
+
});
|
|
3729
|
+
// Also refresh when the user types/pastes a new path (debounced), so the
|
|
3730
|
+
// branch dropdown updates without waiting for the field to lose focus.
|
|
3731
|
+
let _wdInputTimer = null;
|
|
3732
|
+
wdInput.addEventListener('input', () => {
|
|
3733
|
+
clearTimeout(_wdInputTimer);
|
|
3734
|
+
_wdInputTimer = setTimeout(() => {
|
|
3735
|
+
const wd = wdInput.value.trim();
|
|
3736
|
+
refreshWorkdir(wd);
|
|
3737
|
+
}, 400);
|
|
3726
3738
|
});
|
|
3727
3739
|
}
|
|
3728
3740
|
fetchAgentStatus();
|
|
@@ -3734,11 +3746,6 @@ async function loadAgentMonitor() {
|
|
|
3734
3746
|
}
|
|
3735
3747
|
|
|
3736
3748
|
async function loadLastWorkdir() {
|
|
3737
|
-
function _initWorkdir(path) {
|
|
3738
|
-
fetchMcpServers(path);
|
|
3739
|
-
fetchBranches(path);
|
|
3740
|
-
fetchAgentWorkflow(path);
|
|
3741
|
-
}
|
|
3742
3749
|
try {
|
|
3743
3750
|
const input = document.getElementById('agent-workdir-input');
|
|
3744
3751
|
if (input.value.trim()) return;
|
|
@@ -3747,16 +3754,16 @@ async function loadLastWorkdir() {
|
|
|
3747
3754
|
if (data.path) {
|
|
3748
3755
|
input.value = data.path;
|
|
3749
3756
|
localStorage.setItem('memstack-last-workdir', data.path);
|
|
3750
|
-
|
|
3757
|
+
refreshWorkdir(data.path);
|
|
3751
3758
|
} else {
|
|
3752
3759
|
const cached = localStorage.getItem('memstack-last-workdir');
|
|
3753
|
-
if (cached) { input.value = cached;
|
|
3760
|
+
if (cached) { input.value = cached; refreshWorkdir(cached); }
|
|
3754
3761
|
}
|
|
3755
3762
|
} catch(e) {
|
|
3756
3763
|
const cached = localStorage.getItem('memstack-last-workdir');
|
|
3757
3764
|
if (cached) {
|
|
3758
3765
|
const input = document.getElementById('agent-workdir-input');
|
|
3759
|
-
if (!input.value.trim()) { input.value = cached;
|
|
3766
|
+
if (!input.value.trim()) { input.value = cached; refreshWorkdir(cached); }
|
|
3760
3767
|
}
|
|
3761
3768
|
}
|
|
3762
3769
|
}
|
|
@@ -3772,6 +3779,12 @@ function updateMcpToolsSummaryLabel() {
|
|
|
3772
3779
|
el.textContent = name + ' MCP Tools';
|
|
3773
3780
|
}
|
|
3774
3781
|
|
|
3782
|
+
function refreshWorkdir(path) {
|
|
3783
|
+
fetchMcpServers(path);
|
|
3784
|
+
fetchBranches(path);
|
|
3785
|
+
fetchAgentWorkflow(path);
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3775
3788
|
async function fetchMcpServers(workdir) {
|
|
3776
3789
|
const listEl = document.getElementById('mcp-tools-list');
|
|
3777
3790
|
const emptyEl = document.getElementById('mcp-tools-empty');
|
|
@@ -3953,9 +3966,9 @@ async function fetchBranches(workdir) {
|
|
|
3953
3966
|
if (!workdir) { row.style.display = 'none'; sel.innerHTML = ''; return; }
|
|
3954
3967
|
try {
|
|
3955
3968
|
const res = await fetch('/api/git-branches?workdir=' + encodeURIComponent(workdir), {headers: AUTH_GET});
|
|
3956
|
-
if (!res.ok) { row.style.display = 'none'; return; }
|
|
3969
|
+
if (!res.ok) { sel.innerHTML = ''; row.style.display = 'none'; return; }
|
|
3957
3970
|
const d = await res.json();
|
|
3958
|
-
if (!d.branches || !d.branches.length) { row.style.display = 'none'; return; }
|
|
3971
|
+
if (!d.branches || !d.branches.length) { sel.innerHTML = ''; row.style.display = 'none'; return; }
|
|
3959
3972
|
sel.innerHTML = '';
|
|
3960
3973
|
for (const b of d.branches) {
|
|
3961
3974
|
const opt = document.createElement('option');
|
|
@@ -3963,8 +3976,14 @@ async function fetchBranches(workdir) {
|
|
|
3963
3976
|
if (b === d.current) opt.selected = true;
|
|
3964
3977
|
sel.appendChild(opt);
|
|
3965
3978
|
}
|
|
3979
|
+
// Pre-select the repo's actual current branch and record it so the run
|
|
3980
|
+
// path can read the selection reliably (branchSel.value) and compare it
|
|
3981
|
+
// against the true current branch (dataset), not a [selected] attribute
|
|
3982
|
+
// that opt.selected = true never sets.
|
|
3983
|
+
if (d.current) sel.value = d.current;
|
|
3984
|
+
sel.dataset.currentBranch = d.current || '';
|
|
3966
3985
|
row.style.display = 'block';
|
|
3967
|
-
} catch(e) { row.style.display = 'none'; }
|
|
3986
|
+
} catch(e) { sel.innerHTML = ''; row.style.display = 'none'; }
|
|
3968
3987
|
}
|
|
3969
3988
|
|
|
3970
3989
|
async function fetchAgentWorkflow(workdir) {
|
|
@@ -4317,7 +4336,7 @@ function closeDirBrowser() {
|
|
|
4317
4336
|
function selectDirBrowser() {
|
|
4318
4337
|
document.getElementById('agent-workdir-input').value = dirBrowserCurrentPath;
|
|
4319
4338
|
closeDirBrowser();
|
|
4320
|
-
|
|
4339
|
+
refreshWorkdir(dirBrowserCurrentPath);
|
|
4321
4340
|
}
|
|
4322
4341
|
|
|
4323
4342
|
function dirBrowserBack() {
|
{memstack_skill_loader-4.10.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/memory_db.py
RENAMED
|
@@ -11,9 +11,22 @@ DB_PATH = Path.home() / ".memstack" / "memory.db"
|
|
|
11
11
|
SCHEMA_VERSION = 1
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _resolve_db_path() -> Path:
|
|
15
|
+
"""Resolve the memory DB path at call time.
|
|
16
|
+
|
|
17
|
+
Honors the ``MEMSTACK_DB_PATH`` environment override (used by tests to
|
|
18
|
+
redirect writes away from the real ~/.memstack/memory.db); otherwise falls
|
|
19
|
+
back to the module-level ``DB_PATH`` default. Behavior is identical to the
|
|
20
|
+
old hardcoded path when the env var is absent.
|
|
21
|
+
"""
|
|
22
|
+
override = os.environ.get("MEMSTACK_DB_PATH")
|
|
23
|
+
return Path(override) if override else DB_PATH
|
|
24
|
+
|
|
25
|
+
|
|
14
26
|
def _get_conn() -> sqlite3.Connection:
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
db_path = _resolve_db_path()
|
|
28
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
conn = sqlite3.connect(str(db_path), timeout=5)
|
|
17
30
|
conn.row_factory = sqlite3.Row
|
|
18
31
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
19
32
|
_init_tables(conn)
|
|
@@ -124,6 +137,36 @@ def _init_tables(conn: sqlite3.Connection) -> None:
|
|
|
124
137
|
"""
|
|
125
138
|
)
|
|
126
139
|
|
|
140
|
+
cur.execute(
|
|
141
|
+
"""
|
|
142
|
+
CREATE TABLE IF NOT EXISTS failed_approaches (
|
|
143
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
144
|
+
project_dir TEXT NOT NULL,
|
|
145
|
+
session_id TEXT,
|
|
146
|
+
task_text TEXT NOT NULL,
|
|
147
|
+
subtask_title TEXT,
|
|
148
|
+
subtask_spec TEXT,
|
|
149
|
+
approach_summary TEXT NOT NULL,
|
|
150
|
+
reviewer_feedback TEXT NOT NULL,
|
|
151
|
+
iteration INTEGER,
|
|
152
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
153
|
+
)
|
|
154
|
+
"""
|
|
155
|
+
)
|
|
156
|
+
cur.execute(
|
|
157
|
+
"CREATE INDEX IF NOT EXISTS idx_failed_project "
|
|
158
|
+
"ON failed_approaches(project_dir)"
|
|
159
|
+
)
|
|
160
|
+
cur.execute(
|
|
161
|
+
"""
|
|
162
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS failed_approaches_fts USING fts5(
|
|
163
|
+
match_text,
|
|
164
|
+
source_id,
|
|
165
|
+
project_dir
|
|
166
|
+
)
|
|
167
|
+
"""
|
|
168
|
+
)
|
|
169
|
+
|
|
127
170
|
conn.commit()
|
|
128
171
|
|
|
129
172
|
|
|
@@ -228,23 +271,31 @@ def insert_lesson(
|
|
|
228
271
|
project_dir: str | None = None,
|
|
229
272
|
global_: bool = False,
|
|
230
273
|
stack_tags: list[str] | None = None,
|
|
274
|
+
*,
|
|
275
|
+
created_at: str | None = None,
|
|
231
276
|
) -> int | None:
|
|
232
277
|
try:
|
|
233
278
|
conn = _get_conn()
|
|
234
279
|
try:
|
|
235
280
|
pdir = None if global_ else project_dir
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
title,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
281
|
+
tags_json = json.dumps(stack_tags) if stack_tags else None
|
|
282
|
+
if created_at:
|
|
283
|
+
# Caller-supplied timestamp: bind it (never string-format it in).
|
|
284
|
+
cur = conn.execute(
|
|
285
|
+
"INSERT INTO procedural_memory "
|
|
286
|
+
"(project_dir, category, title, content, stack_tags, created_at) "
|
|
287
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
288
|
+
(pdir, category, title, content, tags_json, created_at),
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
# Default path: omit created_at so the column DEFAULT
|
|
292
|
+
# (datetime('now')) applies. Byte-identical to prior behavior.
|
|
293
|
+
cur = conn.execute(
|
|
294
|
+
"INSERT INTO procedural_memory "
|
|
295
|
+
"(project_dir, category, title, content, stack_tags) "
|
|
296
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
297
|
+
(pdir, category, title, content, tags_json),
|
|
298
|
+
)
|
|
248
299
|
row_id = cur.lastrowid
|
|
249
300
|
_index_fts(conn, "procedural_memory", row_id,
|
|
250
301
|
f"{title} {content}", pdir or "")
|
|
@@ -298,6 +349,98 @@ def insert_fact(
|
|
|
298
349
|
return None
|
|
299
350
|
|
|
300
351
|
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
# Failed-approaches memory
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def record_failed_approach(
|
|
357
|
+
project_dir: str,
|
|
358
|
+
task_text: str,
|
|
359
|
+
approach_summary: str,
|
|
360
|
+
reviewer_feedback: str,
|
|
361
|
+
iteration: int,
|
|
362
|
+
subtask_title: str | None = None,
|
|
363
|
+
subtask_spec: str | None = None,
|
|
364
|
+
session_id: str | None = None,
|
|
365
|
+
) -> int | None:
|
|
366
|
+
try:
|
|
367
|
+
conn = _get_conn()
|
|
368
|
+
try:
|
|
369
|
+
cur = conn.execute(
|
|
370
|
+
"INSERT INTO failed_approaches "
|
|
371
|
+
"(project_dir, session_id, task_text, subtask_title, subtask_spec, "
|
|
372
|
+
"approach_summary, reviewer_feedback, iteration) "
|
|
373
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
374
|
+
(project_dir, session_id, task_text, subtask_title, subtask_spec,
|
|
375
|
+
approach_summary, reviewer_feedback, iteration),
|
|
376
|
+
)
|
|
377
|
+
row_id = cur.lastrowid
|
|
378
|
+
match_text = " ".join(
|
|
379
|
+
p for p in (task_text, subtask_title, subtask_spec) if p
|
|
380
|
+
)
|
|
381
|
+
conn.execute(
|
|
382
|
+
"INSERT INTO failed_approaches_fts (match_text, source_id, project_dir) "
|
|
383
|
+
"VALUES (?, ?, ?)",
|
|
384
|
+
(match_text, str(row_id), project_dir),
|
|
385
|
+
)
|
|
386
|
+
conn.commit()
|
|
387
|
+
return row_id
|
|
388
|
+
finally:
|
|
389
|
+
conn.close()
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
print(f"[memstack-memory] record_failed_approach failed: {exc}", file=sys.stderr)
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _fts_or_query(text: str) -> str:
|
|
396
|
+
"""Free text -> safe FTS5 OR-query of unique alphanumeric tokens.
|
|
397
|
+
|
|
398
|
+
Strips punctuation (which FTS5 would treat as query syntax and raise on),
|
|
399
|
+
drops tokens shorter than 3 chars, dedupes, and caps length. Returns "" if
|
|
400
|
+
no usable tokens remain so callers can skip the MATCH entirely.
|
|
401
|
+
"""
|
|
402
|
+
import re
|
|
403
|
+
tokens = re.findall(r"[A-Za-z0-9]+", (text or "").lower())
|
|
404
|
+
seen: list[str] = []
|
|
405
|
+
for t in tokens:
|
|
406
|
+
if len(t) >= 3 and t not in seen:
|
|
407
|
+
seen.append(t)
|
|
408
|
+
return " OR ".join(seen[:40])
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def query_failed_approaches(
|
|
412
|
+
task_text: str, project_dir: str | None = None, top_k: int = 3
|
|
413
|
+
) -> list[dict]:
|
|
414
|
+
if top_k > 50:
|
|
415
|
+
top_k = 50
|
|
416
|
+
match = _fts_or_query(task_text)
|
|
417
|
+
if not match:
|
|
418
|
+
return []
|
|
419
|
+
try:
|
|
420
|
+
conn = _get_conn()
|
|
421
|
+
try:
|
|
422
|
+
sql = (
|
|
423
|
+
"SELECT f.id, f.subtask_title, f.subtask_spec, f.approach_summary, "
|
|
424
|
+
"f.reviewer_feedback, f.iteration, f.created_at "
|
|
425
|
+
"FROM failed_approaches_fts fa "
|
|
426
|
+
"JOIN failed_approaches f ON f.id = CAST(fa.source_id AS INTEGER) "
|
|
427
|
+
"WHERE failed_approaches_fts MATCH ?"
|
|
428
|
+
)
|
|
429
|
+
params: list = [match]
|
|
430
|
+
if project_dir:
|
|
431
|
+
sql += " AND fa.project_dir = ?"
|
|
432
|
+
params.append(project_dir)
|
|
433
|
+
sql += " ORDER BY fa.rank LIMIT ?"
|
|
434
|
+
params.append(top_k)
|
|
435
|
+
rows = conn.execute(sql, params).fetchall()
|
|
436
|
+
return [dict(r) for r in rows]
|
|
437
|
+
finally:
|
|
438
|
+
conn.close()
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
print(f"[memstack-memory] query_failed_approaches failed: {exc}", file=sys.stderr)
|
|
441
|
+
return []
|
|
442
|
+
|
|
443
|
+
|
|
301
444
|
# ---------------------------------------------------------------------------
|
|
302
445
|
# Query functions
|
|
303
446
|
# ---------------------------------------------------------------------------
|
|
@@ -307,7 +450,9 @@ def search_memory(
|
|
|
307
450
|
) -> list[dict]:
|
|
308
451
|
if limit > 500:
|
|
309
452
|
limit = 500
|
|
310
|
-
|
|
453
|
+
match = _fts_or_query(query)
|
|
454
|
+
if not match:
|
|
455
|
+
return []
|
|
311
456
|
try:
|
|
312
457
|
conn = _get_conn()
|
|
313
458
|
try:
|
|
@@ -315,7 +460,7 @@ def search_memory(
|
|
|
315
460
|
"SELECT content, source_table, source_id, project_dir, "
|
|
316
461
|
"rank FROM memory_fts WHERE memory_fts MATCH ?"
|
|
317
462
|
)
|
|
318
|
-
params: list = [
|
|
463
|
+
params: list = [match]
|
|
319
464
|
if project_dir:
|
|
320
465
|
sql += " AND project_dir = ?"
|
|
321
466
|
params.append(project_dir)
|