memstack-skill-loader 4.8.0__tar.gz → 4.10.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.8.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.10.0}/PKG-INFO +1 -1
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/README.md +4 -4
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/pyproject.toml +1 -1
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/agent_runner.py +94 -1
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/categories.py +1 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/dashboard.html +127 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/dashboard.py +50 -0
- memstack_skill_loader-4.10.0/src/memstack_skill_loader/goal.py +118 -0
- memstack_skill_loader-4.10.0/src/memstack_skill_loader/goal_engine.py +326 -0
- memstack_skill_loader-4.10.0/src/memstack_skill_loader/hooks/__init__.py +5 -0
- memstack_skill_loader-4.10.0/src/memstack_skill_loader/hooks/deny_git.py +21 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +4 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/setup.cfg +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/memory_db.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/server.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_body_parser_metric.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_code_read_savings.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_license_environment.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_license_grace.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_pro_compressor.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_pro_skills_update.py +0 -0
- {memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.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.10.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 = [
|
|
@@ -1026,7 +1026,8 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
|
|
|
1026
1026
|
model: str = "", disallowed_tools: Optional[list[str]] = None,
|
|
1027
1027
|
session: Optional["Session"] = None,
|
|
1028
1028
|
display_name: str = "",
|
|
1029
|
-
execution_mode: str = "subscription"
|
|
1029
|
+
execution_mode: str = "subscription",
|
|
1030
|
+
extra_settings: Optional[str] = None) -> tuple[str, int, int, int]:
|
|
1030
1031
|
"""Run a single claude interactive streaming invocation and return the output."""
|
|
1031
1032
|
claude_bin = shutil.which("claude")
|
|
1032
1033
|
if not claude_bin:
|
|
@@ -1040,6 +1041,10 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
|
|
|
1040
1041
|
if disallowed_tools:
|
|
1041
1042
|
patterns = ",".join(f"mcp__{srv}__*" for srv in disallowed_tools)
|
|
1042
1043
|
cmd.extend(["--disallowedTools", patterns])
|
|
1044
|
+
if extra_settings:
|
|
1045
|
+
# Inline --settings JSON (e.g. Builder git-deny PreToolUse hook). Only
|
|
1046
|
+
# passed by Builder call sites; Manager/Reviewer never provide this.
|
|
1047
|
+
cmd.extend(["--settings", extra_settings])
|
|
1043
1048
|
|
|
1044
1049
|
if log_path:
|
|
1045
1050
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1385,10 +1390,53 @@ def _parse_subtasks(text: str) -> list[tuple[str, str]]:
|
|
|
1385
1390
|
return []
|
|
1386
1391
|
|
|
1387
1392
|
|
|
1393
|
+
def _build_builder_hook_settings() -> str:
|
|
1394
|
+
"""Inline --settings JSON that registers the Builder git-deny PreToolUse hook.
|
|
1395
|
+
|
|
1396
|
+
The hook script ships inside this package (hooks/deny_git.py). Its absolute
|
|
1397
|
+
path MUST use forward slashes: backslashes get stripped when the settings
|
|
1398
|
+
command string is executed on Windows, which mangles the path and breaks
|
|
1399
|
+
the hook (see smoke test)."""
|
|
1400
|
+
hook_script = os.path.join(os.path.dirname(__file__), "hooks", "deny_git.py").replace("\\", "/")
|
|
1401
|
+
return json.dumps({
|
|
1402
|
+
"hooks": {"PreToolUse": [{"matcher": "Bash",
|
|
1403
|
+
"hooks": [{"type": "command", "command": f"python {hook_script}"}]}]}
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def _git_head(working_dir: str) -> Optional[str]:
|
|
1408
|
+
"""Return the current HEAD commit SHA, or None if it can't be read."""
|
|
1409
|
+
try:
|
|
1410
|
+
r = subprocess.run(
|
|
1411
|
+
["git", "rev-parse", "HEAD"],
|
|
1412
|
+
cwd=working_dir, capture_output=True, text=True, timeout=30,
|
|
1413
|
+
)
|
|
1414
|
+
return r.stdout.strip() if r.returncode == 0 else None
|
|
1415
|
+
except Exception:
|
|
1416
|
+
return None
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _git_reset_soft(working_dir: str, ref: str) -> bool:
|
|
1420
|
+
"""Best-effort `git reset --soft <ref>` to move HEAD back without touching
|
|
1421
|
+
the working tree. Used to undo a Builder commit so the orchestrator remains
|
|
1422
|
+
the single committer. Returns True on success."""
|
|
1423
|
+
try:
|
|
1424
|
+
r = subprocess.run(
|
|
1425
|
+
["git", "reset", "--soft", ref],
|
|
1426
|
+
cwd=working_dir, capture_output=True, text=True, timeout=30,
|
|
1427
|
+
)
|
|
1428
|
+
return r.returncode == 0
|
|
1429
|
+
except Exception:
|
|
1430
|
+
return False
|
|
1431
|
+
|
|
1432
|
+
|
|
1388
1433
|
def _orchestrate(session: Session) -> None:
|
|
1389
1434
|
"""Run the Manager → Builder → Reviewer loop."""
|
|
1390
1435
|
session._start_monotonic = time.monotonic()
|
|
1391
1436
|
session_log_dir = STATE_DIR / "sessions" / session.session_id
|
|
1437
|
+
# Builder-only git-deny hook (Layer 1). Built once, passed to Builder
|
|
1438
|
+
# _invoke_agent calls via extra_settings; never to Manager/Reviewer.
|
|
1439
|
+
builder_hook_settings = _build_builder_hook_settings()
|
|
1392
1440
|
|
|
1393
1441
|
try:
|
|
1394
1442
|
# Load execution modes and custom display names for this run
|
|
@@ -1512,6 +1560,9 @@ def _orchestrate(session: Session) -> None:
|
|
|
1512
1560
|
)
|
|
1513
1561
|
if session.user_name:
|
|
1514
1562
|
builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
|
|
1563
|
+
# Layer 2 backstop: snapshot HEAD before the Builder runs so
|
|
1564
|
+
# we can detect any git activity that slips past the hook.
|
|
1565
|
+
pre_head = _git_head(session.working_dir)
|
|
1515
1566
|
try:
|
|
1516
1567
|
if agent_modes["builder"] == "subscription":
|
|
1517
1568
|
builder_output, b_in, b_out, b_ctx = _invoke_agent(
|
|
@@ -1524,6 +1575,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
1524
1575
|
session=session,
|
|
1525
1576
|
display_name=display_names["builder"],
|
|
1526
1577
|
execution_mode=agent_modes["builder"],
|
|
1578
|
+
extra_settings=builder_hook_settings,
|
|
1527
1579
|
)
|
|
1528
1580
|
else:
|
|
1529
1581
|
builder_output, b_in, b_out = _invoke_api_agent(
|
|
@@ -1565,6 +1617,24 @@ def _orchestrate(session: Session) -> None:
|
|
|
1565
1617
|
if session.status == "stopped":
|
|
1566
1618
|
break
|
|
1567
1619
|
|
|
1620
|
+
# Layer 2 backstop: if the Builder moved HEAD (commit, reset,
|
|
1621
|
+
# checkout, etc.) it violated the no-git policy. Soft-undo and
|
|
1622
|
+
# fail the subtask rather than trusting its work.
|
|
1623
|
+
post_head = _git_head(session.working_dir)
|
|
1624
|
+
if pre_head and post_head and post_head != pre_head:
|
|
1625
|
+
undone = _git_reset_soft(session.working_dir, pre_head)
|
|
1626
|
+
reason = (
|
|
1627
|
+
f"Builder ran git and moved HEAD (pre={pre_head[:8]} "
|
|
1628
|
+
f"post={post_head[:8]}) - blocked by policy"
|
|
1629
|
+
+ ("; HEAD soft-reset to pre-state" if undone else "")
|
|
1630
|
+
)
|
|
1631
|
+
print(f"[agent-runner] POLICY VIOLATION: {reason}", file=sys.stderr)
|
|
1632
|
+
session.add_message("system", "all", f"Subtask {idx} of {len(subtasks)} failed: {title} - {reason}")
|
|
1633
|
+
subtasks_failed += 1
|
|
1634
|
+
subtask_results.append(f"Subtask {idx}: FAILED - {title} ({reason})")
|
|
1635
|
+
session._save_state()
|
|
1636
|
+
break
|
|
1637
|
+
|
|
1568
1638
|
if not builder_output or len(builder_output.strip()) < 20:
|
|
1569
1639
|
try:
|
|
1570
1640
|
diff_result = subprocess.run(
|
|
@@ -1784,6 +1854,9 @@ def _orchestrate(session: Session) -> None:
|
|
|
1784
1854
|
)
|
|
1785
1855
|
if session.user_name:
|
|
1786
1856
|
builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
|
|
1857
|
+
# Layer 2 backstop: snapshot HEAD before the Builder runs so we can
|
|
1858
|
+
# detect any git activity that slips past the hook.
|
|
1859
|
+
pre_head = _git_head(session.working_dir)
|
|
1787
1860
|
try:
|
|
1788
1861
|
if agent_modes["builder"] == "subscription":
|
|
1789
1862
|
builder_output, b_in, b_out, b_ctx = _invoke_agent(
|
|
@@ -1796,6 +1869,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
1796
1869
|
session=session,
|
|
1797
1870
|
display_name=display_names["builder"],
|
|
1798
1871
|
execution_mode=agent_modes["builder"],
|
|
1872
|
+
extra_settings=builder_hook_settings,
|
|
1799
1873
|
)
|
|
1800
1874
|
else:
|
|
1801
1875
|
builder_output, b_in, b_out = _invoke_api_agent(
|
|
@@ -1836,6 +1910,25 @@ def _orchestrate(session: Session) -> None:
|
|
|
1836
1910
|
session._save_state()
|
|
1837
1911
|
return
|
|
1838
1912
|
|
|
1913
|
+
# Layer 2 backstop: if the Builder moved HEAD (commit, reset,
|
|
1914
|
+
# checkout, etc.) it violated the no-git policy. Soft-undo and fail
|
|
1915
|
+
# the task rather than trusting its work.
|
|
1916
|
+
post_head = _git_head(session.working_dir)
|
|
1917
|
+
if pre_head and post_head and post_head != pre_head:
|
|
1918
|
+
undone = _git_reset_soft(session.working_dir, pre_head)
|
|
1919
|
+
reason = (
|
|
1920
|
+
f"Builder ran git and moved HEAD (pre={pre_head[:8]} "
|
|
1921
|
+
f"post={post_head[:8]}) - blocked by policy"
|
|
1922
|
+
+ ("; HEAD soft-reset to pre-state" if undone else "")
|
|
1923
|
+
)
|
|
1924
|
+
print(f"[agent-runner] POLICY VIOLATION: {reason}", file=sys.stderr)
|
|
1925
|
+
session.agents["builder"]["status"] = "crashed"
|
|
1926
|
+
session.status = "error"
|
|
1927
|
+
session.result = reason
|
|
1928
|
+
session.add_message("system", "all", f"Task failed: {reason}")
|
|
1929
|
+
session._save_state()
|
|
1930
|
+
return
|
|
1931
|
+
|
|
1839
1932
|
if not builder_output or len(builder_output.strip()) < 20:
|
|
1840
1933
|
try:
|
|
1841
1934
|
diff_result = subprocess.run(
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.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
|
|
@@ -1319,6 +1319,10 @@
|
|
|
1319
1319
|
<svg viewBox="0 0 24 24"><path d="M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z"/></svg>
|
|
1320
1320
|
Agent Monitor
|
|
1321
1321
|
</a>
|
|
1322
|
+
<a class="nav-item" data-page="referrals">
|
|
1323
|
+
<svg viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg>
|
|
1324
|
+
Referrals
|
|
1325
|
+
</a>
|
|
1322
1326
|
<a class="nav-item" data-page="settings">
|
|
1323
1327
|
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
|
1324
1328
|
Settings
|
|
@@ -1894,6 +1898,16 @@
|
|
|
1894
1898
|
</div>
|
|
1895
1899
|
|
|
1896
1900
|
<!-- ═══════ Settings (Placeholder) ═══════ -->
|
|
1901
|
+
<div id="page-referrals" class="page">
|
|
1902
|
+
<div class="page-header">
|
|
1903
|
+
<h2>Referral Program</h2>
|
|
1904
|
+
<p class="meta">Stats update when this page loads · <a href="#" id="referrals-refresh" onclick="loadReferrals();return false;">Refresh</a></p>
|
|
1905
|
+
</div>
|
|
1906
|
+
<div id="referrals-content">
|
|
1907
|
+
<span class="empty-state">Loading…</span>
|
|
1908
|
+
</div>
|
|
1909
|
+
</div>
|
|
1910
|
+
|
|
1897
1911
|
<div id="page-settings" class="page">
|
|
1898
1912
|
<div class="page-header">
|
|
1899
1913
|
<h2>Settings</h2>
|
|
@@ -2222,6 +2236,118 @@ overlay.addEventListener('click', () => {
|
|
|
2222
2236
|
overlay.classList.remove('open');
|
|
2223
2237
|
});
|
|
2224
2238
|
|
|
2239
|
+
/* ════════════════════════════════════════════
|
|
2240
|
+
Referrals
|
|
2241
|
+
════════════════════════════════════════════ */
|
|
2242
|
+
async function loadReferrals() {
|
|
2243
|
+
const el = document.getElementById('referrals-content');
|
|
2244
|
+
if (!el) return;
|
|
2245
|
+
el.innerHTML = '<span class="empty-state">Loading…</span>';
|
|
2246
|
+
let data;
|
|
2247
|
+
try {
|
|
2248
|
+
const res = await fetch('/api/referrals', { headers: AUTH_GET });
|
|
2249
|
+
data = await res.json();
|
|
2250
|
+
} catch (e) {
|
|
2251
|
+
data = { state: 'error' };
|
|
2252
|
+
}
|
|
2253
|
+
el.innerHTML = renderReferrals(data);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function referralNotice(msg, linkText, linkHref) {
|
|
2257
|
+
let h = '<div class="panel" style="text-align:center;padding:2rem 1rem">';
|
|
2258
|
+
h += '<p style="color:#c9d1d9;margin-bottom:' + (linkText ? '1rem' : '0') + '">' + escapeHtml(msg) + '</p>';
|
|
2259
|
+
if (linkText && linkHref) {
|
|
2260
|
+
h += '<a href="' + linkHref + '" target="_blank" rel="noopener" class="copy-git-output-btn" style="text-decoration:none;display:inline-block">' + escapeHtml(linkText) + '</a>';
|
|
2261
|
+
}
|
|
2262
|
+
h += '</div>';
|
|
2263
|
+
return h;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function referralBadge(status) {
|
|
2267
|
+
const map = {
|
|
2268
|
+
pending: ['#8b949e', '#161b22'],
|
|
2269
|
+
signed_up: ['#58a6ff', 'rgba(88,166,255,0.1)'],
|
|
2270
|
+
converted: ['#3fb950', 'rgba(63,185,80,0.1)'],
|
|
2271
|
+
paid_out: ['#bc8cff', 'rgba(188,140,255,0.1)'],
|
|
2272
|
+
};
|
|
2273
|
+
const c = map[status] || map.pending;
|
|
2274
|
+
const label = (status || '').replace('_', ' ');
|
|
2275
|
+
return '<span style="display:inline-block;padding:0.1rem 0.5rem;border-radius:10px;font-size:0.75rem;color:' + c[0] + ';background:' + c[1] + ';border:1px solid ' + c[0] + '33">' + escapeHtml(label) + '</span>';
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function renderReferrals(d) {
|
|
2279
|
+
const state = d && d.state;
|
|
2280
|
+
if (state === 'pro_required') {
|
|
2281
|
+
return referralNotice('Referral stats are available for MemStack Pro users.', 'Get MemStack Pro', 'https://memstack.pro');
|
|
2282
|
+
}
|
|
2283
|
+
if (state === 'not_linked') {
|
|
2284
|
+
return referralNotice('Link your license key to your account at memstack.pro/dashboard to see your referral stats here.', 'Link my license', 'https://memstack.pro/dashboard');
|
|
2285
|
+
}
|
|
2286
|
+
if (state !== 'ok') {
|
|
2287
|
+
return referralNotice("Couldn't reach memstack.pro right now. Stats will load when you're back online.", '', '');
|
|
2288
|
+
}
|
|
2289
|
+
const link = d.referral_link || '';
|
|
2290
|
+
const tier = d.tier || {};
|
|
2291
|
+
const cards = [
|
|
2292
|
+
['Clicks', fmt(d.clicks)],
|
|
2293
|
+
['Referrals', fmt(d.total_referrals)],
|
|
2294
|
+
['Signups', fmt(d.signups)],
|
|
2295
|
+
['Conversions', fmt(d.conversions)],
|
|
2296
|
+
['Total Earnings', '$' + Number(d.total_earnings || 0).toFixed(2)],
|
|
2297
|
+
['Tier', tier.name ? (escapeHtml(tier.name) + ' (' + tier.percent + '%)') : '—'],
|
|
2298
|
+
];
|
|
2299
|
+
const colors = ['#58a6ff', '#3fb950', '#d29922', '#bc8cff', '#39d353', '#ff7b72'];
|
|
2300
|
+
let html = '';
|
|
2301
|
+
html += '<div class="panel" style="margin-bottom:1rem">';
|
|
2302
|
+
html += '<h3>Your referral link</h3>';
|
|
2303
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">';
|
|
2304
|
+
html += '<input id="referral-link-input" readonly value="' + escapeHtml(link) + '" style="flex:1;min-width:240px;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:0.5rem 0.75rem;color:#c9d1d9;font-family:monospace;font-size:0.85rem">';
|
|
2305
|
+
html += '<button class="copy-git-output-btn" onclick="copyReferralLink()" title="Copy to clipboard">Copy</button>';
|
|
2306
|
+
html += '</div></div>';
|
|
2307
|
+
html += '<div class="stats-grid">';
|
|
2308
|
+
cards.forEach((c, i) => {
|
|
2309
|
+
html += '<div class="card"><div class="value" style="color:' + colors[i] + '">' + c[1] + '</div><div class="label">' + c[0] + '</div></div>';
|
|
2310
|
+
});
|
|
2311
|
+
html += '</div>';
|
|
2312
|
+
const recent = d.recent || [];
|
|
2313
|
+
html += '<div class="panel" style="margin-top:1rem"><h3>Recent Referrals</h3>';
|
|
2314
|
+
if (!recent.length) {
|
|
2315
|
+
html += '<span class="empty-state">No referrals yet. Share your link to get started!</span>';
|
|
2316
|
+
} else {
|
|
2317
|
+
html += '<table style="width:100%;border-collapse:collapse;font-size:0.85rem">';
|
|
2318
|
+
html += '<thead><tr style="text-align:left;color:#8b949e"><th style="padding:0.4rem 0.5rem">Email</th><th style="padding:0.4rem 0.5rem">Status</th><th style="padding:0.4rem 0.5rem">Amount</th><th style="padding:0.4rem 0.5rem">Date</th></tr></thead><tbody>';
|
|
2319
|
+
recent.forEach((r) => {
|
|
2320
|
+
const amt = r.commission_amount != null ? ('$' + Number(r.commission_amount).toFixed(2)) : '—';
|
|
2321
|
+
const dt = r.created_at ? new Date(r.created_at).toLocaleDateString() : '—';
|
|
2322
|
+
html += '<tr style="border-top:1px solid #21262d">';
|
|
2323
|
+
html += '<td style="padding:0.4rem 0.5rem;color:#c9d1d9">' + escapeHtml(r.referred_email_masked || '') + '</td>';
|
|
2324
|
+
html += '<td style="padding:0.4rem 0.5rem">' + referralBadge(r.status) + '</td>';
|
|
2325
|
+
html += '<td style="padding:0.4rem 0.5rem;color:#8b949e">' + amt + '</td>';
|
|
2326
|
+
html += '<td style="padding:0.4rem 0.5rem;color:#8b949e">' + dt + '</td>';
|
|
2327
|
+
html += '</tr>';
|
|
2328
|
+
});
|
|
2329
|
+
html += '</tbody></table>';
|
|
2330
|
+
}
|
|
2331
|
+
html += '</div>';
|
|
2332
|
+
return html;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
async function copyReferralLink() {
|
|
2336
|
+
const el = document.getElementById('referral-link-input');
|
|
2337
|
+
if (!el) return;
|
|
2338
|
+
try {
|
|
2339
|
+
await navigator.clipboard.writeText(el.value);
|
|
2340
|
+
const btn = document.querySelector('#page-referrals .copy-git-output-btn');
|
|
2341
|
+
if (btn) {
|
|
2342
|
+
const orig = btn.textContent;
|
|
2343
|
+
btn.textContent = 'Copied!';
|
|
2344
|
+
btn.style.color = '#3fb950';
|
|
2345
|
+
btn.style.borderColor = '#3fb950';
|
|
2346
|
+
setTimeout(() => { btn.textContent = orig; btn.style.color = ''; btn.style.borderColor = ''; }, 2000);
|
|
2347
|
+
}
|
|
2348
|
+
} catch (e) { /* ignore */ }
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2225
2351
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
2226
2352
|
item.addEventListener('click', () => {
|
|
2227
2353
|
const page = item.dataset.page;
|
|
@@ -2238,6 +2364,7 @@ document.querySelectorAll('.nav-item').forEach(item => {
|
|
|
2238
2364
|
if (page === 'agent-monitor') loadAgentMonitor();
|
|
2239
2365
|
if (page === 'memory-browser') loadMemoryBrowser();
|
|
2240
2366
|
if (page === 'burn-report') loadBurnReport();
|
|
2367
|
+
if (page === 'referrals') loadReferrals();
|
|
2241
2368
|
if (page === 'settings') loadSettings();
|
|
2242
2369
|
if (page !== 'agent-monitor') stopAgentRefresh();
|
|
2243
2370
|
});
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/dashboard.py
RENAMED
|
@@ -9,6 +9,7 @@ import shutil
|
|
|
9
9
|
import sys
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
|
+
import urllib.error
|
|
12
13
|
import urllib.request
|
|
13
14
|
import webbrowser
|
|
14
15
|
from datetime import datetime, timezone
|
|
@@ -543,6 +544,53 @@ def _collect_browser_entries(project: str | None = None) -> list[dict]:
|
|
|
543
544
|
return entries
|
|
544
545
|
|
|
545
546
|
|
|
547
|
+
# Live referral-stats endpoint on the pro-site. Overridable for testing.
|
|
548
|
+
REFERRALS_URL = os.environ.get(
|
|
549
|
+
"MEMSTACK_REFERRALS_URL", "https://memstack.pro/api/referrals/me"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _get_referral_stats() -> bytes:
|
|
554
|
+
"""Proxy the local license key to the pro-site referral-stats endpoint.
|
|
555
|
+
|
|
556
|
+
The license key is read server-side (never exposed to browser JS) and POSTed
|
|
557
|
+
to REFERRALS_URL. Returns a JSON body with a top-level "state" so the page can
|
|
558
|
+
render each outcome distinctly:
|
|
559
|
+
|
|
560
|
+
- "pro_required": no local license key (free user) — NO network call made
|
|
561
|
+
- "ok": stats present (endpoint payload merged in)
|
|
562
|
+
- "not_linked": key isn't linked to a Pro account on the site (404)
|
|
563
|
+
- "error": timeout / network / unexpected — never breaks the dashboard
|
|
564
|
+
|
|
565
|
+
Fail-soft everywhere: any failure resolves to "error", 5s timeout.
|
|
566
|
+
"""
|
|
567
|
+
from .license import get_license_key
|
|
568
|
+
|
|
569
|
+
key = get_license_key()
|
|
570
|
+
if not key:
|
|
571
|
+
return json.dumps({"state": "pro_required"}).encode()
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
payload = json.dumps({"license_key": key}).encode()
|
|
575
|
+
req = urllib.request.Request(
|
|
576
|
+
REFERRALS_URL,
|
|
577
|
+
data=payload,
|
|
578
|
+
headers={"Content-Type": "application/json"},
|
|
579
|
+
method="POST",
|
|
580
|
+
)
|
|
581
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
582
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
583
|
+
data["state"] = "ok"
|
|
584
|
+
return json.dumps(data).encode()
|
|
585
|
+
except urllib.error.HTTPError as exc:
|
|
586
|
+
if exc.code == 404:
|
|
587
|
+
return json.dumps({"state": "not_linked"}).encode()
|
|
588
|
+
return json.dumps({"state": "error"}).encode()
|
|
589
|
+
except Exception:
|
|
590
|
+
# Timeout, connection refused, DNS failure, bad JSON — all fail soft.
|
|
591
|
+
return json.dumps({"state": "error"}).encode()
|
|
592
|
+
|
|
593
|
+
|
|
546
594
|
class _DashboardServer(HTTPServer):
|
|
547
595
|
allow_reuse_address = False
|
|
548
596
|
|
|
@@ -868,6 +916,8 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
868
916
|
}
|
|
869
917
|
body = json.dumps(data).encode()
|
|
870
918
|
self._respond(200, "application/json", body)
|
|
919
|
+
elif self.path == "/api/referrals":
|
|
920
|
+
self._respond(200, "application/json", _get_referral_stats())
|
|
871
921
|
elif self.path == "/api/license-status":
|
|
872
922
|
from .license import LICENSE_FILE, DISK_CACHE_FILE
|
|
873
923
|
result = {"has_license": False, "key_masked": None, "valid": False, "plan": None, "checked_at": None, "cache_age_days": None}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# MemStack Goal CLI - thin `python -m memstack_skill_loader.goal` wrapper over goal_engine
|
|
2
|
+
"""Command-line front door to the goal engine (see goal_engine.py).
|
|
3
|
+
|
|
4
|
+
This module reimplements NOTHING: every subcommand delegates to the engine's
|
|
5
|
+
existing functions. It exists so the /goal skill and the Architect stage can
|
|
6
|
+
shell out to a stable interface and, critically, gate on an EXIT CODE:
|
|
7
|
+
|
|
8
|
+
template -> print the blank five-field scaffold (exit 0)
|
|
9
|
+
validate -> parse + validate a drafted goal; print problems.
|
|
10
|
+
exit 0 when the goal is valid (warnings allowed),
|
|
11
|
+
exit 2 when it has errors (missing TASK/OUTCOME/
|
|
12
|
+
VERIFICATION). This exit code is the enforcement point.
|
|
13
|
+
format -> parse + re-emit the clean labeled template (exit 0)
|
|
14
|
+
|
|
15
|
+
Input for validate/format resolves in this order: --text, else --file, else
|
|
16
|
+
STDIN — so a caller can pipe a goal in or pass it as an argument.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from .goal_engine import (
|
|
25
|
+
format_goal,
|
|
26
|
+
goal_is_valid,
|
|
27
|
+
goal_template,
|
|
28
|
+
parse_goal,
|
|
29
|
+
validate_goal,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Exit code returned by `validate` when the goal has hard errors. Callers
|
|
33
|
+
# (skill, Architect) branch on this to refuse an unverifiable goal.
|
|
34
|
+
EXIT_INVALID = 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_input(args: argparse.Namespace) -> str:
|
|
38
|
+
"""Resolve the goal text from --text, else --file, else STDIN.
|
|
39
|
+
|
|
40
|
+
Returns the raw text unparsed. An empty result is left to the engine's
|
|
41
|
+
validator to report (it will surface the missing required fields).
|
|
42
|
+
"""
|
|
43
|
+
if args.text is not None:
|
|
44
|
+
return args.text
|
|
45
|
+
if args.file is not None:
|
|
46
|
+
with open(args.file, "r", encoding="utf-8") as fh:
|
|
47
|
+
return fh.read()
|
|
48
|
+
return sys.stdin.read()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _add_input_args(parser: argparse.ArgumentParser) -> None:
|
|
52
|
+
"""Attach the shared --text / --file options to a subparser."""
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--text", default=None,
|
|
55
|
+
help="Goal text passed directly on the command line.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--file", default=None,
|
|
59
|
+
help="Path to a file containing the drafted goal.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_template(_args: argparse.Namespace) -> int:
|
|
64
|
+
"""Print the blank five-field template."""
|
|
65
|
+
print(goal_template())
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
70
|
+
"""Validate a drafted goal; print problems, return 0 (valid) or EXIT_INVALID.
|
|
71
|
+
|
|
72
|
+
Problems are printed one per line in the engine's order (errors first, then
|
|
73
|
+
``WARNING:`` lines). Warnings do not affect the exit code.
|
|
74
|
+
"""
|
|
75
|
+
goal = parse_goal(read_input(args))
|
|
76
|
+
for problem in validate_goal(goal):
|
|
77
|
+
print(problem)
|
|
78
|
+
return 0 if goal_is_valid(goal) else EXIT_INVALID
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_format(args: argparse.Namespace) -> int:
|
|
82
|
+
"""Parse a goal and re-emit it via the engine's clean template."""
|
|
83
|
+
goal = parse_goal(read_input(args))
|
|
84
|
+
print(format_goal(goal))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main() -> None:
|
|
89
|
+
"""CLI entry point. Dispatches to a subcommand and exits with its code."""
|
|
90
|
+
parser = argparse.ArgumentParser(
|
|
91
|
+
prog="python -m memstack_skill_loader.goal",
|
|
92
|
+
description="Structure and validate a task as a goal with a required, "
|
|
93
|
+
"checkable VERIFICATION criterion.",
|
|
94
|
+
)
|
|
95
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
96
|
+
|
|
97
|
+
p_template = sub.add_parser("template", help="Print the blank five-field template.")
|
|
98
|
+
p_template.set_defaults(func=cmd_template)
|
|
99
|
+
|
|
100
|
+
p_validate = sub.add_parser(
|
|
101
|
+
"validate",
|
|
102
|
+
help="Validate a drafted goal (stdin/--file/--text). Exit 2 on errors.",
|
|
103
|
+
)
|
|
104
|
+
_add_input_args(p_validate)
|
|
105
|
+
p_validate.set_defaults(func=cmd_validate)
|
|
106
|
+
|
|
107
|
+
p_format = sub.add_parser(
|
|
108
|
+
"format", help="Re-emit a drafted goal (stdin/--file/--text) as the clean template.",
|
|
109
|
+
)
|
|
110
|
+
_add_input_args(p_format)
|
|
111
|
+
p_format.set_defaults(func=cmd_format)
|
|
112
|
+
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
sys.exit(args.func(args))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# MemStack Goal Engine - structures a task into five fields with a required, checkable VERIFICATION criterion
|
|
2
|
+
"""Goal-structuring engine shared by the /goal skill and the Architect stage.
|
|
3
|
+
|
|
4
|
+
A "goal" is a task decomposed into five fields:
|
|
5
|
+
|
|
6
|
+
TASK what to do
|
|
7
|
+
WHY why it matters / who it's for (optional)
|
|
8
|
+
OUTCOME the exact finished result / definition of done
|
|
9
|
+
CONSTRAINTS what must or must not be done (optional, list)
|
|
10
|
+
VERIFICATION how to prove the OUTCOME is complete (REQUIRED)
|
|
11
|
+
|
|
12
|
+
VERIFICATION is the load-bearing field and the whole reason this module exists.
|
|
13
|
+
The dominant failure mode this engine is built to prevent is a goal that reads
|
|
14
|
+
as "done" without a concrete, checkable way to prove it: an agent (or a person)
|
|
15
|
+
declares success against a vague target and ships something unverified. A goal
|
|
16
|
+
without a specific verification criterion is treated as invalid here — not a
|
|
17
|
+
style nit, but a hard rule enforced by ``validate_goal``. A verification that
|
|
18
|
+
exists but only says "it works" / "tests pass" is flagged as a warning so the
|
|
19
|
+
author is nudged toward a concrete, observable check.
|
|
20
|
+
|
|
21
|
+
Pure standard library. No project imports, no external dependencies.
|
|
22
|
+
|
|
23
|
+
Validation contract (see ``validate_goal``): the returned list mixes two
|
|
24
|
+
severities distinguished by prefix —
|
|
25
|
+
* ERRORS — plain messages (e.g. "TASK is required"). A goal with any error
|
|
26
|
+
is invalid and must not proceed.
|
|
27
|
+
* WARNINGS — messages prefixed with the literal ``"WARNING: "``. Advisory
|
|
28
|
+
only; a goal with warnings but no errors is still valid.
|
|
29
|
+
Use ``goal_has_errors`` / ``goal_is_valid`` to branch without string-matching.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
|
|
37
|
+
# The five recognized field labels, in canonical display order. Parsing is
|
|
38
|
+
# case-insensitive and whitespace-tolerant; this ordering drives format output.
|
|
39
|
+
_LABELS = ("TASK", "WHY", "OUTCOME", "CONSTRAINTS", "VERIFICATION")
|
|
40
|
+
|
|
41
|
+
# Matches a label line like "OUTCOME: ...", tolerating leading whitespace and
|
|
42
|
+
# any casing. Group 1 = the (upper-cased) label, group 2 = the inline value.
|
|
43
|
+
_LABEL_RE = re.compile(
|
|
44
|
+
r"^\s*(" + "|".join(_LABELS) + r")\s*:\s*(.*)$",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Marker prepended to advisory messages so callers can separate them from
|
|
49
|
+
# hard errors without re-deriving the rules.
|
|
50
|
+
_WARNING_PREFIX = "WARNING: "
|
|
51
|
+
|
|
52
|
+
# A verification shorter than this (after normalization) is assumed too terse
|
|
53
|
+
# to name a concrete, checkable criterion.
|
|
54
|
+
_VAGUE_MIN_LEN = 15
|
|
55
|
+
|
|
56
|
+
# Vacuous verification phrasings that may exceed the length floor yet still
|
|
57
|
+
# assert nothing observable. Compared against the normalized (lower-cased,
|
|
58
|
+
# punctuation-stripped, whitespace-collapsed) verification text.
|
|
59
|
+
_VAGUE_PHRASES = frozenset({
|
|
60
|
+
"it works",
|
|
61
|
+
"works",
|
|
62
|
+
"it is done",
|
|
63
|
+
"its done",
|
|
64
|
+
"done",
|
|
65
|
+
"finished",
|
|
66
|
+
"complete",
|
|
67
|
+
"completed",
|
|
68
|
+
"tests pass",
|
|
69
|
+
"all tests pass",
|
|
70
|
+
"the tests pass",
|
|
71
|
+
"passes",
|
|
72
|
+
"it passes",
|
|
73
|
+
"ok",
|
|
74
|
+
"looks good",
|
|
75
|
+
"lgtm",
|
|
76
|
+
"works as expected",
|
|
77
|
+
"it works as expected",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class Goal:
|
|
83
|
+
"""A task structured into five fields.
|
|
84
|
+
|
|
85
|
+
``task``, ``outcome`` and ``verification`` carry the meaning; ``why`` and
|
|
86
|
+
``constraints`` are optional context. Only ``task`` is a positional
|
|
87
|
+
requirement of the dataclass — the required-ness of ``outcome`` and
|
|
88
|
+
``verification`` is a *validation* rule (see ``validate_goal``), not a
|
|
89
|
+
construction rule, so partial goals can be built and then reported on.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
task: str
|
|
93
|
+
why: str = ""
|
|
94
|
+
outcome: str = ""
|
|
95
|
+
constraints: list[str] = field(default_factory=list)
|
|
96
|
+
verification: str = ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _normalize(text: str) -> str:
|
|
100
|
+
"""Lower-case, strip surrounding punctuation, and collapse inner whitespace.
|
|
101
|
+
|
|
102
|
+
Used to compare a verification string against ``_VAGUE_PHRASES`` so that
|
|
103
|
+
"It works." and " it works " both reduce to "it works".
|
|
104
|
+
"""
|
|
105
|
+
lowered = text.strip().lower()
|
|
106
|
+
lowered = re.sub(r"\s+", " ", lowered)
|
|
107
|
+
return lowered.strip(" \t\r\n.!?,;:-*")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _split_constraints(raw_lines: list[str]) -> list[str]:
|
|
111
|
+
"""Turn accumulated CONSTRAINTS text into a clean list.
|
|
112
|
+
|
|
113
|
+
Splits on both newlines and commas, strips common bullet markers
|
|
114
|
+
("-", "*", "•") and surrounding whitespace, and drops empties.
|
|
115
|
+
"""
|
|
116
|
+
items: list[str] = []
|
|
117
|
+
for line in raw_lines:
|
|
118
|
+
for part in re.split(r"[,\n]", line):
|
|
119
|
+
cleaned = part.strip().lstrip("-*• \t").strip()
|
|
120
|
+
if cleaned:
|
|
121
|
+
items.append(cleaned)
|
|
122
|
+
return items
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_goal(text: str) -> Goal:
|
|
126
|
+
"""Parse labeled ``LABEL: value`` text into a :class:`Goal`.
|
|
127
|
+
|
|
128
|
+
Recognizes the five labels (TASK, WHY, OUTCOME, CONSTRAINTS, VERIFICATION)
|
|
129
|
+
case-insensitively and with tolerant whitespace. A field's value may span
|
|
130
|
+
multiple lines: everything after a label line, up to the next label line,
|
|
131
|
+
belongs to that field. Scalar fields are whitespace-joined; CONSTRAINTS is
|
|
132
|
+
split into a list (see :func:`_split_constraints`).
|
|
133
|
+
|
|
134
|
+
Missing fields become empty (``""`` or ``[]``). Parsing never raises for
|
|
135
|
+
missing required fields — that is reported by :func:`validate_goal`.
|
|
136
|
+
"""
|
|
137
|
+
# Accumulate raw text lines per field as we walk the input.
|
|
138
|
+
buckets: dict[str, list[str]] = {label: [] for label in _LABELS}
|
|
139
|
+
current: str | None = None
|
|
140
|
+
|
|
141
|
+
for line in text.splitlines():
|
|
142
|
+
match = _LABEL_RE.match(line)
|
|
143
|
+
if match:
|
|
144
|
+
current = match.group(1).upper()
|
|
145
|
+
inline = match.group(2).strip()
|
|
146
|
+
if inline:
|
|
147
|
+
buckets[current].append(inline)
|
|
148
|
+
elif current is not None:
|
|
149
|
+
# Continuation line for the field currently being read.
|
|
150
|
+
stripped = line.strip()
|
|
151
|
+
if stripped:
|
|
152
|
+
buckets[current].append(stripped)
|
|
153
|
+
|
|
154
|
+
def scalar(label: str) -> str:
|
|
155
|
+
return " ".join(buckets[label]).strip()
|
|
156
|
+
|
|
157
|
+
return Goal(
|
|
158
|
+
task=scalar("TASK"),
|
|
159
|
+
why=scalar("WHY"),
|
|
160
|
+
outcome=scalar("OUTCOME"),
|
|
161
|
+
constraints=_split_constraints(buckets["CONSTRAINTS"]),
|
|
162
|
+
verification=scalar("VERIFICATION"),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _verification_is_vague(verification: str) -> bool:
|
|
167
|
+
"""True when a non-empty verification names no concrete, checkable criterion.
|
|
168
|
+
|
|
169
|
+
Two triggers: it is shorter than ``_VAGUE_MIN_LEN`` after normalization, or
|
|
170
|
+
it matches one of the known vacuous phrasings in ``_VAGUE_PHRASES``.
|
|
171
|
+
"""
|
|
172
|
+
normalized = _normalize(verification)
|
|
173
|
+
if not normalized:
|
|
174
|
+
return False # empty is an ERROR case, handled by validate_goal
|
|
175
|
+
if len(normalized) < _VAGUE_MIN_LEN:
|
|
176
|
+
return True
|
|
177
|
+
return normalized in _VAGUE_PHRASES
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate_goal(goal: Goal) -> list[str]:
|
|
181
|
+
"""Return a list of problems with *goal*; an empty list means fully valid.
|
|
182
|
+
|
|
183
|
+
Errors (plain messages — presence of any makes the goal invalid):
|
|
184
|
+
* "TASK is required"
|
|
185
|
+
* "OUTCOME is required (define what done looks like)"
|
|
186
|
+
* "VERIFICATION is required (state how you'll prove it's complete)"
|
|
187
|
+
|
|
188
|
+
Warnings (prefixed with ``"WARNING: "`` — advisory, do not invalidate):
|
|
189
|
+
* a present-but-vacuous VERIFICATION.
|
|
190
|
+
|
|
191
|
+
Errors are listed before warnings. Use :func:`goal_has_errors` to test
|
|
192
|
+
validity without inspecting the prefix yourself.
|
|
193
|
+
"""
|
|
194
|
+
errors: list[str] = []
|
|
195
|
+
warnings: list[str] = []
|
|
196
|
+
|
|
197
|
+
if not goal.task.strip():
|
|
198
|
+
errors.append("TASK is required")
|
|
199
|
+
if not goal.outcome.strip():
|
|
200
|
+
errors.append("OUTCOME is required (define what done looks like)")
|
|
201
|
+
if not goal.verification.strip():
|
|
202
|
+
errors.append(
|
|
203
|
+
"VERIFICATION is required (state how you'll prove it's complete)"
|
|
204
|
+
)
|
|
205
|
+
elif _verification_is_vague(goal.verification):
|
|
206
|
+
warnings.append(
|
|
207
|
+
_WARNING_PREFIX
|
|
208
|
+
+ "VERIFICATION is vague - state a concrete, checkable criterion"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return errors + warnings
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def goal_has_errors(problems: list[str]) -> bool:
|
|
215
|
+
"""True if *problems* (from :func:`validate_goal`) contains any hard error.
|
|
216
|
+
|
|
217
|
+
A hard error is any message NOT prefixed with ``"WARNING: "``.
|
|
218
|
+
"""
|
|
219
|
+
return any(not p.startswith(_WARNING_PREFIX) for p in problems)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def goal_is_valid(goal: Goal) -> bool:
|
|
223
|
+
"""Convenience: True when *goal* has no hard errors (warnings are allowed)."""
|
|
224
|
+
return not goal_has_errors(validate_goal(goal))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def format_goal(goal: Goal) -> str:
|
|
228
|
+
"""Render *goal* back into the clean labeled template for display/output.
|
|
229
|
+
|
|
230
|
+
Optional empty fields (WHY, CONSTRAINTS) are still emitted as labels so the
|
|
231
|
+
output shape is stable and copy-editable. Constraints render as a bulleted
|
|
232
|
+
list; a single constraint renders inline.
|
|
233
|
+
"""
|
|
234
|
+
lines = [f"TASK: {goal.task}".rstrip()]
|
|
235
|
+
lines.append(f"WHY: {goal.why}".rstrip())
|
|
236
|
+
lines.append(f"OUTCOME: {goal.outcome}".rstrip())
|
|
237
|
+
|
|
238
|
+
if not goal.constraints:
|
|
239
|
+
lines.append("CONSTRAINTS:")
|
|
240
|
+
elif len(goal.constraints) == 1:
|
|
241
|
+
lines.append(f"CONSTRAINTS: {goal.constraints[0]}")
|
|
242
|
+
else:
|
|
243
|
+
lines.append("CONSTRAINTS:")
|
|
244
|
+
lines.extend(f" - {item}" for item in goal.constraints)
|
|
245
|
+
|
|
246
|
+
lines.append(f"VERIFICATION: {goal.verification}".rstrip())
|
|
247
|
+
return "\n".join(lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def goal_template() -> str:
|
|
251
|
+
"""Return the empty five-field template with a one-line hint per label.
|
|
252
|
+
|
|
253
|
+
Handed to a user (or the Architect) starting from scratch, so every field —
|
|
254
|
+
especially VERIFICATION — comes with a prompt for what belongs there.
|
|
255
|
+
"""
|
|
256
|
+
return (
|
|
257
|
+
"TASK: (what to do)\n"
|
|
258
|
+
"WHY: (why it matters / who it's for - optional)\n"
|
|
259
|
+
"OUTCOME: (the exact finished result - what 'done' looks like)\n"
|
|
260
|
+
"CONSTRAINTS: (what must or must not be done - optional, one per line)\n"
|
|
261
|
+
"VERIFICATION: (how you'll prove the outcome is complete - "
|
|
262
|
+
"a concrete, checkable criterion)"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
# Small self-test exercising the four cases: valid, missing-verification
|
|
268
|
+
# (error), vague-verification (warning), and template output.
|
|
269
|
+
print("=" * 64)
|
|
270
|
+
print("CASE 1 — complete valid goal")
|
|
271
|
+
print("=" * 64)
|
|
272
|
+
valid = parse_goal(
|
|
273
|
+
"""
|
|
274
|
+
TASK: Add a --json flag to the report command
|
|
275
|
+
WHY: CI needs machine-readable output for the dashboard
|
|
276
|
+
OUTCOME: `report --json` prints a valid JSON object to stdout and exits 0
|
|
277
|
+
CONSTRAINTS: no new dependencies
|
|
278
|
+
must not change the default human output
|
|
279
|
+
VERIFICATION: run `report --json | python -m json.tool` and confirm it
|
|
280
|
+
parses and contains the "total" key
|
|
281
|
+
"""
|
|
282
|
+
)
|
|
283
|
+
print(format_goal(valid))
|
|
284
|
+
problems = validate_goal(valid)
|
|
285
|
+
print("\nproblems:", problems)
|
|
286
|
+
print("has_errors:", goal_has_errors(problems), "| is_valid:", goal_is_valid(valid))
|
|
287
|
+
print("constraints parsed:", valid.constraints)
|
|
288
|
+
assert problems == [], "expected no problems for a complete goal"
|
|
289
|
+
assert goal_is_valid(valid)
|
|
290
|
+
assert valid.constraints == ["no new dependencies", "must not change the default human output"]
|
|
291
|
+
|
|
292
|
+
print("\n" + "=" * 64)
|
|
293
|
+
print("CASE 2 — missing VERIFICATION (error)")
|
|
294
|
+
print("=" * 64)
|
|
295
|
+
missing = parse_goal(
|
|
296
|
+
"TASK: Refactor the parser\n"
|
|
297
|
+
"OUTCOME: parser split into tokenizer and builder modules"
|
|
298
|
+
)
|
|
299
|
+
problems = validate_goal(missing)
|
|
300
|
+
print("problems:", problems)
|
|
301
|
+
print("has_errors:", goal_has_errors(problems), "| is_valid:", goal_is_valid(missing))
|
|
302
|
+
assert goal_has_errors(problems), "missing verification must be an error"
|
|
303
|
+
assert any("VERIFICATION is required" in p for p in problems)
|
|
304
|
+
assert not goal_is_valid(missing)
|
|
305
|
+
|
|
306
|
+
print("\n" + "=" * 64)
|
|
307
|
+
print("CASE 3 — vague VERIFICATION (warning, still valid)")
|
|
308
|
+
print("=" * 64)
|
|
309
|
+
vague = parse_goal(
|
|
310
|
+
"TASK: Fix the login bug\n"
|
|
311
|
+
"OUTCOME: users can log in again\n"
|
|
312
|
+
"VERIFICATION: it works"
|
|
313
|
+
)
|
|
314
|
+
problems = validate_goal(vague)
|
|
315
|
+
print("problems:", problems)
|
|
316
|
+
print("has_errors:", goal_has_errors(problems), "| is_valid:", goal_is_valid(vague))
|
|
317
|
+
assert not goal_has_errors(problems), "vague verification is a warning, not an error"
|
|
318
|
+
assert any(p.startswith("WARNING: ") for p in problems)
|
|
319
|
+
assert goal_is_valid(vague), "a goal with only warnings is still valid"
|
|
320
|
+
|
|
321
|
+
print("\n" + "=" * 64)
|
|
322
|
+
print("CASE 4 — empty template")
|
|
323
|
+
print("=" * 64)
|
|
324
|
+
print(goal_template())
|
|
325
|
+
|
|
326
|
+
print("\nAll self-test assertions passed.")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""PreToolUse hook: denies any Bash git invocation. Injected only into the
|
|
3
|
+
Builder agent subprocess via --settings, so the Builder cannot run git
|
|
4
|
+
(commit, push, reset, checkout, etc.). Fail-closed: on any error, deny."""
|
|
5
|
+
import sys, json, re
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
data = json.load(sys.stdin)
|
|
9
|
+
cmd = data.get("tool_input", {}).get("command", "")
|
|
10
|
+
except Exception:
|
|
11
|
+
# Fail closed: if we can't parse, deny to be safe.
|
|
12
|
+
print("BLOCKED: Builder git-guard could not parse tool input; denying.", file=sys.stderr)
|
|
13
|
+
sys.exit(2)
|
|
14
|
+
|
|
15
|
+
# Match git as a command token in any form: git, /usr/bin/git, sh -c "git",
|
|
16
|
+
# env VAR=x git, chained "&& git", "; git", "| git", GIT_DIR=... git, etc.
|
|
17
|
+
if re.search(r"(^|[\s;&|/])git(\s|$)", cmd) or re.search(r"\bgit\b", cmd):
|
|
18
|
+
print("BLOCKED: Builder may not run git. Git operations are performed by the orchestrator, not the Builder.", file=sys.stderr)
|
|
19
|
+
sys.exit(2)
|
|
20
|
+
|
|
21
|
+
sys.exit(0)
|
|
@@ -9,6 +9,8 @@ src/memstack_skill_loader/compression.py
|
|
|
9
9
|
src/memstack_skill_loader/config.py
|
|
10
10
|
src/memstack_skill_loader/dashboard.html
|
|
11
11
|
src/memstack_skill_loader/dashboard.py
|
|
12
|
+
src/memstack_skill_loader/goal.py
|
|
13
|
+
src/memstack_skill_loader/goal_engine.py
|
|
12
14
|
src/memstack_skill_loader/indexer.py
|
|
13
15
|
src/memstack_skill_loader/license.py
|
|
14
16
|
src/memstack_skill_loader/memory_db.py
|
|
@@ -24,6 +26,8 @@ src/memstack_skill_loader.egg-info/dependency_links.txt
|
|
|
24
26
|
src/memstack_skill_loader.egg-info/entry_points.txt
|
|
25
27
|
src/memstack_skill_loader.egg-info/requires.txt
|
|
26
28
|
src/memstack_skill_loader.egg-info/top_level.txt
|
|
29
|
+
src/memstack_skill_loader/hooks/__init__.py
|
|
30
|
+
src/memstack_skill_loader/hooks/deny_git.py
|
|
27
31
|
src/memstack_skill_loader/proxy/__init__.py
|
|
28
32
|
src/memstack_skill_loader/proxy/_diag.py
|
|
29
33
|
src/memstack_skill_loader/proxy/body_parser.py
|
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/config.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/indexer.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/license.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/memory_db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/search.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/server.py
RENAMED
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/src/memstack_skill_loader/stats.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_body_parser_metric.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_code_read_savings.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_license_environment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.8.0 → memstack_skill_loader-4.10.0}/tests/test_pro_skills_update.py
RENAMED
|
File without changes
|
|
File without changes
|