memstack-skill-loader 4.9.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.9.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.11.0}/PKG-INFO +1 -1
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/README.md +4 -4
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/pyproject.toml +2 -2
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/agent_runner.py +308 -4
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/categories.py +1 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/dashboard.html +37 -18
- memstack_skill_loader-4.11.0/src/memstack_skill_loader/goal.py +118 -0
- memstack_skill_loader-4.11.0/src/memstack_skill_loader/goal_engine.py +326 -0
- memstack_skill_loader-4.11.0/src/memstack_skill_loader/hooks/__init__.py +5 -0
- memstack_skill_loader-4.11.0/src/memstack_skill_loader/hooks/deny_git.py +21 -0
- {memstack_skill_loader-4.9.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.9.0 → memstack_skill_loader-4.11.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +8 -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.9.0 → memstack_skill_loader-4.11.0}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/setup.cfg +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/dashboard.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/server.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_body_parser_metric.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_code_read_savings.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_license_environment.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_license_grace.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_pro_compressor.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_pro_skills_update.py +0 -0
- {memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/tests/test_skill_drift.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MemStack™ Skill Loader
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**130 skills for Claude Code:** 86 free + 44 Pro exclusive. Vector-indexed so CC loads only the skill it needs, saving your context window.
|
|
4
4
|
|
|
5
5
|
## Quick Start (5 minutes)
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ MemStack installs in two parts: the **skills** (via the Claude Code plugin marke
|
|
|
11
11
|
/plugin marketplace add cwinvestments/memstack
|
|
12
12
|
/plugin install memstack@cwinvestments-memstack
|
|
13
13
|
```
|
|
14
|
-
Run both commands. This unlocks the
|
|
14
|
+
Run both commands. This unlocks the 86 free skills right away.
|
|
15
15
|
|
|
16
16
|
> **SSH error?** ("Host key verification failed" on a fresh server that's never used GitHub over SSH.)
|
|
17
17
|
> **Default fix · (in terminal)** — rewrite GitHub to HTTPS, then retry Step 1:
|
|
@@ -53,7 +53,7 @@ If this doesn't match Step 2's interpreter, the server won't launch (you'll see
|
|
|
53
53
|
```
|
|
54
54
|
activate_license(key="your-key", email="you@example.com")
|
|
55
55
|
```
|
|
56
|
-
Use `key="free"` for the free tier, or your Pro key from [memstack.pro](https://memstack.pro) to unlock all 44 Pro skills (
|
|
56
|
+
Use `key="free"` for the free tier, or your Pro key from [memstack.pro](https://memstack.pro) to unlock all 44 Pro skills (86 free + 44 Pro = 130 total). Then type `list skills` to verify.
|
|
57
57
|
|
|
58
58
|
> To override the skills path, set `MEMSTACK_SKILLS_DIR=/path/to/your/memstack/skills`.
|
|
59
59
|
|
|
@@ -86,7 +86,7 @@ sudo apt install python3-pip
|
|
|
86
86
|
|
|
87
87
|
## How It Works
|
|
88
88
|
|
|
89
|
-
MCP server that vector-indexes all
|
|
89
|
+
MCP server that vector-indexes all 130 MemStack™ skills so Claude Code can call `find_skill("deploy to Railway")` and load **only** the relevant skill on demand, instead of all skills consuming context window.
|
|
90
90
|
|
|
91
91
|
- **No Anthropic API key required:** skill search and indexing run locally — the only outbound network call is license validation, which contacts the MemStack license server on session start ([Data and privacy](#data-and-privacy))
|
|
92
92
|
- **Pro skills auto-detected:** set your license key and they appear automatically
|
|
@@ -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
|
# ---------------------------------------------------------------------------
|
|
@@ -1026,7 +1168,8 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
|
|
|
1026
1168
|
model: str = "", disallowed_tools: Optional[list[str]] = None,
|
|
1027
1169
|
session: Optional["Session"] = None,
|
|
1028
1170
|
display_name: str = "",
|
|
1029
|
-
execution_mode: str = "subscription"
|
|
1171
|
+
execution_mode: str = "subscription",
|
|
1172
|
+
extra_settings: Optional[str] = None) -> tuple[str, int, int, int]:
|
|
1030
1173
|
"""Run a single claude interactive streaming invocation and return the output."""
|
|
1031
1174
|
claude_bin = shutil.which("claude")
|
|
1032
1175
|
if not claude_bin:
|
|
@@ -1040,6 +1183,10 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
|
|
|
1040
1183
|
if disallowed_tools:
|
|
1041
1184
|
patterns = ",".join(f"mcp__{srv}__*" for srv in disallowed_tools)
|
|
1042
1185
|
cmd.extend(["--disallowedTools", patterns])
|
|
1186
|
+
if extra_settings:
|
|
1187
|
+
# Inline --settings JSON (e.g. Builder git-deny PreToolUse hook). Only
|
|
1188
|
+
# passed by Builder call sites; Manager/Reviewer never provide this.
|
|
1189
|
+
cmd.extend(["--settings", extra_settings])
|
|
1043
1190
|
|
|
1044
1191
|
if log_path:
|
|
1045
1192
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1385,10 +1532,53 @@ def _parse_subtasks(text: str) -> list[tuple[str, str]]:
|
|
|
1385
1532
|
return []
|
|
1386
1533
|
|
|
1387
1534
|
|
|
1535
|
+
def _build_builder_hook_settings() -> str:
|
|
1536
|
+
"""Inline --settings JSON that registers the Builder git-deny PreToolUse hook.
|
|
1537
|
+
|
|
1538
|
+
The hook script ships inside this package (hooks/deny_git.py). Its absolute
|
|
1539
|
+
path MUST use forward slashes: backslashes get stripped when the settings
|
|
1540
|
+
command string is executed on Windows, which mangles the path and breaks
|
|
1541
|
+
the hook (see smoke test)."""
|
|
1542
|
+
hook_script = os.path.join(os.path.dirname(__file__), "hooks", "deny_git.py").replace("\\", "/")
|
|
1543
|
+
return json.dumps({
|
|
1544
|
+
"hooks": {"PreToolUse": [{"matcher": "Bash",
|
|
1545
|
+
"hooks": [{"type": "command", "command": f"python {hook_script}"}]}]}
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def _git_head(working_dir: str) -> Optional[str]:
|
|
1550
|
+
"""Return the current HEAD commit SHA, or None if it can't be read."""
|
|
1551
|
+
try:
|
|
1552
|
+
r = subprocess.run(
|
|
1553
|
+
["git", "rev-parse", "HEAD"],
|
|
1554
|
+
cwd=working_dir, capture_output=True, text=True, timeout=30,
|
|
1555
|
+
)
|
|
1556
|
+
return r.stdout.strip() if r.returncode == 0 else None
|
|
1557
|
+
except Exception:
|
|
1558
|
+
return None
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _git_reset_soft(working_dir: str, ref: str) -> bool:
|
|
1562
|
+
"""Best-effort `git reset --soft <ref>` to move HEAD back without touching
|
|
1563
|
+
the working tree. Used to undo a Builder commit so the orchestrator remains
|
|
1564
|
+
the single committer. Returns True on success."""
|
|
1565
|
+
try:
|
|
1566
|
+
r = subprocess.run(
|
|
1567
|
+
["git", "reset", "--soft", ref],
|
|
1568
|
+
cwd=working_dir, capture_output=True, text=True, timeout=30,
|
|
1569
|
+
)
|
|
1570
|
+
return r.returncode == 0
|
|
1571
|
+
except Exception:
|
|
1572
|
+
return False
|
|
1573
|
+
|
|
1574
|
+
|
|
1388
1575
|
def _orchestrate(session: Session) -> None:
|
|
1389
1576
|
"""Run the Manager → Builder → Reviewer loop."""
|
|
1390
1577
|
session._start_monotonic = time.monotonic()
|
|
1391
1578
|
session_log_dir = STATE_DIR / "sessions" / session.session_id
|
|
1579
|
+
# Builder-only git-deny hook (Layer 1). Built once, passed to Builder
|
|
1580
|
+
# _invoke_agent calls via extra_settings; never to Manager/Reviewer.
|
|
1581
|
+
builder_hook_settings = _build_builder_hook_settings()
|
|
1392
1582
|
|
|
1393
1583
|
try:
|
|
1394
1584
|
# Load execution modes and custom display names for this run
|
|
@@ -1484,6 +1674,9 @@ def _orchestrate(session: Session) -> None:
|
|
|
1484
1674
|
session.add_message("system", "all", f"Starting subtask {idx} of {len(subtasks)}: {title}")
|
|
1485
1675
|
builder_input = spec
|
|
1486
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] = []
|
|
1487
1680
|
|
|
1488
1681
|
for iteration in range(1, MAX_ITERATIONS + 1):
|
|
1489
1682
|
if session.status == "stopped":
|
|
@@ -1503,15 +1696,24 @@ def _orchestrate(session: Session) -> None:
|
|
|
1503
1696
|
session.agents["builder"]["last_output"] = ""
|
|
1504
1697
|
session._save_state()
|
|
1505
1698
|
|
|
1699
|
+
fa_block = ""
|
|
1700
|
+
if iteration == 1:
|
|
1701
|
+
fa_block = _failed_approaches_block(
|
|
1702
|
+
session, session.task, title, spec
|
|
1703
|
+
)
|
|
1506
1704
|
builder_prompt = (
|
|
1507
1705
|
SYSTEM_PROMPTS["builder"].format(working_dir=session.working_dir)
|
|
1508
1706
|
+ "\n\n" + context
|
|
1707
|
+
+ (("\n\n" + fa_block) if fa_block else "")
|
|
1509
1708
|
+ f"\n\nOriginal task:\n{session.task}"
|
|
1510
1709
|
+ (f"\n\nAdditional context from user:\n{session.context}" if session.context else "")
|
|
1511
1710
|
+ f"\n\nBuild specification:\n{builder_input}"
|
|
1512
1711
|
)
|
|
1513
1712
|
if session.user_name:
|
|
1514
1713
|
builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
|
|
1714
|
+
# Layer 2 backstop: snapshot HEAD before the Builder runs so
|
|
1715
|
+
# we can detect any git activity that slips past the hook.
|
|
1716
|
+
pre_head = _git_head(session.working_dir)
|
|
1515
1717
|
try:
|
|
1516
1718
|
if agent_modes["builder"] == "subscription":
|
|
1517
1719
|
builder_output, b_in, b_out, b_ctx = _invoke_agent(
|
|
@@ -1524,6 +1726,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
1524
1726
|
session=session,
|
|
1525
1727
|
display_name=display_names["builder"],
|
|
1526
1728
|
execution_mode=agent_modes["builder"],
|
|
1729
|
+
extra_settings=builder_hook_settings,
|
|
1527
1730
|
)
|
|
1528
1731
|
else:
|
|
1529
1732
|
builder_output, b_in, b_out = _invoke_api_agent(
|
|
@@ -1565,6 +1768,24 @@ def _orchestrate(session: Session) -> None:
|
|
|
1565
1768
|
if session.status == "stopped":
|
|
1566
1769
|
break
|
|
1567
1770
|
|
|
1771
|
+
# Layer 2 backstop: if the Builder moved HEAD (commit, reset,
|
|
1772
|
+
# checkout, etc.) it violated the no-git policy. Soft-undo and
|
|
1773
|
+
# fail the subtask rather than trusting its work.
|
|
1774
|
+
post_head = _git_head(session.working_dir)
|
|
1775
|
+
if pre_head and post_head and post_head != pre_head:
|
|
1776
|
+
undone = _git_reset_soft(session.working_dir, pre_head)
|
|
1777
|
+
reason = (
|
|
1778
|
+
f"Builder ran git and moved HEAD (pre={pre_head[:8]} "
|
|
1779
|
+
f"post={post_head[:8]}) - blocked by policy"
|
|
1780
|
+
+ ("; HEAD soft-reset to pre-state" if undone else "")
|
|
1781
|
+
)
|
|
1782
|
+
print(f"[agent-runner] POLICY VIOLATION: {reason}", file=sys.stderr)
|
|
1783
|
+
session.add_message("system", "all", f"Subtask {idx} of {len(subtasks)} failed: {title} - {reason}")
|
|
1784
|
+
subtasks_failed += 1
|
|
1785
|
+
subtask_results.append(f"Subtask {idx}: FAILED - {title} ({reason})")
|
|
1786
|
+
session._save_state()
|
|
1787
|
+
break
|
|
1788
|
+
|
|
1568
1789
|
if not builder_output or len(builder_output.strip()) < 20:
|
|
1569
1790
|
try:
|
|
1570
1791
|
diff_result = subprocess.run(
|
|
@@ -1671,10 +1892,37 @@ def _orchestrate(session: Session) -> None:
|
|
|
1671
1892
|
session.add_message("system", "all", f"Subtask {idx} of {len(subtasks)} approved: {title}")
|
|
1672
1893
|
break
|
|
1673
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
|
+
)
|
|
1674
1921
|
builder_input = (
|
|
1675
1922
|
f"The reviewer requested changes:\n{reviewer_output}\n\n"
|
|
1676
1923
|
f"Original subtask specification:\n{spec}\n\n"
|
|
1677
|
-
"
|
|
1924
|
+
+ (advisor_block + "\n\n" if advisor_block else "")
|
|
1925
|
+
+ "Please address the reviewer's feedback."
|
|
1678
1926
|
)
|
|
1679
1927
|
else:
|
|
1680
1928
|
if not subtask_passed:
|
|
@@ -1756,6 +2004,9 @@ def _orchestrate(session: Session) -> None:
|
|
|
1756
2004
|
|
|
1757
2005
|
# Step 2-3: Builder/Reviewer loop
|
|
1758
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] = []
|
|
1759
2010
|
|
|
1760
2011
|
for iteration in range(1, MAX_ITERATIONS + 1):
|
|
1761
2012
|
if session.status == "stopped":
|
|
@@ -1775,15 +2026,22 @@ def _orchestrate(session: Session) -> None:
|
|
|
1775
2026
|
session.agents["builder"]["last_output"] = ""
|
|
1776
2027
|
session._save_state()
|
|
1777
2028
|
|
|
2029
|
+
fa_block = ""
|
|
2030
|
+
if iteration == 1:
|
|
2031
|
+
fa_block = _failed_approaches_block(session, session.task)
|
|
1778
2032
|
builder_prompt = (
|
|
1779
2033
|
SYSTEM_PROMPTS["builder"].format(working_dir=session.working_dir)
|
|
1780
2034
|
+ "\n\n" + context
|
|
2035
|
+
+ (("\n\n" + fa_block) if fa_block else "")
|
|
1781
2036
|
+ f"\n\nOriginal task:\n{session.task}"
|
|
1782
2037
|
+ (f"\n\nAdditional context from user:\n{session.context}" if session.context else "")
|
|
1783
2038
|
+ f"\n\nBuild specification:\n{builder_input}"
|
|
1784
2039
|
)
|
|
1785
2040
|
if session.user_name:
|
|
1786
2041
|
builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
|
|
2042
|
+
# Layer 2 backstop: snapshot HEAD before the Builder runs so we can
|
|
2043
|
+
# detect any git activity that slips past the hook.
|
|
2044
|
+
pre_head = _git_head(session.working_dir)
|
|
1787
2045
|
try:
|
|
1788
2046
|
if agent_modes["builder"] == "subscription":
|
|
1789
2047
|
builder_output, b_in, b_out, b_ctx = _invoke_agent(
|
|
@@ -1796,6 +2054,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
1796
2054
|
session=session,
|
|
1797
2055
|
display_name=display_names["builder"],
|
|
1798
2056
|
execution_mode=agent_modes["builder"],
|
|
2057
|
+
extra_settings=builder_hook_settings,
|
|
1799
2058
|
)
|
|
1800
2059
|
else:
|
|
1801
2060
|
builder_output, b_in, b_out = _invoke_api_agent(
|
|
@@ -1836,6 +2095,25 @@ def _orchestrate(session: Session) -> None:
|
|
|
1836
2095
|
session._save_state()
|
|
1837
2096
|
return
|
|
1838
2097
|
|
|
2098
|
+
# Layer 2 backstop: if the Builder moved HEAD (commit, reset,
|
|
2099
|
+
# checkout, etc.) it violated the no-git policy. Soft-undo and fail
|
|
2100
|
+
# the task rather than trusting its work.
|
|
2101
|
+
post_head = _git_head(session.working_dir)
|
|
2102
|
+
if pre_head and post_head and post_head != pre_head:
|
|
2103
|
+
undone = _git_reset_soft(session.working_dir, pre_head)
|
|
2104
|
+
reason = (
|
|
2105
|
+
f"Builder ran git and moved HEAD (pre={pre_head[:8]} "
|
|
2106
|
+
f"post={post_head[:8]}) - blocked by policy"
|
|
2107
|
+
+ ("; HEAD soft-reset to pre-state" if undone else "")
|
|
2108
|
+
)
|
|
2109
|
+
print(f"[agent-runner] POLICY VIOLATION: {reason}", file=sys.stderr)
|
|
2110
|
+
session.agents["builder"]["status"] = "crashed"
|
|
2111
|
+
session.status = "error"
|
|
2112
|
+
session.result = reason
|
|
2113
|
+
session.add_message("system", "all", f"Task failed: {reason}")
|
|
2114
|
+
session._save_state()
|
|
2115
|
+
return
|
|
2116
|
+
|
|
1839
2117
|
if not builder_output or len(builder_output.strip()) < 20:
|
|
1840
2118
|
try:
|
|
1841
2119
|
diff_result = subprocess.run(
|
|
@@ -1995,11 +2273,37 @@ def _orchestrate(session: Session) -> None:
|
|
|
1995
2273
|
session._save_state()
|
|
1996
2274
|
return
|
|
1997
2275
|
|
|
1998
|
-
# 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
|
+
)
|
|
1999
2302
|
builder_input = (
|
|
2000
2303
|
f"The reviewer requested changes:\n{reviewer_output}\n\n"
|
|
2001
2304
|
f"Original manager specification:\n{manager_output}\n\n"
|
|
2002
|
-
"
|
|
2305
|
+
+ (advisor_block + "\n\n" if advisor_block else "")
|
|
2306
|
+
+ "Please address the reviewer's feedback."
|
|
2003
2307
|
)
|
|
2004
2308
|
|
|
2005
2309
|
session.status = "completed"
|
{memstack_skill_loader-4.9.0 → memstack_skill_loader-4.11.0}/src/memstack_skill_loader/categories.py
RENAMED
|
@@ -31,6 +31,7 @@ CATEGORY_MAP = {
|
|
|
31
31
|
"performance-audit": "Development", "refactor-planner": "Development", "test-writer": "Development",
|
|
32
32
|
"changelog-generator": "Development", "mentor": "Development", "webapp-testing": "Development",
|
|
33
33
|
# Core
|
|
34
|
+
"goal": "Core",
|
|
34
35
|
"compress": "Core", "diary": "Core", "echo": "Core", "grimoire": "Core",
|
|
35
36
|
"sight": "Core", "token-optimization": "Core",
|
|
36
37
|
# Marketing
|
|
@@ -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() {
|