Skip to content

feat: add live Paperclip workspace tab#3

Open
contentscoin wants to merge 1 commit into
reallygood83:mainfrom
contentscoin:feat/live-paperclip-workspace-tab
Open

feat: add live Paperclip workspace tab#3
contentscoin wants to merge 1 commit into
reallygood83:mainfrom
contentscoin:feat/live-paperclip-workspace-tab

Conversation

@contentscoin
Copy link
Copy Markdown

@contentscoin contentscoin commented May 2, 2026

Summary

  • Add a main Hermes/Paperclip tab switcher with a live embedded Paperclip work screen.
  • Add read-only /api/paperclip/status probing with PAPERCLIP_WEB_URL defaulting to local Paperclip web URL.
  • Update Paperclip approval/workflow UI synchronization and related WebUI packs.
  • Update desktop-install-pack docs/env guidance and rebuild desktop-install-pack.zip.

Verification

  • node --check static/ui.js
  • node --check static/boot.js
  • node --check static/multiagent.js
  • python -m py_compile api/routes.py api/facts.py server.py sharenote-telegram-pack/scripts/check_sharenote_env.py sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py
  • pytest tests/test_fact_routes.py tests/test_facts_store.py tests/test_multiagent_profile_api.py tests/test_multiagent_sessions.py -q => 12 passed
  • desktop-install-pack.zip rebuilt and SHA256: 9a3638ddbf6aacd2f956dba5054736a0542b7debdc453b9b62fa5613afbbc655

Note

Full legacy test suite currently has unrelated/stale baseline failures on this local setup, including tmp path canonicalization, old sprint assertions, and live-server endpoint expectations. The targeted changed-area tests pass.

Summary by CodeRabbit

  • New Features

    • Added GPT-5.5, GPT-5.4, GPT-5.4 Mini, and Codex Mini model options (default now GPT-5.5)
    • Introduced Memory Candidate Inbox for managing fact candidates before approval
    • Added multi-agent workspace mode with per-lane routing and team coordination
    • Enabled Paperclip decision intelligence workflows with approval gates and live console
    • Desktop app installation support (macOS and Windows)
    • Profile-based workspace organization for multi-user scenarios
  • Documentation

    • Added comprehensive guides for AutoResearch, Memory Bank Adoption, ShareNote + Telegram Publishing, and Paperclip Ops packs
    • New desktop installer and cross-platform deployment documentation
    • Templates and checklists for research, publishing, and decision workflows

- Embed the local Paperclip web app as a main workspace tab with status probing and refresh/open controls
- Add Paperclip status endpoint plus approval-safe console synchronization
- Include memory candidate, multi-agent, setup-pack, and desktop install pack updates
- Rebuild desktop-install-pack.zip with PAPERCLIP_WEB_URL guidance
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

The PR introduces a multi-layered enhancement to Hermes WebUI: profile-aware sessions with per-workspace configuration, a new local memory candidate/fact store with approval gates, desktop installers for macOS and Windows, a Paperclip console with decision intelligence and approval workflow management, a multi-agent workspace for parallel lane execution, and comprehensive documentation packs (AutoResearch, ShareNote publishing, Memory Bank adoption). Default model changes to GPT-5.5.

Changes

Profile and Multi-Workspace Support

Layer / File(s) Summary
Configuration Defaults
api/config.py
DEFAULT_MODEL updated to openai/gpt-5.5; fallback/provider model lists include gpt-5.5 in OpenAI and Codex families.
Session Profile Fields
api/models.py
Session.__init__ normalizes profile to "default" when falsy. new_session accepts optional profile parameter, defaulting to active profile name or None if unavailable.
Profile Home Resolution
api/profiles.py
New get_profile_home(name) returns base home for None/"default" or ~/.hermes/profiles/<name> otherwise. get_active_hermes_home() now delegates to get_profile_home.
Route Handling & Environment
api/routes.py, api/streaming.py
/api/session/new passes profile from request. _handle_chat_start copies profile from request into session. _handle_chat_sync and _run_agent_streaming resolve profile-specific HERMES_HOME and restore original on completion.
Tests
tests/test_multiagent_profile_api.py, tests/test_multiagent_sessions.py
Verify session profile persistence, profile-name resolution, and per-profile home directory derivation.

Memory Bank: Candidates and Facts Store

Layer / File(s) Summary
Data Model & Storage
api/facts.py
New module with thread-safe JSONL-backed storage for pending candidates and active facts. Validates scopes/categories/sensitivity; preserves provenance (candidate id, session id, source links).
API Endpoints
api/routes.py
New GET /api/facts, /api/memory-candidates, /api/paperclip/status; POST handlers for candidate create/approve/reject. All operations preserve local-only safety and return store summary.
Memory Inbox UI
static/facts.js
In-memory cache and panel rendering for candidates with editable statements, approve/reject buttons, manual JSON input form, and facts preview.
HTML Integration
static/index.html, static/panels.js
New panelFacts for memory inbox; switchPanel lazy-loads inbox via loadMemoryInbox().
Tests
tests/test_fact_routes.py, tests/test_facts_store.py
Cover candidate lifecycle (create→approve/reject), fact filtering, JSONL parsing resilience, and API validation.

Desktop Installation Infrastructure

Layer / File(s) Summary
macOS Installer
desktop-install-pack/install-macos.sh
Bash script validates prerequisites, clones/updates WebUI repo, generates ~/.hermes/bin launcher and macOS .app bundle, runs health check, opens browser. Supports --yes, --port, --repo, --dir flags.
Windows Installer
desktop-install-pack/install-windows.ps1
PowerShell script with similar flow: validates git/python, clones/updates repo, creates launcher under ~/.hermes/bin, generates desktop shortcut, health check, runs browser.
Electron Wrapper
desktop-install-pack/electron-wrapper/*
Electron main process spawns backend server, opens window pointing to localhost, routes external links to system browser, manages child process lifecycle. package.json configures build targets (dmg/nsis).
Environment Template
desktop-install-pack/templates/env.example
Example env vars for Paperclip, Telegram, Codex CLI with default PAPERCLIP_WEB_URL and credential notes.
Documentation
desktop-install-pack/README.md, docs/plans/2026-05-01-cross-platform-desktop-installer-plan.md
Quick-start guides, post-install verification, phased implementation roadmap covering installer hardening, Electron UI, integration wizards, CI/CD release.

Paperclip Console and Decision Intelligence

Layer / File(s) Summary
Approval Scope Detection
static/boot.js
New detectPaperclipApprovalScope, hasExplicitPaperclipApproval, updatePaperclipApprovalUI manage approval state. Workflow kinds expanded (Decision Report, approval request, reflection/extraction flows).
Workflow & Artifact Management
static/boot.js, static/messages.js
Artifact creation (createPaperclipResultArtifact), workflow history recording (recordPaperclipWorkflowRun), and result renaming (maybeRenameRecentPaperclipArtifactFromMessages) gates writes behind approval.
Console UI & Styling
static/ui.js, static/boot.js, static/style.css
switchMainView toggles Paperclip panel. initPaperclipLiveView fetches /api/paperclip/status and renders iframe. Paperclip console panel renders workflow history and actions. Comprehensive CSS for Paperclip hero/toolbar/status/frame/console grid.
HTML Layout
static/index.html
New main view tabs (Hermes/Paperclip), Paperclip Live panel with iframe/fallback, and Paperclip Console panel with workflow/artifact lists.
Documentation
memory-bank-adoption-pack/design/paperclip-decision-intelligence.md, memory-bank-adoption-pack/templates/paperclip-decision-intelligence-report.md, docs/plans/2026-05-01-memory-bank-paperclip-enhancement-plan.md
Decision intelligence report structure, approval gates, fact candidate lineage, and phased implementation (MVP → enhanced search → dry-run → semantic layer).

Multi-Agent Workspace

Layer / File(s) Summary
Lane Management & State
static/multiagent.js
New module with DEFAULT_LANES (hela/plan/dev/mkt), per-lane session/message/draft state via ensureMultiAgentState(). Per-lane profile/model/workspace selection and telegram identity metadata.
Routing & Execution
static/multiagent.js
routePromptToLanes matches keywords to target lanes. createLaneSession, sendLaneMessage, consumeLaneStream handle per-lane API calls and SSE streaming with error resilience.
Summary & Handoff
static/multiagent.js
renderMultiAgentSummary shows latest assistant per lane. buildHandoffPrompt and bindHandoffButtons enable Hela→other-lane message propagation.
UI Integration
static/index.html, static/ui.js, static/boot.js, static/panels.js
Multi-Agent Workspace hidden container in main view. switchMainView, ensureMultiAgentState, toggleMultiAgentWorkspace exported to window. Boot sequence calls ensureMultiAgentState() multiple times during init.
Styling
static/style.css
body.multi-agent-mode hides existing main shells. Comprehensive lane grid, card, composer, and responsive layout classes covering message history, per-lane controls, and summary panel.

Documentation and Implementation Plans

Layer / File(s) Summary
AutoResearch Pack
autoresearch-pack/README.md, autoresearch-pack/workflows/default-research-loop.md, autoresearch-pack/templates/research-*.md, autoresearch-pack/checklists/deepening-checklist.md
Complete workflow for turning research questions into repeatable loops with broad scan → synthesis → deepening → output stages, templates for questions/outputs, and deepening checklists.
ShareNote+Telegram Pack
sharenote-telegram-pack/README.md, sharenote-telegram-pack/workflows/default-publishing-flow.md, sharenote-telegram-pack/scripts/*, sharenote-telegram-pack/templates/*
Approval-gated publishing pipeline (note → ShareNote → Telegram) with environment checkers and draft generators for Obsidian integration.
Memory Bank Adoption Pack
memory-bank-adoption-pack/README.md, memory-bank-adoption-pack/design/fact-lifecycle.md, memory-bank-adoption-pack/design/scoped-facts.md, memory-bank-adoption-pack/templates/*
Fact lifecycle documentation, scope-guard rules, candidate review and decision intelligence templates.
Setup & Research Packs Registry
docs/setup-packs.md, docs/research-packs.md
Consolidated lists of expected outputs, operator promises (safety/approval gates), and constraints for all packs (Paperclip Ops, AutoResearch, Memory Bank Adoption, ShareNote+Telegram).

Sequence Diagram(s)

sequenceDiagram
    participant CEO as CEO User
    participant Inbox as Memory Inbox Panel
    participant API as /api/memory-candidates
    participant Store as facts.jsonl Storage
    participant Session as Session State

    CEO->>Inbox: Manual JSON entry (category, scope, statement)
    Inbox->>API: POST create_candidate(...)
    API->>Store: Append pending candidate
    Store-->>API: candidate_id
    API-->>Inbox: {candidate, ok: true}
    Inbox->>Inbox: Render card with edit/approve/reject
    
    CEO->>Inbox: Edit statement + Approve
    Inbox->>API: POST approve_candidate(id, edited_statement)
    API->>Store: Mark candidate approved, create fact
    Store-->>API: {candidate, fact}
    API-->>Inbox: {ok: true, summary}
    Inbox->>Inbox: Refresh, show active fact
Loading
sequenceDiagram
    participant CEO as CEO
    participant UI as Paperclip Console
    participant Routes as /api/paperclip/status
    participant Boot as boot.js Workflow
    participant Artifact as /api/file/create
    
    CEO->>UI: Click Workflow Action (e.g., Decision Report)
    UI->>Boot: detectPaperclipApprovalScope()
    Boot-->>UI: {scope, hasApproval}
    alt Approval Present
        UI->>Boot: createPaperclipResultArtifact(kind)
        Boot->>Artifact: POST create artifact
        Artifact-->>Boot: artifact_path
        Boot->>Boot: recordPaperclipWorkflowRun(scope, path)
        Boot-->>UI: artifact_path
        UI->>Routes: Fetch /api/paperclip/status
        Routes-->>UI: {health, url}
        UI->>UI: Show result in iframe/console
    else No Approval
        UI-->>CEO: Block action, show approval hint
    end
Loading
sequenceDiagram
    participant CEO as CEO User
    participant Router as Multi-Agent Router
    participant Hela as Hela Lane
    participant Plan as Plan Lane
    participant Dev as Dev Lane
    
    CEO->>Router: Submit prompt "plan the architecture"
    Router->>Router: resolveTargetLanes(text)
    Router-->>Router: Match keywords → [plan, dev]
    Router->>Plan: createLaneSession(), sendLaneMessage()
    Router->>Dev: createLaneSession(), sendLaneMessage()
    Plan->>Plan: consumeLaneStream() → SSE /api/chat/stream
    Dev->>Dev: consumeLaneStream() → SSE /api/chat/stream
    Plan-->>Router: token events, done
    Dev-->>Router: token events, done
    Router->>Router: renderMultiAgentSummary()
    Router-->>CEO: Show latest Plan/Dev outputs side-by-side
    CEO->>Hela: Handoff: "synthesize insights"
    Hela->>Plan: Read latest Plan summary
    Hela->>Hela: buildHandoffPrompt(plan_summary)
    Hela->>Hela: consumeLaneStream()
    Hela-->>CEO: Synthesis output
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This PR is substantial and heterogeneous, spanning backend infrastructure (profile support, facts store, API routes), desktop packaging (platform-specific installers), frontend UI (three major new panels: memory inbox, Paperclip console, multi-agent workspace), styling overhauls, documentation packs, and comprehensive testing. The logic density is high in multi-agent lane routing, Paperclip approval gating, and JSONL storage semantics. Multiple independent feature streams require separate reasoning. While many changes follow consistent patterns (e.g., repeated UI panel/style blocks), the diversity of technologies (Bash/PowerShell/Electron/Python/JavaScript/CSS/Markdown) and interconnected feature dependencies demand thorough cross-layer validation.

Poem

🐰 Profiles dance in profiles' homes, each workspace gets its own,
Facts bloom in JSONL stone, approval gates are shown,
Four lanes hum in harmony, while Paperclip takes the throne,
Desktop dreams now flutter free—Hermes helps you lead!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
desktop-install-pack/templates/env.example-6-6 (1)

6-6: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

PAPERCLIP_DEFAULT_COMPANY=FMG hardcodes what appears to be a real company name.

In a public repository, shipping an example env file with a real company identifier as a default value is inadvertent information disclosure and will also confuse unrelated users of this template who copy it verbatim. Replace with a generic placeholder.

🔒 Proposed fix
-PAPERCLIP_DEFAULT_COMPANY=FMG
+PAPERCLIP_DEFAULT_COMPANY=YOUR_COMPANY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/templates/env.example` at line 6, The env example
currently hardcodes a real company via the PAPERCLIP_DEFAULT_COMPANY=FMG entry;
replace that value with a generic placeholder (e.g.
PAPERCLIP_DEFAULT_COMPANY=your_company_name or
PAPERCLIP_DEFAULT_COMPANY=REPLACE_WITH_COMPANY) so the template no longer
discloses a real company or causes confusion—update the
PAPERCLIP_DEFAULT_COMPANY line in the env.example to the placeholder and, if
present, adjust any documentation referencing that default to note it must be
set by the user.
sharenote-telegram-pack/templates/telegram-message-template.md-1-9 (1)

1-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Template is disconnected from create_sharenote_telegram_draft.py and is structurally incomplete.

create_sharenote_telegram_draft.py constructs the draft message inline using an f-string (lines 84–98) and never reads or renders this template file. As a result:

  1. Dead template — edits here have no effect on the script's output.
  2. Missing fields — the template omits the 대상: and 전송 상태: 미승인 / 미전송 lines that the script always writes.
  3. Name mismatch — the template uses {{note_path}} while the script variable is note.

Either wire the script to load-and-substitute this template (using string.Template or similar), or update the template to fully mirror the script's actual f-string output so it at least serves as accurate reference documentation.

📄 Template corrected to match script output (reference-doc approach)
 {{title}}
 
 {{summary}}
 
 공유 링크:
 {{share_url}}
 
 저장 위치:
-{{note_path}}
+{{note}}
+
+대상:
+{{target}}
+
+전송 상태: 미승인 / 미전송
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sharenote-telegram-pack/templates/telegram-message-template.md` around lines
1 - 9, The template file telegram-message-template.md is not used by
create_sharenote_telegram_draft.py (the script builds the message inline with an
f-string) and its contents are incomplete/mismatched; either load and render
this template from create_sharenote_telegram_draft.py (e.g., read
templates/telegram-message-template.md and substitute variables using
string.Template or Jinja2) and replace the current inline f-string in the
function that builds the draft (around lines 84–98), or update
telegram-message-template.md to exactly match the script’s f-string output (add
the missing "대상:" and "전송 상태: 미승인 / 미전송" lines and rename {{note_path}} to
{{note}}) so it accurately reflects the message format used by the function that
constructs the share note draft.
sharenote-telegram-pack/scripts/check_sharenote_env.py-8-17 (1)

8-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

json.JSONDecodeError not caught in discover_vaults().

errors="ignore" suppresses UTF-8 decoding errors only; it does not protect against malformed JSON. If obsidian.json contains invalid JSON (e.g., partially written during a crash), the script aborts with an unhandled traceback rather than gracefully returning an empty list.

🐛 Proposed fix
 def discover_vaults():
     if not APP_CONFIG.exists():
         return []
-    data = json.loads(APP_CONFIG.read_text(errors="ignore"))
+    try:
+        data = json.loads(APP_CONFIG.read_text(errors="ignore"))
+    except (json.JSONDecodeError, OSError):
+        return []
     out = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sharenote-telegram-pack/scripts/check_sharenote_env.py` around lines 8 - 17,
The discover_vaults() function calls json.loads(APP_CONFIG.read_text(...))
without handling malformed JSON; wrap the json.loads call in a try/except that
catches json.JSONDecodeError (and optionally ValueError) and returns [] on error
so the function fails gracefully instead of raising; keep the existing
read_text(errors="ignore") behavior and then proceed to iterate
data.get("vaults") only when data is a dict to avoid attribute errors.
desktop-install-pack/install-macos.sh-83-85 (1)

83-85: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fixed sleep 2 health-check is fragile — fails on slow or loaded machines.

A 2-second sleep before hitting /health will cause the installer to abort (exit 1) on any machine where the WebUI process takes longer to bind its port. There is no retry, no cleanup, and the failure message only prints the log path without indicating this is likely a timing issue.

💡 Proposed fix — poll with retries
-"$HOME/.hermes/bin/hermes-ceo-console" >/tmp/hermes-ceo-console.log 2>&1 &
-sleep 2
-curl -fsS "http://127.0.0.1:$PORT/health" || { echo "Health check failed; see /tmp/hermes-ceo-console.log"; exit 1; }
+"$HOME/.hermes/bin/hermes-ceo-console" >/tmp/hermes-ceo-console.log 2>&1 &
+echo "Waiting for WebUI to start..."
+for _i in $(seq 1 15); do
+  if curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
+    break
+  fi
+  if [ "$_i" -eq 15 ]; then
+    echo "Health check failed after 15s; see /tmp/hermes-ceo-console.log"
+    exit 1
+  fi
+  sleep 1
+done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/install-macos.sh` around lines 83 - 85, Replace the
brittle fixed sleep + single curl with a retrying poll loop that checks
http://127.0.0.1:$PORT/health until success or a configurable timeout;
specifically, after launching "$HOME/.hermes/bin/hermes-ceo-console" (logging to
/tmp/hermes-ceo-console.log) implement a loop that attempts curl -fsS to /health
every 0.5–1s up to N seconds, exiting 0 on success and on final failure printing
a clear error that includes the log path and a hint that it likely timed out,
then perform any needed cleanup/kill of the background process; reference the
launched command string, the $PORT variable, /tmp/hermes-ceo-console.log, and
the /health endpoint when making the changes.
sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py-11-21 (1)

11-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

json.JSONDecodeError in discover_vault() is unhandled.

errors="ignore" at line 16 only suppresses UTF-8 decoding problems; malformed JSON (e.g., partially-written obsidian.json) will propagate a json.JSONDecodeError that bypasses the SystemExit at line 21 and surfaces as a confusing raw traceback.

🐛 Proposed fix
     if APP_CONFIG.exists():
-        data = json.loads(APP_CONFIG.read_text(errors="ignore"))
+        try:
+            data = json.loads(APP_CONFIG.read_text(errors="ignore"))
+        except (json.JSONDecodeError, OSError):
+            raise SystemExit("Could not parse Obsidian config. Set OBSIDIAN_VAULT_PATH instead.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py` around
lines 11 - 21, discover_vault() currently calls
json.loads(APP_CONFIG.read_text(...)) without handling malformed JSON so a
json.JSONDecodeError can leak; wrap the json.loads(...) (or the whole
APP_CONFIG.read_text + json.loads sequence) in a try/except that catches
json.JSONDecodeError (and optionally ValueError) and treats it the same as "no
vault found" — e.g., warn or ignore the broken APP_CONFIG and proceed to raise
SystemExit("No Obsidian vault found. Set OBSIDIAN_VAULT_PATH first.") if no
valid vault path is discovered; reference the discover_vault function and
APP_CONFIG when making this change.
sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py-55-77 (1)

55-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Generated frontmatter diverges from publishing-note-template.md on two fields.

  1. The script writes created: (line 58) but the template declares created_at:. Any downstream parser or operator workflow that keys on created_at will not find the field.
  2. telegram_target appears only in the Markdown body (line 75) but is absent from the YAML frontmatter, while the template defines it there — automation that reads frontmatter for routing will miss the target.
💡 Proposed fix — align with template field names
     note_text = f"""---
 title: "{args.title}"
-created: "{created}"
+created_at: "{created}"
 status: draft
 share: ""
+telegram_target: "{args.target}"
 tags:
   - publishing
   - sharenote
 ---
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py` around
lines 55 - 77, The YAML frontmatter written into note_text uses "created" and
lacks a frontmatter "telegram_target", diverging from
publishing-note-template.md; update the multi-line string assigned to note_text
(inside the block guarded by if not note.exists()) to use created_at:
"{created}" instead of created: and add a telegram_target: "{args.target}" entry
in the frontmatter so downstream parsers read the same fields as the template.
desktop-install-pack/install-macos.sh-4-18 (1)

4-18: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

YES flag is defined but never read — --yes has no effect.

ShellCheck (SC2034) correctly flags this. The script has no interactive prompts and never branches on $YES, so passing --yes/-y does nothing. Either add a confirmation prompt that --yes bypasses, or remove the flag to avoid misleading callers.

💡 Minimal fix — remove the dead flag
-YES=0
 PORT=8788
 REPO_URL="https://github.com/reallygood83/hermes-for-web.git"
 INSTALL_DIR="$HOME/.hermes/webui/workspace/hermes-for-web"
 APP_NAME="Hermes CEO Console"

 while [[ $# -gt 0 ]]; do
   case "$1" in
-    --yes|-y) YES=1; shift ;;
     --port) PORT="${2:-8788}"; shift 2 ;;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/install-macos.sh` around lines 4 - 18, The YES variable
and the --yes|-y case branch are dead and misleading; remove the YES=0
declaration and the "--yes|-y) YES=1; shift ;;" case entry (and any references
if added elsewhere) so the script no longer accepts a no-op flag, or
alternatively implement a confirmation prompt using YES to skip it—prefer the
minimal fix of deleting the YES variable and that case branch in the
argument-parsing block (referencing YES and the "--yes|-y" case) to keep
behavior consistent.
api/streaming.py-97-101 (1)

97-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

get_profile_home() is called without validating the profile directory exists.

Unlike switch_profile(), which validates profile existence before calling get_profile_home(), streaming uses the session's stored profile name directly. If that profile was deleted after the session was created, HERMES_HOME is set to a non-existent path, silently breaking any agent tool that accesses state.db or config files under that directory for the duration of the stream. The previous get_active_hermes_home() always returned a valid directory.

🛡️ Proposed fix — fall back to default profile home if directory is missing
         try:
             from api.profiles import get_profile_home
-            _profile_home = str(get_profile_home(getattr(s, 'profile', None)))
+            _ph = get_profile_home(getattr(s, 'profile', None))
+            if not _ph.exists():
+                _ph = get_profile_home('default')
+            _profile_home = str(_ph)
         except ImportError:
             _profile_home = os.environ.get('HERMES_HOME', '')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/streaming.py` around lines 97 - 101, The code calls
get_profile_home(getattr(s, 'profile', None)) and assigns it to _profile_home
without verifying the directory exists, which can point HERMES_HOME to a deleted
profile; modify the block after importing get_profile_home to check that the
returned path is a valid directory (os.path.isdir), and if not, fall back to a
known-good home (e.g., call get_active_hermes_home() if available or use
os.environ.get('HERMES_HOME','') / default) so _profile_home always references
an existing directory; keep the ImportError fallback but ensure the existence
check and fallback occur whether get_profile_home returns None/invalid or
raises.
desktop-install-pack/electron-wrapper/package.json-19-19 (1)

19-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

preload.js listed in files but the file does not exist

electron-builder will attempt to locate preload.js during packaging. If the file is absent, the build may silently omit it or fail with a "does not exist" error, blocking the .dmg/.nsis build step. Either create a minimal preload.js stub (even an empty one) or remove it from the files array until a preload script is actually needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/electron-wrapper/package.json` at line 19,
package.json's "files" array references preload.js but that file is missing;
either add a minimal preload.js stub in the electron-wrapper package (e.g., an
empty file or a simple DOM-safe export) so electron-builder can include it, or
remove "preload.js" from the "files" array in package.json to stop packaging
from expecting it—update the package.json "files" entry accordingly and ensure
the preload.js file (if added) is placed alongside main.js and preload.js in the
package root so electron-builder can find it.
tests/test_fact_routes.py-44-49 (1)

44-49: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing FACTS_DIR isolation may leave side effects in the real facts store

Unlike the first test, test_memory_candidate_create_requires_statement does not monkeypatch routes.facts_store.FACTS_DIR. If _handle_memory_candidate_create touches the filesystem before it validates the missing statement field, this test will create artifacts under the real FACTS_DIR. Add tmp_path isolation for safety:

🛡️ Proposed fix
-def test_memory_candidate_create_requires_statement(monkeypatch):
+def test_memory_candidate_create_requires_statement(tmp_path, monkeypatch):
+    monkeypatch.setattr(routes.facts_store, 'FACTS_DIR', tmp_path)
     monkeypatch.setattr(routes, 'j', lambda handler, payload, status=200: (payload, status))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_fact_routes.py` around lines 44 - 49, The test
test_memory_candidate_create_requires_statement should isolate the facts storage
by monkeypatching routes.facts_store.FACTS_DIR to a temporary directory so the
call to routes._handle_memory_candidate_create(SimpleNamespace(), {'scope':
'global'}) cannot create real artifacts; update the test to accept a tmp_path
fixture and set monkeypatch.setattr(routes.facts_store, 'FACTS_DIR',
str(tmp_path)) (or equivalent) before invoking _handle_memory_candidate_create,
keeping the existing monkeypatches for routes.j and routes.bad.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/facts.py`:
- Around line 67-70: _write_jsonl currently writes directly to the target file
which can leave it truncated if the process crashes; change it to write
atomically by creating a temp file in the same directory (e.g., using
tempfile.NamedTemporaryFile or a .tmp Path in path.parent), write the JSONL
contents to that temp file, flush and fsync the file, then atomically replace
the target with os.replace(temp_path, path) (and optionally fsync the parent
directory) so readers (_read_jsonl) never see a partially-written file; keep
path.parent.mkdir as is so the temp file can be created.

In `@api/routes.py`:
- Around line 1420-1470: The three handlers _handle_memory_candidate_create,
_handle_memory_candidate_approve, and _handle_memory_candidate_reject are
implemented but never dispatched from handle_post(), so POST requests for
create/approve/reject return 404; update the request routing in handle_post() to
recognize the corresponding POST paths or action names used by static/facts.js
and call these helpers (e.g., call _handle_memory_candidate_create(handler,
body) for the create route, _handle_memory_candidate_approve(handler, body) for
approve, and _handle_memory_candidate_reject(handler, body) for reject), ensure
you extract the request body the same way handle_post() does and return their
responses directly so the inbox is writable.

In `@desktop-install-pack/electron-wrapper/main.js`:
- Around line 9-13: In startServer, the Windows spawn currently passes a
'--port' arg which server.py ignores; update the Windows branch so args do not
include '--port' (use ['server.py'] or empty as appropriate) and pass the port
via the spawn env (set HERMES_WEBUI_PORT = port while preserving process.env) so
server.py reads the correct port; adjust the spawn call that assigns to server
(spawn(cmd, args, {cwd: repo, shell: isWin, env: {...process.env,
HERMES_WEBUI_PORT: String(port)}, stdio: 'inherit' /* or 'ignore' for production
*/})) to ensure the server binds to the intended port.
- Line 22: Replace the fixed 1800ms delay after startServer() with a
health-check polling loop that waits for the server to be ready before calling
createWindow(): implement a helper (e.g., pollServerHealth or
waitForServerReady) that repeatedly requests the server's /health endpoint with
a short interval and timeout/max-retries, call startServer() then await
pollServerHealth() inside the app.whenReady() promise, and only call
createWindow() after the health check succeeds (on failure after max retries log
an error and show a retry/fatal UI instead of opening the window). Ensure the
code uses the existing startServer() and createWindow() symbols and removes the
setTimeout-based call.
- Line 23: The current app.on('window-all-closed') handler unconditionally calls
server.kill(), which kills the backend on macOS when windows close; change the
logic so server.kill() is only invoked when the app is actually quitting (i.e.,
keep the server running when process.platform === 'darwin' and only kill it in
an app.on('before-quit' or quit) handler), and add an app.on('activate') handler
that recreates the window (call the existing createWindow or equivalent) and
restarts/creates the server if it is not running so clicking the dock icon
restores the UI and backend; ensure you reference and update the handlers around
app.on('window-all-closed'), app.on('activate'), and the server lifecycle calls
(server.kill(), server start/creation function) accordingly.

In `@desktop-install-pack/install-windows.ps1`:
- Around line 12-13: The installer currently fails when only the Windows Python
launcher (py.exe) exists because the launcher generation code uses a hardcoded
python executable; update the prerequisite check and launcher creation to accept
the Windows Python launcher by reusing the discovered $PythonExe variable (which
may be "py.exe" or "python.exe") wherever the script builds the launcher (the
code that generates the launcher/entry point), so the script uses $PythonExe
instead of assuming "python.exe" and thus works for both launcher and full
Python installs.

In `@sharenote-telegram-pack/README.md`:
- Around line 8-22: Replace all personal and machine-specific data in the
README: remove or redact the absolute path "/Users/jakeshin/antigravity/..." and
the Obsidian app path if it reveals a username, replacing them with generic
placeholders like "<USER_HOME>/obsidian-vault" and "/Applications/Obsidian.app"
(or a note "path to Obsidian app"); remove or genericize all Telegram target
names such as "telegram:홈원", "telegram:Pax Team Group", and "telegram:조계종" and
instead show the target format (e.g. "telegram:<target-name> or
telegram:<group-name>") or a short example like "telegram:your-target"; ensure
any other occurrences of "jakeshin" or other personal identifiers are replaced
with "<USERNAME>" and that credentials/redacted entries remain non-identifying.

In `@static/boot.js`:
- Around line 616-618: The current logic returns early when no issue key is
found (const match = text.match(...); if(!match) return null;), which leaves
runs stuck; instead ensure finalizePaperclipWorkflowRun() is always invoked for
comment-only or no-key cases before returning. Modify the flow around the
match/identifier extraction so that when match is falsy you call
finalizePaperclipWorkflowRun() (or delegate to the same finish path used when an
identifier exists) and then return; keep using the existing symbols (match,
identifier) but guard access to identifier only when match is truthy.
- Around line 355-373: The function detectPaperclipApprovalScope treats
patterns.generic matches as full approval, which is too permissive; update the
logic in detectPaperclipApprovalScope so that patterns.generic does NOT set
scope='full' — instead either set a lower-privilege scope (e.g.,
scope='comment') or leave scope unchanged (keep 'none') unless an explicit full
pattern in patterns.full matched; specifically change the last branch that
checks patterns.generic (currently "else
if(patterns.generic.some(rx=>rx.test(text)) && scope==='none') scope='full'") to
assign a non-full scope (or no assignment), ensuring only patterns.full can set
scope='full' and preserve the existing precedence rules in that function.
- Around line 541-550: The handler currently calls createPaperclipResultArtifact
and recordPaperclipWorkflowRun even when S.busy is true, causing orphan
artifacts; before awaiting createPaperclipResultArtifact or recording history,
check S.busy and return/abort if true (or wrap the artifact creation + record
steps inside a conditional that only runs when !S.busy). Specifically, guard
around the artifactPath creation and the call to recordPaperclipWorkflowRun
(references: S.busy, createPaperclipResultArtifact, recordPaperclipWorkflowRun,
buildWorkflowPrompt, artifactPath) so no result file or console history is
created while a workflow is already in-flight.

In `@static/multiagent.js`:
- Around line 180-191: routePromptToLanes currently awaits sendLaneText inside a
for loop which causes requests to each lane to run serially; instead collect
sendLaneText promises for each valid lane (use getLane and resolveTargetLanes to
filter/identify lanes) and then await them concurrently with Promise.all so all
targeted lanes are dispatched in parallel; keep clearing the input value before
awaiting and preserve existing continue/skip behavior for missing lanes.
- Around line 340-345: The 'error' event handler on src currently just clears
lane.busy, lane.streamId and re-renders, leaving an empty assistant bubble;
update the handler in the src.addEventListener('error', ...) block to capture
the ErrorEvent (or its message), set a visible error field on the lane (e.g.
lane.apperror or lane.errorMessage) with a user-friendly message plus the native
error detail, clear lane.streamId and lane.busy, call renderMultiAgentShell(),
and then resolve(); ensure you reference the existing symbols
(src.addEventListener('error', ...), lane.streamId, lane.busy,
renderMultiAgentShell(), resolve()) so the UI shows a failure message instead of
a blank assistant bubble.

---

Minor comments:
In `@api/streaming.py`:
- Around line 97-101: The code calls get_profile_home(getattr(s, 'profile',
None)) and assigns it to _profile_home without verifying the directory exists,
which can point HERMES_HOME to a deleted profile; modify the block after
importing get_profile_home to check that the returned path is a valid directory
(os.path.isdir), and if not, fall back to a known-good home (e.g., call
get_active_hermes_home() if available or use os.environ.get('HERMES_HOME','') /
default) so _profile_home always references an existing directory; keep the
ImportError fallback but ensure the existence check and fallback occur whether
get_profile_home returns None/invalid or raises.

In `@desktop-install-pack/electron-wrapper/package.json`:
- Line 19: package.json's "files" array references preload.js but that file is
missing; either add a minimal preload.js stub in the electron-wrapper package
(e.g., an empty file or a simple DOM-safe export) so electron-builder can
include it, or remove "preload.js" from the "files" array in package.json to
stop packaging from expecting it—update the package.json "files" entry
accordingly and ensure the preload.js file (if added) is placed alongside
main.js and preload.js in the package root so electron-builder can find it.

In `@desktop-install-pack/install-macos.sh`:
- Around line 83-85: Replace the brittle fixed sleep + single curl with a
retrying poll loop that checks http://127.0.0.1:$PORT/health until success or a
configurable timeout; specifically, after launching
"$HOME/.hermes/bin/hermes-ceo-console" (logging to /tmp/hermes-ceo-console.log)
implement a loop that attempts curl -fsS to /health every 0.5–1s up to N
seconds, exiting 0 on success and on final failure printing a clear error that
includes the log path and a hint that it likely timed out, then perform any
needed cleanup/kill of the background process; reference the launched command
string, the $PORT variable, /tmp/hermes-ceo-console.log, and the /health
endpoint when making the changes.
- Around line 4-18: The YES variable and the --yes|-y case branch are dead and
misleading; remove the YES=0 declaration and the "--yes|-y) YES=1; shift ;;"
case entry (and any references if added elsewhere) so the script no longer
accepts a no-op flag, or alternatively implement a confirmation prompt using YES
to skip it—prefer the minimal fix of deleting the YES variable and that case
branch in the argument-parsing block (referencing YES and the "--yes|-y" case)
to keep behavior consistent.

In `@desktop-install-pack/templates/env.example`:
- Line 6: The env example currently hardcodes a real company via the
PAPERCLIP_DEFAULT_COMPANY=FMG entry; replace that value with a generic
placeholder (e.g. PAPERCLIP_DEFAULT_COMPANY=your_company_name or
PAPERCLIP_DEFAULT_COMPANY=REPLACE_WITH_COMPANY) so the template no longer
discloses a real company or causes confusion—update the
PAPERCLIP_DEFAULT_COMPANY line in the env.example to the placeholder and, if
present, adjust any documentation referencing that default to note it must be
set by the user.

In `@sharenote-telegram-pack/scripts/check_sharenote_env.py`:
- Around line 8-17: The discover_vaults() function calls
json.loads(APP_CONFIG.read_text(...)) without handling malformed JSON; wrap the
json.loads call in a try/except that catches json.JSONDecodeError (and
optionally ValueError) and returns [] on error so the function fails gracefully
instead of raising; keep the existing read_text(errors="ignore") behavior and
then proceed to iterate data.get("vaults") only when data is a dict to avoid
attribute errors.

In `@sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py`:
- Around line 11-21: discover_vault() currently calls
json.loads(APP_CONFIG.read_text(...)) without handling malformed JSON so a
json.JSONDecodeError can leak; wrap the json.loads(...) (or the whole
APP_CONFIG.read_text + json.loads sequence) in a try/except that catches
json.JSONDecodeError (and optionally ValueError) and treats it the same as "no
vault found" — e.g., warn or ignore the broken APP_CONFIG and proceed to raise
SystemExit("No Obsidian vault found. Set OBSIDIAN_VAULT_PATH first.") if no
valid vault path is discovered; reference the discover_vault function and
APP_CONFIG when making this change.
- Around line 55-77: The YAML frontmatter written into note_text uses "created"
and lacks a frontmatter "telegram_target", diverging from
publishing-note-template.md; update the multi-line string assigned to note_text
(inside the block guarded by if not note.exists()) to use created_at:
"{created}" instead of created: and add a telegram_target: "{args.target}" entry
in the frontmatter so downstream parsers read the same fields as the template.

In `@sharenote-telegram-pack/templates/telegram-message-template.md`:
- Around line 1-9: The template file telegram-message-template.md is not used by
create_sharenote_telegram_draft.py (the script builds the message inline with an
f-string) and its contents are incomplete/mismatched; either load and render
this template from create_sharenote_telegram_draft.py (e.g., read
templates/telegram-message-template.md and substitute variables using
string.Template or Jinja2) and replace the current inline f-string in the
function that builds the draft (around lines 84–98), or update
telegram-message-template.md to exactly match the script’s f-string output (add
the missing "대상:" and "전송 상태: 미승인 / 미전송" lines and rename {{note_path}} to
{{note}}) so it accurately reflects the message format used by the function that
constructs the share note draft.

In `@tests/test_fact_routes.py`:
- Around line 44-49: The test test_memory_candidate_create_requires_statement
should isolate the facts storage by monkeypatching routes.facts_store.FACTS_DIR
to a temporary directory so the call to
routes._handle_memory_candidate_create(SimpleNamespace(), {'scope': 'global'})
cannot create real artifacts; update the test to accept a tmp_path fixture and
set monkeypatch.setattr(routes.facts_store, 'FACTS_DIR', str(tmp_path)) (or
equivalent) before invoking _handle_memory_candidate_create, keeping the
existing monkeypatches for routes.j and routes.bad.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e363bd6e-4647-4a6a-b42b-8587a375a2a2

📥 Commits

Reviewing files that changed from the base of the PR and between b46f64b and 54d4017.

⛔ Files ignored due to path filters (1)
  • desktop-install-pack.zip is excluded by !**/*.zip
📒 Files selected for processing (48)
  • CHANGELOG.md
  • api/config.py
  • api/facts.py
  • api/models.py
  • api/profiles.py
  • api/routes.py
  • api/streaming.py
  • autoresearch-pack/README.md
  • autoresearch-pack/checklists/deepening-checklist.md
  • autoresearch-pack/templates/research-output-template.md
  • autoresearch-pack/templates/research-question-template.md
  • autoresearch-pack/workflows/default-research-loop.md
  • desktop-install-pack/README.md
  • desktop-install-pack/electron-wrapper/main.js
  • desktop-install-pack/electron-wrapper/package.json
  • desktop-install-pack/electron-wrapper/preload.js
  • desktop-install-pack/install-macos.sh
  • desktop-install-pack/install-windows.ps1
  • desktop-install-pack/templates/env.example
  • docs/plans/2026-05-01-cross-platform-desktop-installer-plan.md
  • docs/plans/2026-05-01-memory-bank-paperclip-enhancement-plan.md
  • docs/research-packs.md
  • docs/setup-packs.md
  • memory-bank-adoption-pack/README.md
  • memory-bank-adoption-pack/design/fact-lifecycle.md
  • memory-bank-adoption-pack/design/paperclip-decision-intelligence.md
  • memory-bank-adoption-pack/design/scoped-facts.md
  • memory-bank-adoption-pack/templates/memory-candidate-review.md
  • memory-bank-adoption-pack/templates/paperclip-decision-intelligence-report.md
  • sharenote-telegram-pack/README.md
  • sharenote-telegram-pack/checklists/prepublish-checklist.md
  • sharenote-telegram-pack/scripts/check_sharenote_env.py
  • sharenote-telegram-pack/scripts/create_sharenote_telegram_draft.py
  • sharenote-telegram-pack/templates/publishing-note-template.md
  • sharenote-telegram-pack/templates/telegram-message-template.md
  • sharenote-telegram-pack/workflows/default-publishing-flow.md
  • static/boot.js
  • static/facts.js
  • static/index.html
  • static/messages.js
  • static/multiagent.js
  • static/panels.js
  • static/style.css
  • static/ui.js
  • tests/test_fact_routes.py
  • tests/test_facts_store.py
  • tests/test_multiagent_profile_api.py
  • tests/test_multiagent_sessions.py

Comment thread api/facts.py
Comment on lines +67 to +70
def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
text = ''.join(json.dumps(row, ensure_ascii=False, sort_keys=True) + '\n' for row in rows)
path.write_text(text, encoding='utf-8')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make JSONL rewrites atomic.

_write_jsonl() writes straight to the live file. If the process dies mid-write, the candidates file can be truncated, and _read_jsonl() will silently drop the broken rows on the next load. That makes approve/reject capable of corrupting the local store.

Suggested fix
 def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
     path.parent.mkdir(parents=True, exist_ok=True)
     text = ''.join(json.dumps(row, ensure_ascii=False, sort_keys=True) + '\n' for row in rows)
-    path.write_text(text, encoding='utf-8')
+    tmp = path.with_suffix(path.suffix + '.tmp')
+    tmp.write_text(text, encoding='utf-8')
+    tmp.replace(path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/facts.py` around lines 67 - 70, _write_jsonl currently writes directly to
the target file which can leave it truncated if the process crashes; change it
to write atomically by creating a temp file in the same directory (e.g., using
tempfile.NamedTemporaryFile or a .tmp Path in path.parent), write the JSONL
contents to that temp file, flush and fsync the file, then atomically replace
the target with os.replace(temp_path, path) (and optionally fsync the parent
directory) so readers (_read_jsonl) never see a partially-written file; keep
path.parent.mkdir as is so the temp file can be created.

Comment thread api/routes.py
Comment on lines +1420 to +1470
def _handle_memory_candidate_create(handler, body):
try:
require(body, 'statement')
candidate = facts_store.create_candidate(
category=body.get('category', 'knowledge'),
scope=body.get('scope', 'global'),
scope_ref=body.get('scope_ref', 'default'),
statement=body.get('statement', ''),
source_session_id=body.get('source_session_id'),
source_message_ids=body.get('source_message_ids') or [],
confidence=body.get('confidence'),
reason=body.get('reason'),
sensitivity=body.get('sensitivity', 'internal'),
recommended_action=body.get('recommended_action', 'approve'),
metadata=body.get('metadata') or {},
)
return j(handler, {'ok': True, 'candidate': candidate, 'summary': facts_store.store_summary()})
except ValueError as e:
return bad(handler, str(e), 400)
except Exception as e:
return bad(handler, str(e), 500)


def _handle_memory_candidate_approve(handler, body):
try:
candidate_id = body.get('candidate_id') or body.get('id')
if not candidate_id:
return bad(handler, 'candidate_id is required')
result = facts_store.approve_candidate(candidate_id, edited_statement=body.get('edited_statement'))
return j(handler, {'ok': True, **result, 'summary': facts_store.store_summary()})
except KeyError as e:
return bad(handler, str(e), 404)
except ValueError as e:
return bad(handler, str(e), 409)
except Exception as e:
return bad(handler, str(e), 500)


def _handle_memory_candidate_reject(handler, body):
try:
candidate_id = body.get('candidate_id') or body.get('id')
if not candidate_id:
return bad(handler, 'candidate_id is required')
candidate = facts_store.reject_candidate(candidate_id, reason=body.get('reason'))
return j(handler, {'ok': True, 'candidate': candidate, 'summary': facts_store.store_summary()})
except KeyError as e:
return bad(handler, str(e), 404)
except ValueError as e:
return bad(handler, str(e), 409)
except Exception as e:
return bad(handler, str(e), 500)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Wire these handlers into handle_post().

These helpers are never dispatched from handle_post(), so the new inbox stays read-only: every create/approve/reject request from static/facts.js will 404.

Suggested router wiring
+    if parsed.path == '/api/memory-candidates':
+        return _handle_memory_candidate_create(handler, body)
+
+    if parsed.path == '/api/memory-candidates/approve':
+        return _handle_memory_candidate_approve(handler, body)
+
+    if parsed.path == '/api/memory-candidates/reject':
+        return _handle_memory_candidate_reject(handler, body)
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 1439-1439: Do not catch blind exception: Exception

(BLE001)


[warning] 1454-1454: Do not catch blind exception: Exception

(BLE001)


[warning] 1469-1469: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/routes.py` around lines 1420 - 1470, The three handlers
_handle_memory_candidate_create, _handle_memory_candidate_approve, and
_handle_memory_candidate_reject are implemented but never dispatched from
handle_post(), so POST requests for create/approve/reject return 404; update the
request routing in handle_post() to recognize the corresponding POST paths or
action names used by static/facts.js and call these helpers (e.g., call
_handle_memory_candidate_create(handler, body) for the create route,
_handle_memory_candidate_approve(handler, body) for approve, and
_handle_memory_candidate_reject(handler, body) for reject), ensure you extract
the request body the same way handle_post() does and return their responses
directly so the inbox is writable.

Comment on lines +9 to +13
function startServer(){
const isWin = process.platform === 'win32';
const cmd = isWin ? 'python' : './start.sh';
const args = isWin ? ['server.py', '--port', port] : [port];
server = spawn(cmd, args, {cwd: repo, shell: isWin, stdio: 'ignore'});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Windows server spawned on wrong port — Electron will always fail to connect when HERMES_WEBUI_PORT is unset

server.py does not accept a --port CLI argument; it reads the port exclusively from the HERMES_WEBUI_PORT environment variable (see api/config.py:27-29). On Windows the --port flag is silently ignored, so server.py falls back to its own default of 8787, while main.js connects to 8788 (its own default). The window always loads a connection-refused error page on a cold install.

start.sh (non-Windows path) already receives the port as a positional argument and re-exports it as HERMES_WEBUI_PORT, so macOS/Linux are unaffected.

Fix: drop --port from Windows args and inject the port via the env option of spawn:

🐛 Proposed fix
 function startServer(){
   const isWin = process.platform === 'win32';
   const cmd = isWin ? 'python' : './start.sh';
-  const args = isWin ? ['server.py', '--port', port] : [port];
-  server = spawn(cmd, args, {cwd: repo, shell: isWin, stdio: 'ignore'});
+  const args = isWin ? ['server.py'] : [port];
+  server = spawn(cmd, args, {
+    cwd: repo, shell: isWin, stdio: 'inherit',
+    env: { ...process.env, HERMES_WEBUI_PORT: port },
+  });
 }

(stdio: 'inherit' is also suggested here so server errors surface during development; revert to 'ignore' for production if desired.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/electron-wrapper/main.js` around lines 9 - 13, In
startServer, the Windows spawn currently passes a '--port' arg which server.py
ignores; update the Windows branch so args do not include '--port' (use
['server.py'] or empty as appropriate) and pass the port via the spawn env (set
HERMES_WEBUI_PORT = port while preserving process.env) so server.py reads the
correct port; adjust the spawn call that assigns to server (spawn(cmd, args,
{cwd: repo, shell: isWin, env: {...process.env, HERMES_WEBUI_PORT:
String(port)}, stdio: 'inherit' /* or 'ignore' for production */})) to ensure
the server binds to the intended port.

win.webContents.setWindowOpenHandler(({url}) => { shell.openExternal(url); return {action:'deny'}; });
}

app.whenReady().then(()=>{ startServer(); setTimeout(createWindow, 1800); });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Race condition: 1800 ms is not enough on slow machines or cold starts

start.sh kills existing processes, may activate a venv, and runs server.py — any of which can exceed 1800 ms, leaving the window showing a connection error. A lightweight health-probe loop is far more reliable:

⏱️ Proposed fix — poll /health before opening the window
-app.whenReady().then(()=>{ startServer(); setTimeout(createWindow, 1800); });
+function waitForServer(cb, n = 0) {
+  require('http').get(`http://127.0.0.1:${port}/health`, cb)
+    .on('error', () =>
+      n < 30 ? setTimeout(() => waitForServer(cb, n + 1), 500) : cb()
+    );
+}
+app.whenReady().then(() => { startServer(); waitForServer(createWindow); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/electron-wrapper/main.js` at line 22, Replace the fixed
1800ms delay after startServer() with a health-check polling loop that waits for
the server to be ready before calling createWindow(): implement a helper (e.g.,
pollServerHealth or waitForServerReady) that repeatedly requests the server's
/health endpoint with a short interval and timeout/max-retries, call
startServer() then await pollServerHealth() inside the app.whenReady() promise,
and only call createWindow() after the health check succeeds (on failure after
max retries log an error and show a retry/fatal UI instead of opening the
window). Ensure the code uses the existing startServer() and createWindow()
symbols and removes the setTimeout-based call.

}

app.whenReady().then(()=>{ startServer(); setTimeout(createWindow, 1800); });
app.on('window-all-closed', ()=>{ if(server) server.kill(); if(process.platform !== 'darwin') app.quit(); });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

macOS: server is killed on window-close but there is no activate handler — the app becomes permanently unusable

When all windows are closed on macOS, server.kill() runs unconditionally (the darwin guard only protects app.quit()). The Electron process stays alive in the dock, but the backend is dead. Clicking the dock icon fires activate — which has no handler — so no window reopens and no server restarts. The only recovery is to force-quit and relaunch.

The standard Electron pattern keeps the server alive while the app is running on macOS, kills it only on actual quit, and recreates the window on activate:

🔒️ Proposed fix
-app.on('window-all-closed', ()=>{ if(server) server.kill(); if(process.platform !== 'darwin') app.quit(); });
+app.on('before-quit', () => { if (server) server.kill(); });
+app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
+app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop-install-pack/electron-wrapper/main.js` at line 23, The current
app.on('window-all-closed') handler unconditionally calls server.kill(), which
kills the backend on macOS when windows close; change the logic so server.kill()
is only invoked when the app is actually quitting (i.e., keep the server running
when process.platform === 'darwin' and only kill it in an app.on('before-quit'
or quit) handler), and add an app.on('activate') handler that recreates the
window (call the existing createWindow or equivalent) and restarts/creates the
server if it is not running so clicking the dock icon restores the UI and
backend; ensure you reference and update the handlers around
app.on('window-all-closed'), app.on('activate'), and the server lifecycle calls
(server.kill(), server start/creation function) accordingly.

Comment thread static/boot.js
Comment on lines +355 to +373
function detectPaperclipApprovalScope(){
const msgs=(S&&S.messages)||[];
const patterns={
full:[/full execution/, /전체 반영/, /전부 반영/, /이 안으로 반영해/, /승인, 이 안으로 반영해/],
issue:[/issue create/, /이슈 생성 승인/, /이슈 만들어/, /issue 생성/],
comment:[/comment only/, /코멘트만/, /comment 승인/, /comment only 승인/],
generic:[/(^|\s)승인(,|\s|$)/, /반영해/, /실행해/],
};
let scope='none';
for(const m of msgs){
if(!m || m.role!=='user') continue;
const text=String(m.content||'').trim();
if(!text) continue;
if([/좋아/,/괜찮네/,/그 방향으로 보자/,/맞는 듯/,/오케이/].some(rx=>rx.test(text))) continue;
if(patterns.full.some(rx=>rx.test(text))) scope='full';
else if(patterns.issue.some(rx=>rx.test(text)) && scope!=='full') scope='issue';
else if(patterns.comment.some(rx=>rx.test(text)) && !['full','issue'].includes(scope)) scope='comment';
else if(patterns.generic.some(rx=>rx.test(text)) && scope==='none') scope='full';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not treat generic wording as full execution approval.

Bare phrases like 승인, 반영해, or 실행해 currently unlock full scope. That breaks the “명시적 실행승인” guarantee and can enable the most privileged Paperclip actions off casual chat wording.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/boot.js` around lines 355 - 373, The function
detectPaperclipApprovalScope treats patterns.generic matches as full approval,
which is too permissive; update the logic in detectPaperclipApprovalScope so
that patterns.generic does NOT set scope='full' — instead either set a
lower-privilege scope (e.g., scope='comment') or leave scope unchanged (keep
'none') unless an explicit full pattern in patterns.full matched; specifically
change the last branch that checks patterns.generic (currently "else
if(patterns.generic.some(rx=>rx.test(text)) && scope==='none') scope='full'") to
assign a non-full scope (or no assignment), ensuring only patterns.full can set
scope='full' and preserve the existing precedence rules in that function.

Comment thread static/boot.js
Comment on lines +541 to +550
const artifactPath=gatedKinds.includes(kind) ? await createPaperclipResultArtifact(kind) : null;
let text=buildWorkflowPrompt(kind);
if(artifactPath){
const scopeLabelMap={
'reflect-paperclip-comment':'comment',
'reflect-paperclip-issue':'issue',
'reflect-paperclip-full':'full',
};
recordPaperclipWorkflowRun(scopeLabelMap[kind]||kind, artifactPath);
text += ` 반영 결과는 워크스페이스의 ${artifactPath} 파일에도 정리해줘. 최소한 summary, approval scope, generated/updated identifiers, final status 를 파일에 남겨줘.`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check S.busy before creating result artifacts/history.

Right now a click during an in-flight request still creates the Paperclip result file and records a running workflow before the busy guard returns. That leaves orphan artifacts and stale console history even though nothing actually ran.

Suggested fix
-    const artifactPath=gatedKinds.includes(kind) ? await createPaperclipResultArtifact(kind) : null;
-    let text=buildWorkflowPrompt(kind);
+    if(S.busy){showToast('현재 작업이 끝난 뒤 다시 시도해 주세요');return;}
+    const artifactPath=gatedKinds.includes(kind) ? await createPaperclipResultArtifact(kind) : null;
+    let text=buildWorkflowPrompt(kind);
@@
-    if(S.busy){showToast('현재 작업이 끝난 뒤 다시 시도해 주세요');return;}
     $('msg').value=text;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/boot.js` around lines 541 - 550, The handler currently calls
createPaperclipResultArtifact and recordPaperclipWorkflowRun even when S.busy is
true, causing orphan artifacts; before awaiting createPaperclipResultArtifact or
recording history, check S.busy and return/abort if true (or wrap the artifact
creation + record steps inside a conditional that only runs when !S.busy).
Specifically, guard around the artifactPath creation and the call to
recordPaperclipWorkflowRun (references: S.busy, createPaperclipResultArtifact,
recordPaperclipWorkflowRun, buildWorkflowPrompt, artifactPath) so no result file
or console history is created while a workflow is already in-flight.

Comment thread static/boot.js
Comment on lines +616 to +618
const match=text.match(/\b([A-Z]{2,10}-\d+)\b/);
if(!match) return null;
const identifier=match[1];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Finalize runs even when no issue key is present.

If the assistant output does not contain an ABC-123 token, this returns early and never calls finalizePaperclipWorkflowRun(). Comment-only flows are especially likely to get stuck in running forever.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/boot.js` around lines 616 - 618, The current logic returns early when
no issue key is found (const match = text.match(...); if(!match) return null;),
which leaves runs stuck; instead ensure finalizePaperclipWorkflowRun() is always
invoked for comment-only or no-key cases before returning. Modify the flow
around the match/identifier extraction so that when match is falsy you call
finalizePaperclipWorkflowRun() (or delegate to the same finish path used when an
identifier exists) and then return; keep using the existing symbols (match,
identifier) but guard access to identifier only when match is truthy.

Comment thread static/multiagent.js
Comment on lines +180 to +191
async function routePromptToLanes(){
ensureMultiAgentState();
const input=$('multiAgentPrompt');
const text=(input&&input.value||'').trim();
if(!text) return;
const targetLaneIds=resolveTargetLanes(text);
if(input) input.value='';
for(const laneId of targetLaneIds){
const lane=getLane(laneId);
if(!lane) continue;
await sendLaneText(lane, text);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dispatch routed lanes concurrently.

This loop waits for each lane stream to finish before starting the next one. A prompt that targets Hela + Plan + Dev therefore runs strictly serially, which defeats the new multi-agent workspace behavior.

Suggested fix
-    for(const laneId of targetLaneIds){
-      const lane=getLane(laneId);
-      if(!lane) continue;
-      await sendLaneText(lane, text);
-    }
+    await Promise.all(targetLaneIds.map(async laneId=>{
+      const lane=getLane(laneId);
+      if(!lane) return;
+      await sendLaneText(lane, text);
+    }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function routePromptToLanes(){
ensureMultiAgentState();
const input=$('multiAgentPrompt');
const text=(input&&input.value||'').trim();
if(!text) return;
const targetLaneIds=resolveTargetLanes(text);
if(input) input.value='';
for(const laneId of targetLaneIds){
const lane=getLane(laneId);
if(!lane) continue;
await sendLaneText(lane, text);
}
async function routePromptToLanes(){
ensureMultiAgentState();
const input=$('multiAgentPrompt');
const text=(input&&input.value||'').trim();
if(!text) return;
const targetLaneIds=resolveTargetLanes(text);
if(input) input.value='';
await Promise.all(targetLaneIds.map(async laneId=>{
const lane=getLane(laneId);
if(!lane) return;
await sendLaneText(lane, text);
}));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/multiagent.js` around lines 180 - 191, routePromptToLanes currently
awaits sendLaneText inside a for loop which causes requests to each lane to run
serially; instead collect sendLaneText promises for each valid lane (use getLane
and resolveTargetLanes to filter/identify lanes) and then await them
concurrently with Promise.all so all targeted lanes are dispatched in parallel;
keep clearing the input value before awaiting and preserve existing
continue/skip behavior for missing lanes.

Comment thread static/multiagent.js
Comment on lines +340 to +345
src.addEventListener('error',()=>{
src.close();
lane.streamId=null;
lane.busy=false;
renderMultiAgentShell();
resolve();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Surface SSE failures inside the lane.

The generic error handler only clears busy and re-renders. If the stream handshake fails or drops before apperror, the lane is left with an empty assistant bubble and no visible failure message.

Suggested fix
       src.addEventListener('error',()=>{
         src.close();
+        lane.messages[lane.messages.length-1]={
+          role:'assistant',
+          content:'**Error:** stream connection failed'
+        };
         lane.streamId=null;
         lane.busy=false;
         renderMultiAgentShell();
+        showToast(`Lane ${lane.label}: stream connection failed`);
         resolve();
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/multiagent.js` around lines 340 - 345, The 'error' event handler on
src currently just clears lane.busy, lane.streamId and re-renders, leaving an
empty assistant bubble; update the handler in the src.addEventListener('error',
...) block to capture the ErrorEvent (or its message), set a visible error field
on the lane (e.g. lane.apperror or lane.errorMessage) with a user-friendly
message plus the native error detail, clear lane.streamId and lane.busy, call
renderMultiAgentShell(), and then resolve(); ensure you reference the existing
symbols (src.addEventListener('error', ...), lane.streamId, lane.busy,
renderMultiAgentShell(), resolve()) so the UI shows a failure message instead of
a blank assistant bubble.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant