lampson 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.env.example +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +382 -0
  4. package/bin/lampson.js +81 -0
  5. package/chat.syn +799 -0
  6. package/lamps/example-hello/lamp.json +16 -0
  7. package/lamps/example-hello/lamp.syn +19 -0
  8. package/lampson.cmd +4 -0
  9. package/lampson.ps1 +88 -0
  10. package/lampson.sh +42 -0
  11. package/lib/agents.syn +471 -0
  12. package/lib/git.syn +58 -0
  13. package/lib/lamps.syn +386 -0
  14. package/lib/loop.syn +455 -0
  15. package/lib/lsp.syn +503 -0
  16. package/lib/mcp.syn +403 -0
  17. package/lib/permission.syn +154 -0
  18. package/lib/prompt.syn +75 -0
  19. package/lib/provider.syn +522 -0
  20. package/lib/session.syn +111 -0
  21. package/lib/settings.syn +70 -0
  22. package/lib/skills.syn +179 -0
  23. package/lib/tools/bash.syn +105 -0
  24. package/lib/tools/common.sh +49 -0
  25. package/lib/tools/common.syn +91 -0
  26. package/lib/tools/edit.syn +32 -0
  27. package/lib/tools/find.syn +35 -0
  28. package/lib/tools/grep.syn +31 -0
  29. package/lib/tools/img.ps1 +36 -0
  30. package/lib/tools/img.sh +22 -0
  31. package/lib/tools/ls.syn +18 -0
  32. package/lib/tools/memo.syn +148 -0
  33. package/lib/tools/proc.sh +42 -0
  34. package/lib/tools/proc.syn +314 -0
  35. package/lib/tools/process.syn +46 -0
  36. package/lib/tools/read.syn +25 -0
  37. package/lib/tools/skill.syn +14 -0
  38. package/lib/tools/todo.syn +97 -0
  39. package/lib/tools/write.syn +22 -0
  40. package/lib/tools.syn +198 -0
  41. package/lib/trace.syn +116 -0
  42. package/lib/tree.syn +59 -0
  43. package/lib/update.syn +57 -0
  44. package/package.json +40 -0
  45. package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
  46. package/public/fonts/plex-mono-400-latin.woff2 +0 -0
  47. package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
  48. package/public/fonts/plex-mono-600-latin.woff2 +0 -0
  49. package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
  50. package/public/fonts/plex-serif-400-latin.woff2 +0 -0
  51. package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
  52. package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
  53. package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
  54. package/public/fonts/plex-serif-600-latin.woff2 +0 -0
  55. package/public/index.html +1268 -0
  56. package/public/vendor/xterm-addon-fit.js +2 -0
  57. package/public/vendor/xterm.css +218 -0
  58. package/public/vendor/xterm.js +2 -0
  59. package/skills/debugging/SKILL.md +33 -0
  60. package/skills/lampson/SKILL.md +117 -0
  61. package/skills/synsema/SKILL.md +75 -0
  62. package/web.syn +468 -0
package/.env.example ADDED
@@ -0,0 +1,29 @@
1
+ # Lampson — copia a .env (gitignored). Las claves nunca entran al proceso del programa: secret() es opaco.
2
+
3
+ # Proveedor: openai | deepseek | kimi | groq | grok | openrouter | ollama | anthropic | minimax
4
+ LAMPSON_PROVIDER=deepseek
5
+ LAMPSON_API_KEY=sk-...
6
+
7
+ # Overrides opcionales
8
+ # LAMPSON_WIRE=openai # openai | anthropic (forzar wire protocol para un endpoint custom)
9
+ # LAMPSON_BASE_URL=http://127.0.0.1:11434/v1 # cualquier endpoint compatible (sin barra final)
10
+ # LAMPSON_MODEL=deepseek-chat
11
+ # LAMPSON_MAX_TOKENS=8192 # tope de SALIDA por respuesta, NO el presupuesto del turno. Ojo: un valor
12
+ # # fijo acá PISA el default del código. Si un modelo razonador lo agota
13
+ # # pensando ("empty response"), el harness reintenta solo con el doble
14
+ # # (hasta 65536) — no hace falta inflarlo a mano.
15
+ # LAMPSON_TIMEOUT=180
16
+
17
+ # Loop
18
+ # LAMPSON_MAX_STEPS=40 # tool-steps máximos por turno
19
+ # LAMPSON_BUDGET_TOKENS=400000 # presupuesto de COSTO por turno: suma lo enviado en cada paso; los tokens
20
+ # # servidos desde el prompt cache pesan 10% (la traza imprime tokens crudos)
21
+ # LAMPSON_COMPACT_AT=80000 # compacta el contexto al superar esta estimación
22
+ # LAMPSON_PERMISSION=ask # ask | yolo | strict
23
+
24
+ # Shell para la tool bash (Windows: Git Bash por defecto; usa cmd si prefieres)
25
+ # LAMPSON_SHELL=C:\Program Files\Git\bin\bash.exe
26
+ # LAMPSON_SHELL_FLAG=-c
27
+
28
+ # Reanudar una sesión en chat.syn
29
+ # LAMPSON_SESSION=20260826-233000
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kitecosmic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,382 @@
1
+ # Lampson
2
+
3
+ **A coding agent harness written in [Synsema](https://synsema.com).** You point it at a project; it
4
+ reads, searches, edits and runs commands through tools that are confined to that project by the
5
+ language itself — and every step is visible, in the terminal or in a web UI.
6
+
7
+ - **Terminal or web**: `lampson` (REPL) or `lampson --web` (http://127.0.0.1:8080).
8
+ - **Any OpenAI- or Anthropic-compatible API** over raw HTTP: DeepSeek, Kimi, Groq, Grok, OpenRouter,
9
+ Ollama, Anthropic, MiniMax… one `.env` line to switch.
10
+ - **Least-privilege by construction**: each tool declares its capabilities; the runtime enforces
11
+ them. File tools can only touch the mounted workspace (absolute paths, `..`, sibling dirs → denied).
12
+ - **Permissions you control**: `ask` (approve dangerous commands, in the terminal or with a button in
13
+ the web UI), `yolo`, `strict`. Destructive system commands are always blocked.
14
+ - **Agents**: `build` (edits), `plan` (read-only), `review` (runs tests, never edits), `explore`,
15
+ `worker` (scoped implementation). **Sub-agents**: `delegate` runs several of them *in parallel*
16
+ (real threads) with their own context and a restricted toolset, or in the *background* — the report
17
+ lands in the parent's inbox when it finishes; every child has a live log (`/agents`, web panel).
18
+ - **Managed processes**: the agent starts servers with `process`, and the new log lines of every
19
+ server are appended to each command result — it *sees its own console*. Live logs in the web UI.
20
+ - **Skills**: Markdown procedures the model loads on demand (yours in `workspace/skills/`).
21
+ - **MCP servers**: global (`lampson/.lampson/mcp.json` — one config, every project you open) or per
22
+ project (`.lampson/mcp.json` in the repo), same JSON as Claude Code / Cursor. Their tools join the
23
+ model's catalog as `mcp_<server>_<tool>`; calling one asks you first (yolo allows, strict denies).
24
+ - **LSP**: the agent navigates code through the project's language servers — `lsp symbols` gives a
25
+ file's structure without reading it, `definition`/`references`/`hover` resolve what grep leaves
26
+ ambiguous. Nothing is bundled: `/lsp add typescript|python|rust|go|css|html` (or your own command),
27
+ global or per project; each server starts on its first query.
28
+ - **Lamps** (tool plugins): a folder with a `lamp.json` manifest plus code — a Synsema program that runs
29
+ under a capability ceiling, or any executable (js, py, sh…). Global in `lampson/lamps/<name>/`, per
30
+ project in `.lampson/lamps/<name>/` (the agent can write those). **Off by default**: you turn them on
31
+ from the top bar of the web UI or `/lamps on <name>`; their tools join the catalog as `lamp_<lamp>_<tool>`.
32
+ - **Project memory**: the agent keeps its own notes per project (`memory/<project>/*.md`, outside
33
+ the repo) — how to run it, gotchas, decisions — and rereads them in the next session. You can read
34
+ and edit them (web panel, `/memory`).
35
+ - **`!command`**: run something yourself from the chat inside a real pseudo-terminal (prompts, passwords
36
+ and REPLs work); the output lands in the agent's context.
37
+ - **Terminal in the browser**: the web UI opens a real shell (pty, cwd = your project) in the center pane
38
+ — `>_ terminal` in the header. Synsema 0.6.8+.
39
+ - **Local only**: the web API (chat with tools, terminal, process control) answers loopback clients
40
+ only; anything else gets 401. To let a script in from elsewhere, set `LAMPSON_WEB_TOKEN` in `.env`
41
+ and send `Authorization: Bearer <token>`.
42
+ - **Sessions** persisted as JSON, including every tool call, result, error and denial — plus a
43
+ human-readable **trace** per session (`.lampson/trace/<id>.log`: one line per step, tool call,
44
+ result, error, denial, with elapsed time and tokens) to see where the agent does well or badly:
45
+ `/trace [n]` in the terminal, `≡` next to a session in the web UI, or `tail -f` the file.
46
+
47
+ ## Install
48
+
49
+ With npm (brings `synsema` along as a dependency):
50
+
51
+ ```sh
52
+ npm i -g lampson
53
+ cd /path/to/your/project
54
+ lampson # terminal
55
+ lampson --web # http://127.0.0.1:8080
56
+ ```
57
+
58
+ The package keeps your config, sessions, memory and global lamps in `~/lampson` (`LAMPSON_HOME` to change
59
+ it) and refreshes the code there whenever you `npm i -g lampson@latest`. On Windows it needs PowerShell
60
+ (pwsh or the built-in one) and Git for Windows (its bash is what the `bash` tool uses).
61
+
62
+ Or the git-based installer — same result, `lampson --update` then means `git pull`. One line. It installs `synsema` if missing, puts Lampson in `~/lampson`, adds it to your PATH and
63
+ asks for your provider + API key.
64
+
65
+ **Windows (PowerShell)**
66
+
67
+ ```powershell
68
+ irm https://raw.githubusercontent.com/kitecosmic/lampson/main/install.ps1 | iex
69
+ ```
70
+
71
+ **Linux / macOS**
72
+
73
+ ```bash
74
+ bash -c "$(curl -fsSL https://raw.githubusercontent.com/kitecosmic/lampson/main/install.sh)"
75
+ ```
76
+
77
+ Then, in a new terminal:
78
+
79
+ ```bash
80
+ cd /path/to/your/project
81
+ lampson # terminal
82
+ lampson --web # http://127.0.0.1:8080
83
+ ```
84
+
85
+ **First run**: Lampson asks for a provider, a model and its API key — a short wizard in the terminal, a
86
+ welcome window in the web UI. That goes to `lampson/.lampson/config.json` (local, gitignored) and is
87
+ shared by both UIs. Change it any time: `/setup` or `/provider <name> [model]` in the terminal, or click
88
+ the `provider · model` pill in the web header (one key per provider is kept, so switching is instant).
89
+ `.env` still works for the same settings (`LAMPSON_PROVIDER`, `LAMPSON_API_KEY`, `LAMPSON_API_KEY_<PROVIDER>`).
90
+
91
+ **Images**: paste (Ctrl+V) or drop images into the web composer; they are downscaled in the browser
92
+ (≤ 1568 px) and sent inline. In the terminal, `/paste` attaches the image on the clipboard (copy a
93
+ screenshot first) and `/image <path>` a file; they go with your next message (`📎2 ❯`). If the current model does not declare image input, you are warned before
94
+ sending and the model receives a text note instead of a 400. The table is `supports_vision` in
95
+ `lib/provider.syn` (anthropic, minimax, gpt-4o/4.1/5, o-series, groq llama-4, and models named
96
+ `*vision*`/`*vl*` on openrouter/ollama/kimi/grok); set `"vision": true` in `config.json` to override
97
+ it for a model Lampson does not know — or `false` to force the text note.
98
+
99
+ **Updating**: Lampson checks `origin/main` on start and tells you when a newer version exists (terminal
100
+ banner, web header button). Run `lampson --update` (or `/update` in the REPL, or the web button) and
101
+ restart. Re-running the installer does the same. `LAMPSON_HOME` changes the install folder.
102
+ Requirements the installer handles for you: `synsema`, `git` (on Windows, Git for Windows also
103
+ provides the bash the tools use — the installer offers to install it with winget). Linux: the
104
+ `synsema` binary needs glibc ≥ 2.39 (Ubuntu 24.04+, Debian 13+, Fedora 40+).
105
+
106
+ **Docker** (the `bash` tool is then confined by the container too)
107
+
108
+ ```bash
109
+ docker run --rm -p 8080:8080 -v "$PWD:/lampson/workspace" \
110
+ -e LAMPSON_PROVIDER=deepseek -e LAMPSON_API_KEY=sk-... ghcr.io/kitecosmic/lampson
111
+ ```
112
+
113
+ Or from a clone: `docker build -t lampson .` and use `lampson` as the image. `docker-compose.yml`
114
+ does the same with persistent sessions and memory.
115
+
116
+ > Status: developed and tested on Windows 11; the Docker image (Ubuntu 24.04) is built by CI. The Linux/macOS
117
+ > installer is written but not yet exercised on a real machine — issues welcome.
118
+
119
+ ## Use
120
+
121
+ ```bash
122
+ cd /path/to/your/project
123
+ lampson # terminal REPL; the current directory becomes the workspace
124
+ lampson --web # web UI at http://127.0.0.1:8080
125
+ lampson --agent plan # start in plan (read-only) mode
126
+ lampson --yolo # never ask for dangerous commands (--strict: deny them; --ask: default)
127
+ lampson --workspace /other # pick a project without cd
128
+ lampson --help
129
+ ```
130
+
131
+ In the REPL, `/` lists every command (`/agent`, `/ask` `/yolo` `/strict`, `/model`, `/config`,
132
+ `/files`, `/procs`, `/logs <name>`, `/stop <name>`, `/kill <pid>`, `/skills`, `/sessions`,
133
+ `/resume <id>`, `/tokens`, `/flags`). `!cmd` runs a command yourself.
134
+
135
+ The project is mounted as `lampson/workspace` (an NTFS junction on Windows, a symlink elsewhere)
136
+ and every tool declares `file("workspace/*")` — that literal, named scope is what makes the
137
+ confinement real. Config, sessions and process logs live in the `lampson` folder, never in your project.
138
+
139
+ ### Providers
140
+
141
+ | `LAMPSON_PROVIDER` | wire | endpoint |
142
+ |---|---|---|
143
+ | `openai` `deepseek` `kimi` `groq` `grok` `openrouter` `ollama` | OpenAI | `{base_url}/chat/completions` |
144
+ | `anthropic` `minimax` | Anthropic | `{base_url}/messages` |
145
+
146
+ Any compatible endpoint: set `LAMPSON_BASE_URL` (and `LAMPSON_WIRE` if the preset can't guess).
147
+ Model names are normalized to lowercase for providers whose APIs are case-sensitive (DeepSeek, OpenAI,
148
+ Anthropic, Groq, Kimi, Grok, OpenRouter — `DeepSeek-V4-Pro` would be a 400); MiniMax and Ollama keep
149
+ theirs. `/model` with no argument (or the model window in the web UI) lists what the provider's API
150
+ actually offers (`GET /models`), so you never type a name blind.
151
+ The API key is a Synsema `secret()`: the program can pass it as a header but never read or print it.
152
+ See `.env.example` for every knob (steps, token budget, compaction threshold, shell).
153
+
154
+ ### Permissions
155
+
156
+ | level | examples | ask | yolo | strict |
157
+ |---|---|---|---|---|
158
+ | hardline | `rm -rf /`, formatting disks, fork bombs, force-push to main | blocked | blocked | blocked |
159
+ | dangerous | recursive deletes, `git reset --hard`, `sudo`, `curl \| sh`, `DROP TABLE`, writing `.env` | **asks you** | allowed | blocked |
160
+ | everything else | read, edit, tests, `git status`… | allowed | allowed | allowed |
161
+
162
+ Change the mode with the web selector, `/ask` `/yolo` `/strict`, or the launch flags — `.env` only
163
+ sets the default. The `bash` child process itself is not confined by Synsema; for a public deployment,
164
+ run Lampson in a container.
165
+
166
+ ## Architecture
167
+
168
+ ```
169
+ lampson.ps1 / .sh launcher: mounts ./workspace, starts terminal or web
170
+ chat.syn terminal REPL (colors, approvals via Synsema's native `approve`)
171
+ web.syn HTTP server: POST /api/chat → SSE events; sessions, tree, file viewer, processes, ports
172
+ public/index.html web UI (no build step, no dependencies)
173
+ lib/
174
+ provider.syn config from .env · chat(cfg, messages, catalog) · retry with backoff
175
+ loop.syn run_turn(): LLM → tool calls → permissions → call_tool → results → repeat; doom-loop guard; compaction
176
+ tools.syn tool registry (allow-list) + JSON-Schema catalog + per-profile subsets
177
+ tools/<x>.syn one tool per file: read write edit ls find grep bash process skill
178
+ tools/common.sh shared shell helpers (kill process trees on Windows/unix)
179
+ tools/proc.sh ports, command line of a pid, kill a foreign pid tree (the managed processes are native)
180
+ agents.syn profiles (build / plan / review / explore / worker) + `delegate` (sub-agents: batch, background, inbox, steer/stop)
181
+ permission.syn evaluate(tool, args, mode) → allow | deny | ask
182
+ prompt.syn system prompt (rules, tools, environment, skills index, AGENTS.md of the project)
183
+ skills.syn SKILL.md index (harness / project / local)
184
+ session.syn .lampson/sessions/<id>.json
185
+ tree.syn · git.syn workspace tree, git status
186
+ skills/ built-in skills: lampson (how the harness works), synsema, debugging
187
+ tests/mock_llm.syn scripted fake LLM (both wires) for end-to-end runs without an API key
188
+ unit.test.syn unit tests — run them with tests/run.ps1 · tests/run.sh (throwaway workspace)
189
+ ```
190
+
191
+ ### The loop
192
+
193
+ ```
194
+ while steps < max_steps and tokens <= budget
195
+ r = provider.chat(cfg, messages, catalog) -- last step: no tools → forces a final answer
196
+ messages += assistant(r.text, r.tool_calls)
197
+ if no tool_calls → return
198
+ for each call:
199
+ doom loop (3× identical) → error back to the model
200
+ 50% of the token budget → nudge (stop exploring, act); 90% → final answer without tools
201
+ permission.evaluate → deny | ask | allow
202
+ out = call_tool(registry[name], args) -- least-privilege; errors go back as text
203
+ messages += tool(id, out) -- everything is kept, including failures
204
+ ```
205
+
206
+ ### MCP servers
207
+
208
+ `lib/mcp.syn` is a stdio MCP client. Config, in the format you already have elsewhere:
209
+
210
+ ```json
211
+ {"mcpServers": {"github": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"],
212
+ "env": {"GITHUB_TOKEN": "…"}}}}
213
+ ```
214
+
215
+ - `lampson/.lampson/mcp.json` is **global**: lampson is installed once, so a server declared there is
216
+ available in every repo you open. `workspace/.lampson/mcp.json` is per project (a server with the same
217
+ name overrides the global one). `"disabled": true` keeps an entry without starting it; `"cwd"` defaults
218
+ to the workspace.
219
+ - Each server runs inside a supervisor `agent` (like managed processes): `initialize` → `tools/list`,
220
+ then requests travel over the bus (`mcp.req.<server>` / `mcp.res.<server>.<id>`). State on the
221
+ blackboard (`mcp:<server>`), status in `/mcp` and the web sidebar; a server that dies shows `error`.
222
+ - Tools appear to the model as `mcp_<server>_<tool>` with the server's JSON Schema. `build`/`worker` see
223
+ all of them; `plan`/`review`/`explore` only those marked `readOnlyHint`. Every call goes through
224
+ `permission.syn`: **ask** by default, allow in yolo, deny in strict — and lands in the session trace.
225
+ - Not yet: HTTP/SSE transports, resources/prompts, sampling.
226
+
227
+ ### LSP (language servers)
228
+
229
+ `lib/lsp.syn`, same shape as the MCP client (deepseek-harness `packages/lsp` was the reference: the harness
230
+ installs nothing, the user declares a server per language, it starts lazily, and the model gets a closed
231
+ set of operations — no raw JSON-RPC). Config:
232
+
233
+ ```json
234
+ {"servers": {"typescript": {"command": "npx", "args": ["--yes", "typescript-language-server", "--stdio"],
235
+ "languages": {".ts": "typescript", ".tsx": "typescriptreact"}}}}
236
+ ```
237
+
238
+ - `lampson/.lampson/lsp.json` is global, `workspace/.lampson/lsp.json` per project. Presets (`/lsp add
239
+ <name>`, or the sidebar): typescript, python (pyright), rust (rust-analyzer), go (gopls), css, html.
240
+ `cwd` defaults to the workspace; `env` and `"disabled": true` as in MCP. On Windows a bare `npx`/`npm`
241
+ command is retried as `.cmd`. The typescript preset needs `typescript` in the project's `node_modules`
242
+ (any TS project has it) — verified against a real `typescript-language-server` via `npx`.
243
+ - One supervisor `agent` per server; stdio with `Content-Length` framing (`line_mode: false`, byte-exact
244
+ cut); server→client requests (`workspace/configuration`, `client/registerCapability`…) are answered with
245
+ `null`, notifications (diagnostics) ignored. `rootUri` is the real project path (`LAMPSON_WORKSPACE`),
246
+ results come back relative to it (case-insensitive on Windows, `%3A` decoded).
247
+ - Tool `lsp(op, path, line, character)` — `symbols` (hierarchical outline with line ranges), `definition`,
248
+ `references` (declaration included), `implementation`, `hover`. Each query opens the document
249
+ transiently (`didOpen` → request → `didClose`), positions are 1-based like the editor. Read-only: allowed
250
+ in every mode and profile. Test: `lsp_test.syn` against `tests/mock_lsp.js`.
251
+
252
+ ### Lamps (tool plugins)
253
+
254
+ `lib/lamps.syn`. Synsema has no dynamic `use` (on purpose: no supply chain inside the process), so a lamp
255
+ never loads into lampson — every call is **one child process that ends**, the "safe runner" pattern from
256
+ the Synsema sandbox docs. `lamps/<name>/lamp.json`:
257
+
258
+ ```json
259
+ {"name": "hello", "description": "greets", "kind": "syn", "entry": "lamp.syn",
260
+ "caps": "file.read=workspace/*", "timeout": 60,
261
+ "tools": [{"name": "greet", "description": "…", "parameters": {"type": "object", "properties": {"who": {"type": "string"}}}, "readonly": true}]}
262
+ ```
263
+
264
+ - `kind: "syn"` runs `synsema run --cap-set stdout,time,env=LAMP_*[,caps] <entry>`: the ceiling is the
265
+ manifest you approved when you turned it on; a `require` above it in the lamp's code fails with
266
+ *above the host ceiling* — nothing escalates from inside. `kind: "exec"` runs `"command"` as is (no
267
+ language ceiling — which is why enabling is always a human decision).
268
+ - The tool call travels by env: `LAMP_TOOL`, `LAMP_ARGS` (JSON), `LAMP_DIR`, `LAMP_WORKSPACE`; the lamp
269
+ prints its result to stdout. Exit ≠ 0 or timeout → `ERROR:` for the model.
270
+ - Discovery: `lamps/` (global) and `workspace/.lampson/lamps/` (project; same name overrides). State in
271
+ `.lampson/lamps.json` — **off by default**. On/off: web top bar, `/lamps on|off <name>`, or the model's
272
+ `lamp(action=enable)` which always asks. Lamp tools: ask by default, allow in yolo, deny in strict;
273
+ `readonly` ones also reach `plan`/`review`/`explore`. Example: `lamps/example-hello/`.
274
+ - The agent can build lamps itself: `lamp(action=create, name, manifest, files)` writes a project lamp,
275
+ validates the manifest and runs `synsema check` on a syn entry — define-and-validate only, like dsh's
276
+ `cordis_define`; turning it on is still yours (`cordis_run`'s approval, here `enable` → ask).
277
+
278
+ ### Keeping the model aware of what it did
279
+
280
+ Borrowed from the harness that does each part best (see `notes/*.md`):
281
+
282
+ | Problem | Mechanism | From |
283
+ |---|---|---|
284
+ | A huge tool result floods the context | **Spill**: results over 10k chars are saved whole to `.lampson/spill/<call>.txt`; the model sees head + tail + the path (`read` is exempt, it pages) | deepseek |
285
+ | Old results are re-sent with every call | **Receipts**: before each model call, tool results beyond the last 40k tokens become one line — `[bash result pruned: exit code 0 · 412 lines/18k chars · Full result saved to …]` | hermes / opencode |
286
+ | Same call, same args, again and again | **Repeat reminder**: 3rd and 5th identical call (canonical JSON) still run but carry a reminder; the 8th is refused | deepseek / opencode |
287
+ | Budget runs out silently | 80 %: a notice appended to the latest tool result (no new user message, cache stays warm); 95 %: last step without tools, summary required | hermes / opencode |
288
+ | Edits a file it never read, or one that changed | **Observation gate in code**: `read` records the file hash; `edit`/`write` on an existing file are rejected without it, or if the file changed since | deepseek |
289
+ | Loses the plan | `todo` tool (whole-list replacement, one `in_progress` at a time, scoped to the session like the three references); re-injected only after context compaction, active items only | hermes, opencode |
290
+ | Reads the whole project before touching anything | **Exploration cap** (ours): after 8 read-only calls in a row (read/ls/find/grep) without an edit/write/command the result carries a warning; after 16 they are refused until it acts (`LAMPSON_EXPLORE_CAP`) | — |
291
+
292
+ ### Sub-agents
293
+
294
+ `delegate(tasks=[{agent, brief, context}…], background?)` — or `action=list|steer|stop|result`.
295
+
296
+ - **Batch, in parallel**: each task runs `loop.run_turn` in its own thread (`parallel_map`, 4 at a time,
297
+ max 6 per call) with a fresh history, the profile's toolset and *no* `delegate` (depth 1). The parent
298
+ gets one consolidated report; every child writes `.lampson/agents/<id>.log` (tail it) and `<id>.json`.
299
+ - **Background**: returns the ids at once; the child runs inside a Synsema `agent` (own interpreter) and
300
+ its report enters the parent's **inbox**: `loop.run_turn` checks `opts.inbox_fn` before every model call
301
+ and appends pending reports as new `user` messages (never mutating past context — prefix cache intact);
302
+ in the terminal an idle parent gets an automatic turn (max 3 in a row, reset by real input).
303
+ `steer` = text the child reads before its next step; `stop` = cut and return the partial report.
304
+ - **Permissions**: a child never asks the user — it runs `strict` (dangerous → denied with the reason;
305
+ it reports the limitation) or `yolo` if `LAMPSON_PERMISSION=yolo`. Reports are self-reports: the
306
+ prompt tells the parent to verify (read the file, run the test) before claiming success.
307
+ - **History**: finished sub-agents are pruned (older than 30 min, or beyond the last 10) whenever a new one
308
+ is delegated or the list is shown; the test suite cleans its own.
309
+ - **Live UI**: `GET /api/events` is an SSE stream fed by the Synsema bus — process supervisors publish every
310
+ log line and status change (`proc.<name>`), sub-agents their start/steps/end (`subagent.*`) — so the web
311
+ panels and open logs refresh on events, not on timers (timers stay only as a slow fallback).
312
+ - Runtime detail: a Synsema `agent` only sees its spawn parameters, the builtins and the *top-level* tasks
313
+ of the entry program, so `chat.syn`/`web.syn` define `task lampson_subagent(spec)` as the child's door.
314
+
315
+ ### Adding a tool
316
+
317
+ `lib/tools/<name>.syn` exporting `task tool(...)` (its `require` lines at the top of the body) and
318
+ `let SPEC` (JSON Schema); register it in `lib/tools.syn` and in the profiles of `lib/agents.syn`.
319
+ `synsema check chat.syn` validates the whole graph. There is deliberately no dynamic loading: the
320
+ allow-list is a file you can read.
321
+
322
+ ### Skills
323
+
324
+ A folder with `SKILL.md` (`name:` + `description:` front matter) — the [Agent Skills](https://agentskills.io)
325
+ format, so anything published on [skills.sh](https://skills.sh) works as-is. The system prompt carries only
326
+ the index; the content enters the context when the model calls `skill(name)`. Search order (later wins):
327
+ `~/.agents/skills`, `~/.claude/skills` (global) < `skills/` (harness) < `.claude/skills`, `.agents/skills`,
328
+ `skills/` of the project < `.lampson/skills/` of the project (local, not committed).
329
+
330
+ **Installing external skills** — the agent can do it for you: ask *"install the frontend-design skill from
331
+ anthropics/skills"* and it calls `skill(action=install, source, name, scope=global|project)`, which runs the
332
+ standard installer below. Installing is **always human-in-the-loop** (it asks even in yolo mode; strict denies
333
+ it) because it brings third-party instructions and scripts onto your machine. Global (default) means
334
+ `~/.agents/skills`: a Go skill installed once serves every Go project. Or do it yourself:
335
+
336
+ ```bash
337
+ npx skills add anthropics/skills --skill frontend-design # → ./.agents/skills (this project)
338
+ npx skills add anthropics/skills --skill frontend-design -g # → ~/.agents/skills (every project)
339
+ npx skills list · npx skills update · npx skills remove
340
+ ```
341
+
342
+ The home folders are mounted as junctions/symlinks under `.lampson/skills-global` and
343
+ `.lampson/skills-claude` by `lampson.ps1` / `lampson.sh` (a capability cannot point at a dynamic path).
344
+
345
+ ### Testing without an API key
346
+
347
+ ```bash
348
+ synsema serve tests/mock_llm.syn
349
+ LAMPSON_PROVIDER=openai LAMPSON_WIRE=openai LAMPSON_BASE_URL=http://127.0.0.1:8765/v1 LAMPSON_API_KEY=x lampson
350
+ tests/run.sh # Windows: .\tests\run.ps1
351
+ ```
352
+
353
+ The unit tests write into `workspace/`, so the runner mounts a temporary folder for them and
354
+ restores your project afterwards; `unit.test.syn` refuses to run on anything else.
355
+
356
+ ## Synsema runtime notes (v0.6.7)
357
+
358
+ Things that cost time and are handled in the code:
359
+
360
+ - `file("./*")` behaves like `"*"` (whole disk); `file("dir/*")` confines — hence the `workspace` mount.
361
+ - `http_post` with a map body sends `text(map)`, not JSON → always `json_encode` + `Content-Type`.
362
+ - Responses expose `status, ok, body, headers` (no `json`); on network error `status 0` + `error`.
363
+ - `number()` returns floats → `floor()` for every integer that goes on the wire.
364
+ - `run()` hangs forever if the command leaves a descendant holding stdout (Windows inherits handles)
365
+ and its timeout only kills the direct child → the `bash` tool and the `process` tool use `proc_spawn`
366
+ (v0.6.9): events per line, real deadline, `proc_close` kills the whole tree (Job Object / process group).
367
+ - Under `serve`, a `proc_spawn` made in a handler dies with the request. A managed process therefore lives
368
+ inside an `agent` (own lifecycle: verified under `run` and `serve`): state on the blackboard
369
+ (`proc:<name>`), stop via `bus_publish("proc.stop.<name>")`, output drained to `.lampson/proc/<name>.log`.
370
+ Processes die with lampson (`/exit` publishes `proc.stop_all`; Ctrl-C under serve → `agent_stop`).
371
+ - Git Bash (MSYS) has two process trees: `bash -c "a && npm run dev"` execs its last command, so the
372
+ MSYS pid shows as `cmd`/`node` (never trust the name to decide liveness — compare the WINPID), and native
373
+ grandchildren (`npm.cmd → cmd → node`) are invisible to MSYS `ps` → `kill_tree` (foreign pids only) does
374
+ `taskkill /T /F` per WINPID. The managed log ends with `[process exited with code N]`; `bash` refuses
375
+ server-looking commands and points to `process`.
376
+ - `localhost` may resolve to IPv6 while `serve` listens on IPv4 → use `127.0.0.1`.
377
+ - Reserved words that bite: `reason task ask stop decide analyze generate show approve confirm`.
378
+ - `and`/`or` short-circuit only from Synsema v0.6.10 (before, `contains(m, "k") and m["k"]` exploded — the code still nests `when` for that); a task named `run` shadows the builtin (and a module task named `read`/`write` breaks `read_file`/`write_file` calls in sibling tasks); modules cannot import `../`; `split(s, "")` is an error; `slice` past the end errors.
379
+
380
+ ## License
381
+
382
+ MIT — see `LICENSE`.
package/bin/lampson.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // bin/lampson.js — entry point of the npm package (`npm i -g lampson`).
3
+ //
4
+ // The code ships inside node_modules, but Lampson keeps STATE next to its code: the mounted `workspace`
5
+ // junction, `.lampson/` (config, sessions, traces, spill), `memory/` and global `lamps/`. Living inside
6
+ // node_modules would lose all of that on every `npm i -g lampson@latest`. So this launcher keeps a stable
7
+ // home (LAMPSON_HOME, default ~/lampson), syncs the package's code files into it when the version changes,
8
+ // and runs the same launcher the git install uses (lampson.ps1 / lampson.sh). Everything else — synsema on
9
+ // the PATH (it is a dependency of this package), the workspace mount, the terminal/web modes — is identical.
10
+ //
11
+ // If LAMPSON_HOME already holds a git checkout (the curl/irm installer), it is used as is and nothing is
12
+ // copied: `lampson --update` there still means `git pull`.
13
+ 'use strict';
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+ const { spawnSync } = require('child_process');
18
+
19
+ const pkgDir = path.resolve(__dirname, '..');
20
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
21
+ const home = process.env.LAMPSON_HOME || path.join(os.homedir(), 'lampson');
22
+ const marker = path.join(home, '.npm-installed');
23
+
24
+ // what gets copied into the home: the runtime, never state. Keep in sync with "files" in package.json.
25
+ const CODE = ['lib', 'public', 'skills', 'chat.syn', 'web.syn', 'lampson.ps1', 'lampson.sh', 'lampson.cmd', '.env.example', 'README.md', 'LICENSE'];
26
+
27
+ function copyDir(src, dst) {
28
+ fs.mkdirSync(dst, { recursive: true });
29
+ for (const e of fs.readdirSync(src, { withFileTypes: true })) {
30
+ const s = path.join(src, e.name), d = path.join(dst, e.name);
31
+ if (e.isDirectory()) copyDir(s, d); else fs.copyFileSync(s, d);
32
+ }
33
+ }
34
+
35
+ function sync() {
36
+ const isGit = fs.existsSync(path.join(home, '.git'));
37
+ if (isGit) return 'git';
38
+ let installed = '';
39
+ try { installed = fs.readFileSync(marker, 'utf8').trim(); } catch (e) { /* first run */ }
40
+ if (installed === pkg.version && fs.existsSync(path.join(home, 'chat.syn'))) return 'ok';
41
+ fs.mkdirSync(home, { recursive: true });
42
+ for (const item of CODE) {
43
+ const src = path.join(pkgDir, item);
44
+ if (!fs.existsSync(src)) continue;
45
+ const dst = path.join(home, item);
46
+ if (fs.statSync(src).isDirectory()) { fs.rmSync(dst, { recursive: true, force: true }); copyDir(src, dst); }
47
+ else fs.copyFileSync(src, dst);
48
+ }
49
+ // global lamps are user content: seed the example once, never overwrite what the user put there
50
+ const lampsSrc = path.join(pkgDir, 'lamps'), lampsDst = path.join(home, 'lamps');
51
+ if (fs.existsSync(lampsSrc)) {
52
+ for (const e of fs.readdirSync(lampsSrc, { withFileTypes: true })) {
53
+ if (e.isDirectory() && !fs.existsSync(path.join(lampsDst, e.name))) copyDir(path.join(lampsSrc, e.name), path.join(lampsDst, e.name));
54
+ }
55
+ }
56
+ fs.writeFileSync(marker, pkg.version + '\n');
57
+ return installed ? 'updated' : 'installed';
58
+ }
59
+
60
+ const args = process.argv.slice(2);
61
+ if (args.some(a => /^--?(version|v)$/i.test(a))) { console.log('lampson ' + pkg.version); process.exit(0); }
62
+
63
+ let state;
64
+ try { state = sync(); } catch (e) { console.error('lampson: could not prepare ' + home + ': ' + e.message); process.exit(1); }
65
+ if (state === 'installed') console.log('lampson ' + pkg.version + ' → ' + home + ' (your config, sessions and lamps live there)');
66
+ if (state === 'updated') console.log('lampson updated to ' + pkg.version + ' in ' + home);
67
+
68
+ // --update under npm: the code comes from the registry, not from git
69
+ if (args.some(a => /^--?(update|u)$/i.test(a)) && state !== 'git') {
70
+ console.log('installed with npm — update with: npm i -g lampson@latest');
71
+ process.exit(0);
72
+ }
73
+
74
+ let r;
75
+ if (process.platform === 'win32') {
76
+ const shell = spawnSync('pwsh', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.Major'], { stdio: 'ignore' }).status === 0 ? 'pwsh' : 'powershell';
77
+ r = spawnSync(shell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', path.join(home, 'lampson.ps1'), ...args], { stdio: 'inherit', cwd: process.cwd() });
78
+ } else {
79
+ r = spawnSync('bash', [path.join(home, 'lampson.sh'), ...args], { stdio: 'inherit', cwd: process.cwd() });
80
+ }
81
+ process.exit(r.status == null ? 1 : r.status);