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/lib/mcp.syn ADDED
@@ -0,0 +1,403 @@
1
+ -- lib/mcp.syn — cliente MCP (Model Context Protocol) por stdio: servers globales y por proyecto
2
+ --
3
+ -- Config (mismo formato que Claude Code / Cursor, para copiar y pegar):
4
+ -- .lampson/mcp.json GLOBAL: lampson se instala una vez (~/lampson), así que esto vale para
5
+ -- TODOS los proyectos que abras — "el mcp de GitHub en todos los repos"
6
+ -- workspace/.lampson/mcp.json por proyecto (no committeado); un server con el mismo nombre pisa al global
7
+ -- {"mcpServers": {"github": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"],
8
+ -- "env": {"GITHUB_TOKEN": "…"}, "disabled": false}}}
9
+ --
10
+ -- Arquitectura (la misma que los procesos gestionados, lib/tools/proc.syn):
11
+ -- * un agente supervisor `McpSup` por server: proc_spawn (pipes, una línea JSON por mensaje), handshake
12
+ -- initialize → notifications/initialized → tools/list, y después un loop `select` entre el proceso y
13
+ -- el bus. Vive mientras vive lampson; bajo `serve` sobrevive a los requests. Al cerrar, muere el árbol.
14
+ -- * estado en el blackboard "mcp:<server>" = {status, tools: [{name, description, inputSchema,
15
+ -- readonly}], error}; lo leen chat/web/registry con observe.
16
+ -- * llamada: el tool publica "mcp.req.<server>" {id, method, params} y espera "mcp.res.<server>.<id>";
17
+ -- el supervisor serializa las requests por server (un MCP stdio es secuencial de todos modos).
18
+ -- * cada tool MCP entra al catálogo del modelo como `mcp_<server>_<tool>` con su inputSchema tal cual;
19
+ -- loop.execute las despacha acá (no son tasks Synsema: los args son libres). permission.syn: ask por
20
+ -- defecto (una tool de terceros con efectos fuera del workspace), yolo permite, strict deniega; las
21
+ -- de solo lectura (annotations.readOnlyHint) están también en plan/review/explore.
22
+ -- Sin HTTP/SSE por ahora (stdio cubre casi todos los servers publicados); es el siguiente paso natural.
23
+
24
+ use "./tools/common.syn" as c
25
+
26
+ export let GLOBAL_CONFIG be ".lampson/mcp.json"
27
+ export let PROJECT_CONFIG be "workspace/.lampson/mcp.json"
28
+ let CALL_TIMEOUT be 120
29
+
30
+ task valid_name(name)
31
+ when name == nothing or name == ""
32
+ give false
33
+ give matches(name, "[a-zA-Z0-9_-]{1,32}")
34
+
35
+ task read_config(path, scope)
36
+ let out be []
37
+ try
38
+ let doc be json_decode(read_file(path))
39
+ when contains(doc, "mcpServers")
40
+ each name in keys(doc["mcpServers"])
41
+ let s be doc["mcpServers"][name]
42
+ let disabled be false
43
+ when contains(s, "disabled")
44
+ set disabled to s["disabled"] == true
45
+ when valid_name(name) and contains(s, "command") and not disabled
46
+ set out to append(out, {"name": name, "command": text(s["command"]), "args": when contains(s, "args") then s["args"] otherwise [], "env": when contains(s, "env") then s["env"] otherwise {}, "cwd": when contains(s, "cwd") then text(s["cwd"]) otherwise "workspace", "scope": scope})
47
+ recover err
48
+ give out
49
+ give out
50
+
51
+ -- servers configurados: proyecto pisa a global por nombre. LAMPSON_MCP_CONFIG = un archivo extra (tests).
52
+ export task servers()
53
+ require file(".lampson")
54
+ require file(".lampson/*")
55
+ require file("workspace")
56
+ require file("workspace/*")
57
+ require env("LAMPSON_*")
58
+ let by_name be {}
59
+ each s in read_config(GLOBAL_CONFIG, "global")
60
+ set by_name[s["name"]] to s
61
+ each s in read_config(PROJECT_CONFIG, "project")
62
+ set by_name[s["name"]] to s
63
+ when env("LAMPSON_MCP_CONFIG", "") != ""
64
+ each s in read_config(env("LAMPSON_MCP_CONFIG", ""), "extra")
65
+ set by_name[s["name"]] to s
66
+ let out be []
67
+ each n in sort_by(keys(by_name), (x) => x)
68
+ set out to append(out, by_name[n])
69
+ give out
70
+
71
+ -- ---------- supervisor: un agente por server ----------
72
+ -- Parámetros del spawn: name, command, args_json, env_json, cwd (un agente no ve el módulo: todo va por spawn,
73
+ -- y las constantes PROTOCOL/timeouts van como literales acá adentro).
74
+ agent McpSup
75
+ require exec
76
+ require time
77
+ require env("LAMPSON_*")
78
+ require env("OS")
79
+ require file("workspace")
80
+ require file("workspace/*")
81
+ let key be "mcp:" + name
82
+ let args be json_decode(args_json)
83
+ let envs be json_decode(env_json)
84
+ share {"status": "starting", "tools": [], "error": nothing, "started": now()} as key
85
+ let p be nothing
86
+ try
87
+ set p to proc_spawn(command, args, {"cwd": cwd, "env": envs, "stderr": "separate", "line_mode": true, "on_full": "drop_oldest"})
88
+ recover err
89
+ -- Windows: `npx`/`npm` son .cmd — proc_spawn no pasa por el shell, así que se reintenta con la extensión
90
+ when env("OS", "") == "Windows_NT" and contains(text(err), "not found") and not contains(lower(command), ".")
91
+ try
92
+ set p to proc_spawn(command + ".cmd", args, {"cwd": cwd, "env": envs, "stderr": "separate", "line_mode": true, "on_full": "drop_oldest"})
93
+ recover err2
94
+ share {"status": "error", "tools": [], "error": "cannot start: " + text(err2), "started": now()} as key
95
+ otherwise
96
+ share {"status": "error", "tools": [], "error": "cannot start: " + text(err), "started": now()} as key
97
+ when p != nothing
98
+ let seq be 0
99
+ let pending be nothing
100
+ let sub be bus_subscribe(["mcp.req." + name, "mcp.stop." + name, "mcp.stop_all"])
101
+ -- request síncrona (handshake y tools/list): manda y espera la respuesta con ese id, saltando
102
+ -- notificaciones y basura; devuelve {result} | {error} | nothing (timeout / proceso muerto)
103
+ task rpc(method, params, timeout)
104
+ set seq to seq + 1
105
+ let id be seq
106
+ proc_send(p, json_encode({"jsonrpc": "2.0", "id": id, "method": method, "params": params}) + "\n")
107
+ let deadline be now() + timeout
108
+ let answer be nothing
109
+ while answer == nothing and now() < deadline
110
+ let ev be proc_recv(p, deadline - now())
111
+ when ev == nothing
112
+ set answer to {"error": "timeout waiting for " + method}
113
+ otherwise when ev["type"] == "exit"
114
+ set answer to {"error": "server exited with code " + text(ev["data"]["exit_code"])}
115
+ otherwise when ev["type"] == "stdout"
116
+ try
117
+ let m be json_decode(ev["data"])
118
+ when contains(m, "id") and m["id"] == id
119
+ set answer to when contains(m, "error") then {"error": text(m["error"]["message"])} otherwise {"result": m["result"]}
120
+ recover e2
121
+ set answer to answer
122
+ give answer
123
+ task notify(method, params)
124
+ proc_send(p, json_encode({"jsonrpc": "2.0", "method": method, "params": params}) + "\n")
125
+ let init be rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "lampson", "version": "0.1"}}, 30)
126
+ when init == nothing or contains(init, "error")
127
+ share {"status": "error", "tools": [], "error": "initialize failed: " + (when init == nothing then "no answer" otherwise init["error"]), "started": now()} as key
128
+ proc_close(p)
129
+ otherwise
130
+ notify("notifications/initialized", {})
131
+ let listed be rpc("tools/list", {}, 30)
132
+ let tools be []
133
+ when listed != nothing and contains(listed, "result") and contains(listed["result"], "tools")
134
+ each t in listed["result"]["tools"]
135
+ let ro be false
136
+ when contains(t, "annotations") and contains(t["annotations"], "readOnlyHint")
137
+ set ro to t["annotations"]["readOnlyHint"] == true
138
+ set tools to append(tools, {"name": t["name"], "description": when contains(t, "description") then t["description"] otherwise "", "inputSchema": when contains(t, "inputSchema") then t["inputSchema"] otherwise {"type": "object", "properties": {}}, "readonly": ro})
139
+ share {"status": "ready", "tools": tools, "error": nothing, "started": now()} as key
140
+ bus_publish("mcp." + name, {"name": name, "status": "ready", "tools": length(tools)})
141
+ -- loop de servicio: requests por bus, salida/muerte del proceso
142
+ let open be true
143
+ while open
144
+ let ev be select({"p": p, "bus": sub}, 60)
145
+ when ev == nothing
146
+ set open to proc_status(p) == "running"
147
+ otherwise when ev["name"] == "bus"
148
+ when ev["topic"] == "mcp.req." + name
149
+ let r be ev["data"]
150
+ let ans be rpc(r["method"], r["params"], 120)
151
+ bus_publish("mcp.res." + name + "." + text(r["id"]), when ans == nothing then {"error": "no answer"} otherwise ans)
152
+ otherwise
153
+ set open to false
154
+ otherwise when ev["type"] == "exit"
155
+ share {"status": "exited", "tools": [], "error": "server exited with code " + text(ev["data"]["exit_code"]), "started": now()} as key
156
+ bus_publish("mcp." + name, {"name": name, "status": "exited"})
157
+ set open to false
158
+ otherwise when ev["type"] == "stderr"
159
+ bus_publish("mcp." + name, {"name": name, "status": "ready", "line": ev["data"]})
160
+ proc_close(p)
161
+ when proc_status(p) != "running"
162
+ observe key as st
163
+ when st["status"] == "ready"
164
+ share {"status": "stopped", "tools": [], "error": nothing, "started": now()} as key
165
+ bus_unsubscribe(sub)
166
+
167
+ export task state(name)
168
+ observe "mcp:" + name as st
169
+ give st
170
+
171
+ -- arrancar todos los servers configurados (chat/web al iniciar); espera hasta `wait_s` a que estén listos
172
+ export task start_all(wait_s)
173
+ require exec
174
+ require time
175
+ require env("LAMPSON_*")
176
+ require env("OS")
177
+ require file(".lampson")
178
+ require file(".lampson/*")
179
+ require file("workspace")
180
+ require file("workspace/*")
181
+ let list be servers()
182
+ each s in list
183
+ let st be state(s["name"])
184
+ when st == nothing or (st["status"] != "ready" and st["status"] != "starting")
185
+ spawn McpSup with name = s["name"], command = s["command"], args_json = json_encode(s["args"]), env_json = json_encode(s["env"]), cwd = s["cwd"]
186
+ let waited be 0
187
+ while waited < wait_s * 10 and length(where(list, (s) => state(s["name"]) == nothing or state(s["name"])["status"] == "starting")) > 0
188
+ sleep(0.1)
189
+ set waited to waited + 1
190
+ give list
191
+
192
+ export task stop_all()
193
+ bus_publish("mcp.stop_all", {})
194
+
195
+ -- ---------- conectar / desconectar servers (usuario: web o /mcp add · modelo: tool `mcp`, con aprobación) ----------
196
+
197
+ -- "npx -y @modelcontextprotocol/server-github" → {command, args[]}; respeta comillas simples y dobles
198
+ -- (rutas de Windows con espacios). No hay expansión de nada: los tokens van tal cual al spawn.
199
+ export task parse_command(line)
200
+ let toks be []
201
+ let cur be ""
202
+ let quote be ""
203
+ let i be 0
204
+ let n be length(text(line))
205
+ while i < n
206
+ let ch be slice(text(line), i, i + 1)
207
+ when quote != ""
208
+ when ch == quote
209
+ set quote to ""
210
+ otherwise
211
+ set cur to cur + ch
212
+ otherwise when ch == "\"" or ch == "'"
213
+ set quote to ch
214
+ otherwise when ch == " " or ch == "\t"
215
+ when cur != ""
216
+ set toks to append(toks, cur)
217
+ set cur to ""
218
+ otherwise
219
+ set cur to cur + ch
220
+ set i to i + 1
221
+ when cur != ""
222
+ set toks to append(toks, cur)
223
+ when length(toks) == 0
224
+ give {"command": "", "args": []}
225
+ give {"command": toks[0], "args": slice(toks, 1, length(toks))}
226
+
227
+ task config_path(scope)
228
+ give when scope == "project" then PROJECT_CONFIG otherwise GLOBAL_CONFIG
229
+
230
+ -- alta (o reemplazo) en el mcp.json del scope + arranque del supervisor; devuelve una línea de estado.
231
+ -- El archivo queda en el formato estándar {"mcpServers": {...}} — editable a mano y compatible copy-paste.
232
+ export task add_server(name, command_line, env_map, scope)
233
+ require exec
234
+ require time
235
+ require env("LAMPSON_*")
236
+ require env("OS")
237
+ require file(".lampson")
238
+ require file(".lampson/*")
239
+ require file("workspace")
240
+ require file("workspace/*")
241
+ when not valid_name(name)
242
+ raise("invalid server name '" + text(name) + "' (letters, digits, - or _, max 32)")
243
+ let parsed be parse_command(command_line)
244
+ when parsed["command"] == ""
245
+ raise("empty command")
246
+ let sc be when scope == "project" then "project" otherwise "global"
247
+ let path be config_path(sc)
248
+ let doc be {"mcpServers": {}}
249
+ try
250
+ set doc to json_decode(read_file(path))
251
+ recover err
252
+ set doc to {"mcpServers": {}}
253
+ when not contains(doc, "mcpServers")
254
+ set doc["mcpServers"] to {}
255
+ let entry be {"command": parsed["command"], "args": parsed["args"]}
256
+ when env_map != nothing
257
+ when length(keys(env_map)) > 0
258
+ set entry["env"] to env_map
259
+ let servers_map be doc["mcpServers"]
260
+ set servers_map[name] to entry
261
+ set doc["mcpServers"] to servers_map
262
+ write_file(path, json_encode(doc))
263
+ -- reconexión: si ya había un supervisor con este nombre, apagarlo antes de arrancar el nuevo
264
+ let st be state(name)
265
+ when st != nothing
266
+ when st["status"] == "ready" or st["status"] == "starting"
267
+ bus_publish("mcp.stop." + name, {})
268
+ let w be 0
269
+ while w < 20 and state(name)["status"] == "ready"
270
+ sleep(0.1)
271
+ set w to w + 1
272
+ start_all(10)
273
+ let after be state(name)
274
+ when after == nothing
275
+ give "added '" + name + "' (" + sc + ") to " + path + " but it did not start — check the command"
276
+ when after["status"] == "ready"
277
+ give "connected '" + name + "' (" + sc + "): " + text(length(after["tools"])) + " tools — available as mcp_" + name + "_<tool> from the next turn"
278
+ give "added '" + name + "' (" + sc + ") but status is " + after["status"] + (when after["error"] != nothing then ": " + text(after["error"]) otherwise "")
279
+
280
+ -- baja: lo saca del archivo donde esté (de ambos si aparece dos veces) y apaga su supervisor;
281
+ -- las tools mcp_<name>_* desaparecen del catálogo en el próximo turno
282
+ export task remove_server(name)
283
+ require time
284
+ require env("LAMPSON_*")
285
+ require file(".lampson")
286
+ require file(".lampson/*")
287
+ require file("workspace")
288
+ require file("workspace/*")
289
+ when not valid_name(name)
290
+ raise("invalid server name")
291
+ let removed be []
292
+ each pth in [PROJECT_CONFIG, GLOBAL_CONFIG]
293
+ try
294
+ let doc be json_decode(read_file(pth))
295
+ when contains(doc, "mcpServers")
296
+ when contains(doc["mcpServers"], name)
297
+ let kept be {}
298
+ each k in keys(doc["mcpServers"])
299
+ when k != name
300
+ set kept[k] to doc["mcpServers"][k]
301
+ set doc["mcpServers"] to kept
302
+ write_file(pth, json_encode(doc))
303
+ set removed to append(removed, pth)
304
+ recover err
305
+ set removed to removed
306
+ bus_publish("mcp.stop." + name, {})
307
+ when length(removed) == 0
308
+ give "no server named '" + name + "' in " + PROJECT_CONFIG + " or " + GLOBAL_CONFIG
309
+ give "removed '" + name + "' (" + join(removed, ", ") + ") and stopped its supervisor"
310
+
311
+ -- [{name, scope, status, error, tools: n}]
312
+ export task summary()
313
+ require file(".lampson")
314
+ require file(".lampson/*")
315
+ require file("workspace")
316
+ require file("workspace/*")
317
+ let out be []
318
+ each s in servers()
319
+ let st be state(s["name"])
320
+ set out to append(out, {"name": s["name"], "scope": s["scope"], "command": s["command"] + " " + join(apply((a) => text(a), s["args"]), " "), "status": when st == nothing then "off" otherwise st["status"], "error": when st == nothing then nothing otherwise st["error"], "tools": when st == nothing then [] otherwise apply((t) => t["name"], st["tools"])})
321
+ give out
322
+
323
+ -- ---------- tools para el modelo ----------
324
+
325
+ export task tool_name(server, tool)
326
+ give "mcp_" + server + "_" + tool
327
+
328
+ -- catálogo (specs OpenAI-style) de las tools MCP listas; readonly_only = true para perfiles de solo lectura
329
+ export task catalog(readonly_only)
330
+ require file(".lampson")
331
+ require file(".lampson/*")
332
+ require file("workspace")
333
+ require file("workspace/*")
334
+ let out be []
335
+ each s in servers()
336
+ let st be state(s["name"])
337
+ when st != nothing and st["status"] == "ready"
338
+ each t in st["tools"]
339
+ when t["readonly"] or not readonly_only
340
+ set out to append(out, {"name": tool_name(s["name"], t["name"]), "description": "[MCP " + s["name"] + (when t["readonly"] then ", read-only" otherwise "") + "] " + t["description"], "parameters": t["inputSchema"]})
341
+ give out
342
+
343
+ export task names(readonly_only)
344
+ require file(".lampson")
345
+ require file(".lampson/*")
346
+ require file("workspace")
347
+ require file("workspace/*")
348
+ give apply((s) => s["name"], catalog(readonly_only))
349
+
350
+ task split_name(full)
351
+ -- mcp_<server>_<tool>: el server no tiene "_" garantizado, así que probamos cada server configurado
352
+ let rest be slice(full, 4, length(full))
353
+ each s in servers()
354
+ let pre be s["name"] + "_"
355
+ when length(rest) > length(pre) and slice(rest, 0, length(pre)) == pre
356
+ give {"server": s["name"], "tool": slice(rest, length(pre), length(rest))}
357
+ give nothing
358
+
359
+ -- llamada: request por bus al supervisor, respuesta por bus; el contenido vuelve como texto para el modelo
360
+ export task call(full_name, args)
361
+ require time
362
+ require file(".lampson")
363
+ require file(".lampson/*")
364
+ require file("workspace")
365
+ require file("workspace/*")
366
+ let parts be split_name(full_name)
367
+ when parts == nothing
368
+ raise("unknown MCP tool " + full_name)
369
+ let st be state(parts["server"])
370
+ when st == nothing or st["status"] != "ready"
371
+ raise("MCP server '" + parts["server"] + "' is not ready (" + (when st == nothing then "off" otherwise st["status"]) + ")")
372
+ let id be text(floor(now() * 1000000))
373
+ let sub be bus_subscribe("mcp.res." + parts["server"] + "." + id)
374
+ bus_publish("mcp.req." + parts["server"], {"id": id, "method": "tools/call", "params": {"name": parts["tool"], "arguments": when args == nothing then {} otherwise args}})
375
+ let ev be bus_recv(sub, CALL_TIMEOUT + 5)
376
+ bus_unsubscribe(sub)
377
+ when ev == nothing
378
+ raise("MCP call timed out (" + full_name + ")")
379
+ let ans be ev["data"]
380
+ when contains(ans, "error")
381
+ raise("MCP error: " + text(ans["error"]))
382
+ give render(ans["result"])
383
+
384
+ -- resultado MCP → texto: bloques text concatenados; otros tipos se describen; isError → ERROR: …
385
+ task render(result)
386
+ let parts be []
387
+ when contains(result, "content")
388
+ each b in result["content"]
389
+ let ty be when contains(b, "type") then b["type"] otherwise "?"
390
+ when ty == "text"
391
+ set parts to append(parts, text(b["text"]))
392
+ otherwise when ty == "image"
393
+ set parts to append(parts, "[image " + (when contains(b, "mimeType") then b["mimeType"] otherwise "") + ", " + text(length(text(b["data"]))) + " base64 chars]")
394
+ otherwise when ty == "resource"
395
+ set parts to append(parts, "[resource] " + json_encode(b["resource"]))
396
+ otherwise
397
+ set parts to append(parts, json_encode(b))
398
+ when length(parts) == 0 and contains(result, "structuredContent")
399
+ set parts to append(parts, json_encode(result["structuredContent"]))
400
+ let out be join(parts, "\n")
401
+ when contains(result, "isError") and result["isError"] == true
402
+ give "ERROR: " + out
403
+ give c.truncate(out, c.MAX_OUTPUT)
@@ -0,0 +1,154 @@
1
+ -- lib/permission.syn — política de permisos (allow | deny | ask) sobre las tool calls
2
+ --
3
+ -- Mezcla de los tres harnesses:
4
+ -- * evaluate(tool, args, mode) → allow | deny | ask; "ask" suspende hasta que el humano responde.
5
+ -- * 3 niveles — HARDLINE (nunca, ni en yolo), DANGEROUS (pide aprobación; yolo lo pasa; strict lo niega), resto.
6
+ --
7
+ -- Nota Synsema: el aislamiento de FILESYSTEM ya lo da el lenguaje (file("./*") es el workspace,
8
+ -- call_tool intersecta), así que aquí solo miramos el CONTENIDO de los comandos de shell.
9
+ --
10
+ -- Modos (LAMPSON_PERMISSION): "ask" (default) | "yolo" (dangerous → allow) | "strict" (dangerous → deny)
11
+
12
+ -- Tier 1: nunca. Coincidencia por substring, case-insensitive.
13
+ -- (el comando se evalúa con un espacio final añadido, así "rm -rf / " matchea la raíz pero NO "rm -rf /tmp/x")
14
+ export let HARDLINE be [
15
+ "rm -rf / ", "rm -rf /* ", "rm -fr / ", "rm -rf ~ ", "rm -rf ~/ ", "rm -rf * ", "rm -rf . ", "rm -rf ./ ", "rm -rf .. ",
16
+ "mkfs", "dd if=", ":(){", "fork bomb",
17
+ "format c:", "del /s /q c:\\", "rd /s /q c:\\", "rmdir /s /q c:\\",
18
+ "shutdown", "reboot", "> /dev/sda", "chmod -r 777 /",
19
+ "git push --force origin main", "git push -f origin main", "git push --force origin master"
20
+ ]
21
+
22
+ -- Tier 2: pide aprobación (o deny en strict, allow en yolo).
23
+ export let DANGEROUS be [
24
+ "rm -rf", "rm -r", "del /s", "rd /s", "rmdir /s", "remove-item -recurse",
25
+ "git push --force", "git push -f", "git reset --hard", "git clean -f", "git checkout --", "git branch -d",
26
+ "sudo", "curl | sh", "curl | bash", "wget | sh", "| sh", "| bash",
27
+ "drop table", "drop database", "truncate table", "delete from",
28
+ "npm publish", "cargo publish", "pip upload", "twine upload",
29
+ "docker rm", "docker system prune", "kubectl delete",
30
+ "chmod", "chown", "> ~/", "crontab",
31
+ "> .env", ">.env", ">> .env", "tee .env", "> ./.env"
32
+ ]
33
+
34
+ task contains_any(haystack, needles)
35
+ each n in needles
36
+ when contains(haystack, lower(n))
37
+ give n
38
+ give nothing
39
+
40
+ -- ¿es un archivo de secretos real? ".env", ".env.local", ".env.production"… sí; ".env.example/.sample/.template" no.
41
+ task is_secret_env(base)
42
+ when base == ".env"
43
+ give true
44
+ when not starts_with(base, ".env.")
45
+ give false
46
+ each ok in [".env.example", ".env.sample", ".env.template", ".env.dist", ".env.test.example"]
47
+ when base == ok
48
+ give false
49
+ give true
50
+
51
+ -- (`and` no cortocircuita en Synsema: indexar args["action"] sin la clave explota → helper con when anidado)
52
+ task is_install(args)
53
+ when not contains(args, "action")
54
+ give false
55
+ give args["action"] == "install"
56
+
57
+ -- evaluate(tool_name, args, mode) → {decision: "allow"|"deny"|"ask", reason}
58
+ export task evaluate(name, args, mode)
59
+ when name == "bash"
60
+ let cmd be (when contains(args, "command") then lower(text(args["command"])) otherwise "") + " "
61
+ let hard be contains_any(cmd, HARDLINE)
62
+ when hard != nothing
63
+ give {"decision": "deny", "reason": "hardline pattern: " + hard}
64
+ let danger be contains_any(cmd, DANGEROUS)
65
+ when danger != nothing
66
+ when mode == "yolo"
67
+ give {"decision": "allow", "reason": "yolo mode (dangerous: " + danger + ")"}
68
+ when mode == "strict"
69
+ give {"decision": "deny", "reason": "strict mode (dangerous: " + danger + ")"}
70
+ give {"decision": "ask", "reason": "dangerous pattern: " + danger}
71
+ give {"decision": "allow", "reason": "command"}
72
+ when name == "write" or name == "edit"
73
+ -- el scope file("./*") ya impide salir del workspace; aquí solo miramos secretos obvios
74
+ let p be when contains(args, "path") then lower(replace_text(text(args["path"]), "\\", "/")) otherwise ""
75
+ let parts be split(p, "/")
76
+ let base be parts[length(parts) - 1]
77
+ when is_secret_env(base)
78
+ when mode == "yolo"
79
+ give {"decision": "allow", "reason": "yolo mode (.env)"}
80
+ give {"decision": "ask", "reason": "writes a .env file (secrets)"}
81
+ give {"decision": "allow", "reason": "workspace write"}
82
+ when starts_with(name, "mcp_")
83
+ -- tool de un server MCP (terceros, efectos fuera del workspace): humano por defecto; yolo permite; strict deniega
84
+ when mode == "yolo"
85
+ give {"decision": "allow", "reason": "yolo mode (MCP tool)"}
86
+ when mode == "strict"
87
+ give {"decision": "deny", "reason": "strict mode (MCP tool)"}
88
+ give {"decision": "ask", "reason": "MCP tool " + name}
89
+ when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
90
+ -- configurar un language server = el harness va a correr un comando (npx …) del preset: humano siempre
91
+ when mode == "strict"
92
+ give {"decision": "deny", "reason": "strict mode (adds an LSP server)"}
93
+ give {"decision": "ask", "reason": "configures LSP server '" + (when contains(args, "server") then text(args["server"]) otherwise "?") + "' (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ") — it will run the preset command on the first query"}
94
+ when starts_with(name, "lamp_")
95
+ -- tool de una lámpara (código del usuario/agente, un proceso por llamada): como las MCP
96
+ when mode == "yolo"
97
+ give {"decision": "allow", "reason": "yolo mode (lamp tool)"}
98
+ when mode == "strict"
99
+ give {"decision": "deny", "reason": "strict mode (lamp tool)"}
100
+ give {"decision": "ask", "reason": "lamp tool " + name}
101
+ when name == "lamp"
102
+ -- encender una lámpara = autorizar código a correr con las capacidades de su manifiesto: humano SIEMPRE
103
+ let act be when contains(args, "action") then text(args["action"]) otherwise "list"
104
+ when act == "enable" or act == "disable"
105
+ when mode == "strict"
106
+ give {"decision": "deny", "reason": "strict mode (" + act + " lamp)"}
107
+ give {"decision": "ask", "reason": act + "s lamp '" + (when contains(args, "name") then text(args["name"]) otherwise "?") + "'"}
108
+ give {"decision": "allow", "reason": "read-only tool"}
109
+ when name == "mcp"
110
+ -- conectar un server MCP = ejecutar un comando de terceros con env propio: humano siempre, incluso en yolo
111
+ let act be when contains(args, "action") then text(args["action"]) otherwise "list"
112
+ when act == "add" or act == "remove"
113
+ when mode == "strict"
114
+ give {"decision": "deny", "reason": "strict mode (" + act + " MCP server)"}
115
+ give {"decision": "ask", "reason": act + "s MCP server '" + (when contains(args, "name") then text(args["name"]) otherwise "?") + "'" + (when act == "add" then " → runs: " + (when contains(args, "command") then text(args["command"]) otherwise "?") otherwise "")}
116
+ give {"decision": "allow", "reason": "read-only tool"}
117
+ when name == "skill" and is_install(args)
118
+ -- instala instrucciones/scripts de terceros FUERA del workspace (~/.agents/skills): humano siempre, incluso en yolo
119
+ when mode == "strict"
120
+ give {"decision": "deny", "reason": "strict mode (installs a third-party skill)"}
121
+ give {"decision": "ask", "reason": "installs a third-party skill from " + (when contains(args, "source") then text(args["source"]) otherwise "?") + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"}
122
+ give {"decision": "allow", "reason": "read-only tool"}
123
+
124
+ -- Resumen legible de una tool call, para mostrar al humano antes de aprobar / en el log.
125
+ task one_line(s, max)
126
+ let t be replace_text(replace_text(s, "\r", ""), "\n", " ⏎ ")
127
+ when length(t) > max
128
+ give slice(t, 0, max) + "…"
129
+ give t
130
+
131
+ export task describe_call(name, args)
132
+ when starts_with(name, "mcp_") or starts_with(name, "lamp_")
133
+ give name + " " + one_line(json_encode(args), 160)
134
+ when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
135
+ give "lsp add " + (when contains(args, "server") then text(args["server"]) otherwise "?") + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
136
+ when name == "lsp"
137
+ give "lsp " + (when contains(args, "op") then text(args["op"]) otherwise "?") + " " + (when contains(args, "path") then text(args["path"]) otherwise "?") + (when contains(args, "line") then ":" + text(args["line"]) + (when contains(args, "character") then ":" + text(args["character"]) otherwise "") otherwise "")
138
+ when name == "lamp"
139
+ let lact be when contains(args, "action") then text(args["action"]) otherwise "list"
140
+ give "lamp " + lact + (when lact != "list" then " " + (when contains(args, "name") then text(args["name"]) otherwise "?") otherwise "") + (when lact == "create" and contains(args, "files") then " [" + join(keys(args["files"]), ", ") + "]" otherwise "")
141
+ when name == "mcp"
142
+ let act be when contains(args, "action") then text(args["action"]) otherwise "list"
143
+ when act == "add"
144
+ give "mcp add " + (when contains(args, "name") then text(args["name"]) otherwise "?") + " $ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "?", 120) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
145
+ when act == "remove"
146
+ give "mcp remove " + (when contains(args, "name") then text(args["name"]) otherwise "?")
147
+ give "mcp list"
148
+ when name == "skill" and is_install(args)
149
+ give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
150
+ when name == "bash"
151
+ give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
152
+ when contains(args, "path")
153
+ give name + " " + text(args["path"])
154
+ give name + " " + json_encode(args)
package/lib/prompt.syn ADDED
@@ -0,0 +1,75 @@
1
+ -- lib/prompt.syn — el system prompt del agente
2
+ --
3
+ -- Estructura:
4
+ -- identidad → reglas de trabajo → uso de tools → entorno (cwd, OS, fecha) → contexto del proyecto
5
+ -- El contexto del proyecto se lee de AGENTS.md / LAMPSON.md si existen (convención habitual de los coding agents).
6
+
7
+ use "./skills.syn" as skills
8
+ use "./tools/memo.syn" as memo
9
+
10
+ let CONTEXT_FILES be ["AGENTS.md", "LAMPSON.md"]
11
+
12
+ task read_context_files()
13
+ require file.read("workspace")
14
+ require file.read("workspace/*")
15
+ let out be ""
16
+ each f in CONTEXT_FILES
17
+ try
18
+ let c be read_file("workspace/" + f)
19
+ set out to out + `\n\n# Project instructions ({f})\n` + c
20
+ recover err
21
+ set out to out
22
+ give out
23
+
24
+ export task build(env_info, addendum)
25
+ require file.read("workspace")
26
+ require file.read("workspace/*")
27
+ require file.read("skills")
28
+ require file.read("skills/*")
29
+ require file.read("memory")
30
+ require file.read("memory/*")
31
+ require env("LAMPSON_*")
32
+ let core be `You are Lampson, a coding agent that works inside the user's project through tools.
33
+ Operate like a careful senior engineer: precise, honest, and economical with words.
34
+
35
+ # Method — follow these five steps on every non-trivial request
36
+ 1. PLAN. If the task needs 3+ distinct steps (not 3 tool calls for one conceptual step), call todo FIRST and keep it current: one item in_progress at a time, marked completed the moment its work is verified — never batched, never on intent. The todo list survives context compaction; your narration does not. Skip it for a single straightforward change; when in doubt, use it.
37
+ 2. LOCATE. Find code with grep and find, never by reading directories at random. For a broad, open-ended question about how the codebase works, delegate ONE focused question to an explore sub-agent instead of reading everything yourself — the search stays out of your context. Batch independent lookups into a single turn.
38
+ 3. READ ONLY WHAT YOU NEED. Read the files you will change plus the ones you must understand to change them — not the whole project. Every file you read is re-sent on every later call. Use offset/limit for long files, but avoid tiny repeated slices. Trace a symbol to its definition and usages rather than guessing its shape. Never invent files, symbols, APIs or imports — if you have not seen it in this repo, go look. HARD LIMITS enforced by the harness: after 8 read-only calls in a row without acting you get a warning, after 16 read/ls/find/grep are refused until you act; a file read twice unchanged comes back as a one-line receipt.
39
+ 4. CHANGE. Act as soon as you know enough — usually within the first 6-10 reads. Prefer edit (exact-string replacement; old_string must be unique, include surrounding context) over write; write only for new files or full rewrites. edit/write on an existing file are rejected unless you read that file in this session and it has not changed since. Minimal, targeted changes: no drive-by refactors, renames or reformatting; add every import or dependency your code needs. Do NOT print code blocks to the user as a substitute for editing — apply the change, then summarise it. If an edit fails to apply, re-read the file and retry with the exact current text; if the same region fails twice, rewrite the enclosing function with write.
40
+ 5. VERIFY AND REPORT. Run the relevant tests, linter or build with bash and confirm they pass before claiming the work is done — AFTER your edits, not before as reconnaissance. Never assume a test command: check the README, the package manifest or the project instructions. Finish with a short summary: what changed (files, path:line for specific code) and how you verified it. Do not narrate every step.
41
+
42
+ # Finishing the job
43
+ - The deliverable is a working artifact backed by real tool output — not a description of one. Do not stop after a stub, a plan or a single command. If a tool, install or network call fails and blocks the real path, say so and try an alternative. NEVER substitute plausible-looking fabricated output for results you could not produce; reporting a blocker honestly beats inventing a result.
44
+ - You MUST use your tools to take action — do not describe what you would do. When you say "I will run the tests" or "let me check the file", make that tool call in the same response. Never end a turn with a promise of future action. Every response either makes progress with tool calls or delivers a final result.
45
+ - When the harness tells you the token budget is nearly exhausted, stop exploring and produce the deliverable from what you already have.
46
+
47
+ # What NOT to do
48
+ - Never assume a library is available, even a well-known one: check the project manifest and how neighbouring files import it.
49
+ - Do exactly what was asked: no unrequested refactors, no extra features, no gratuitous comments, no new files unless necessary (prefer editing an existing one, including markdown).
50
+ - Do not commit, push or rewrite history unless asked. Never read, print or store secrets — leave .env and credential files alone unless the user asks.
51
+ - If an action is denied by policy or by the user, say so and propose an alternative; never retry the same denied action.
52
+ - Never use placeholders or guess missing parameters in tool calls.
53
+
54
+ # Tools
55
+ - read (offset/limit), ls, find (glob), grep (regex): use them instead of bash with cat/ls/find/grep — line-numbered and cheaper. lsp (symbols/definition/references/hover): the file's structure without reading it, and exact navigation when grep is ambiguous — prefer lsp symbols plus a ranged read over reading a big file whole. edit: targeted replacement. write: create or fully replace.
56
+ - bash: shell commands from the workspace root. State does not persist between calls (cd resets) — chain with &&. Check the [exit code: N] marker on every result (it is the last line) and investigate failures before moving on. Never pipe a build or test through head/tail: the output is already truncated for you (the full text is saved to a file whose path you get) and the pipe hides the real exit code. Never run a server or watcher here.
57
+ - process: start/logs/stop long-running commands (dev servers, watchers). Their new log lines arrive automatically with every later tool result — do not sleep or poll. Stop what you started when the task is done.
58
+ - delegate: sub-agents with a fresh context (explore, plan, review, worker); several tasks in one call run in parallel, background=true returns at once and the report arrives later as a message. Give a highly detailed, self-contained brief, say whether it should write code or only research, how to verify, and exactly what to return. Its report is a self-report — verify what matters before telling the user it is done. Its output is not visible to the user: summarise it.
59
+ - memory: durable facts across sessions (user preferences, environment details, tool quirks, conventions). Check it before investigating. Write declarative facts, not instructions to yourself; do not store task progress or anything stale in a week.
60
+ - skill: before starting a kind of task, scan the skill list below; if one matches or is even partially relevant, load it and follow it. Err on the side of loading.
61
+ - todo: your task list (whole-list replacement, one in_progress at a time; see Method step 1).
62
+ - MCP tools are named mcp_<server>_<tool>; calling one may ask the user for approval. The mcp tool lists, connects (action=add, with the user's approval) and disconnects servers; a new server's tools enter your catalog on the next turn.
63
+ - Call several independent tools in one turn (reads, searches, read-only commands); serialize only when a later call depends on an earlier result. Tool outputs are truncated at 30k chars — narrow your queries.
64
+
65
+ # Tone
66
+ Concise and direct; lead with the change or the answer, not a preamble. Prefer technical accuracy over agreeing with the user: investigate before confirming a belief, disagree when the facts warrant it. Communicate only in your response text — never through bash echo or code comments.
67
+
68
+ # Environment
69
+ - Workspace root (all tool paths are relative to it; you cannot access anything outside it): {env_info["cwd"]}
70
+ - OS: {env_info["os"]} Shell used by bash tool: {env_info["shell"]}
71
+ - Date: {env_info["date"]}
72
+ - Model: {env_info["model"]} via {env_info["provider"]}
73
+ - The user talks to you from the {env_info["ui"]} UI. When something needs the user's hand (turning a lamp on, adding an LSP server, connecting an MCP), tell them the way that exists in THEIR UI: web → the «lámparas» switch / the LSP and MCP sections of the sidebar; terminal → /lamps on <name>, /lsp add <preset>, /mcp add. Never tell a web user to type a slash command — or simply offer to do it yourself (the tool asks them for approval).`
74
+ let mode be when addendum == nothing then "" otherwise (when addendum == "" then "" otherwise "\n\n# Mode\n" + addendum)
75
+ give core + mode + skills.prompt_section(skills.index()) + memo.prompt_section() + read_context_files()