lampson 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -95,7 +95,8 @@ plugins were called *lamps*; old `.lampson/lamps/` folders and `LAMP_*` variable
95
95
 
96
96
  Sub-agents that work in parallel · skills (`SKILL.md` procedures, anything on [skills.sh](https://skills.sh)
97
97
  works) · MCP servers with the JSON you already have · language servers for real go-to-definition · project
98
- memory it reads back next session · sessions with a readable trace of every step · paste a screenshot and ask.
98
+ memory it reads back next session · sessions with a readable trace of every step · web pages fetched as
99
+ Markdown (a tenth of the tokens of raw HTML) · paste a screenshot and ask.
99
100
 
100
101
  ## Learn more
101
102
 
package/chat.syn CHANGED
@@ -165,7 +165,7 @@ task print_diff_with(lead, path, d)
165
165
 
166
166
  -- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
167
167
  task is_instant(name)
168
- give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp"
168
+ give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp" or name == "fetch"
169
169
 
170
170
  let pending_call be ""
171
171
 
package/lib/agents.syn CHANGED
@@ -40,27 +40,27 @@ let PARALLEL be 4
40
40
 
41
41
  export let PROFILES be {
42
42
  "build": {
43
- "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "schedule", "delegate"],
43
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "schedule", "delegate"],
44
44
  "steps": 40,
45
45
  "addendum": "You are in BUILD mode: you may read, edit, create files and run commands. Delegate exploration of large codebases to the `explore` agent and independent code review to the `review` agent when it saves context; several independent questions can go in one delegate call (they run in parallel)."
46
46
  },
47
47
  "plan": {
48
- "tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
48
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "todo", "skill", "delegate"],
49
49
  "steps": 30,
50
50
  "addendum": "You are in PLAN mode: READ-ONLY. You cannot edit files or run commands (those tools are not available). Investigate the codebase, then answer with a concrete, numbered implementation plan: files to touch, what changes in each, risks, and how to verify. Do not write code beyond short illustrative snippets."
51
51
  },
52
52
  "review": {
53
- "tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
53
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "skill"],
54
54
  "steps": 25,
55
55
  "addendum": "You are a REVIEWER: read-only on files (no write/edit), but you may run commands (tests, linters, git diff). Find real bugs, missing error handling, security issues and violations of the project's conventions. Report findings as a list with file:line, severity and a suggested fix. Do not modify files."
56
56
  },
57
57
  "explore": {
58
- "tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
58
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "skill"],
59
59
  "steps": 20,
60
60
  "addendum": "You are an EXPLORER: read-only. Locate the code relevant to the question (files, functions, line numbers) and report it concisely with paths and short excerpts. Do not propose changes."
61
61
  },
62
62
  "worker": {
63
- "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
63
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill"],
64
64
  "steps": 40,
65
65
  "addendum": "You are a WORKER sub-agent: implement exactly the scoped task you were given (read, edit, run tests), then report what you changed (files) and how you verified it. Stay inside the scope; if something outside it is needed, report it instead of doing it."
66
66
  }
package/lib/fs.syn ADDED
@@ -0,0 +1,226 @@
1
+ -- lib/fs.syn — operaciones de archivos del explorador web: crear, renombrar, mover, copiar, borrar
2
+ --
3
+ -- El runtime tiene write_file/list_dir/file_info pero NO rename/delete/copy/mkdir (v0.6.14), así que mover,
4
+ -- copiar y borrar corren por el shell POSIX (Git Bash en Windows), igual que plugins.remove. Todo path viene
5
+ -- del navegador y pasa por common.ws(): relativo al workspace, sin `..` ni absolutos (raise instructivo).
6
+ -- Reglas al estilo VS Code: nunca pisar un destino existente; duplicar = "<nombre> copy<.ext>", luego
7
+ -- "<nombre> copy 2<.ext>"…; una carpeta no se mueve/copia dentro de sí misma; la raíz no se toca.
8
+ use "./tools/common.syn" as c
9
+
10
+ task sh_quote(s)
11
+ give "'" + replace_text(s, "'", "'\\''") + "'"
12
+
13
+ -- shell POSIX aunque LAMPSON_SHELL apunte a cmd: mv/cp/rm/mkdir son de Git Bash
14
+ task shell()
15
+ require env("LAMPSON_*")
16
+ require env("OS")
17
+ let is_win be env("OS", "") == "Windows_NT"
18
+ let sh be env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
19
+ when is_win and not c.ends_with(lower(sh), "bash.exe")
20
+ set sh to "C:\\Program Files\\Git\\bin\\bash.exe"
21
+ give sh
22
+
23
+ -- corre un comando con cwd = workspace; error legible si falla
24
+ task sh(cmd)
25
+ require exec
26
+ require env("LAMPSON_*")
27
+ require env("OS")
28
+ let r be run(shell(), ["-c", cmd], 30, {"cwd": c.ROOT})
29
+ when r["exit_code"] != 0
30
+ let err be trim(text(r["stderr"]))
31
+ raise(when err == "" then "command failed: " + cmd otherwise err)
32
+ give true
33
+
34
+ -- path del navegador → {rel (como lo ve el modelo, "." = raíz), real ("workspace/…"), info}
35
+ task target(path)
36
+ require file.read("workspace")
37
+ require file.read("workspace/*")
38
+ let real be c.ws(path)
39
+ give {"rel": c.unws(real), "real": real, "info": file_info(real)}
40
+
41
+ task parent_of(rel)
42
+ when not contains(rel, "/")
43
+ give "."
44
+ let parts be split(rel, "/")
45
+ give join(slice(parts, 0, length(parts) - 1), "/")
46
+
47
+ task base_of(rel)
48
+ let parts be split(rel, "/")
49
+ give parts[length(parts) - 1]
50
+
51
+ task join_rel(dir, name)
52
+ give when dir == "." or dir == "" then name otherwise dir + "/" + name
53
+
54
+ task check_name(name)
55
+ let n be trim(text(name))
56
+ when n == "" or n == "." or n == ".." or contains(n, "/") or contains(n, "\\")
57
+ raise("invalid name \"" + text(name) + "\": one path segment, without / or \\")
58
+ when length(find_all(n, "[<>:\"|?*]")) > 0
59
+ raise("invalid name \"" + n + "\": < > : \" | ? * are not allowed in file names")
60
+ give n
61
+
62
+ task must_exist(t)
63
+ when not t["info"]["exists"]
64
+ raise("\"" + t["rel"] + "\" does not exist")
65
+
66
+ task must_not_exist(rel)
67
+ require file.read("workspace")
68
+ require file.read("workspace/*")
69
+ when file_exists(c.ROOT + "/" + rel)
70
+ raise("\"" + rel + "\" already exists")
71
+
72
+ -- ¿`path` es `dir` o está dentro de `dir`?
73
+ task inside(dir, path)
74
+ when dir == "."
75
+ give true
76
+ give path == dir or starts_with(path, dir + "/")
77
+
78
+ task must_be_dir(rel, info)
79
+ when not info["exists"]
80
+ raise("\"" + rel + "\" does not exist")
81
+ when not info["is_dir"]
82
+ raise("\"" + rel + "\" is not a folder")
83
+
84
+ export task mkdir(dir, name)
85
+ require exec
86
+ require env("LAMPSON_*")
87
+ require env("OS")
88
+ require file("workspace")
89
+ require file("workspace/*")
90
+ let t be target(dir)
91
+ let rel be join_rel(t["rel"], check_name(name))
92
+ must_not_exist(rel)
93
+ sh("mkdir -p " + sh_quote(rel))
94
+ give {"path": rel, "is_dir": true}
95
+
96
+ export task create(dir, name, content)
97
+ require file("workspace")
98
+ require file("workspace/*")
99
+ let t be target(dir)
100
+ let rel be join_rel(t["rel"], check_name(name))
101
+ must_not_exist(rel)
102
+ write_file(c.ROOT + "/" + rel, when content == nothing then "" otherwise text(content))
103
+ give {"path": rel, "is_dir": false}
104
+
105
+ export task rename(path, name)
106
+ require exec
107
+ require env("LAMPSON_*")
108
+ require env("OS")
109
+ require file("workspace")
110
+ require file("workspace/*")
111
+ let t be target(path)
112
+ must_exist(t)
113
+ when t["rel"] == "."
114
+ raise("the workspace root cannot be renamed")
115
+ let rel be join_rel(parent_of(t["rel"]), check_name(name))
116
+ when rel == t["rel"]
117
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
118
+ -- solo cambian mayúsculas: en un FS case-insensitive el destino "existe" → pasar por un temporal
119
+ when lower(rel) == lower(t["rel"])
120
+ let tmp be rel + ".lampson-rename-tmp"
121
+ sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(tmp) + " && mv " + sh_quote(tmp) + " " + sh_quote(rel))
122
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
123
+ must_not_exist(rel)
124
+ sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
125
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
126
+
127
+ export task move(path, to_dir)
128
+ require exec
129
+ require env("LAMPSON_*")
130
+ require env("OS")
131
+ require file("workspace")
132
+ require file("workspace/*")
133
+ let t be target(path)
134
+ must_exist(t)
135
+ when t["rel"] == "."
136
+ raise("the workspace root cannot be moved")
137
+ let d be target(to_dir)
138
+ must_be_dir(d["rel"], d["info"])
139
+ when t["info"]["is_dir"] and inside(t["rel"], d["rel"])
140
+ raise("cannot move a folder into itself")
141
+ let rel be join_rel(d["rel"], base_of(t["rel"]))
142
+ when rel == t["rel"]
143
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
144
+ must_not_exist(rel)
145
+ sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
146
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
147
+
148
+ -- "b.ts" en `dir` → "b copy.ts", "b copy 2.ts"…; carpetas y dotfiles: "sub copy", ".env copy"
149
+ task copy_name(dir, base)
150
+ require file.read("workspace")
151
+ require file.read("workspace/*")
152
+ let stem be base
153
+ let ext be ""
154
+ let parts be split(base, ".")
155
+ when length(parts) > 1 and parts[0] != ""
156
+ set ext to "." + parts[length(parts) - 1]
157
+ set stem to slice(base, 0, length(base) - length(ext))
158
+ let n be 1
159
+ let cand be stem + " copy" + ext
160
+ while file_exists(c.ROOT + "/" + join_rel(dir, cand)) and n < 100
161
+ set n to n + 1
162
+ set cand to stem + " copy " + text(n) + ext
163
+ give cand
164
+
165
+ -- to_dir = nothing → duplicar al lado; otra carpeta → copiar ahí (mismo nombre, o "copy" si ya hay uno)
166
+ export task copy(path, to_dir)
167
+ require exec
168
+ require env("LAMPSON_*")
169
+ require env("OS")
170
+ require file("workspace")
171
+ require file("workspace/*")
172
+ let t be target(path)
173
+ must_exist(t)
174
+ when t["rel"] == "."
175
+ raise("the workspace root cannot be copied")
176
+ let dir be when to_dir == nothing then parent_of(t["rel"]) otherwise target(to_dir)["rel"]
177
+ must_be_dir(dir, file_info(c.ws(dir)))
178
+ when t["info"]["is_dir"] and inside(t["rel"], dir)
179
+ raise("cannot copy a folder into itself")
180
+ let base be base_of(t["rel"])
181
+ let rel be join_rel(dir, base)
182
+ when rel == t["rel"] or file_exists(c.ROOT + "/" + rel)
183
+ set rel to join_rel(dir, copy_name(dir, base))
184
+ sh("cp -r " + sh_quote(t["rel"]) + " " + sh_quote(rel))
185
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
186
+
187
+ export task remove(path)
188
+ require exec
189
+ require env("LAMPSON_*")
190
+ require env("OS")
191
+ require file("workspace")
192
+ require file("workspace/*")
193
+ let t be target(path)
194
+ must_exist(t)
195
+ when t["rel"] == "."
196
+ raise("the workspace root cannot be deleted")
197
+ when t["rel"] == ".git" or t["rel"] == ".lampson"
198
+ raise("\"" + t["rel"] + "\" is not deleted from the explorer")
199
+ sh("rm -rf " + sh_quote(t["rel"]))
200
+ give {"path": t["rel"], "is_dir": t["info"]["is_dir"]}
201
+
202
+ -- POST /api/fs: {op, path, name?, to?, content?} → {path, is_dir}
203
+ export task apply(b)
204
+ require exec
205
+ require env("LAMPSON_*")
206
+ require env("OS")
207
+ require file("workspace")
208
+ require file("workspace/*")
209
+ let op be when contains(b, "op") then lower(text(b["op"])) otherwise ""
210
+ let path be when contains(b, "path") then text(b["path"]) otherwise "."
211
+ let name be when contains(b, "name") then b["name"] otherwise nothing
212
+ -- (`to` es palabra reservada: `set x to`)
213
+ let dest be when contains(b, "to") then text(b["to"]) otherwise nothing
214
+ when op == "mkdir"
215
+ give mkdir(path, name)
216
+ when op == "create"
217
+ give create(path, name, when contains(b, "content") then b["content"] otherwise "")
218
+ when op == "rename"
219
+ give rename(path, name)
220
+ when op == "move"
221
+ give move(path, when dest == nothing then "." otherwise dest)
222
+ when op == "copy"
223
+ give copy(path, dest)
224
+ when op == "delete"
225
+ give remove(path)
226
+ raise("unknown op '" + op + "' (mkdir, create, rename, move, copy, delete)")
package/lib/loop.syn CHANGED
@@ -47,7 +47,7 @@ let REPEAT_HARD be 8
47
47
  -- seguidas sin ninguna acción (edit/write/bash/process/delegate/mcp). Visto 2026-08-28 con deepseek-v4-pro:
48
48
  -- 39 read/ls/find seguidos, 0 ediciones, 419k tokens, dos veces, con las reglas de prompt ignoradas.
49
49
  -- A la mitad del tope el resultado lleva un aviso; al tope, read/ls/find/grep se rechazan hasta que actúe.
50
- let READ_ONLY_TOOLS be ["read", "ls", "find", "grep"]
50
+ let READ_ONLY_TOOLS be ["read", "ls", "find", "grep", "fetch"]
51
51
  let ACTION_TOOLS be ["edit", "write", "bash", "process", "delegate"]
52
52
 
53
53
  export task explore_cap()
@@ -67,7 +67,7 @@ export task explore_verdict(streak, name, cap)
67
67
  when n >= cap
68
68
  give {"streak": n, "note": "", "refuse": true}
69
69
  when n == floor(cap / 2)
70
- give {"streak": n, "note": "\n\n[harness] " + text(n) + " read-only calls in a row without changing anything. You likely know enough: edit/write now, or run the relevant command. After " + text(cap) + " read-only calls in a row, read/ls/find/grep are refused until you act. If the codebase is genuinely too big, delegate ONE focused question to an `explore` sub-agent instead of reading everything yourself.", "refuse": false}
70
+ give {"streak": n, "note": "\n\n[harness] " + text(n) + " read-only calls in a row without changing anything. You likely know enough: edit/write now, or run the relevant command. After " + text(cap) + " read-only calls in a row, read/ls/find/grep/fetch are refused until you act. If the codebase is genuinely too big, delegate ONE focused question to an `explore` sub-agent instead of reading everything yourself.", "refuse": false}
71
71
  give {"streak": n, "note": "", "refuse": false}
72
72
 
73
73
  -- JSON canónico (claves ordenadas en profundidad): reordenar propiedades no engaña al detector
@@ -98,7 +98,8 @@ task safe_id(id, step)
98
98
  export task spill(name, id, out)
99
99
  require file(".lampson")
100
100
  require file(".lampson/*")
101
- when name == "read" or length(out) <= SPILL_CAP
101
+ -- read y fetch se recortan solas (fetch: cabeza+cola con el texto completo ya en .lampson/spill)
102
+ when name == "read" or name == "fetch" or length(out) <= SPILL_CAP
102
103
  give out
103
104
  -- el informe de los subagentes es el entregable: no se manda a disco (hasta 60k)
104
105
  when name == "delegate" and length(out) <= SPILL_CAP * 6
@@ -394,7 +395,7 @@ export task run_turn(cfg, messages, opts, on_event)
394
395
  set out to "ERROR: you already called " + tc["name"] + " with these exact arguments " + text(repeats) + " times in a row. The result will not change; try a different approach or report the problem."
395
396
  emit(on_event, "tool_denied", {"call": tc, "reason": "repeated call (" + text(repeats) + "x)"}, tag)
396
397
  otherwise when ex["refuse"]
397
- set out to "ERROR: exploration cap reached — " + text(explore_streak) + " read-only calls in a row (read/ls/find/grep) without a single change. Reading more will not help. Do one of: (1) edit/write the files you already read; (2) run a command (bash/process) that moves the task; (3) delegate ONE focused question to an `explore` sub-agent; (4) tell the user what you need. read/ls/find/grep are refused until you do."
398
+ set out to "ERROR: exploration cap reached — " + text(explore_streak) + " read-only calls in a row (read/ls/find/grep) without a single change. Reading more will not help. Do one of: (1) edit/write the files you already read; (2) run a command (bash/process) that moves the task; (3) delegate ONE focused question to an `explore` sub-agent; (4) tell the user what you need. read/ls/find/grep/fetch are refused until you do."
398
399
  emit(on_event, "tool_denied", {"call": tc, "reason": "exploration cap (" + text(explore_streak) + " read-only calls without acting)"}, tag)
399
400
  otherwise when contains(READ_ONLY_TOOLS, tc["name"]) and reads_total >= turn_cap
400
401
  set out to "ERROR: this turn already made " + text(reads_total) + " read-only calls (limit " + text(turn_cap) + "). Everything you read is in your context. Act on it now (edit/write/run), delegate ONE focused question to an `explore` sub-agent, or report to the user."
@@ -9,6 +9,8 @@
9
9
  --
10
10
  -- Modos (LAMPSON_PERMISSION): "ask" (default) | "yolo" (dangerous → allow) | "strict" (dangerous → deny)
11
11
 
12
+ use "./tools/url.syn" as u
13
+
12
14
  -- Tier 1: nunca. Coincidencia por substring, case-insensitive.
13
15
  -- (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
16
  export let HARDLINE be [
@@ -69,6 +71,27 @@ export task evaluate(name, args, mode)
69
71
  give {"decision": "deny", "reason": "strict mode (dangerous: " + danger + ")"}
70
72
  give {"decision": "ask", "reason": "dangerous pattern: " + danger}
71
73
  give {"decision": "allow", "reason": "command"}
74
+ when name == "fetch"
75
+ -- política de hosts de url.syn (hermes url_safety): secretos en la URL y metadata de la nube se
76
+ -- deniegan SIEMPRE (incluso en yolo); hosts privados/loopback (el dev server del usuario) piden;
77
+ -- lo público se permite. Una URL inválida se permite: la tool devuelve el error explicado.
78
+ let furl be when contains(args, "url") then text(args["url"]) otherwise ""
79
+ let why be u.sensitive(furl)
80
+ when why != nothing
81
+ give {"decision": "deny", "reason": "the URL carries " + why + " — secrets never travel in URLs"}
82
+ let fp be u.parse(furl)
83
+ when not fp["ok"]
84
+ give {"decision": "allow", "reason": "invalid URL (the tool explains)"}
85
+ let cls be u.host_class(fp["host"])
86
+ when cls == "blocked"
87
+ give {"decision": "deny", "reason": "cloud metadata endpoint " + fp["host"]}
88
+ when cls == "private"
89
+ when mode == "yolo"
90
+ give {"decision": "allow", "reason": "yolo mode (private host " + fp["host"] + ")"}
91
+ when mode == "strict"
92
+ give {"decision": "deny", "reason": "strict mode (private host " + fp["host"] + ")"}
93
+ give {"decision": "ask", "reason": "fetches a private/loopback host (" + fp["origin"] + ")"}
94
+ give {"decision": "allow", "reason": "public URL"}
72
95
  when name == "write" or name == "edit"
73
96
  -- el scope file("./*") ya impide salir del workspace; aquí solo miramos secretos obvios
74
97
  let p be when contains(args, "path") then lower(replace_text(text(args["path"]), "\\", "/")) otherwise ""
@@ -167,6 +190,9 @@ export task describe_call(name, args)
167
190
  give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
168
191
  when name == "bash"
169
192
  give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
193
+ when name == "fetch"
194
+ let ffmt be arg(args, "format", "markdown")
195
+ give "fetch " + one_line(arg(args, "url", "?"), 140) + (when ffmt != "markdown" then " (" + ffmt + ")" otherwise "")
170
196
  -- lectura/búsqueda: como lo escribiría un humano en la shell
171
197
  when name == "read"
172
198
  let rng be when contains(args, "offset") or contains(args, "limit") then " (" + (when contains(args, "offset") then "desde " + text(floor(number(text(args["offset"])))) otherwise "") + (when contains(args, "limit") then " " + text(floor(number(text(args["limit"])))) + " líneas" otherwise "") + ")" otherwise ""
package/lib/prompt.syn CHANGED
@@ -53,6 +53,7 @@ Operate like a careful senior engineer: precise, honest, and economical with wor
53
53
 
54
54
  # Tools
55
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
+ - fetch (url, format, max_chars): web pages and HTTP APIs as Markdown — docs, READMEs, issues, changelogs, JSON. Use it instead of bash with curl/wget for anything you will read: it asks the site for Markdown first and strips scripts, styles and navigation, so a page costs a tenth of its HTML. A long page comes back head+tail with the full text saved to a file: read (offset/limit) or grep that file, never fetch the same URL again. Cite the URL when you use its content. Use bash+curl only for POST, custom headers or downloads.
56
57
  - 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
58
  - 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
59
  - 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.