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
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "example-hello",
3
+ "description": "Example lamp: a Synsema program run under a capability ceiling. Copy this folder to make your own.",
4
+ "kind": "syn",
5
+ "entry": "lamp.syn",
6
+ "caps": "",
7
+ "timeout": 20,
8
+ "tools": [
9
+ {
10
+ "name": "greet",
11
+ "description": "Greets someone (demo). Returns a short text.",
12
+ "parameters": {"type": "object", "properties": {"who": {"type": "string", "description": "name to greet"}}, "required": ["who"]},
13
+ "readonly": true
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,19 @@
1
+ -- lamps/example-hello/lamp.syn — la lámpara de ejemplo
2
+ --
3
+ -- Lampson la corre así por cada llamada: synsema run --cap-set stdout,time,env=LAMP_* lamp.syn
4
+ -- El techo (--cap-set) sale del manifiesto (lamp.json → "caps") que el humano aprobó al encenderla;
5
+ -- pedir más acá (p. ej. `require net`) falla con "above the host ceiling".
6
+ -- Entrada por env: LAMP_TOOL (qué tool), LAMP_ARGS (sus args en JSON), LAMP_DIR, LAMP_WORKSPACE.
7
+ -- Salida: lo que imprimas por stdout vuelve al modelo como resultado de la tool.
8
+ intent: "example lamp for lampson: greet"
9
+
10
+ require env("LAMP_*")
11
+
12
+ let tool be env("LAMP_TOOL", "")
13
+ let args be json_decode(env("LAMP_ARGS", "{}"))
14
+
15
+ when tool == "greet"
16
+ let who be when contains(args, "who") then text(args["who"]) otherwise "world"
17
+ print("hello, " + who + "! (from the example-hello lamp, running under a capability ceiling)")
18
+ otherwise
19
+ print("ERROR: unknown tool '" + tool + "'")
package/lampson.cmd ADDED
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ rem lampson.cmd — shim para usar `lampson` desde cualquier carpeta (cmd o PowerShell) una vez en el PATH.
3
+ rem El directorio actual pasa a ser el workspace (ver lampson.ps1).
4
+ pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0lampson.ps1" %*
package/lampson.ps1 ADDED
@@ -0,0 +1,88 @@
1
+ # lampson.ps1 — launcher Windows. Monta el proyecto como ./workspace (junction) y arranca.
2
+ #
3
+ # cd C:\mi\proyecto ; lampson # el directorio ACTUAL es el workspace (REPL)
4
+ # cd C:\mi\proyecto ; lampson --web # servidor web en http://127.0.0.1:8080 (también -Web)
5
+ # lampson --workspace C:\otro\proyecto # elegir la ubicación explícitamente (también -Workspace)
6
+ # lampson --agent plan # perfil inicial: build | plan | review | explore
7
+ # lampson --yolo | --strict | --ask # permisos para comandos peligrosos (--dangerously-skip-permissions = --yolo)
8
+ # lampson --update # actualizar Lampson (git pull) y salir
9
+ # lampson --help
10
+ #
11
+ # Por qué una junction: el scope file("./*") de Synsema v0.6.7 equivale a "*" (disco entero); el scope
12
+ # file("workspace/*") sí confina. Montar el proyecto bajo un nombre literal es lo que hace real el
13
+ # least-privilege de las tools (mismo modelo mental que `docker -v proyecto:/workspace`).
14
+ $ErrorActionPreference = "Stop"
15
+ $here = Split-Path -Parent $MyInvocation.MyCommand.Path
16
+ $caller = (Get-Location).Path
17
+ $mount = Join-Path $here "workspace"
18
+
19
+ # --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
20
+ $Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""
21
+ $i = 0
22
+ while ($i -lt $args.Count) {
23
+ $a = [string]$args[$i]
24
+ switch -Regex ($a.ToLower()) {
25
+ '^--?(web|w)$' { $Web = $true }
26
+ '^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
27
+ '^--?(agent|a)$' { $i++; $Agent = [string]$args[$i] }
28
+ '^--?(yolo|y|dangerously-skip-permissions)$' { $Perm = "yolo" }
29
+ '^--?(strict|s)$' { $Perm = "strict" }
30
+ '^--?(ask)$' { $Perm = "ask" }
31
+ '^--?(permission|p)$' { $i++; $Perm = ([string]$args[$i]).ToLower() }
32
+ '^--?(update|u)$' { Write-Host "actualizando Lampson en $here"; git -C $here pull --ff-only origin main; Write-Host ("lampson " + (git -C $here rev-parse --short HEAD)); exit $LASTEXITCODE }
33
+ '^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First 9 | ForEach-Object { $_.TrimStart('#',' ') }; exit 0 }
34
+ default { if ($Workspace -eq "" -and -not $a.StartsWith("-")) { $Workspace = $a } else { Write-Error "argumento desconocido: $a (probá lampson --help)"; exit 1 } }
35
+ }
36
+ $i++
37
+ }
38
+
39
+ # --- resolver el proyecto: explícito > directorio actual (si no es el propio lampson) > montado previo ---
40
+ if ($Workspace -eq "" -and $caller -ne $here) { $Workspace = $caller }
41
+ if ($Workspace -ne "") {
42
+ if (-not (Test-Path -LiteralPath $Workspace -PathType Container)) { Write-Error "el workspace no existe o no es un directorio: $Workspace"; exit 1 }
43
+ $target = (Resolve-Path -LiteralPath $Workspace).Path
44
+ $home_ = [Environment]::GetFolderPath("UserProfile")
45
+ if ($target.TrimEnd('\') -eq $home_.TrimEnd('\') -or $target -match '^[A-Za-z]:\\?$') {
46
+ Write-Error "'$target' es tu carpeta personal / la raíz del disco, no un proyecto. Entrá al repo (cd) y volvé a correr lampson, o usá --workspace C:\ruta\al\proyecto"; exit 1
47
+ }
48
+ if ($target -eq $here) { Write-Error "no montes el propio directorio de lampson como workspace desde fuera; usá --workspace"; exit 1 }
49
+ if (Test-Path -LiteralPath $mount) {
50
+ $item = Get-Item -LiteralPath $mount -Force
51
+ if ($item.LinkType -ne "Junction") { Write-Error "./workspace existe y no es una junction; movelo antes de montar otro proyecto"; exit 1 }
52
+ $item.Delete()
53
+ }
54
+ New-Item -ItemType Junction -Path $mount -Target $target | Out-Null
55
+ } elseif (-not (Test-Path -LiteralPath $mount)) {
56
+ Write-Error "no hay workspace: corré lampson desde el directorio del proyecto, o lampson --workspace C:\ruta"; exit 1
57
+ }
58
+ $target = (Get-Item -LiteralPath $mount -Force).Target
59
+ if ($target -is [array]) { $target = $target[0] }
60
+
61
+ # --- skills externas globales (npx skills add -g → ~/.agents/skills; las de Claude Code → ~/.claude/skills) ---
62
+ # Se montan como junction bajo .lampson/ porque una capability no puede apuntar a una ruta dinámica (HOME).
63
+ $skillMounts = @{ "skills-global" = (Join-Path $HOME ".agents\skills"); "skills-claude" = (Join-Path $HOME ".claude\skills") }
64
+ New-Item -ItemType Directory -Force (Join-Path $here ".lampson") | Out-Null
65
+ foreach ($k in $skillMounts.Keys) {
66
+ $link = Join-Path $here ".lampson\$k"; $src = $skillMounts[$k]
67
+ if (Test-Path -LiteralPath $link) { $li = Get-Item -LiteralPath $link -Force; if ($li.LinkType -eq "Junction") { $li.Delete() } }
68
+ if (Test-Path -LiteralPath $src -PathType Container) { New-Item -ItemType Junction -Path $link -Target $src | Out-Null }
69
+ }
70
+
71
+ # --- arrancar desde el directorio de lampson (ahí viven .env, lib/, skills/, .lampson/) ---
72
+ # Push/Pop: si PowerShell ejecuta este .ps1 en la shell del usuario (pasa cuando lampson.ps1 y lampson.cmd
73
+ # comparten nombre en el PATH), el cwd del usuario debe quedar como estaba al salir.
74
+ Push-Location $here
75
+ try {
76
+ $env:LAMPSON_WORKSPACE = $target
77
+ if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
78
+ if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
79
+ if ($Web) {
80
+ Write-Host "Lampson web · workspace: $target"
81
+ Write-Host "abrí http://127.0.0.1:8080 (Ctrl+C para parar)"
82
+ synsema serve web.syn
83
+ } else {
84
+ synsema run chat.syn
85
+ }
86
+ } finally {
87
+ Pop-Location
88
+ }
package/lampson.sh ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # lampson.sh — launcher unix. El directorio ACTUAL es el workspace (o --workspace /ruta).
3
+ # cd /mi/proyecto && lampson # REPL (con lampson/ en el PATH)
4
+ # cd /mi/proyecto && lampson --web # servidor web en http://127.0.0.1:8080
5
+ # lampson --workspace /otro/proyecto [--web] [--agent plan]
6
+ set -euo pipefail
7
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ caller="$(pwd)"
9
+ ws=""; web=0
10
+ while [ $# -gt 0 ]; do
11
+ case "$1" in
12
+ --web) web=1 ;;
13
+ --workspace) shift; ws="$1" ;;
14
+ --agent) shift; export LAMPSON_AGENT="$1" ;;
15
+ --yolo|--dangerously-skip-permissions) export LAMPSON_PERMISSION=yolo ;;
16
+ --strict) export LAMPSON_PERMISSION=strict ;;
17
+ --ask) export LAMPSON_PERMISSION=ask ;;
18
+ --update) echo "actualizando Lampson en $here"; git -C "$here" pull --ff-only origin main; echo "lampson $(git -C "$here" rev-parse --short HEAD)"; exit $? ;;
19
+ *) echo "uso: lampson [--web] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
20
+ esac
21
+ shift
22
+ done
23
+ [ -z "$ws" ] && [ "$caller" != "$here" ] && ws="$caller"
24
+ if [ -n "$ws" ]; then
25
+ [ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
26
+ ws="$(cd "$ws" && pwd)"
27
+ if [ -e "$here/workspace" ] && [ ! -L "$here/workspace" ]; then echo "./workspace existe y no es un symlink" >&2; exit 1; fi
28
+ rm -f "$here/workspace"; ln -s "$ws" "$here/workspace"
29
+ elif [ ! -e "$here/workspace" ]; then
30
+ echo "no hay workspace: corré lampson desde el directorio del proyecto, o --workspace /ruta" >&2; exit 1
31
+ fi
32
+ export LAMPSON_WORKSPACE="$(readlink -f "$here/workspace")"
33
+ # skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson/
34
+ mkdir -p "$here/.lampson"
35
+ for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/skills"; do
36
+ link="$here/.lampson/${pair%%:*}"; src="${pair#*:}"
37
+ [ -L "$link" ] && rm -f "$link"
38
+ [ -d "$src" ] && ln -s "$src" "$link"
39
+ done
40
+ echo "workspace -> $LAMPSON_WORKSPACE"
41
+ cd "$here"
42
+ if [ "$web" = 1 ]; then echo "web: http://127.0.0.1:8080"; exec synsema serve web.syn; else exec synsema run chat.syn; fi
package/lib/agents.syn ADDED
@@ -0,0 +1,471 @@
1
+ -- lib/agents.syn — perfiles de agente + la tool `delegate` (subagentes)
2
+ --
3
+ -- Un "modo" (plan, review…) NO es un estado del runtime: es un agente con un allow-list de tools
4
+ -- restringido y un addendum al system prompt. Cambiar de modo = cambiar de perfil.
5
+ --
6
+ -- SUBAGENTES (2026-08-27, tomado de hermes/deepseek/opencode — ver notes/*.md):
7
+ -- * `delegate(tasks=[{agent, brief, context}…])` corre N hijos EN PARALELO (parallel_map: hilos reales),
8
+ -- cada uno con historial propio, perfil restringido y sin `delegate` (profundidad 1). Devuelve un
9
+ -- informe consolidado: por hijo {status, steps, tokens, texto}. El texto es un SELF-REPORT: el padre
10
+ -- debe verificar (rutas, tests) antes de darlo por bueno.
11
+ -- * `background=true`: vuelve enseguida con el id; el hijo corre en un `agent` de Synsema (hilo con
12
+ -- intérprete propio) y, al terminar, el resultado entra en el buzón del padre (loop.inbox_fn) como
13
+ -- mensaje nuevo en su próximo paso — o, si el padre está idle en la terminal, como turno nuevo.
14
+ -- Nunca se muta contexto pasado (prefix cache intacto).
15
+ -- * Cada hijo deja un log vivo en .lampson/agents/<id>.log (tail -f / panel web) y su resultado en
16
+ -- <id>.json. `delegate(action=list|steer|stop|result, id, message)`: steer = texto que el hijo ve en
17
+ -- su próximo paso; stop = corta y devuelve lo parcial.
18
+ -- * Permisos del hijo: NUNCA pregunta al humano (no tiene ask_fn): en modo ask/strict del padre corre en
19
+ -- strict (peligroso → denegado con motivo, el hijo lo informa); en yolo hereda yolo.
20
+ --
21
+ -- Least-privilege en dos capas:
22
+ -- 1. el perfil recorta el ALLOW-LIST (plan no tiene write/edit/bash → el modelo ni las ve);
23
+ -- 2. cada tool sigue corriendo con call_tool (sus `require` ∩ programa).
24
+ --
25
+ -- MIGA runtime: un `agent` de Synsema NO ve las tasks ni imports de su módulo — solo sus parámetros de
26
+ -- spawn, las builtins y las tasks TOP-LEVEL del programa de entrada. Por eso el hijo en background es
27
+ -- `agent Child` → llama `lampson_subagent(spec_json)`, una task de UNA línea que chat.syn y web.syn
28
+ -- definen a nivel top: `task lampson_subagent(spec) give agents.run_child_json(spec)`.
29
+
30
+ use "./tools.syn" as tools
31
+ use "./loop.syn" as loop
32
+ use "./provider.syn" as provider
33
+ use "./prompt.syn" as prompt
34
+ use "./mcp.syn" as mcp
35
+ use "./lamps.syn" as lamps
36
+
37
+ export let DIR be ".lampson/agents"
38
+ let MAX_TASKS be 6
39
+ let PARALLEL be 4
40
+
41
+ export let PROFILES be {
42
+ "build": {
43
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "delegate"],
44
+ "steps": 40,
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
+ },
47
+ "plan": {
48
+ "tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
49
+ "steps": 30,
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
+ },
52
+ "review": {
53
+ "tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
54
+ "steps": 25,
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
+ },
57
+ "explore": {
58
+ "tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
59
+ "steps": 20,
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
+ },
62
+ "worker": {
63
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
64
+ "steps": 40,
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
+ }
67
+ }
68
+
69
+ let CHILD_NOTE be "\n\n# Sub-agent rules\n- You are a delegated sub-agent. Your permission scope was fixed when you started and you cannot ask the user: a denied action stays denied — do not retry it; state the limitation in your report so the delegating agent can handle it.\n- Your final message is your REPORT: concrete, with paths and line numbers (or test output) the parent can verify. No questions."
70
+
71
+ -- perfiles que el usuario puede elegir (selector web, /agent). `worker` es interno: el perfil de los
72
+ -- subagentes que `build` lanza con delegate (sin delegate ni mcp para que un hijo no delegue a su vez).
73
+ export task names()
74
+ give where(keys(PROFILES), (n) => n != "worker")
75
+
76
+ export task profile(name)
77
+ when contains(PROFILES, name)
78
+ give PROFILES[name]
79
+ give PROFILES["build"]
80
+
81
+ -- pasos por turno: LAMPSON_MAX_STEPS (si está definido) gana; si no, el del perfil
82
+ export task steps_for(name)
83
+ require env("LAMPSON_*")
84
+ let e be env("LAMPSON_MAX_STEPS", "")
85
+ when e != ""
86
+ give floor(number(e))
87
+ give profile(name)["steps"]
88
+
89
+ export task addendum_for(name)
90
+ give profile(name)["addendum"]
91
+
92
+ -- tools MCP: build/worker ven todas; plan/review/explore solo las de lectura (annotations.readOnlyHint)
93
+ task mcp_readonly_only(name)
94
+ give not (name == "build" or name == "worker")
95
+
96
+ export task registry_for(name)
97
+ require file(".lampson")
98
+ require file(".lampson/*")
99
+ require file("workspace")
100
+ require file("workspace/*")
101
+ require env("LAMPSON_*")
102
+ let p be profile(name)
103
+ let reg be tools.registry_subset(p["tools"])
104
+ when contains(p["tools"], "delegate")
105
+ set reg["delegate"] to delegate
106
+ each m in mcp.names(mcp_readonly_only(name))
107
+ set reg[m] to "mcp"
108
+ each m in lamps.names(mcp_readonly_only(name))
109
+ set reg[m] to "lamp"
110
+ give reg
111
+
112
+ export task catalog_for(name)
113
+ require file(".lampson")
114
+ require file(".lampson/*")
115
+ require file("workspace")
116
+ require file("workspace/*")
117
+ require env("LAMPSON_*")
118
+ let p be profile(name)
119
+ let cat be tools.catalog_subset(p["tools"])
120
+ when contains(p["tools"], "delegate")
121
+ set cat to append(cat, DELEGATE_SPEC)
122
+ give cat + mcp.catalog(mcp_readonly_only(name)) + lamps.catalog(mcp_readonly_only(name))
123
+
124
+ -- Info de entorno para el system prompt (compartida por chat.syn / web.syn / delegate)
125
+ export task env_info(cfg)
126
+ require env("LAMPSON_*")
127
+ require env("OS")
128
+ require time
129
+ let sc be tools.shell_config()
130
+ -- desde dónde habla el usuario (blackboard "lampson:ui", lo comparte el entry): el modelo NO debe decirle
131
+ -- "/lamps" a alguien que está en la web (2026-08-28)
132
+ observe "lampson:ui" as ui
133
+ give {
134
+ "ui": when ui == nothing then "terminal" otherwise text(ui["kind"]),
135
+ "cwd": env("LAMPSON_WORKSPACE", "./workspace"),
136
+ "os": env("OS", "unix"),
137
+ "shell": sc["shell"],
138
+ "date": format_time(now(), "%Y-%m-%d"),
139
+ "model": cfg["model"],
140
+ "provider": cfg["provider"]
141
+ }
142
+
143
+ -- ---------- estado de los hijos: archivos en .lampson/agents ----------
144
+
145
+ task path(id, ext)
146
+ give DIR + "/" + id + "." + ext
147
+
148
+ task read_json(id)
149
+ try
150
+ give json_decode(read_file(path(id, "json")))
151
+ recover err
152
+ give nothing
153
+
154
+ task write_json(id, m)
155
+ write_file(path(id, "json"), json_encode(m))
156
+
157
+ task one_line(s, max)
158
+ let t be replace_text(replace_text(text(s), "\r", ""), "\n", " ⏎ ")
159
+ when length(t) > max
160
+ give slice(t, 0, max) + "…"
161
+ give t
162
+
163
+ -- log vivo del hijo (una línea por evento) — el padre y el humano lo pueden mirar mientras corre
164
+ export task child_event(kind, data, tag)
165
+ require file(".lampson")
166
+ require file(".lampson/*")
167
+ let line be ""
168
+ when kind == "assistant"
169
+ set line to "[assistant] " + one_line(data, 600)
170
+ otherwise when kind == "tool_call"
171
+ set line to "⚙ " + data["name"] + " " + one_line(json_encode(data["args"]), 300)
172
+ otherwise when kind == "tool_result"
173
+ let out be data["output"]
174
+ let bad be starts_with(out, "ERROR") or starts_with(out, "DENIED")
175
+ set line to (when bad then "✗ " otherwise "✓ ") + one_line(out, 300)
176
+ otherwise when kind == "inbox"
177
+ set line to "[inbox] " + one_line(data, 300)
178
+ otherwise when kind == "error"
179
+ set line to "‼ " + one_line(data, 300)
180
+ otherwise when kind == "compact"
181
+ set line to "⧗ compact"
182
+ when line != ""
183
+ append_file(path(tag, "log"), line + "\n")
184
+ bus_publish("subagent." + tag, {"kind": kind, "line": line})
185
+
186
+ -- buzón del hijo: steer (mensajes del padre) y stop
187
+ export task child_inbox(step, tag)
188
+ require file(".lampson")
189
+ require file(".lampson/*")
190
+ let stop_now be false
191
+ try
192
+ read_file(path(tag, "stop"))
193
+ set stop_now to true
194
+ recover err
195
+ set stop_now to false
196
+ let msgs be []
197
+ try
198
+ let raw be trim(read_file(path(tag, "steer")))
199
+ when raw != ""
200
+ set msgs to ["[harness] Message from the delegating agent: " + raw]
201
+ write_file(path(tag, "steer"), "")
202
+ recover err
203
+ set msgs to msgs
204
+ when stop_now or length(msgs) > 0
205
+ give {"messages": msgs, "stop": stop_now}
206
+ give nothing
207
+
208
+ -- buzón del padre: hijos en background terminados y no entregados
209
+ export task parent_inbox(step, tag)
210
+ require file(".lampson")
211
+ require file(".lampson/*")
212
+ let msgs be []
213
+ each c in list_children()
214
+ when c["status"] != "running" and not c["delivered"]
215
+ set msgs to append(msgs, notice_text(c))
216
+ let m be read_json(c["id"])
217
+ set m["delivered"] to true
218
+ write_json(c["id"], m)
219
+ when length(msgs) > 0
220
+ give {"messages": msgs, "stop": false}
221
+ give nothing
222
+
223
+ export task notice_text(c)
224
+ give "[harness] Sub-agent " + c["id"] + " (" + c["agent"] + ") finished: " + c["status"] + " · " + text(c["steps"]) + " steps · " + text(c["tokens"]) + " tokens.\nTask: " + one_line(c["brief"], 200) + "\nReport (self-report — verify before relying on it):\n" + c["text"]
225
+
226
+ task ends_with(s, suffix)
227
+ when length(s) < length(suffix)
228
+ give false
229
+ give slice(s, length(s) - length(suffix), length(s)) == suffix
230
+
231
+ -- [{id, agent, brief, status, steps, tokens, started, finished, delivered, text}]
232
+ export task list_children()
233
+ require file(".lampson")
234
+ require file(".lampson/*")
235
+ let out be []
236
+ try
237
+ each e in list_dir(DIR)
238
+ when ends_with(e["name"], ".json")
239
+ let m be read_json(slice(e["name"], 0, length(e["name"]) - 5))
240
+ when m != nothing
241
+ set out to append(out, m)
242
+ recover err
243
+ give []
244
+ give sort_by(out, (x) => x["started"])
245
+
246
+ -- poda del historial: los terminados hace más de `max_age_s` segundos, y los que sobran por encima de
247
+ -- `keep` terminados (los más viejos primero). No hay builtin de borrado → rm por bash con ids validados.
248
+ export task prune(keep, max_age_s)
249
+ require exec
250
+ require time
251
+ require env("LAMPSON_*")
252
+ require env("OS")
253
+ require file(".lampson")
254
+ require file(".lampson/*")
255
+ -- un "running" de hace más de 6 h es un fantasma (lampson cerrado con el hijo vivo, o un crash viejo)
256
+ let finished be where(list_children(), (c) => c["status"] != "running" or now() - c["started"] > 21600)
257
+ let victims be []
258
+ let n be length(finished)
259
+ let i be 0
260
+ each c in finished
261
+ set i to i + 1
262
+ let fin be when c["finished"] == nothing then c["started"] otherwise c["finished"]
263
+ let old be false
264
+ when fin != nothing
265
+ set old to now() - fin > max_age_s
266
+ when (old or n - i >= keep) and matches(c["id"], "[a-z]+-[0-9]+")
267
+ set victims to append(victims, c["id"])
268
+ when length(victims) > 0
269
+ let files be []
270
+ each v in victims
271
+ each ext in ["json", "log", "steer", "stop"]
272
+ set files to append(files, "'" + DIR + "/" + v + "." + ext + "'")
273
+ let is_win be env("OS", "") == "Windows_NT"
274
+ let sh be env("LAMPSON_SHELL", when is_win then "C:\Program Files\Git\bin\bash.exe" otherwise "bash")
275
+ run(sh, ["-c", "rm -f " + join(files, " ")], 30, {"cwd": "."})
276
+ give length(victims)
277
+
278
+ -- pedir a todos los hijos vivos que paren (salida de lampson)
279
+ export task stop_all()
280
+ require file(".lampson")
281
+ require file(".lampson/*")
282
+ each c in list_children()
283
+ when c["status"] == "running"
284
+ write_file(path(c["id"], "stop"), "stop")
285
+
286
+ -- ---------- correr UN hijo hasta el final (foreground: parallel_map · background: agent Child) ----------
287
+
288
+ export task run_child(spec)
289
+ require net
290
+ require time
291
+ require exec
292
+ require env("LAMPSON_*")
293
+ require env("OS")
294
+ require secret("LAMPSON_*")
295
+ require file("workspace")
296
+ require file("workspace/*")
297
+ require file.read("skills")
298
+ require file.read("skills/*")
299
+ require file("memory")
300
+ require file("memory/*")
301
+ require file(".lampson")
302
+ require file(".lampson/*")
303
+ let id be spec["id"]
304
+ let agent be spec["agent"]
305
+ let p be PROFILES[agent]
306
+ let cfg be provider.config()
307
+ write_json(id, {"id": id, "agent": agent, "brief": spec["brief"], "status": "running", "steps": 0, "tokens": 0, "started": spec["started"], "finished": nothing, "delivered": not spec["background"], "text": "", "background": spec["background"]})
308
+ write_file(path(id, "log"), "# sub-agent " + id + " (" + agent + ")\n# " + one_line(spec["brief"], 300) + "\n")
309
+ -- TODO lo que puede fallar va dentro del try: si algo revienta (prompt, provider, loop) el hijo queda
310
+ -- en status "error" y no como un "running" fantasma (pasó 2026-08-28 con un prompt.syn roto)
311
+ let result be nothing
312
+ try
313
+ let system be {"role": "system", "content": prompt.build(env_info(cfg), p["addendum"] + CHILD_NOTE)}
314
+ let full_brief be when spec["context"] == nothing or spec["context"] == "" then spec["brief"] otherwise spec["brief"] + "\n\nContext from the delegating agent:\n" + spec["context"]
315
+ let messages be [system, {"role": "user", "content": full_brief}]
316
+ let opts be loop.default_opts(tools.registry_subset(p["tools"]), tools.catalog_subset(p["tools"]), nothing)
317
+ set opts to loop.with_steps(opts, p["steps"])
318
+ set opts to loop.with_mode(opts, when spec["mode"] == "yolo" then "yolo" otherwise "strict")
319
+ set opts to loop.with_inbox(opts, child_inbox, id)
320
+ set result to loop.run_turn(cfg, messages, opts, child_event)
321
+ recover err
322
+ append_file(path(id, "log"), "‼ " + one_line(err, 300) + "\n")
323
+ set result to {"text": "(the sub-agent crashed: " + one_line(err, 300) + ")", "steps": 0, "usage": {"input": 0, "output": 0}, "stopped": "error"}
324
+ let status be when result["stopped"] == "done" then "done" otherwise result["stopped"]
325
+ let tokens be result["usage"]["input"] + result["usage"]["output"]
326
+ let final be {"id": id, "agent": agent, "brief": spec["brief"], "status": status, "steps": result["steps"], "tokens": tokens, "started": spec["started"], "finished": now(), "delivered": not spec["background"], "text": when result["text"] == "" then "(the sub-agent produced no final text)" otherwise result["text"], "background": spec["background"]}
327
+ write_json(id, final)
328
+ append_file(path(id, "log"), "# finished: " + status + " · " + text(result["steps"]) + " steps · " + text(tokens) + " tokens\n")
329
+ bus_publish("subagent.done", {"id": id, "status": status})
330
+ give final
331
+
332
+ -- entrada para el agente en background (recibe JSON porque los parámetros del spawn se copian como valores)
333
+ export task run_child_json(spec_json)
334
+ require net
335
+ require time
336
+ require exec
337
+ require env("LAMPSON_*")
338
+ require env("OS")
339
+ require secret("LAMPSON_*")
340
+ require file("workspace")
341
+ require file("workspace/*")
342
+ require file.read("skills")
343
+ require file.read("skills/*")
344
+ require file("memory")
345
+ require file("memory/*")
346
+ require file(".lampson")
347
+ require file(".lampson/*")
348
+ give run_child(json_decode(spec_json))
349
+
350
+ -- hilo con intérprete propio: solo puede llamar tasks top-level del programa de entrada. Declara sus
351
+ -- capabilities (un agente no hereda las del padre: sin `require` propio, env()/net fallan adentro).
352
+ agent Child
353
+ require net
354
+ require time
355
+ require exec
356
+ require env("LAMPSON_*")
357
+ require env("OS")
358
+ require secret("LAMPSON_*")
359
+ require file("workspace")
360
+ require file("workspace/*")
361
+ require file.read("skills")
362
+ require file.read("skills/*")
363
+ require file("memory")
364
+ require file("memory/*")
365
+ require file(".lampson")
366
+ require file(".lampson/*")
367
+ lampson_subagent(spec_json)
368
+
369
+ task format_report(c)
370
+ give "[" + c["id"] + " · " + c["agent"] + " · " + c["status"] + " · " + text(c["steps"]) + " steps · " + text(c["tokens"]) + " tokens · log: " + DIR + "/" + c["id"] + ".log]\n" + c["text"]
371
+
372
+ -- el hijo nunca pregunta: hereda yolo si el operador lo fijó (LAMPSON_PERMISSION); si no, strict
373
+ task child_mode()
374
+ require env("LAMPSON_*")
375
+ give when lower(env("LAMPSON_PERMISSION", "ask")) == "yolo" then "yolo" otherwise "strict"
376
+
377
+ -- id único aun para tasks creadas en el mismo milisegundo (índice dentro del batch)
378
+ task new_id(agent, i)
379
+ give agent + "-" + slice(text(floor(now() * 1000)), 7, 13) + text(i)
380
+
381
+ -- ---------- delegate (tool) ----------
382
+
383
+ export task delegate(agent, brief, context, tasks, background, action, id, message)
384
+ require net
385
+ require time
386
+ require exec
387
+ require env("LAMPSON_*")
388
+ require env("OS")
389
+ require secret("LAMPSON_*")
390
+ require file("workspace")
391
+ require file("workspace/*")
392
+ require file.read("skills")
393
+ require file.read("skills/*")
394
+ require file("memory")
395
+ require file("memory/*")
396
+ require file(".lampson")
397
+ require file(".lampson/*")
398
+ let act be when action == nothing or action == "" then "spawn" otherwise action
399
+ when act == "list"
400
+ let lines be []
401
+ each c in list_children()
402
+ set lines to append(lines, c["id"] + " " + c["status"] + " " + text(c["steps"]) + " steps " + one_line(c["brief"], 80))
403
+ give when length(lines) == 0 then "no sub-agents yet" otherwise join(lines, "\n")
404
+ when act == "steer" or act == "stop" or act == "result"
405
+ let c be when id == nothing then nothing otherwise read_json(id)
406
+ when c == nothing
407
+ raise("unknown sub-agent id '" + text(id) + "' (use action=list)")
408
+ when act == "result"
409
+ give format_report(c)
410
+ when c["status"] != "running"
411
+ give "sub-agent " + id + " already finished (" + c["status"] + "); use action=result to read its report"
412
+ when act == "stop"
413
+ write_file(path(id, "stop"), "stop")
414
+ give "stop requested for " + id + "; its partial report will arrive in your inbox"
415
+ when message == nothing or message == ""
416
+ raise("steer needs a message")
417
+ append_file(path(id, "steer"), message + "\n")
418
+ give "message queued for " + id + " (it reads it before its next step)"
419
+ -- spawn
420
+ prune(10, 1800)
421
+ let items be [{"agent": agent, "brief": brief, "context": context}]
422
+ when tasks != nothing
423
+ when length(tasks) > 0
424
+ set items to tasks
425
+ when length(items) > MAX_TASKS
426
+ raise("at most " + text(MAX_TASKS) + " tasks per delegate call")
427
+ let specs be []
428
+ let i be 0
429
+ each t in items
430
+ set i to i + 1
431
+ let a be when contains(t, "agent") then text(t["agent"]) otherwise ""
432
+ when not contains(PROFILES, a)
433
+ raise("unknown agent '" + a + "'; available: explore, plan, review, worker")
434
+ when a == "build"
435
+ raise("cannot delegate to 'build' (a child never gets `delegate`): use worker for scoped implementation, or explore/plan/review")
436
+ when not contains(t, "brief") or trim(text(t["brief"])) == ""
437
+ raise("every task needs a brief")
438
+ set specs to append(specs, {"id": new_id(a, i), "agent": a, "brief": text(t["brief"]), "context": when contains(t, "context") then text(t["context"]) otherwise "", "mode": child_mode(), "background": background == true, "started": now()})
439
+ when background == true
440
+ let lines be []
441
+ each sp in specs
442
+ write_json(sp["id"], {"id": sp["id"], "agent": sp["agent"], "brief": sp["brief"], "status": "running", "steps": 0, "tokens": 0, "started": sp["started"], "finished": nothing, "delivered": false, "text": "", "background": true})
443
+ write_file(path(sp["id"], "log"), "# starting…\n")
444
+ spawn Child with spec_json = json_encode(sp)
445
+ bus_publish("subagent.started", {"id": sp["id"], "agent": sp["agent"]})
446
+ set lines to append(lines, "started sub-agent " + sp["id"] + " (" + sp["agent"] + ") in the background · live log: " + DIR + "/" + sp["id"] + ".log")
447
+ give join(lines, "\n") + "\nYou will receive each report automatically as a new message when it finishes — keep working on independent steps; do NOT poll or sleep. delegate(action=steer|stop, id=…) to redirect or cut it."
448
+ let results be parallel_map(run_child, specs, PARALLEL)
449
+ let parts be []
450
+ each c in results
451
+ set parts to append(parts, format_report(c))
452
+ give join(parts, "\n\n") + "\n\n(Reports are self-reports: verify paths, diffs and test results before relying on them.)"
453
+
454
+ export let DELEGATE_SPEC be {
455
+ "name": "delegate",
456
+ "description": "Run sub-agents: each gets its own fresh context, a restricted toolset and returns only its final report (it does not see your conversation — give it a self-contained brief). Agents: `explore` (read-only; find where things are), `plan` (read-only; implementation plan), `review` (read-only files, can run tests/commands; find bugs), `worker` (can edit files and run commands: a scoped implementation task). Several tasks in one call run IN PARALLEL and come back consolidated — use that for independent questions or independent changes in different files. background=true returns immediately with ids; each report arrives later as a message in your context (do not poll). action=list|steer|stop|result manages background sub-agents (steer sends a message the child reads before its next step; stop returns its partial report). Reports are SELF-REPORTS: verify what matters (read the file, run the test) before telling the user it is done. Sub-agents cannot ask the user and cannot delegate.",
457
+ "parameters": {"type": "object", "properties": {
458
+ "tasks": {"type": "array", "description": "Batch: [{agent, brief, context?}] — they run in parallel (max 6)", "items": {"type": "object", "properties": {
459
+ "agent": {"type": "string", "enum": ["explore", "plan", "review", "worker"]},
460
+ "brief": {"type": "string"},
461
+ "context": {"type": "string"}
462
+ }, "required": ["agent", "brief"]}},
463
+ "agent": {"type": "string", "enum": ["explore", "plan", "review", "worker"], "description": "Single task form (alternative to tasks)"},
464
+ "brief": {"type": "string", "description": "Self-contained instructions for the sub-agent"},
465
+ "context": {"type": "string", "description": "Optional extra context (relevant paths, findings so far)"},
466
+ "background": {"type": "boolean", "description": "true: return immediately; reports arrive later as messages"},
467
+ "action": {"type": "string", "enum": ["spawn", "list", "steer", "stop", "result"], "description": "default spawn"},
468
+ "id": {"type": "string", "description": "steer/stop/result: the sub-agent id"},
469
+ "message": {"type": "string", "description": "steer: text the sub-agent will read before its next step"}
470
+ }}
471
+ }