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.
- package/.env.example +29 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/lampson.js +81 -0
- package/chat.syn +799 -0
- package/lamps/example-hello/lamp.json +16 -0
- package/lamps/example-hello/lamp.syn +19 -0
- package/lampson.cmd +4 -0
- package/lampson.ps1 +88 -0
- package/lampson.sh +42 -0
- package/lib/agents.syn +471 -0
- package/lib/git.syn +58 -0
- package/lib/lamps.syn +386 -0
- package/lib/loop.syn +455 -0
- package/lib/lsp.syn +503 -0
- package/lib/mcp.syn +403 -0
- package/lib/permission.syn +154 -0
- package/lib/prompt.syn +75 -0
- package/lib/provider.syn +522 -0
- package/lib/session.syn +111 -0
- package/lib/settings.syn +70 -0
- package/lib/skills.syn +179 -0
- package/lib/tools/bash.syn +105 -0
- package/lib/tools/common.sh +49 -0
- package/lib/tools/common.syn +91 -0
- package/lib/tools/edit.syn +32 -0
- package/lib/tools/find.syn +35 -0
- package/lib/tools/grep.syn +31 -0
- package/lib/tools/img.ps1 +36 -0
- package/lib/tools/img.sh +22 -0
- package/lib/tools/ls.syn +18 -0
- package/lib/tools/memo.syn +148 -0
- package/lib/tools/proc.sh +42 -0
- package/lib/tools/proc.syn +314 -0
- package/lib/tools/process.syn +46 -0
- package/lib/tools/read.syn +25 -0
- package/lib/tools/skill.syn +14 -0
- package/lib/tools/todo.syn +97 -0
- package/lib/tools/write.syn +22 -0
- package/lib/tools.syn +198 -0
- package/lib/trace.syn +116 -0
- package/lib/tree.syn +59 -0
- package/lib/update.syn +57 -0
- package/package.json +40 -0
- package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-400-latin.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin.woff2 +0 -0
- package/public/index.html +1268 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +218 -0
- package/public/vendor/xterm.js +2 -0
- package/skills/debugging/SKILL.md +33 -0
- package/skills/lampson/SKILL.md +117 -0
- package/skills/synsema/SKILL.md +75 -0
- 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()
|