memstack-skill-loader 4.0.5__tar.gz → 4.0.7__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.
Files changed (28) hide show
  1. {memstack_skill_loader-4.0.5/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.0.7}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/agent_runner.py +25 -2
  5. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/dashboard.html +92 -58
  6. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/dashboard.py +43 -17
  7. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/stats.py +57 -1
  8. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  9. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/MANIFEST.in +0 -0
  10. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/README.md +0 -0
  11. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/setup.cfg +0 -0
  12. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/__main__.py +0 -0
  13. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/categories.py +0 -0
  14. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/compression.py +0 -0
  15. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/config.py +0 -0
  16. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/indexer.py +0 -0
  17. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/license.py +0 -0
  18. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/memory_db.py +0 -0
  19. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/search.py +0 -0
  20. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/server.py +0 -0
  21. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/skill_config.py +0 -0
  22. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/tfidf_search.py +0 -0
  23. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader/version_check.py +0 -0
  24. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
  25. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  26. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  27. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  28. {memstack_skill_loader-4.0.5 → memstack_skill_loader-4.0.7}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.5
3
+ Version: 4.0.7
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.0.5"
7
+ version = "4.0.7"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.0.5"
3
+ __version__ = "4.0.7"
@@ -66,7 +66,8 @@ SYSTEM_PROMPTS = {
66
66
  "external stylesheet.\n"
67
67
  "- Never hardcode secrets, API keys, tokens, passwords, or credentials in source code. "
68
68
  "Always use environment variables loaded from .env files. If a secret is needed, create "
69
- "or update a .env.example file with the variable name and a placeholder value."
69
+ "or update a .env.example file with the variable name and a placeholder value.\n"
70
+ "- Do NOT run git add, git commit, or any git commands. Only make file changes."
70
71
  ),
71
72
  "reviewer": (
72
73
  "You are a Reviewer agent. You review the Builder's changes against the original "
@@ -189,6 +190,24 @@ def _extract_task_text(raw: str) -> str:
189
190
  return result[:72]
190
191
 
191
192
 
193
+ def _clean_task_description(task: str) -> str:
194
+ """Strip Working directory/Branch boilerplate, returning the user's task description."""
195
+ import re
196
+ _BOILERPLATE_LINE = re.compile(
197
+ r'^(working\s+directory:|branch:|read\s+all\s+files\s+before\s+modifying\.?)',
198
+ re.IGNORECASE
199
+ )
200
+ lines = task.splitlines()
201
+ start = 0
202
+ for i, line in enumerate(lines):
203
+ stripped = line.strip()
204
+ if not stripped or _BOILERPLATE_LINE.match(stripped):
205
+ start = i + 1
206
+ else:
207
+ break
208
+ return "\n".join(lines[start:]).strip()
209
+
210
+
192
211
  def _extract_commit_from_reviewer(reviewer_output: str) -> str:
193
212
  """Extract a commit-friendly summary from the reviewer's APPROVED message."""
194
213
  import re
@@ -333,6 +352,7 @@ def _extract_text_from_stream_line(line: str) -> Optional[str]:
333
352
  def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
334
353
  log_path: Optional[Path] = None, timeout: int = 600,
335
354
  model: str = "", session_id: Optional[str] = None,
355
+ working_dir: str = "",
336
356
  ) -> tuple[str, int, int]:
337
357
  """Call the Anthropic Messages API directly via httpx.
338
358
 
@@ -410,7 +430,7 @@ def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
410
430
 
411
431
  try:
412
432
  log_agent_invocation(
413
- name, len(prompt), len(output), session_id, "",
433
+ name, len(prompt), len(output), session_id, working_dir,
414
434
  input_tokens=input_tokens, output_tokens=output_tokens, cost_usd=0.0,
415
435
  )
416
436
  except Exception:
@@ -645,6 +665,7 @@ class Session:
645
665
  meta = {
646
666
  "session_id": self.session_id,
647
667
  "task": self.task,
668
+ "task_description": _clean_task_description(self.task),
648
669
  "status": self.status,
649
670
  "started_at": self.started_at,
650
671
  "iteration": self.iteration,
@@ -727,6 +748,7 @@ def _orchestrate(session: Session) -> None:
727
748
  timeout=min(600, session.timeout),
728
749
  model=session.models.get("manager", ""),
729
750
  session_id=session.session_id,
751
+ working_dir=session.working_dir,
730
752
  )
731
753
  except subprocess.TimeoutExpired:
732
754
  session.agents["manager"]["status"] = "timeout"
@@ -855,6 +877,7 @@ def _orchestrate(session: Session) -> None:
855
877
  timeout=session.timeout,
856
878
  model=session.models.get("reviewer", ""),
857
879
  session_id=session.session_id,
880
+ working_dir=session.working_dir,
858
881
  )
859
882
  except subprocess.TimeoutExpired:
860
883
  session.agents["reviewer"]["status"] = "timeout"
@@ -686,6 +686,8 @@
686
686
  color: #484f58;
687
687
  font-size: 0.85rem;
688
688
  }
689
+ .stat-green, .card .value.stat-green { color: #3fb950; }
690
+ .stat-orange, .card .value.stat-orange { color: #f0883e; }
689
691
  .session-row {
690
692
  display: flex;
691
693
  align-items: center;
@@ -1401,7 +1403,7 @@
1401
1403
  <div class="page-header" style="display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:0.5rem">
1402
1404
  <div>
1403
1405
  <h2>Burn Report</h2>
1404
- <p class="meta">Token usage &amp; cost analytics &middot; Anthropic Opus pricing ($15/M in, $75/M out)</p>
1406
+ <p class="meta">Token usage &amp; cost analytics &middot; Opus ($15/$75 per M) &middot; Sonnet ($3/$15 per M)</p>
1405
1407
  </div>
1406
1408
  <div style="display:flex;align-items:center;gap:0.6rem">
1407
1409
  <div class="burn-period-toggle">
@@ -1508,7 +1510,6 @@
1508
1510
  <input id="agent-workdir-input" type="text" placeholder="C:\\Projects\\my-app" style="flex:1;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;padding:0.6rem 0.7rem;font-size:0.88rem;font-family:monospace;">
1509
1511
  <button onclick="openDirBrowser()" style="background:#21262d;color:#8b949e;border:1px solid #30363d;padding:0.6rem 0.9rem;border-radius:6px;cursor:pointer;font-size:0.82rem;font-weight:600;white-space:nowrap;transition:background 0.2s,color 0.2s;" onmouseenter="this.style.background='#30363d';this.style.color='#e6edf3'" onmouseleave="this.style.background='#21262d';this.style.color='#8b949e'">Browse</button>
1510
1512
  </div>
1511
- <div id="recent-projects-dropdown" style="margin-bottom:1rem;max-height:140px;overflow-y:auto;border:1px solid #30363d;border-radius:6px;background:#0d1117;display:none;"></div>
1512
1513
  <label style="display:block;font-size:0.82rem;color:#8b949e;margin-bottom:0.4rem;font-weight:600;">Additional Context (optional)</label>
1513
1514
  <textarea id="agent-context-input" rows="3" placeholder="Optional. Examples:&#10;- This project uses Flask, not FastAPI&#10;- Don't touch files in the /api directory&#10;- Follow existing code style, no type hints&#10;- The database is SQLite, not Postgres" style="width:100%;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;padding:0.7rem;font-size:0.88rem;resize:vertical;font-family:inherit;margin:0 0 0.8rem 0;"></textarea>
1514
1515
  <div style="display:flex;align-items:center;gap:1rem;margin-bottom:0.8rem;">
@@ -2710,6 +2711,7 @@ async function saveDiary() {
2710
2711
  b.textContent = 'Diary Saved!';
2711
2712
  b.disabled = true;
2712
2713
  }
2714
+ localStorage.setItem('memstack-diary-saved-session', currentAgentSessionId);
2713
2715
  if (summary) _renderDiarySummary(summary);
2714
2716
  }
2715
2717
  function _diaryFail(b, msg) {
@@ -2776,6 +2778,7 @@ async function saveDiaryCompleted() {
2776
2778
  btn.classList.add('saved');
2777
2779
  btn.textContent = 'Diary Saved!';
2778
2780
  btn.disabled = true;
2781
+ localStorage.setItem('memstack-diary-saved-session', currentAgentSessionId);
2779
2782
  if (summary) _renderDiarySummary(summary);
2780
2783
  }
2781
2784
  function _completedDiaryFail(msg) {
@@ -2885,17 +2888,38 @@ async function resetAgentUI() {
2885
2888
  document.getElementById('agent-completed').style.display = 'none';
2886
2889
  document.getElementById('agent-commit-msg').value = '';
2887
2890
  document.getElementById('agent-git-output').style.display = 'none';
2891
+ // Clear task and context inputs
2892
+ document.getElementById('agent-task-input').value = '';
2888
2893
  document.getElementById('agent-context-input').value = '';
2894
+ // Reset timeout to default
2895
+ document.getElementById('agent-timeout-input').value = '60';
2896
+ // Collapse Model Selection and MCP Tools
2897
+ const modelDetails = document.querySelector('.model-selection-details');
2898
+ if (modelDetails) modelDetails.removeAttribute('open');
2899
+ const mcpDetails = document.getElementById('mcp-tools-details');
2900
+ if (mcpDetails) mcpDetails.removeAttribute('open');
2901
+ // Uncheck auto-commit
2902
+ const autoCommit = document.getElementById('agent-autocommit-checkbox');
2903
+ if (autoCommit) autoCommit.checked = false;
2904
+ // Reset git buttons
2889
2905
  for (const [id, label] of [['agent-commit-btn', 'Commit'], ['agent-push-btn', 'Push'], ['agent-commit-push-btn', 'Commit & Push']]) {
2890
2906
  const b = document.getElementById(id);
2891
2907
  b.textContent = label; b.disabled = false; b.style.cursor = 'pointer'; b.style.opacity = '1'; b.style.background = '';
2892
2908
  }
2893
2909
  lastGitOutput = '';
2894
2910
  lastGitSuccess = true;
2911
+ // Reset diary summary card
2895
2912
  const dsCard = document.getElementById('diary-summary-card');
2896
2913
  if (dsCard) { dsCard.style.display = 'none'; dsCard.innerHTML = ''; }
2897
- document.getElementById('agent-workdir-input').value = '';
2898
- loadLastWorkdir();
2914
+ // Reset both diary buttons back to default
2915
+ for (const btnId of ['header-diary-btn', 'completed-diary-btn']) {
2916
+ const b = document.getElementById(btnId);
2917
+ if (b) { b.textContent = 'Save Diary'; b.classList.remove('saving', 'saved', 'save-failed'); b.disabled = false; }
2918
+ }
2919
+ // Restore working directory from localStorage (user likely wants same project)
2920
+ const cachedWd = localStorage.getItem('memstack-last-workdir');
2921
+ const wdInput = document.getElementById('agent-workdir-input');
2922
+ if (cachedWd) { wdInput.value = cachedWd; fetchMcpServers(cachedWd); }
2899
2923
  }
2900
2924
 
2901
2925
  async function startAgentTask() {
@@ -2921,6 +2945,7 @@ async function startAgentTask() {
2921
2945
  body.user_name = userProfile.user_name || '';
2922
2946
  const blockedMcp = getBlockedMcpServers();
2923
2947
  if (blockedMcp.length) body.blocked_mcp_servers = blockedMcp;
2948
+ if (workDir) localStorage.setItem('memstack-last-workdir', workDir);
2924
2949
  const res = await fetch('/api/agent-run', {method:'POST', headers: AUTH_HEADERS, body: JSON.stringify(body)});
2925
2950
  const data = await res.json();
2926
2951
  if (data.error) { alert(data.error); return; }
@@ -2966,7 +2991,6 @@ async function loadAgentMonitor() {
2966
2991
  wdInput.addEventListener('blur', () => fetchMcpServers(wdInput.value.trim()));
2967
2992
  }
2968
2993
  fetchAgentStatus();
2969
- loadRecentProjects();
2970
2994
  loadLastWorkdir();
2971
2995
  loadModelPrefs();
2972
2996
  if (!agentRefreshInterval) {
@@ -2980,8 +3004,21 @@ async function loadLastWorkdir() {
2980
3004
  if (input.value.trim()) return;
2981
3005
  const res = await fetch('/api/last-workdir', {headers: {'X-Auth-Token': AUTH_TOKEN}});
2982
3006
  const data = await res.json();
2983
- if (data.path) { input.value = data.path; fetchMcpServers(data.path); }
2984
- } catch(e) { /* ignore */ }
3007
+ if (data.path) {
3008
+ input.value = data.path;
3009
+ localStorage.setItem('memstack-last-workdir', data.path);
3010
+ fetchMcpServers(data.path);
3011
+ } else {
3012
+ const cached = localStorage.getItem('memstack-last-workdir');
3013
+ if (cached) { input.value = cached; fetchMcpServers(cached); }
3014
+ }
3015
+ } catch(e) {
3016
+ const cached = localStorage.getItem('memstack-last-workdir');
3017
+ if (cached) {
3018
+ const input = document.getElementById('agent-workdir-input');
3019
+ if (!input.value.trim()) { input.value = cached; fetchMcpServers(cached); }
3020
+ }
3021
+ }
2985
3022
  }
2986
3023
 
2987
3024
  /* ─── Builder MCP Tools ─── */
@@ -3128,13 +3165,8 @@ async function agentGitPush() {
3128
3165
  try {
3129
3166
  const res = await fetch('/api/agent/push', {method: 'POST', headers: AUTH_HEADERS, body: '{}'});
3130
3167
  const data = await res.json();
3131
- if (data.status === 'error') {
3132
- lastGitOutput = data.message || 'Push failed.';
3133
- lastGitSuccess = false;
3134
- } else {
3135
- lastGitOutput = 'Pushed!';
3136
- lastGitSuccess = true;
3137
- }
3168
+ lastGitOutput = data.output || data.error || data.message || 'Done';
3169
+ lastGitSuccess = !!data.success;
3138
3170
  outputEl.textContent = lastGitOutput;
3139
3171
  outputEl.style.color = lastGitSuccess ? '#3fb950' : '#f85149';
3140
3172
  if (lastGitSuccess) _btnSuccess(btn, 'Pushed!');
@@ -3152,27 +3184,12 @@ async function agentGitCommitAndPush() {
3152
3184
  const btn = document.getElementById('agent-commit-push-btn');
3153
3185
  const outputEl = document.getElementById('agent-git-output');
3154
3186
  outputEl.style.display = 'block';
3155
- outputEl.textContent = 'Committing...';
3187
+ outputEl.textContent = 'Committing & pushing...';
3156
3188
  try {
3157
- const commitRes = await fetch('/api/agent-commit', {method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify({message: msg})});
3158
- const commitData = await commitRes.json();
3159
- if (!commitData.success && !(commitData.output || '').toLowerCase().includes('nothing to commit')) {
3160
- lastGitOutput = commitData.output || commitData.error || 'Commit failed.';
3161
- lastGitSuccess = false;
3162
- outputEl.textContent = lastGitOutput;
3163
- outputEl.style.color = '#f85149';
3164
- return;
3165
- }
3166
- outputEl.textContent = 'Pushing...';
3167
- const pushRes = await fetch('/api/agent/push', {method: 'POST', headers: AUTH_HEADERS, body: '{}'});
3168
- const pushData = await pushRes.json();
3169
- if (pushData.status === 'error') {
3170
- lastGitOutput = pushData.message || 'Push failed.';
3171
- lastGitSuccess = false;
3172
- } else {
3173
- lastGitOutput = 'Committed & pushed!';
3174
- lastGitSuccess = true;
3175
- }
3189
+ const res = await fetch('/api/agent-commit-push', {method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify({message: msg})});
3190
+ const data = await res.json();
3191
+ lastGitOutput = data.output || data.error || 'Done';
3192
+ lastGitSuccess = !!data.success;
3176
3193
  outputEl.textContent = lastGitOutput;
3177
3194
  outputEl.style.color = lastGitSuccess ? '#3fb950' : '#f85149';
3178
3195
  if (lastGitSuccess) _btnSuccess(btn, 'Committed & Pushed!');
@@ -3184,19 +3201,6 @@ async function agentGitCommitAndPush() {
3184
3201
  }
3185
3202
  }
3186
3203
 
3187
- async function loadRecentProjects() {
3188
- try {
3189
- const res = await fetch('/api/recent-projects', {headers: AUTH_GET});
3190
- const dirs = await res.json();
3191
- const dropdown = document.getElementById('recent-projects-dropdown');
3192
- if (!dirs.length) { dropdown.style.display = 'none'; return; }
3193
- dropdown.style.display = 'block';
3194
- dropdown.innerHTML = dirs.map(d =>
3195
- `<div style="padding:0.4rem 0.7rem;cursor:pointer;font-size:0.8rem;font-family:monospace;color:#8b949e;border-bottom:1px solid #21262d;transition:background 0.15s;" onmouseenter="this.style.background='#161b22';this.style.color='#e6edf3'" onmouseleave="this.style.background='';this.style.color='#8b949e'" onclick="document.getElementById('agent-workdir-input').value=this.textContent;fetchMcpServers(this.textContent)">${escapeHtml(d)}</div>`
3196
- ).join('');
3197
- } catch(e) { /* ignore */ }
3198
- }
3199
-
3200
3204
  /* ─── Directory Browser ─── */
3201
3205
  let dirBrowserCurrentPath = '';
3202
3206
 
@@ -3340,6 +3344,25 @@ function renderAgentUI(data) {
3340
3344
  }, 1000);
3341
3345
  }
3342
3346
  }
3347
+ // Auto-save diary on task completion
3348
+ const _diaryAlreadySaved = currentAgentSessionId && localStorage.getItem('memstack-diary-saved-session') === currentAgentSessionId;
3349
+ if (_diaryAlreadySaved) {
3350
+ setTimeout(() => {
3351
+ const cBtn = document.getElementById('completed-diary-btn');
3352
+ if (cBtn) {
3353
+ cBtn.classList.add('saved');
3354
+ cBtn.textContent = 'Diary Saved!';
3355
+ cBtn.disabled = true;
3356
+ }
3357
+ }, 50);
3358
+ } else {
3359
+ setTimeout(() => {
3360
+ const cBtn = document.getElementById('completed-diary-btn');
3361
+ if (cBtn && cBtn.textContent === 'Save Diary') {
3362
+ saveDiaryCompleted();
3363
+ }
3364
+ }, 500);
3365
+ }
3343
3366
  }
3344
3367
  launcher.style.display = 'none';
3345
3368
  active.style.display = 'none';
@@ -3386,8 +3409,19 @@ function renderAgentUI(data) {
3386
3409
  document.getElementById('agent-token-summary').innerHTML = '<h4>Token Usage</h4>' + tokenRows;
3387
3410
  const commitInput = document.getElementById('agent-commit-msg');
3388
3411
  if (commitInput && !commitInput.value) {
3389
- const summary = data.commit_summary || '';
3390
- commitInput.value = 'agent: ' + (summary || 'dashboard updates').substring(0, 72);
3412
+ const taskDesc = data.task || '';
3413
+ const prefix = /fix|bug|error/i.test(taskDesc) ? 'fix' : 'feat';
3414
+ let sourceText = (data.result || '').trim();
3415
+ sourceText = sourceText.replace(/approved after \d+ iteration\(s\):?\s*/gi, '').trim();
3416
+ sourceText = sourceText.replace(/approved:\s*/gi, '').trim();
3417
+ if (!sourceText) sourceText = taskDesc;
3418
+ let summary = sourceText.split(/\.\s+|\.$|\n/)[0].trim() || 'dashboard updates';
3419
+ if (summary.length > 72) {
3420
+ const cut = summary.substring(0, 72);
3421
+ const sp = cut.lastIndexOf(' ');
3422
+ summary = sp > 0 ? cut.substring(0, sp) : cut;
3423
+ }
3424
+ commitInput.value = 'agent: ' + prefix + ': ' + summary;
3391
3425
  autoResize(commitInput);
3392
3426
  }
3393
3427
  const commitBtn = document.getElementById('agent-commit-btn');
@@ -3487,7 +3521,7 @@ function renderAgentUI(data) {
3487
3521
  const barColor = pct < 0 ? '' : pct <= 50 ? 'context-bar-green' : pct <= 77 ? 'context-bar-yellow' : 'context-bar-red';
3488
3522
  const contextHtml = pct < 0
3489
3523
  ? '<div class="context-bar-container">Context: unknown</div>'
3490
- : '<div class="context-bar-container">' + tokens.toLocaleString() + ' / 200,000 tokens (' + pct + '%)<div class="context-bar"><div class="context-bar-fill ' + barColor + '" style="width:' + pct + '%"></div></div></div>';
3524
+ : '<div class="context-bar-container">' + tokens.toLocaleString() + ' / 200,000 - Context Window: ' + pct + '%<div class="context-bar"><div class="context-bar-fill ' + barColor + '" style="width:' + pct + '%"></div></div></div>';
3491
3525
  if (pct >= 65 && !diaryAutoSaved[role] && tokens > 0) {
3492
3526
  const anyStarted = roleOrder.some(r => {
3493
3527
  const ag = agents[r] || {};
@@ -3864,15 +3898,15 @@ async function loadHeadroomStats(burnData) {
3864
3898
  const el = document.getElementById('headroom-cards');
3865
3899
  const lifetimeEl = document.getElementById('headroom-lifetime');
3866
3900
 
3867
- const saved = (burnData && burnData.tokens_saved) || 0;
3868
- const pct = (burnData && burnData.savings_pct) || 0;
3869
- const costSaved = (burnData && burnData.cost_saved) || 0;
3870
- const reqs = (burnData && burnData.compress_count) || 0;
3901
+ const opusTokens = (burnData && burnData.opus_tokens) || 0;
3902
+ const opusCost = (burnData && burnData.opus_cost) || 0;
3903
+ const sonnetTokens = (burnData && burnData.sonnet_tokens) || 0;
3904
+ const sonnetCost = (burnData && burnData.sonnet_cost) || 0;
3871
3905
  el.innerHTML = `
3872
- <div class="card"><div class="value" style="color:#3fb950">${fmt(saved)}</div><div class="label">Tokens Saved</div></div>
3873
- <div class="card"><div class="value" style="color:#3fb950">${pct.toFixed(2)}%</div><div class="label">Savings Rate</div></div>
3874
- <div class="card"><div class="value" style="color:#3fb950">$${costSaved.toFixed(2)}</div><div class="label">Cost Saved</div></div>
3875
- <div class="card"><div class="value" style="color:#58a6ff">${fmt(reqs)}</div><div class="label">Compressions</div></div>
3906
+ <div class="card"><div class="value stat-green">${fmt(opusTokens)}</div><div class="label">Opus Tokens</div></div>
3907
+ <div class="card"><div class="value stat-orange">$${opusCost.toFixed(2)}</div><div class="label">Opus Cost</div></div>
3908
+ <div class="card"><div class="value stat-green">${fmt(sonnetTokens)}</div><div class="label">Sonnet Tokens</div></div>
3909
+ <div class="card"><div class="value stat-orange">$${sonnetCost.toFixed(2)}</div><div class="label">Sonnet Cost</div></div>
3876
3910
  `;
3877
3911
 
3878
3912
  // Show Headroom lifetime stats as a subtle info line when available
@@ -740,7 +740,7 @@ class _Handler(BaseHTTPRequestHandler):
740
740
  _LAST_WORKDIR_FILE.write_text(working_dir, encoding="utf-8")
741
741
  except OSError:
742
742
  pass
743
- auto_commit = data.get("auto_commit", True)
743
+ auto_commit = data.get("auto_commit", False)
744
744
  timeout_minutes = int(data.get("timeout_minutes", 60))
745
745
  manager_model = data.get("manager_model", "")
746
746
  builder_model = data.get("builder_model", "")
@@ -987,7 +987,7 @@ class _Handler(BaseHTTPRequestHandler):
987
987
  status = agent_runner.get_status()
988
988
  work_dir = status.get("working_dir", "")
989
989
  if not work_dir or not Path(work_dir).is_dir():
990
- body = json.dumps({"status": "error", "message": "No valid working directory from last session."}).encode()
990
+ body = json.dumps({"success": False, "error": "No valid working directory from last session."}).encode()
991
991
  self._respond(400, "application/json", body)
992
992
  return
993
993
 
@@ -998,34 +998,60 @@ class _Handler(BaseHTTPRequestHandler):
998
998
 
999
999
  status_r = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, cwd=work_dir, timeout=30)
1000
1000
  if status_r.stdout.strip():
1001
- body = json.dumps({"status": "error", "message": "Uncommitted changes. Commit first."}).encode()
1001
+ body = json.dumps({"success": False, "error": "Uncommitted changes. Commit first."}).encode()
1002
1002
  self._respond(200, "application/json", body)
1003
1003
  return
1004
1004
 
1005
+ output_parts = []
1006
+ ok, out = _run_git_p(["branch", "--show-current"], work_dir)
1007
+ current_branch = out.split("\n", 1)[-1].strip() if ok else ""
1008
+ output_parts.append(out)
1009
+ if not ok or not current_branch:
1010
+ body = json.dumps({"success": False, "error": "Could not detect current branch", "output": "\n".join(output_parts)}).encode()
1011
+ self._respond(500, "application/json", body)
1012
+ return
1013
+
1014
+ default_branch = None
1015
+ for candidate in ("main", "master"):
1016
+ chk, _ = _run_git_p(["rev-parse", "--verify", candidate], work_dir)
1017
+ if chk:
1018
+ default_branch = candidate
1019
+ break
1020
+
1005
1021
  try:
1006
- steps = [
1007
- (["checkout", "master"], 30),
1008
- (["merge", "dev", "--ff-only"], 30),
1009
- (["push", "origin", "master"], 60),
1010
- (["checkout", "dev"], 30),
1011
- (["reset", "--hard", "master"], 30),
1012
- ]
1013
- for args, t in steps:
1014
- ok, out = _run_git_p(args, work_dir, timeout=t)
1022
+ if default_branch and current_branch != default_branch:
1023
+ steps = [
1024
+ (["checkout", default_branch], 30),
1025
+ (["merge", current_branch, "--ff-only"], 30),
1026
+ (["push", "origin", default_branch], 60),
1027
+ (["checkout", current_branch], 30),
1028
+ (["reset", "--hard", default_branch], 30),
1029
+ ]
1030
+ for args, t in steps:
1031
+ ok, out = _run_git_p(args, work_dir, timeout=t)
1032
+ output_parts.append(out)
1033
+ if not ok:
1034
+ body = json.dumps({"success": False, "error": f"git {args[0]} failed", "output": "\n".join(output_parts)}).encode()
1035
+ self._respond(500, "application/json", body)
1036
+ return
1037
+ else:
1038
+ ok, out = _run_git_p(["push", "origin", current_branch], work_dir, timeout=60)
1039
+ output_parts.append(out)
1015
1040
  if not ok:
1016
- body = json.dumps({"status": "error", "message": f"git {args[0]} failed: {out}"}).encode()
1041
+ body = json.dumps({"success": False, "error": "git push failed", "output": "\n".join(output_parts)}).encode()
1017
1042
  self._respond(500, "application/json", body)
1018
1043
  return
1019
1044
  finally:
1020
- _run_git_p(["checkout", "dev"], work_dir, timeout=30)
1045
+ if current_branch:
1046
+ _run_git_p(["checkout", current_branch], work_dir, timeout=30)
1021
1047
 
1022
- body = json.dumps({"status": "ok", "message": "Pushed to master"}).encode()
1048
+ body = json.dumps({"success": True, "output": "\n\n".join(output_parts)}).encode()
1023
1049
  self._respond(200, "application/json", body)
1024
1050
  except subprocess.TimeoutExpired:
1025
- body = json.dumps({"status": "error", "message": "Git command timed out."}).encode()
1051
+ body = json.dumps({"success": False, "error": "Git command timed out."}).encode()
1026
1052
  self._respond(500, "application/json", body)
1027
1053
  except Exception as exc:
1028
- body = json.dumps({"status": "error", "message": str(exc)}).encode()
1054
+ body = json.dumps({"success": False, "error": str(exc)}).encode()
1029
1055
  self._respond(500, "application/json", body)
1030
1056
  elif self.path == "/api/agent-save-diary":
1031
1057
  try:
@@ -537,10 +537,30 @@ def get_skill_fire_counts() -> dict[str, int]:
537
537
 
538
538
  OPUS_INPUT_COST = 15.0 # $/M tokens
539
539
  OPUS_OUTPUT_COST = 75.0 # $/M tokens
540
+ SONNET_INPUT_COST = 3.0 # $/M tokens
541
+ SONNET_OUTPUT_COST = 15.0 # $/M tokens
540
542
  AVG_OUTPUT_RATIO = 0.3 # assume ~30% of tokens are output
541
543
 
542
544
  _SESSIONS_DIR = Path.home() / ".memstack" / "agent-runner" / "sessions"
543
545
 
546
+ _TASK_BOILERPLATE = re.compile(
547
+ r'^(working\s+directory:|branch:|read\s+all\s+files\s+before\s+modifying\.?)',
548
+ re.IGNORECASE
549
+ )
550
+
551
+
552
+ def _clean_task_description(task: str) -> str:
553
+ """Strip Working directory/Branch boilerplate, returning the user's task description."""
554
+ lines = task.splitlines()
555
+ start = 0
556
+ for i, line in enumerate(lines):
557
+ stripped = line.strip()
558
+ if not stripped or _TASK_BOILERPLATE.match(stripped):
559
+ start = i + 1
560
+ else:
561
+ break
562
+ return "\n".join(lines[start:]).strip()
563
+
544
564
 
545
565
  def get_recent_agent_sessions(limit: int = 10) -> list[dict]:
546
566
  """Scan agent-runner session directories for recent sessions."""
@@ -560,7 +580,7 @@ def get_recent_agent_sessions(limit: int = 10) -> list[dict]:
560
580
  mtime = meta_file.stat().st_mtime
561
581
  entries.append((mtime, {
562
582
  "session_id": meta.get("session_id", d.name),
563
- "task": (meta.get("task") or "")[:60],
583
+ "task": (_clean_task_description(meta.get("task_description") or meta.get("task") or ""))[:60],
564
584
  "status": meta.get("status", "unknown"),
565
585
  "started_at": meta.get("started_at", ""),
566
586
  "iteration": meta.get("iteration", 0),
@@ -893,6 +913,38 @@ def get_burn_report_data(period: str = "daily", range_filter: str = "all") -> di
893
913
  except Exception:
894
914
  pass
895
915
 
916
+ # ── Model-level breakdown (Opus = manager+reviewer, Sonnet = builder) ──
917
+ opus_input = 0
918
+ opus_output = 0
919
+ sonnet_input = 0
920
+ sonnet_output = 0
921
+ try:
922
+ conn = _get_conn()
923
+ try:
924
+ row = conn.execute(
925
+ "SELECT SUM(input_tokens), SUM(output_tokens) FROM skill_fires "
926
+ "WHERE skill_name IN ('agent:manager', 'agent:reviewer') AND timestamp >= ?",
927
+ (since,),
928
+ ).fetchone()
929
+ opus_input = int(row[0] or 0)
930
+ opus_output = int(row[1] or 0)
931
+ row2 = conn.execute(
932
+ "SELECT SUM(input_tokens), SUM(output_tokens) FROM skill_fires "
933
+ "WHERE skill_name = 'agent:builder' AND timestamp >= ?",
934
+ (since,),
935
+ ).fetchone()
936
+ sonnet_input = int(row2[0] or 0)
937
+ sonnet_output = int(row2[1] or 0)
938
+ finally:
939
+ conn.close()
940
+ except Exception:
941
+ pass
942
+
943
+ opus_tokens = opus_input + opus_output
944
+ sonnet_tokens = sonnet_input + sonnet_output
945
+ opus_cost = round(opus_input / 1_000_000 * OPUS_INPUT_COST + opus_output / 1_000_000 * OPUS_OUTPUT_COST, 4)
946
+ sonnet_cost = round(sonnet_input / 1_000_000 * SONNET_INPUT_COST + sonnet_output / 1_000_000 * SONNET_OUTPUT_COST, 4)
947
+
896
948
  return {
897
949
  "period": period,
898
950
  "range": range_filter,
@@ -907,6 +959,10 @@ def get_burn_report_data(period: str = "daily", range_filter: str = "all") -> di
907
959
  "trend": trend,
908
960
  "per_skill": per_skill[:20],
909
961
  "recent_sessions": recent_sessions,
962
+ "opus_tokens": opus_tokens,
963
+ "opus_cost": opus_cost,
964
+ "sonnet_tokens": sonnet_tokens,
965
+ "sonnet_cost": sonnet_cost,
910
966
  }
911
967
 
912
968
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.5
3
+ Version: 4.0.7
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0