remove tools/ — now at repo root

This commit is contained in:
Levi Neely 2026-07-19 12:37:15 +02:00
parent f9a2e3249d
commit ebc82f338b
29 changed files with 0 additions and 1760 deletions

View File

View File

@ -1,22 +0,0 @@
"""Adapter registry: detect language from file path, return appropriate adapter."""
import os
from .gopls import Gopls
from .clangd import Clangd
from .intelephense import Intelephense
_ADAPTERS = [Gopls, Clangd, Intelephense]
_EXT_MAP: dict[str, type] = {}
for _a in _ADAPTERS:
for _ext in _a.extensions:
_EXT_MAP[_ext] = _a
def detect(filepath: str):
"""Return an adapter class for the given file, or None."""
ext = os.path.splitext(filepath)[1]
cls = _EXT_MAP.get(ext)
if cls:
return cls()
return None

View File

@ -1,19 +0,0 @@
"""clangd adapter."""
from ..workspace import find_root
class Clangd:
name = "clangd"
extensions = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx"]
root_markers = ["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]
cmd = ["clangd", "--background-index"]
def initialization_options(self):
return {}
def workspace_root(self, filepath: str, fallback: str | None = None) -> str:
return find_root(filepath, self.root_markers, fallback)
def prepare_open(self, filepath: str) -> dict | None:
return None

View File

@ -1,24 +0,0 @@
"""gopls adapter."""
from ..workspace import find_root
class Gopls:
name = "gopls"
extensions = [".go"]
root_markers = ["go.work", "go.mod"]
cmd = ["gopls", "serve"]
def initialization_options(self):
return {
"usePlaceholders": True,
"staticcheck": True,
"gofumpt": True,
}
def workspace_root(self, filepath: str, fallback: str | None = None) -> str:
return find_root(filepath, self.root_markers, fallback)
def prepare_open(self, filepath: str) -> dict | None:
"""Return extra didOpen params if needed. gopls needs didOpen before queries."""
return None # handled generically by the bridge

View File

@ -1,19 +0,0 @@
"""intelephense adapter (PHP)."""
from ..workspace import find_root
class Intelephense:
name = "intelephense"
extensions = [".php"]
root_markers = ["composer.json", "composer.lock", ".git"]
cmd = ["intelephense", "--stdio"]
def initialization_options(self):
return {}
def workspace_root(self, filepath: str, fallback: str | None = None) -> str:
return find_root(filepath, self.root_markers, fallback)
def prepare_open(self, filepath: str) -> dict | None:
return None

View File

@ -1,418 +0,0 @@
"""LSP bridge daemon: manages LSP server processes, accepts requests over a Unix socket."""
import asyncio
import json
import os
import signal
import sys
import time
from pathlib import Path
from urllib.parse import quote as url_quote
# Ensure _lib is importable when running as a standalone daemon
_tools_dir = str(Path(__file__).resolve().parent.parent.parent)
if _tools_dir not in sys.path:
sys.path.insert(0, _tools_dir)
SOCK_PATH = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "ollie-lsp.sock")
PID_PATH = SOCK_PATH + ".pid"
IDLE_TIMEOUT = 300 # 5 minutes
def file_uri(path: str) -> str:
return "file://" + os.path.abspath(path)
def uri_to_path(uri: str) -> str:
if uri.startswith("file://"):
return uri[7:]
return uri
class LspServer:
"""A running LSP server process."""
def __init__(self, adapter, root: str):
self.adapter = adapter
self.root = root
self.proc: asyncio.subprocess.Process | None = None
self.req_id = 0
self.pending: dict[int, asyncio.Future] = {}
self.initialized = False
self.open_files: set[str] = set()
self._read_task: asyncio.Task | None = None
self._buf = b""
async def start(self):
self.proc = await asyncio.create_subprocess_exec(
*self.adapter.cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
self._read_task = asyncio.create_task(self._read_loop())
await self._initialize()
async def stop(self):
if self.proc and self.proc.returncode is None:
await self._request("shutdown", {})
self._notify("exit", None)
try:
await asyncio.wait_for(self.proc.wait(), timeout=5)
except asyncio.TimeoutError:
self.proc.kill()
if self._read_task:
self._read_task.cancel()
async def _initialize(self):
result = await self._request("initialize", {
"processId": os.getpid(),
"rootUri": file_uri(self.root),
"rootPath": self.root,
"capabilities": {
"textDocument": {
"definition": {"linkSupport": True},
"references": {},
"hover": {"contentFormat": ["plaintext", "markdown"]},
"rename": {"prepareSupport": True},
"documentSymbol": {"hierarchicalDocumentSymbolSupport": True},
"completion": {"completionItem": {"snippetSupport": False}},
"publishDiagnostics": {},
},
"workspace": {
"workspaceFolders": True,
"symbol": {},
},
},
"initializationOptions": self.adapter.initialization_options(),
"workspaceFolders": [{"uri": file_uri(self.root), "name": os.path.basename(self.root)}],
})
self._notify("initialized", {})
self.initialized = True
return result
async def ensure_open(self, filepath: str):
if filepath in self.open_files:
return
try:
with open(filepath, "r") as f:
text = f.read()
except (OSError, UnicodeDecodeError):
text = ""
ext = os.path.splitext(filepath)[1]
lang_map = {
".go": "go", ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp",
".cxx": "cpp", ".hpp": "cpp", ".hxx": "cpp", ".php": "php",
}
self._notify("textDocument/didOpen", {
"textDocument": {
"uri": file_uri(filepath),
"languageId": lang_map.get(ext, "plaintext"),
"version": 1,
"text": text,
}
})
self.open_files.add(filepath)
# Give the server a moment to index the file
await asyncio.sleep(0.1)
async def definition(self, filepath: str, line: int, col: int):
await self.ensure_open(filepath)
return await self._request("textDocument/definition", {
"textDocument": {"uri": file_uri(filepath)},
"position": {"line": line, "character": col},
})
async def references(self, filepath: str, line: int, col: int):
await self.ensure_open(filepath)
return await self._request("textDocument/references", {
"textDocument": {"uri": file_uri(filepath)},
"position": {"line": line, "character": col},
"context": {"includeDeclaration": True},
})
async def hover(self, filepath: str, line: int, col: int):
await self.ensure_open(filepath)
return await self._request("textDocument/hover", {
"textDocument": {"uri": file_uri(filepath)},
"position": {"line": line, "character": col},
})
async def completion(self, filepath: str, line: int, col: int):
await self.ensure_open(filepath)
return await self._request("textDocument/completion", {
"textDocument": {"uri": file_uri(filepath)},
"position": {"line": line, "character": col},
})
async def rename(self, filepath: str, line: int, col: int, new_name: str):
await self.ensure_open(filepath)
return await self._request("textDocument/rename", {
"textDocument": {"uri": file_uri(filepath)},
"position": {"line": line, "character": col},
"newName": new_name,
})
async def document_symbols(self, filepath: str):
await self.ensure_open(filepath)
return await self._request("textDocument/documentSymbol", {
"textDocument": {"uri": file_uri(filepath)},
})
async def workspace_symbols(self, query: str):
return await self._request("workspace/symbol", {"query": query})
async def diagnostics(self, filepath: str):
await self.ensure_open(filepath)
# Most servers push diagnostics via notification; we wait briefly then return cached
await asyncio.sleep(1.0)
return self._cached_diagnostics.get(file_uri(filepath), [])
_cached_diagnostics: dict[str, list] = {}
def _notify(self, method: str, params):
msg = {"jsonrpc": "2.0", "method": method}
if params is not None:
msg["params"] = params
self._send(msg)
async def _request(self, method: str, params: dict, timeout: float = 30.0) -> dict | list | None:
self.req_id += 1
rid = self.req_id
msg = {"jsonrpc": "2.0", "id": rid, "method": method, "params": params}
fut = asyncio.get_event_loop().create_future()
self.pending[rid] = fut
self._send(msg)
try:
return await asyncio.wait_for(fut, timeout=timeout)
except asyncio.TimeoutError:
self.pending.pop(rid, None)
raise
def _send(self, msg: dict):
body = json.dumps(msg).encode()
header = f"Content-Length: {len(body)}\r\n\r\n".encode()
if self.proc and self.proc.stdin:
self.proc.stdin.write(header + body)
async def _read_loop(self):
try:
while self.proc and self.proc.returncode is None:
# Read header
header_line = await self.proc.stdout.readline()
if not header_line:
break
content_length = 0
while header_line and header_line.strip():
if header_line.startswith(b"Content-Length:"):
content_length = int(header_line.split(b":")[1].strip())
header_line = await self.proc.stdout.readline()
if content_length == 0:
continue
body = await self.proc.stdout.readexactly(content_length)
msg = json.loads(body)
self._handle_message(msg)
except (asyncio.CancelledError, asyncio.IncompleteReadError):
pass
def _handle_message(self, msg: dict):
if "id" in msg and "method" not in msg:
# Response
rid = msg["id"]
fut = self.pending.pop(rid, None)
if fut and not fut.done():
if "error" in msg:
fut.set_exception(RuntimeError(msg["error"].get("message", str(msg["error"]))))
else:
fut.set_result(msg.get("result"))
elif "method" in msg and "id" not in msg:
# Notification
if msg["method"] == "textDocument/publishDiagnostics":
params = msg.get("params", {})
self._cached_diagnostics[params.get("uri", "")] = params.get("diagnostics", [])
class Bridge:
"""The bridge daemon: manages LSP servers, handles tool requests."""
def __init__(self):
self.servers: dict[str, LspServer] = {} # key: "adapter_name:root"
self.last_activity = time.time()
async def handle_request(self, data: dict) -> dict:
self.last_activity = time.time()
method = data.get("method")
filepath = data.get("file")
if not filepath:
return {"error": "missing 'file' parameter"}
filepath = os.path.abspath(filepath)
from _lib.lsp.adapters import detect
adapter = detect(filepath)
if not adapter:
return {"error": f"no adapter for file: {filepath}"}
fallback = data.get("cwd")
root = adapter.workspace_root(filepath, fallback)
server = await self._get_server(adapter, root)
try:
if method == "definition":
result = await server.definition(filepath, data["line"], data["col"])
elif method == "references":
result = await server.references(filepath, data["line"], data["col"])
elif method == "hover":
result = await server.hover(filepath, data["line"], data["col"])
elif method == "completion":
result = await server.completion(filepath, data["line"], data["col"])
elif method == "rename":
result = await server.rename(filepath, data["line"], data["col"], data["new_name"])
if result:
applied = apply_workspace_edit(result)
return {"result": applied}
return {"result": None}
elif method == "symbols":
if data.get("workspace"):
result = await server.workspace_symbols(data.get("query", ""))
else:
result = await server.document_symbols(filepath)
elif method == "diagnostics":
result = await server.diagnostics(filepath)
else:
return {"error": f"unknown method: {method}"}
return {"result": result}
except Exception as e:
return {"error": str(e)}
async def _get_server(self, adapter, root: str) -> LspServer:
key = f"{adapter.name}:{root}"
if key not in self.servers:
server = LspServer(adapter, root)
await server.start()
self.servers[key] = server
return self.servers[key]
async def shutdown(self):
for server in self.servers.values():
await server.stop()
self.servers.clear()
def apply_workspace_edit(edit: dict) -> dict:
"""Apply a WorkspaceEdit to disk. Returns summary of changes."""
changes = {}
# Handle documentChanges (versioned) or changes (simple)
if "documentChanges" in edit:
for doc_change in edit["documentChanges"]:
if doc_change.get("kind") in ("create", "rename", "delete"):
continue # skip resource operations for now
uri = doc_change["textDocument"]["uri"]
edits = doc_change.get("edits", [])
path = uri_to_path(uri)
changes.setdefault(path, []).extend(edits)
elif "changes" in edit:
for uri, edits in edit["changes"].items():
path = uri_to_path(uri)
changes[path] = edits
summary = {}
for path, edits in changes.items():
try:
with open(path, "r") as f:
lines = f.readlines()
except OSError:
continue
# Apply edits in reverse order (bottom-up) to preserve positions
sorted_edits = sorted(edits, key=lambda e: (
e["range"]["start"]["line"],
e["range"]["start"]["character"]
), reverse=True)
for e in sorted_edits:
start = e["range"]["start"]
end = e["range"]["end"]
new_text = e["newText"]
# Convert line/char positions to string manipulation
start_line, start_char = start["line"], start["character"]
end_line, end_char = end["line"], end["character"]
# Build the new content
before = "".join(lines[:start_line]) + lines[start_line][:start_char] if start_line < len(lines) else "".join(lines)
after = lines[end_line][end_char:] + "".join(lines[end_line + 1:]) if end_line < len(lines) else ""
content = before + new_text + after
lines = content.splitlines(True)
with open(path, "w") as f:
f.writelines(lines)
summary[path] = len(sorted_edits)
return {"files_changed": summary, "total_edits": sum(summary.values())}
async def run_bridge():
bridge = Bridge()
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
try:
data = await asyncio.wait_for(reader.read(1024 * 1024), timeout=5.0)
if not data:
return
request = json.loads(data)
if request.get("method") == "__shutdown":
response = {"result": "ok"}
writer.write(json.dumps(response).encode())
await writer.drain()
writer.close()
raise SystemExit(0)
response = await bridge.handle_request(request)
writer.write(json.dumps(response).encode())
await writer.drain()
except (json.JSONDecodeError, asyncio.TimeoutError) as e:
writer.write(json.dumps({"error": str(e)}).encode())
await writer.drain()
finally:
writer.close()
# Clean up stale socket
if os.path.exists(SOCK_PATH):
os.unlink(SOCK_PATH)
server = await asyncio.start_unix_server(handle_client, path=SOCK_PATH)
os.chmod(SOCK_PATH, 0o600)
# Write PID
with open(PID_PATH, "w") as f:
f.write(str(os.getpid()))
# Idle timeout loop
async def idle_watchdog():
while True:
await asyncio.sleep(30)
if time.time() - bridge.last_activity > IDLE_TIMEOUT:
await bridge.shutdown()
server.close()
return
watchdog = asyncio.create_task(idle_watchdog())
try:
await server.serve_forever()
except (asyncio.CancelledError, SystemExit):
pass
finally:
watchdog.cancel()
await bridge.shutdown()
if os.path.exists(SOCK_PATH):
os.unlink(SOCK_PATH)
if os.path.exists(PID_PATH):
os.unlink(PID_PATH)
def main():
asyncio.run(run_bridge())
if __name__ == "__main__":
main()

View File

@ -1,77 +0,0 @@
"""Client library for LSP tool scripts. Connects to the bridge daemon, starting it if needed."""
import json
import os
import socket
import subprocess
import sys
import time
SOCK_PATH = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "ollie-lsp.sock")
PID_PATH = SOCK_PATH + ".pid"
def _bridge_running() -> bool:
if not os.path.exists(PID_PATH):
return False
try:
pid = int(open(PID_PATH).read().strip())
os.kill(pid, 0)
return True
except (OSError, ValueError):
return False
def _start_bridge():
"""Start the bridge daemon as a background process."""
lib_dir = os.path.dirname(os.path.abspath(__file__))
bridge_script = os.path.join(lib_dir, "bridge.py")
subprocess.Popen(
[sys.executable, bridge_script],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
# Wait for socket to appear
for _ in range(50):
if os.path.exists(SOCK_PATH):
return
time.sleep(0.1)
print("error: bridge daemon failed to start", file=sys.stderr)
sys.exit(1)
def request(method: str, **kwargs) -> dict:
"""Send a request to the bridge and return the response."""
if not _bridge_running():
_start_bridge()
payload = {"method": method, **kwargs}
data = json.dumps(payload).encode()
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(SOCK_PATH)
sock.sendall(data)
sock.shutdown(socket.SHUT_WR)
chunks = []
while True:
chunk = sock.recv(4096)
if not chunk:
break
chunks.append(chunk)
response = json.loads(b"".join(chunks))
except ConnectionRefusedError:
# Stale socket, restart
if os.path.exists(SOCK_PATH):
os.unlink(SOCK_PATH)
_start_bridge()
return request(method, **kwargs)
finally:
sock.close()
if "error" in response:
print(f"error: {response['error']}", file=sys.stderr)
sys.exit(1)
return response.get("result")

View File

@ -1,20 +0,0 @@
"""Workspace root detection: walk up from a file to find language-specific markers."""
import os
from pathlib import Path
def find_root(filepath: str, markers: list[str], fallback: str | None = None) -> str:
"""Walk up from filepath looking for any marker file/dir. Return the directory containing it."""
p = Path(filepath).resolve()
if p.is_file():
p = p.parent
while True:
for marker in markers:
if (p / marker).exists():
return str(p)
parent = p.parent
if parent == p:
break
p = parent
return fallback or os.getcwd()

View File

@ -1,66 +0,0 @@
#!/usr/bin/env bash
# capabilities: web
# description: Capture browser window showing a specific URL: capture_browser.sh [output_path] [url_pattern]
set -e
# Verify running under landrun (test filesystem restriction)
OUTPUT="${1:-${TMPDIR:-/tmp}/browser_capture.png}"
URL_PATTERN="${2:-localhost}"
case "$(uname -s)" in
Darwin)
# macOS: use osascript + screencapture
osascript -e 'tell application "Google Chrome" to activate' 2>/dev/null
sleep 0.3
osascript -e "tell application \"Google Chrome\" to set index of (first window whose name contains \"$URL_PATTERN\") to 1" 2>/dev/null
sleep 0.3
screencapture -x "$OUTPUT"
;;
Linux)
if [ -z "$DISPLAY" ]; then
echo "ERROR: DISPLAY not set" >&2
exit 1
fi
# Linux: use brotab + wmctrl + import
TAB_INFO=$(brotab list | grep -i "$URL_PATTERN" | head -1)
if [ -z "$TAB_INFO" ]; then
echo "ERROR: No tab matching '$URL_PATTERN'" >&2
exit 1
fi
TAB_ID=$(echo "$TAB_INFO" | cut -f1)
TAB_TITLE=$(echo "$TAB_INFO" | cut -f2)
echo "Found tab: $TAB_ID ($TAB_TITLE)" >&2
brotab activate "$TAB_ID"
sleep 0.5
WIN_ID=$(wmctrl -l | grep -i "$TAB_TITLE" | head -1 | awk '{print $1}')
if [ -z "$WIN_ID" ]; then
WIN_ID=$(wmctrl -l | grep -iE "firefox|chrome|chromium" | head -1 | awk '{print $1}')
fi
if [ -z "$WIN_ID" ]; then
echo "ERROR: Could not find browser window" >&2
exit 1
fi
echo "Capturing window: $WIN_ID" >&2
wmctrl -i -a "$WIN_ID"
sleep 0.3
import -window "$WIN_ID" "$OUTPUT"
;;
*)
echo "ERROR: Unsupported OS: $(uname -s)" >&2
exit 1
;;
esac
if [ -f "$OUTPUT" ]; then
echo "$OUTPUT"
else
echo "ERROR: Failed to capture" >&2
exit 1
fi

View File

@ -1,31 +0,0 @@
#!/bin/bash
# ollie:parallel read
# capabilities: agent-kb
# description: View a denote document by identifier
# Usage: view_denote.sh --id <denote:identifier|identifier>
set -euo pipefail
DENOTE="${DENOTE_9MOUNT:-$HOME/mnt/denote}"
ID=""
while [[ $# -gt 0 ]]; do
case "$1" in
--id) ID="$2"; shift 2 ;;
*) echo "unknown argument: $1" >&2; exit 1 ;;
esac
done
if [ -z "$ID" ]; then
echo "usage: view_denote.sh --id <denote:identifier|identifier>" >&2
exit 1
fi
ID="${ID#denote:}"
if [ -d "$DENOTE/n/$ID" ]; then
cat "$DENOTE/n/$ID/body"
else
echo "Error: no denote document found for identifier $ID" >&2
exit 1
fi

View File

@ -1,132 +0,0 @@
#!/usr/bin/env python3
# description: Replace text in a file; old_string not found is the guard
# Args: <file_path> <old_string> <new_string> [replace_all=true|1|yes]
import sys
import os
import difflib
def format_diff(path, old, new):
old_lines = old.splitlines(keepends=True)
new_lines = new.splitlines(keepends=True)
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
if not diff:
return None
return f"```diff\n{diff}```"
def find_exact(content, find):
return find if find in content else None
def find_ws_normalized(content, find):
nf = ' '.join(find.split())
find_lines = find.split('\n')
n = len(find_lines)
lines = content.split('\n')
for i in range(len(lines) - n + 1):
block = '\n'.join(lines[i:i+n])
if ' '.join(block.split()) == nf:
return block
return None
def find_indent_flexible(content, find):
def deindent(text):
ls = text.split('\n')
min_indent = float('inf')
for l in ls:
stripped = l.lstrip(' \t')
if stripped:
min_indent = min(min_indent, len(l) - len(stripped))
if min_indent == float('inf'):
return text
return '\n'.join(l[min_indent:] if len(l) > min_indent else l.lstrip(' \t') for l in ls)
nf = deindent(find)
find_lines = find.split('\n')
n = len(find_lines)
lines = content.split('\n')
for i in range(len(lines) - n + 1):
block = '\n'.join(lines[i:i+n])
if deindent(block) == nf:
return block
return None
def find_trimmed_boundary(content, find):
trimmed = find.strip()
if trimmed == find:
return None
if trimmed in content:
return trimmed
find_lines = find.split('\n')
n = len(find_lines)
lines = content.split('\n')
for i in range(len(lines) - n + 1):
block = '\n'.join(lines[i:i+n])
if block.strip() == trimmed:
return block
return None
def robust_replace(content, old_string, new_string, replace_all):
match = (find_exact(content, old_string) or
find_ws_normalized(content, old_string) or
find_indent_flexible(content, old_string) or
find_trimmed_boundary(content, old_string))
if match is None:
return None, "old_string not found in file", False
if replace_all:
return content.replace(match, new_string), None, False
n = content.count(match)
if n > 1:
return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context", True
return content.replace(match, new_string, 1), None, False
if len(sys.argv) < 4:
print("usage: file_edit <file_path> <old_string> <new_string> [replace_all=true|1|yes]")
sys.exit(1)
file_path = sys.argv[1]
old_string = sys.argv[2]
new_string = sys.argv[3]
_ra = sys.argv[4].lower() if len(sys.argv) > 4 else ''
replace_all = _ra.split('=')[-1] in ('true', '1', 'yes')
if not os.path.isabs(file_path):
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: path must be absolute, got: {file_path}")
sys.exit(1)
if old_string == new_string:
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
print("error: old_string and new_string must be different")
sys.exit(1)
if not os.path.exists(file_path):
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: file not found: {file_path}")
sys.exit(1)
try:
with open(file_path, 'r', encoding='utf-8') as f:
original = f.read()
except UnicodeDecodeError:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: file is not valid UTF-8: {file_path}")
sys.exit(1)
new_content, err, retryable = robust_replace(original, old_string, new_string, replace_all)
if err:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE={'true' if retryable else 'false'}")
print(f"error: {err}")
sys.exit(1)
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
except OSError as e:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: {e}")
sys.exit(1)
diff = format_diff(file_path, original, new_content)
if diff is None:
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
print("(no changes)")
else:
print("STATUS=done STATE_CHANGED=true")
print(diff)

View File

@ -1,38 +0,0 @@
#!/usr/bin/env bash
# ollie:parallel read
# description: Find files by name/path pattern, sorted by modification time (newest first)
# Args: <pattern> [path]
pattern="$1"
search_path="${2:-$PWD}"
[ -z "$pattern" ] && echo "usage: file_glob <pattern> [path]" && exit 1
if [ ! -d "$search_path" ]; then
echo "STATUS=error"
echo "error: path is not a directory: $search_path"
exit 1
fi
# collect matches and sort by mtime descending
rg_out=$(rg --no-config --files -g "$pattern" "$search_path" 2>&1)
rg_exit=$?
if [ $rg_exit -gt 1 ]; then
echo "STATUS=error"
echo "error: $rg_out"
exit 1
fi
if [ -z "$rg_out" ]; then
echo "STATUS=no_matches MATCH_COUNT=0"
echo "no matches"
exit 0
fi
sorted=$(echo "$rg_out" \
| xargs -d '\n' stat --printf '%Y\t%n\n' 2>&1 \
| sort -rn \
| cut -f2-)
count=$(echo "$sorted" | wc -l | tr -d ' ')
echo "STATUS=ok MATCH_COUNT=$count"
echo "$sorted"

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: Search file contents using ripgrep. Default output: file:lineno| content (one match per line).
# Args: <pattern> [--path=<dir>] [--mode=files_with_matches|content|count] [--glob=<pattern>] [--type=<type>] [-i] [-A <n>] [-B <n>] [-C <n>] [--multiline] [--head=<n>] [--offset=<n>]
import sys
import os
import subprocess
import argparse
parser = argparse.ArgumentParser(prog='file_grep', add_help=False)
parser.add_argument('pattern')
parser.add_argument('--path', default='')
parser.add_argument('--mode', default='content',
choices=['files_with_matches', 'content', 'count'])
parser.add_argument('--glob', default='')
parser.add_argument('--type', default='')
parser.add_argument('-i', action='store_true')
parser.add_argument('-n', action='store_true', default=True)
parser.add_argument('-A', type=int, default=None)
parser.add_argument('-B', type=int, default=None)
parser.add_argument('-C', type=int, default=None)
parser.add_argument('--multiline', action='store_true')
parser.add_argument('--head', type=int, default=0)
parser.add_argument('--offset', type=int, default=0)
args = parser.parse_args()
search_path = args.path or os.getcwd()
cmd = ['rg', '--no-config', '--color=never']
if args.mode == 'files_with_matches':
cmd.append('-l')
elif args.mode == 'count':
cmd.append('--count')
elif args.mode == 'content':
if args.n:
cmd.append('-n')
if args.i:
cmd.append('-i')
if args.multiline:
cmd += ['-U', '--multiline-dotall']
if args.glob:
cmd += ['-g', args.glob]
if args.type:
cmd += ['-t', args.type]
if args.C is not None:
cmd += ['-C', str(args.C)]
if args.A is not None:
cmd += ['-A', str(args.A)]
if args.B is not None:
cmd += ['-B', str(args.B)]
cmd += ['-e', args.pattern, search_path]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 1:
print("STATUS=no_matches MATCH_COUNT=0")
print("no matches")
sys.exit(0)
if result.returncode > 1:
print(f"STATUS=error")
print(f"error: {result.stderr.strip() or f'rg exited with {result.returncode}'}")
sys.exit(1)
lines = result.stdout.splitlines()
total = len(lines)
if args.offset:
lines = lines[args.offset:]
max_matches = args.head if args.head else 50
capped = len(lines) > max_matches
if capped:
lines = lines[:max_matches]
if args.mode == 'content':
formatted = []
for line in lines:
if line == '--':
formatted.append(line)
continue
for sep in (':', '-'):
parts = line.split(sep, 2)
if len(parts) == 3 and parts[1].isdigit():
formatted.append(f"{parts[0]}:{parts[1]:>5}| {parts[2]}")
break
else:
formatted.append(line)
lines = formatted
print(f"STATUS=ok MATCH_COUNT={total}")
print('\n'.join(lines))
if capped:
print(f"\n(showing {max_matches} of {total} matches; use --head=N or --offset=N for more)")

View File

@ -1,111 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# ollie:tier cold
# description: Read a file with accurate line numbers. Prefer segments (start/end) over full-file reads.
# Args: <file_path> [start] [end] [--head=<n>]
import sys
import os
from itertools import islice
MAX_LINES = 500 # default window for whole-file reads (no start/end)
MAX_SEGMENT = 2000 # max window when start/end are given
MAX_LINE_LEN = 2000
# Parse --head=N from args
head = None
args = []
for a in sys.argv[1:]:
if a.startswith('--head='):
head = int(a.split('=', 1)[1])
else:
args.append(a)
if not args:
print("usage: file_read <file_path> [start] [end] [--head=N]")
sys.exit(1)
file_path = args[0]
start = int(args[1]) if len(args) > 1 else 1
end = int(args[2]) if len(args) > 2 else None
if not os.path.isabs(file_path):
print(f"STATUS=error")
print(f"error: path must be absolute, got: {file_path}")
sys.exit(1)
if not os.path.exists(file_path):
print("STATUS=not_found")
print("(new file)")
sys.exit(0)
if not os.path.isfile(file_path):
print(f"STATUS=error")
print(f"error: not a regular file: {file_path}")
sys.exit(1)
with open(file_path, 'rb') as f:
raw_head = f.read(8192)
if b'\x00' in raw_head:
# Binary file -- try to detect image type
IMAGE_SIGS = [
b'\x89PNG\r\n\x1a\n', b'\xff\xd8\xff',
b'GIF87a', b'GIF89a', b'RIFF',
]
is_image = any(raw_head.startswith(s) for s in IMAGE_SIGS)
if is_image:
print("STATUS=ok BINARY=true IMAGE=true")
print("(image file — use image_read for visual content)")
else:
print("STATUS=ok BINARY=true")
print("(binary file)")
sys.exit(0)
# Count total lines
try:
with open(file_path, 'r', encoding='utf-8') as f:
total_lines = sum(1 for _ in f)
except UnicodeDecodeError:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
total_lines = sum(1 for _ in f)
start = max(1, start)
if head:
end = start + head - 1
elif end is None:
end = start + MAX_LINES - 1
else:
end = min(end, start + MAX_SEGMENT - 1)
try:
with open(file_path, 'r', encoding='utf-8') as f:
for _ in islice(f, start - 1):
pass
segment_lines = list(islice(f, end - start + 1))
except UnicodeDecodeError:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
for _ in islice(f, start - 1):
pass
segment_lines = list(islice(f, end - start + 1))
if not segment_lines:
if start == 1:
print(f"STATUS=ok LINES=0 TOTAL_LINES={total_lines} TRUNCATED=false EMPTY=true")
print("(empty file)")
else:
print(f"STATUS=ok LINES=0 TOTAL_LINES={total_lines} TRUNCATED=false EMPTY=true")
print("(no lines in range)")
sys.exit(0)
out = []
for i, line in enumerate(segment_lines):
lineno = start + i
text = line.rstrip('\n')
if len(text) > MAX_LINE_LEN:
text = text[:MAX_LINE_LEN] + '...'
out.append(f"{lineno:>6}\t{text}")
last_line_shown = start + len(out) - 1
truncated = last_line_shown < total_lines
print(f"STATUS=ok LINES={len(out)} TOTAL_LINES={total_lines} TRUNCATED={str(truncated).lower()}")
print('\n'.join(out))

View File

@ -1,58 +0,0 @@
#!/usr/bin/env python3
# description: Write or overwrite a file
# Args: <file_path> <content>
import sys
import os
import difflib
def format_diff(path, old, new):
old_lines = old.splitlines(keepends=True)
new_lines = new.splitlines(keepends=True)
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
if not diff:
return None
return f"```diff\n{diff}```"
if len(sys.argv) < 3:
print("usage: file_write <file_path> <content>")
sys.exit(1)
file_path = sys.argv[1]
content = sys.argv[2]
if not os.path.isabs(file_path):
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: path must be absolute, got: {file_path}")
sys.exit(1)
parent = os.path.dirname(file_path)
if parent and not os.path.isdir(parent):
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: parent directory does not exist: {parent}")
sys.exit(1)
old_content = ""
if os.path.exists(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
old_content = f.read()
except UnicodeDecodeError:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
old_content = f.read()
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
except OSError as e:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: {e}")
sys.exit(1)
diff = format_diff(file_path, old_content, content)
if diff is None:
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
print("(no changes)")
else:
print("STATUS=done STATE_CHANGED=true")
print(diff)

View File

@ -1,62 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# ollie:tier cold
# description: Read an image file, recompressing/resizing to fit context budget. Returns an image content block.
# args: <file_path> [--region=x,y,w,h] [--scale=N] [--quality=N]
import sys
import os
import base64
import json
import mimetypes
file_path = None
for a in sys.argv[1:]:
if not a.startswith('--'):
file_path = a
if not file_path:
print("usage: image_read <file_path>")
sys.exit(1)
if not os.path.isabs(file_path):
print("STATUS=error")
print("error: path must be absolute")
sys.exit(1)
if not os.path.exists(file_path):
print("STATUS=not_found")
sys.exit(0)
mime, _ = mimetypes.guess_type(file_path)
if not mime:
mime = 'application/octet-stream'
with open(file_path, 'rb') as f:
data = f.read()
MAX_SIZE = 128 * 1024
if mime == 'image/png' and len(data) > MAX_SIZE:
from PIL import Image
import io
img = Image.open(io.BytesIO(data))
# progressively scale down until under budget
for scale in [1.0, 0.75, 0.5, 0.35, 0.25]:
buf = io.BytesIO()
if scale < 1.0:
new_size = (int(img.width * scale), int(img.height * scale))
resized = img.resize(new_size, Image.LANCZOS)
else:
resized = img
resized.save(buf, format='PNG', optimize=True)
data = buf.getvalue()
if len(data) <= MAX_SIZE:
break
b64 = base64.b64encode(data).decode('ascii')
print(json.dumps({"isError": False, "content": [
{"type": "image", "media_type": mime, "data": b64},
{"type": "text", "text": f"[image: {os.path.basename(file_path)} {len(data)} bytes {mime}]"}
]}))

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: Get completions at cursor position from the language server.
# Args: <file> <line> <col>
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 4:
print("usage: lsp_completion <file> <line> <col>")
sys.exit(1)
filepath = os.path.abspath(sys.argv[1])
line = int(sys.argv[2]) - 1
col = int(sys.argv[3]) - 1
result = request("completion", file=filepath, line=line, col=col)
if not result:
sys.exit(0)
items = result.get("items", result) if isinstance(result, dict) else result
if not isinstance(items, list):
sys.exit(0)
for item in items[:20]:
label = item.get("label", "")
detail = item.get("detail", "")
if detail:
print(f"{label}\t{detail}")
else:
print(label)

View File

@ -1,34 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: Go to definition of symbol at position. Returns file:line:col.
# Args: <file> <line> <col>
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 4:
print("usage: lsp_definition <file> <line> <col>")
sys.exit(1)
filepath = os.path.abspath(sys.argv[1])
line = int(sys.argv[2]) - 1 # convert 1-indexed to 0-indexed
col = int(sys.argv[3]) - 1
result = request("definition", file=filepath, line=line, col=col)
if not result:
print("(no definition found)")
sys.exit(0)
# Normalize: result can be a Location or list of Locations
locations = result if isinstance(result, list) else [result]
for loc in locations:
uri = loc.get("uri", loc.get("targetUri", ""))
path = uri[7:] if uri.startswith("file://") else uri
pos = loc.get("range", loc.get("targetRange", {})).get("start", {})
ln = pos.get("line", 0) + 1
ch = pos.get("character", 0) + 1
print(f"{path}:{ln}:{ch}")

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: Get diagnostics (errors, warnings) for a file.
# Args: <file>
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 2:
print("usage: lsp_diagnostics <file>")
sys.exit(1)
filepath = os.path.abspath(sys.argv[1])
result = request("diagnostics", file=filepath)
if not result:
print("(no diagnostics)")
sys.exit(0)
SEVERITY = {1: "error", 2: "warning", 3: "info", 4: "hint"}
for diag in result:
sev = SEVERITY.get(diag.get("severity", 3), "info")
ln = diag.get("range", {}).get("start", {}).get("line", 0) + 1
col = diag.get("range", {}).get("start", {}).get("character", 0) + 1
msg = diag.get("message", "")
src = diag.get("source", "")
prefix = f"{src}: " if src else ""
print(f"{ln}:{col} {sev}: {prefix}{msg}")
print(f"\n({len(result)} diagnostics)")

View File

@ -1,37 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: Get type/documentation info for symbol at position.
# Args: <file> <line> <col>
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 4:
print("usage: lsp_hover <file> <line> <col>")
sys.exit(1)
filepath = os.path.abspath(sys.argv[1])
line = int(sys.argv[2]) - 1
col = int(sys.argv[3]) - 1
result = request("hover", file=filepath, line=line, col=col)
if not result:
print("(no hover info)")
sys.exit(0)
contents = result.get("contents", "")
if isinstance(contents, dict):
# MarkedString or MarkupContent
print(contents.get("value", str(contents)))
elif isinstance(contents, list):
for item in contents:
if isinstance(item, dict):
print(item.get("value", str(item)))
else:
print(item)
else:
print(contents)

View File

@ -1,41 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: Find all references to symbol at position. Returns file:line:col per reference.
# Args: <file> <line> <col>
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 4:
print("usage: lsp_references <file> <line> <col>")
sys.exit(1)
filepath = os.path.abspath(sys.argv[1])
line = int(sys.argv[2]) - 1
col = int(sys.argv[3]) - 1
result = request("references", file=filepath, line=line, col=col)
if not result:
print("(no references found)")
sys.exit(0)
total = len(result)
cap = 50
refs = result[:cap] if total > cap else result
for loc in refs:
uri = loc.get("uri", "")
path = uri[7:] if uri.startswith("file://") else uri
pos = loc.get("range", {}).get("start", {})
ln = pos.get("line", 0) + 1
ch = pos.get("character", 0) + 1
print(f"{path}:{ln}:{ch}")
if total > cap:
print(f"\n(showing {cap} of {total} references)")
else:
print(f"\n({total} references)")

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python3
# description: Rename symbol at position across the workspace. Applies edits directly to files.
# Args: <file> <line> <col> <new_name>
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 5:
print("usage: lsp_rename <file> <line> <col> <new_name>")
sys.exit(1)
filepath = os.path.abspath(sys.argv[1])
line = int(sys.argv[2]) - 1
col = int(sys.argv[3]) - 1
new_name = sys.argv[4]
result = request("rename", file=filepath, line=line, col=col, new_name=new_name)
if not result:
print("(rename returned no edits)")
sys.exit(0)
files_changed = result.get("files_changed", {})
total = result.get("total_edits", 0)
for path, count in files_changed.items():
print(f" {path} ({count} edits)")
print(f"\nRenamed to '{new_name}': {total} edits across {len(files_changed)} files.")

View File

@ -1,56 +0,0 @@
#!/usr/bin/env python3
# ollie:parallel read
# description: List symbols in a file or search workspace symbols.
# Args: <file_or_query> [--workspace]
import os
import sys
sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't'))
from _lib.lsp.client import request
if len(sys.argv) < 2:
print("usage: lsp_symbols <file_or_query> [--workspace]")
sys.exit(1)
workspace = "--workspace" in sys.argv
arg = sys.argv[1]
SYMBOL_KINDS = {
1: "File", 2: "Module", 3: "Namespace", 4: "Package", 5: "Class",
6: "Method", 7: "Property", 8: "Field", 9: "Constructor", 10: "Enum",
11: "Interface", 12: "Function", 13: "Variable", 14: "Constant",
15: "String", 16: "Number", 17: "Boolean", 18: "Array", 19: "Object",
20: "Key", 21: "Null", 22: "EnumMember", 23: "Struct", 24: "Event",
25: "Operator", 26: "TypeParameter",
}
if workspace:
result = request("symbols", file=arg, workspace=True, query=arg)
else:
filepath = os.path.abspath(arg)
result = request("symbols", file=filepath, workspace=False)
if not result:
print("(no symbols found)")
sys.exit(0)
def print_symbols(symbols, indent=0):
for sym in symbols:
kind = SYMBOL_KINDS.get(sym.get("kind", 0), "?")
name = sym.get("name", "")
# DocumentSymbol has range; SymbolInformation has location
if "range" in sym:
ln = sym["range"]["start"]["line"] + 1
print(f"{' ' * indent}{kind:12} {name} (line {ln})")
if "children" in sym:
print_symbols(sym["children"], indent + 1)
elif "location" in sym:
uri = sym["location"].get("uri", "")
path = uri[7:] if uri.startswith("file://") else uri
ln = sym["location"]["range"]["start"]["line"] + 1
print(f"{kind:12} {name} {path}:{ln}")
print_symbols(result)

View File

@ -1,46 +0,0 @@
#!/usr/bin/env bash
# ollie:parallel read
# ollie:tier cold
# description: Search memories by keyword, tag, or title
# Args: <query>
[ -z "$*" ] && echo "usage: memory_recall <query>" && exit 1
mount="${OLLIE:-$HOME/mnt/ollie}"
mem_dir="$mount/m"
[ ! -d "$mem_dir" ] && echo "error: memory directory does not exist: $mem_dir" && exit 1
query="$*"
max_files=10
max_lines=50
stale_days=14
now=$(date +%s)
# Search filenames and bodies; discard I/O errors on stale inodes
matches=$(grep -ril --include='*.md' "$query" "$mem_dir" 2>/dev/null)
[ -z "$matches" ] && echo "no matches for: $query" && exit 0
total=$(echo "$matches" | wc -l)
count=0
while IFS= read -r file; do
count=$((count + 1))
[ $count -gt $max_files ] && echo "(showing $max_files of $total matched files)" && break
fname=$(basename "$file")
# Extract date from filename instead of stat
ts=${fname%%__*}
if [[ $ts =~ ^([0-9]{4})([0-9]{2})([0-9]{2})T ]]; then
fdate=$(date -d "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}" +%s 2>/dev/null)
[ -n "$fdate" ] && age_days=$(( (now - fdate) / 86400 )) || age_days=0
else
age_days=0
fi
[ $age_days -ge $stale_days ] && status="STALE (${age_days}d)" || status="fresh"
echo "=== $fname [$status] ==="
awk -v max="$max_lines" 'NR<=max{print} NR>max{over++} END{if(over) printf "... (%d lines total, showing first %d)\n", NR, max}' "$file" 2>/dev/null || echo "error: could not read $file"
echo
done <<< "$matches"

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python3
# description: Save a memory as a denote-formatted flat file
# Args: <title> <tags_csv> <body>
import sys
import os
import re
from datetime import datetime
if len(sys.argv) < 4:
print("usage: memory_remember <title> <tags_csv> <body>")
sys.exit(1)
title = sys.argv[1]
tags_csv = sys.argv[2]
body = sys.argv[3]
mount = os.environ["OLLIE"]
mem_dir = os.path.join(mount, "m")
if not os.path.isdir(mem_dir):
print(f"error: memory directory does not exist: {mem_dir}")
sys.exit(1)
now = datetime.now()
ident = now.strftime("%Y%m%dT%H%M%S")
date = now.strftime("%Y-%m-%d %a %H:%M")
tags = [t.strip() for t in tags_csv.split(",") if t.strip()]
def slugify(s):
s = s.lower()
s = re.sub(r"[^a-z0-9]+", "-", s)
return s.strip("-")
slug = slugify(title)
tag_slug = "-".join(slugify(t) for t in tags)
filename = f"{ident}__{slug}--{tag_slug}.md" if tag_slug else f"{ident}__{slug}.md"
path = os.path.join(mem_dir, filename)
tags_yaml = "[" + ", ".join(f'"{t}"' for t in tags) + "]"
content = f"""---
title: {title}
date: {date}
tags: {tags_yaml}
identifier: {ident}
signature:
---
{body}
"""
try:
with open(path, "w") as f:
f.write(content)
except OSError as e:
print(f"error: failed to write {path}: {e}")
sys.exit(1)
print(f"saved: {filename}")

View File

@ -1,7 +0,0 @@
#!/usr/bin/env bash
# ollie:parallel read
# description: Internal reasoning scratchpad; think through complex problems before acting
# Args: <thought>
[ -z "$*" ] && echo "usage: reasoning_think <thought>" && exit 1
exit 0

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python3
# description: Generate a JIT subagent config. No args prints the JSON template. With JSON as first arg or on stdin, creates the config and prints the agent name.
# Args: [json] — JSON config as first arg, or piped via stdin
# Output: template (no input) or agent name
import json
import os
import secrets
import sys
OLLIE = os.environ.get("OLLIE", os.path.expanduser("~/mnt/ollie"))
AGENTS_DIR = f"{OLLIE}/a"
BASE_CONFIG = f"{AGENTS_DIR}/default.json"
TEMPLATE = {
"name": "",
"role": "",
"roster": "",
"shared_task": "",
"protocol": "",
"constraints": [],
"prompt": "",
}
def die(msg):
print(f"subagent_generate: {msg}", file=sys.stderr)
sys.exit(1)
def main():
# No input: print template.
if len(sys.argv) < 2 and sys.stdin.isatty():
json.dump(TEMPLATE, sys.stdout, indent=2)
print()
return
# Read config from arg or stdin.
if len(sys.argv) > 1:
text = sys.argv[1]
else:
text = sys.stdin.read()
if not text.strip():
die("empty input")
try:
raw = json.loads(text)
except json.JSONDecodeError as e:
die(f"invalid JSON: {e}")
name = raw.get("name", "") or f"jit-{secrets.token_hex(8)}"
role = raw.get("role", "")
roster = raw.get("roster", "")
shared_task = raw.get("shared_task", "")
protocol = raw.get("protocol", "")
constraints = raw.get("constraints", [])
if not isinstance(constraints, list):
die("constraints must be an array of strings")
legacy = raw.get("prompt", "")
if not role and not legacy:
die("role or prompt required")
# Build identity text.
if role:
parts = ["## Role", role]
if roster:
parts += ["## Roster", roster]
if shared_task:
parts += ["## Shared Task", shared_task]
if protocol:
parts += ["## Coordination Protocol", protocol]
if constraints:
parts += ["## Constraints", "\n".join(f"- {c}" for c in constraints)]
identity = "\n\n".join(parts)
else:
identity = legacy
# Load base config.
try:
with open(BASE_CONFIG) as f:
cfg = json.load(f)
except (OSError, json.JSONDecodeError) as e:
die(f"failed to read base config: {e}")
cfg["prompt"] = ["$OLLIE/x/prime SYSTEM_PROMPT", f"echo {json.dumps(identity)}"]
# Write JIT config.
out_path = f"{AGENTS_DIR}/{name}.json"
try:
with open(out_path, "w") as f:
json.dump(cfg, f, indent=2)
except OSError as e:
die(f"failed to write config: {e}")
print(name)
if __name__ == "__main__":
main()

View File

@ -1,82 +0,0 @@
#!/usr/bin/env bash
# description: Spawn a subagent session, submit a prompt, and return immediately. The subagent runs independently; use prompt files for coordination.
# Args: [-name NAME] [-agent A] [-backend B] [-model M] [-cwd DIR] <prompt...>
# Output: structured KV (session, path, backend, model)
set -euo pipefail
OLLIE="${OLLIE:-$HOME/mnt/ollie}"
die() { printf 'subagent_spawn: %s\n' "$*" >&2; exit 1; }
name=; agent=; backend=; model=; cwd=
while [ $# -gt 0 ]; do
case $1 in
-name) name=$2; shift ;;
-agent) agent=$2; shift ;;
-backend) backend=$2; shift ;;
-model) model=$2; shift ;;
-cwd) cwd=$2; shift ;;
--) shift; break ;;
-*) die "unknown flag: $1" ;;
*) break ;;
esac
shift
done
[ $# -eq 0 ] && die "prompt required"
# Read agent name from stdin if not provided via flag and stdin is piped.
if [ -z "$agent" ] && ! [ -t 0 ]; then
agent=$(head -1)
fi
name=${name:-$(/bin/date +%s%N)-$$}
# Prefix with parent session ID for global uniqueness.
[ -n "$OLLIE_SESSION_ID" ] && name="${OLLIE_SESSION_ID}__${name}"
cwd=${cwd:-$(pwd)}
# Prepend parent identity and plan so the child always knows who spawned it.
parent_plan=""
if [ -n "$OLLIE_SESSION_ID" ]; then
plan_file="$OLLIE/s/$OLLIE_SESSION_ID/plan"
[ -s "$plan_file" ] && parent_plan=$(cat "$plan_file")
fi
prompt="[parent_session_id=${OLLIE_SESSION_ID}]
[session_id=${name}]${parent_plan:+
[parent_plan_start]
${parent_plan}
[parent_plan_end]}
$*"
kv="name=${name}
cwd=${cwd}"
[ -n "$agent" ] && kv="${kv}
agent=${agent}"
[ -n "$backend" ] && kv="${kv}
backend=${backend}"
[ -n "$model" ] && kv="${kv}
model=${model}"
/bin/echo "$kv" > "$OLLIE/s/new" || die "failed to create session"
B="$OLLIE/s/$name"
wait=0.1
for _ in 1 2 3 4 5 6 7 8; do
[ -d "$B" ] && break
sleep "$wait"
wait=$(awk "BEGIN{printf \"%.2f\", $wait * 2}")
done
[ -d "$B" ] || die "timeout waiting for session $name"
/bin/echo "$prompt" > "$B/prompt" || die "failed to submit prompt"
# Clean up JIT agent config now that the server has loaded it.
# Only remove configs with the jit- prefix (generated by subagent_generate).
[[ "${agent:-}" == jit-* ]] && rm -f "${OLLIE_CFG_PATH:-$HOME/.config/ollie}/agents/${agent}.json" 2>/dev/null || true
# Report resolved values from the live session.
resolved_backend=$(grep -m1 '^backend=' "$B/cfg" 2>/dev/null | cut -d= -f2- || echo "${backend:-default}")
resolved_model=$(grep -m1 '^model=' "$B/cfg" 2>/dev/null | cut -d= -f2- || echo "${model:-default}")
printf 'session=%s\npath=%s\nbackend=%s\nmodel=%s\n' "$name" "$B" "$resolved_backend" "$resolved_model"