lampson 0.1.2 → 0.1.4
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 +9 -0
- package/README.md +60 -2
- package/chat.syn +264 -9
- package/lampson.ps1 +17 -3
- package/lampson.sh +17 -3
- package/lib/agents.syn +1 -1
- package/lib/approvals.syn +176 -0
- package/lib/diff.syn +37 -11
- package/lib/line.syn +13 -8
- package/lib/loop.syn +25 -11
- package/lib/md.syn +76 -18
- package/lib/permission.syn +18 -0
- package/lib/prompt.syn +1 -1
- package/lib/sched_run.syn +161 -0
- package/lib/schedule.syn +660 -0
- package/lib/session.syn +38 -1
- package/lib/settings.syn +55 -0
- package/lib/tools.syn +94 -2
- package/lib/ui.syn +161 -0
- package/package.json +1 -1
- package/public/css/chat.css +56 -0
- package/public/css/layout.css +97 -0
- package/public/css/panel.css +125 -0
- package/public/css/sidebar.css +80 -0
- package/public/css/tokens.css +57 -0
- package/public/index.html +49 -1187
- package/public/js/agents.js +31 -0
- package/public/js/app.js +21 -0
- package/public/js/approvals.js +26 -0
- package/public/js/chat.js +92 -0
- package/public/js/config.js +98 -0
- package/public/js/core.js +88 -0
- package/public/js/events.js +31 -0
- package/public/js/lamps.js +112 -0
- package/public/js/lsp.js +81 -0
- package/public/js/mcp.js +74 -0
- package/public/js/memory.js +17 -0
- package/public/js/panel.js +101 -0
- package/public/js/procs.js +45 -0
- package/public/js/schedules.js +118 -0
- package/public/js/sessions.js +69 -0
- package/public/js/sidebar.js +33 -0
- package/public/js/terminal.js +49 -0
- package/public/js/theme.js +6 -0
- package/public/js/todo.js +10 -0
- package/public/js/tree.js +53 -0
- package/public/js/update.js +14 -0
- package/skills/lampson/SKILL.md +16 -0
- package/web.syn +106 -15
package/.env.example
CHANGED
|
@@ -27,3 +27,12 @@ LAMPSON_API_KEY=sk-...
|
|
|
27
27
|
|
|
28
28
|
# Reanudar una sesión en chat.syn
|
|
29
29
|
# LAMPSON_SESSION=20260826-233000
|
|
30
|
+
|
|
31
|
+
# Tareas programadas y aprobaciones a distancia (proceso residente: lampson --daemon start)
|
|
32
|
+
# LAMPSON_TZ=-03:00 # zona horaria para "daily 09:00" (default: la del sistema, `date +%z`)
|
|
33
|
+
# LAMPSON_PUBLIC_URL=https://lampson.midominio.com # URL por la que se llega a este lampson desde afuera (túnel, VPS,
|
|
34
|
+
# # edge Synsema con TLS): con esto cada aprobación pendiente trae links de un solo uso
|
|
35
|
+
# # (GET /approve/<id>/<token>?d=yes|no) para decidir desde el móvil
|
|
36
|
+
# LAMPSON_WEBHOOK_URL=https://hooks.example.com/lampson # POST JSON por cada aprobación pendiente (id, mensaje, links);
|
|
37
|
+
# # reenvialo a Telegram/Slack/mail con n8n, un bot o un .syn de 6 líneas
|
|
38
|
+
# LAMPSON_WEBHOOK_SECRET=… # firma HMAC-SHA256 del body (X-Lampson-Signature: sha256=<hex>)
|
package/README.md
CHANGED
|
@@ -29,6 +29,14 @@ language itself — and every step is visible, in the terminal or in a web UI.
|
|
|
29
29
|
under a capability ceiling, or any executable (js, py, sh…). Global in `lampson/lamps/<name>/`, per
|
|
30
30
|
project in `.lampson/lamps/<name>/` (the agent can write those). **Off by default**: you turn them on
|
|
31
31
|
from the top bar of the web UI or `/lamps on <name>`; their tools join the catalog as `lamp_<lamp>_<tool>`.
|
|
32
|
+
- **Scheduled tasks**: "every 6h", "daily 09:00", "mon,wed 08:30" — a lamp tool, a fixed shell command, or a
|
|
33
|
+
full unattended agent run from a prompt with a permission envelope fixed when you create it (`strict` /
|
|
34
|
+
`ask` / `yolo`). They run inside Lampson's resident process (`lampson --daemon start`, or the open web UI),
|
|
35
|
+
the run's session shows up as `⏰ name`, and a webhook can receive each result (for "search and send me" tasks).
|
|
36
|
+
- **Approvals from anywhere**: an unattended run that hits something dangerous waits for you — in the web UI
|
|
37
|
+
(«Aprobaciones»), and, with `LAMPSON_PUBLIC_URL` + `LAMPSON_WEBHOOK_URL`, through a signed webhook carrying
|
|
38
|
+
one-time **decision links** you can open from your phone (Telegram, mail, Slack…). Never auto-approved:
|
|
39
|
+
no answer in time = denied.
|
|
32
40
|
- **Project memory**: the agent keeps its own notes per project (`memory/<project>/*.md`, outside
|
|
33
41
|
the repo) — how to run it, gotchas, decisions — and rereads them in the next session. You can read
|
|
34
42
|
and edit them (web panel, `/memory`).
|
|
@@ -116,6 +124,46 @@ does the same with persistent sessions and memory.
|
|
|
116
124
|
> Status: developed and tested on Windows 11; the Docker image (Ubuntu 24.04) is built by CI. The Linux/macOS
|
|
117
125
|
> installer is written but not yet exercised on a real machine — issues welcome.
|
|
118
126
|
|
|
127
|
+
## Scheduled tasks and the resident process
|
|
128
|
+
|
|
129
|
+
Lampson can run things with nobody at the keyboard. Scheduled tasks run inside **any** open Lampson — the web UI
|
|
130
|
+
or the terminal REPL (a background thread ticks every 30 s). To run them with nothing open:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
lampson --daemon start # web UI + scheduler + approvals, in the background (synsema daemon)
|
|
134
|
+
lampson --daemon status # also: stop · logs · restart
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Timezone, public URL and webhook are set from the ⚙ button of the web header (tabs: General · Aprobaciones a
|
|
138
|
+
distancia · Proveedor; stored in `lampson/.lampson/config.json`, `.env` wins) or from `.env`.
|
|
139
|
+
|
|
140
|
+
Create a task from the chat («todos los días a las 9 corré los tests y avisame» — the agent calls the
|
|
141
|
+
`schedule` tool and asks you to approve the task, showing exactly what will run and with which permissions),
|
|
142
|
+
from the web sidebar («Programadas» → +), or from the terminal (`/schedule add <json>`). Three kinds:
|
|
143
|
+
|
|
144
|
+
| kind | what runs | authorization |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `lamp` | a tool of a lamp that is ON | turning the lamp on |
|
|
147
|
+
| `bash` | one fixed command (must finish on its own) | approved once, at creation |
|
|
148
|
+
| `prompt` | a full agent turn with your instructions and a profile (`build` / `review` / `plan` / `explore`) | the permission envelope: `strict` (dangerous → denied), `ask` (dangerous → approval request, denied if unanswered within `approval_timeout`, 2 h by default), `yolo` |
|
|
149
|
+
|
|
150
|
+
Schedules: `every 6h` · `every 30m` · `daily 09:00` · `mon,wed 08:30` · `weekdays 09:00` (local time; `LAMPSON_TZ`
|
|
151
|
+
overrides). Synsema's `cron_every` is a pure interval, so a 30 s tick in `web.syn` translates it to wall-clock time;
|
|
152
|
+
a run missed while the daemon was down executes when it comes back and is marked as late. State lives in
|
|
153
|
+
`.lampson/schedules.json`, logs in `.lampson/schedules/<id>.log`; a `prompt` run also leaves a normal session (`⏰ name`)
|
|
154
|
+
with its trace. `notify` = a webhook URL that receives each result as JSON.
|
|
155
|
+
|
|
156
|
+
**Approving from your phone.** Set `LAMPSON_PUBLIC_URL` (how this Lampson is reached from outside — a tunnel, a VPS,
|
|
157
|
+
or a Synsema edge with TLS in front) and `LAMPSON_WEBHOOK_URL` (+ `LAMPSON_WEBHOOK_SECRET`, HMAC-SHA256 in
|
|
158
|
+
`X-Lampson-Signature`). Every pending approval — from a scheduled run or from the chat — POSTs `{id, message, why,
|
|
159
|
+
expires_at, respond_link_yes, respond_link_no}`; forward the links wherever you read (n8n, a bot, a 6-line `.syn`).
|
|
160
|
+
`GET /approve/<id>/<token>?d=yes|no` is public on purpose: the 32-byte one-time token is the authorization, and it
|
|
161
|
+
dies with the deadline. `GET /api/approvals` (loopback) lists what is pending, never the tokens.
|
|
162
|
+
|
|
163
|
+
For a real server use systemd (`synsema serve web.syn`, `Restart=always`) instead of `synsema daemon`, which has no
|
|
164
|
+
boot start / crash restart. One box, several projects: one `web.syn` per project on its own port, behind a Synsema
|
|
165
|
+
edge that terminates TLS and routes by host (see the Synsema deploy docs).
|
|
166
|
+
|
|
119
167
|
## Use
|
|
120
168
|
|
|
121
169
|
```bash
|
|
@@ -139,6 +187,10 @@ servers, flags), `↑↓` browse history or the menu, `Alt+Enter` inserts a newl
|
|
|
139
187
|
last tool result in full, `Ctrl+U`/`Ctrl+W` clear the line/word, `Esc` closes the menu. Approvals are
|
|
140
188
|
an arrow-key menu (`permitir`/`denegar`, or `p`/`d`).
|
|
141
189
|
|
|
190
|
+
While the model thinks or a slow tool runs (a long `bash`, a sub-agent), a status line shows what is
|
|
191
|
+
running, the elapsed time and a bar that fills as you wait (`⠋ pensando ▰▰▱▱▱▱▱▱▱▱ 12s`); it appears
|
|
192
|
+
after 0.4 s so instant tools do not flicker, and anything you type meanwhile lands in the prompt.
|
|
193
|
+
|
|
142
194
|
The terminal renders the model's markdown (headings, lists, tables, code fences) and shows every tool
|
|
143
195
|
result: `edit`/`write` print a line diff (`- red / + green`, line numbers, 2 lines of context); other
|
|
144
196
|
tools are collapsed to 15 lines. `/out [n]` prints the n-th last result of the turn in full and
|
|
@@ -181,7 +233,13 @@ run Lampson in a container.
|
|
|
181
233
|
lampson.ps1 / .sh launcher: mounts ./workspace, starts terminal or web
|
|
182
234
|
chat.syn terminal REPL (colors, approvals via Synsema's native `approve`)
|
|
183
235
|
web.syn HTTP server: POST /api/chat → SSE events; sessions, tree, file viewer, processes, ports
|
|
184
|
-
public/
|
|
236
|
+
public/ web UI (no build step, no dependencies; classic scripts served by `static`)
|
|
237
|
+
index.html markup only: header, the two side panels, the chat; loads css/ and js/ in order
|
|
238
|
+
css/ tokens (fonts, palette, base) · layout (grid, header, panels, chat, composer) · sidebar · chat · panel
|
|
239
|
+
js/core.js shared state + helpers ($, esc, md, add, api, showPane/showText, inlineConfirm, debounce, empty)
|
|
240
|
+
js/panel.js THE modal component: one shell, three layouts (browse = search + list + detail, tabs, form)
|
|
241
|
+
js/<view>.js one file per thing on screen: sessions, chat, tree, terminal, procs, agents, memory, todo,
|
|
242
|
+
mcp, lsp, lamps, schedules, approvals, config (+ provider), update, events (SSE), app (boot)
|
|
185
243
|
lib/
|
|
186
244
|
provider.syn config from .env · chat(cfg, messages, catalog) · retry with backoff
|
|
187
245
|
loop.syn run_turn(): LLM → tool calls → permissions → call_tool → results → repeat; doom-loop guard; compaction
|
|
@@ -299,7 +357,7 @@ Borrowed from the harness that does each part best (see `notes/*.md`):
|
|
|
299
357
|
| Budget runs out silently | 80 %: a notice appended to the latest tool result (no new user message, cache stays warm); 95 %: last step without tools, summary required | hermes / opencode |
|
|
300
358
|
| Edits a file it never read, or one that changed | **Observation gate in code**: `read` records the file hash; `edit`/`write` on an existing file are rejected without it, or if the file changed since | deepseek |
|
|
301
359
|
| Loses the plan | `todo` tool (whole-list replacement, one `in_progress` at a time, scoped to the session like the three references); re-injected only after context compaction, active items only | hermes, opencode |
|
|
302
|
-
| Reads the whole project before touching anything | **Exploration cap** (ours): after
|
|
360
|
+
| Reads the whole project before touching anything | **Exploration cap** (ours): after 12 read-only calls in a row (read/ls/find/grep) without an edit/write/command the result carries a warning; after 24 they are refused until it acts (`LAMPSON_EXPLORE_CAP`) | — |
|
|
303
361
|
|
|
304
362
|
### Sub-agents
|
|
305
363
|
|
package/chat.syn
CHANGED
|
@@ -14,6 +14,7 @@ require net
|
|
|
14
14
|
require time
|
|
15
15
|
require stdin
|
|
16
16
|
require exec
|
|
17
|
+
require random
|
|
17
18
|
require env("LAMPSON_*")
|
|
18
19
|
require env("OS")
|
|
19
20
|
require secret("LAMPSON_*")
|
|
@@ -46,10 +47,14 @@ use "./lib/trace.syn" as trace
|
|
|
46
47
|
use "./lib/md.syn" as md
|
|
47
48
|
use "./lib/diff.syn" as diff
|
|
48
49
|
use "./lib/line.syn" as ed
|
|
50
|
+
use "./lib/ui.syn" as ui
|
|
49
51
|
use "./lib/mcp.syn" as mcp
|
|
50
52
|
use "./lib/lamps.syn" as lamps
|
|
51
53
|
use "./lib/lsp.syn" as lsp
|
|
52
54
|
use "./lib/tools/todo.syn" as todo
|
|
55
|
+
use "./lib/schedule.syn" as schedule
|
|
56
|
+
use "./lib/sched_run.syn" as sched_run
|
|
57
|
+
use "./lib/approvals.syn" as approvals
|
|
53
58
|
|
|
54
59
|
-- ---------- UI de terminal ----------
|
|
55
60
|
-- Colores ANSI (Windows Terminal, PowerShell 7, cualquier terminal moderna). LAMPSON_NO_COLOR=1 los apaga.
|
|
@@ -107,6 +112,15 @@ task summarize(name, out)
|
|
|
107
112
|
set used to used + length(l) + 2
|
|
108
113
|
let rest be length(lines) - length(shown)
|
|
109
114
|
give text(length(lines)) + (when name == "ls" then " entradas: " otherwise " archivos: ") + join(shown, " ") + (when rest > 0 then dim(" +" + text(rest)) otherwise "")
|
|
115
|
+
when name == "delegate" and not VERBOSE
|
|
116
|
+
-- una línea por subagente: el encabezado [id · agente · estado · pasos · tokens · log]
|
|
117
|
+
let heads be []
|
|
118
|
+
each l in lines
|
|
119
|
+
when starts_with(trim(l), "[") and contains(l, " steps") or (starts_with(trim(l), "[") and contains(l, " · log:"))
|
|
120
|
+
set heads to append(heads, trim(l))
|
|
121
|
+
when length(heads) == 0
|
|
122
|
+
give first_line(out, 120)
|
|
123
|
+
give join(heads, "\n ")
|
|
110
124
|
when name == "grep" and not VERBOSE
|
|
111
125
|
let files be {}
|
|
112
126
|
each l in lines
|
|
@@ -136,11 +150,21 @@ task diff_of(name, out)
|
|
|
136
150
|
give diff.diff(d["old"], d["new"], 2, COLOR)
|
|
137
151
|
|
|
138
152
|
task print_diff(path, d)
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
print_diff_with(" ", path, d)
|
|
154
|
+
|
|
155
|
+
-- lead = lo que va antes del ✓ (la llamada, si se fusionó en la misma línea)
|
|
156
|
+
task print_diff_with(lead, path, d)
|
|
157
|
+
let head be green("✓") + " " + c("34", path) + " " + green("+" + text(d["added"])) + " " + red("−" + text(d["removed"]))
|
|
158
|
+
print(lead + head)
|
|
141
159
|
each l in d["lines"]
|
|
142
160
|
print(" " + l)
|
|
143
161
|
|
|
162
|
+
-- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
|
|
163
|
+
task is_instant(name)
|
|
164
|
+
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"
|
|
165
|
+
|
|
166
|
+
let pending_call be ""
|
|
167
|
+
|
|
144
168
|
-- /out: resultado completo de una tool del turno
|
|
145
169
|
task print_full(o)
|
|
146
170
|
when o["diff"] != nothing
|
|
@@ -149,17 +173,110 @@ task print_full(o)
|
|
|
149
173
|
each l in split(o["output"], "\n")
|
|
150
174
|
print(" " + l)
|
|
151
175
|
|
|
176
|
+
-- ---------- indicador de espera ----------
|
|
177
|
+
-- El intérprete principal se bloquea en la llamada al modelo y en cada tool; el que dibuja es un agente
|
|
178
|
+
-- (hilo real) que mira el blackboard: `lampson:busy` = {label, kind, since} mientras hay algo en curso.
|
|
179
|
+
-- Arranca a dibujar recién a los 0,4 s (las tools instantáneas no parpadean) y suelta la terminal
|
|
180
|
+
-- (`lampson:spinner:holding` = false) antes de que el principal vuelva a imprimir.
|
|
181
|
+
agent Spinner
|
|
182
|
+
require time
|
|
183
|
+
let frames be ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
184
|
+
let i be 0
|
|
185
|
+
let drawn be false
|
|
186
|
+
let run be true
|
|
187
|
+
-- MIGA: un agente NO puede abrir la terminal (term_open → nothing) pero sí print+flush. Cada print de un
|
|
188
|
+
-- agente sale con el prefijo "[Spinner_0] " en la línea de abajo: cada frame limpia esa línea (\r ESC[K),
|
|
189
|
+
-- sube una (ESC[1A) y redibuja la suya. Al terminar deja el cursor en la línea del spinner, vacía.
|
|
190
|
+
let clear_here be "\r" + esc + "[K"
|
|
191
|
+
let up be esc + "[1A"
|
|
192
|
+
while run
|
|
193
|
+
observe "lampson:spinner:stop" as st
|
|
194
|
+
observe "lampson:busy" as b
|
|
195
|
+
when st == true
|
|
196
|
+
set run to false
|
|
197
|
+
otherwise when b == nothing
|
|
198
|
+
when drawn
|
|
199
|
+
print(clear_here + up + clear_here + up)
|
|
200
|
+
flush()
|
|
201
|
+
set drawn to false
|
|
202
|
+
share false as "lampson:spinner:holding"
|
|
203
|
+
sleep(0.05)
|
|
204
|
+
otherwise
|
|
205
|
+
let el be now() - b["since"]
|
|
206
|
+
when el >= 0.4
|
|
207
|
+
-- barra de "costo hundido": se llena rápido al principio y cada vez más despacio
|
|
208
|
+
let fill be floor(10 * (1 - exp(0 - el / 40)))
|
|
209
|
+
let bar be ""
|
|
210
|
+
let k be 0
|
|
211
|
+
while k < 10
|
|
212
|
+
set bar to bar + (when k < fill then "▰" otherwise "▱")
|
|
213
|
+
set k to k + 1
|
|
214
|
+
let hint be when el > 90 then " esto está tardando (Ctrl+C corta)" otherwise (when el > 30 then " ya casi" otherwise (when el > 8 then " sigue trabajando" otherwise ""))
|
|
215
|
+
let secs be text(floor(el)) + "s"
|
|
216
|
+
let label be b["label"]
|
|
217
|
+
when length(label) > 60
|
|
218
|
+
set label to slice(label, 0, 59) + "…"
|
|
219
|
+
let frame be frames[i % 10]
|
|
220
|
+
let body be when color then esc + "[36m" + frame + esc + "[0m " + label + " " + esc + "[2m" + bar + " " + secs + hint + esc + "[0m" otherwise frame + " " + label + " " + bar + " " + secs + hint
|
|
221
|
+
when drawn
|
|
222
|
+
print(clear_here + up + clear_here + " " + body)
|
|
223
|
+
otherwise
|
|
224
|
+
share true as "lampson:spinner:holding"
|
|
225
|
+
print(clear_here + " " + body)
|
|
226
|
+
set drawn to true
|
|
227
|
+
flush()
|
|
228
|
+
set i to i + 1
|
|
229
|
+
sleep(0.08)
|
|
230
|
+
when drawn
|
|
231
|
+
print(clear_here + up + clear_here + up)
|
|
232
|
+
flush()
|
|
233
|
+
share false as "lampson:spinner:holding"
|
|
234
|
+
|
|
235
|
+
task busy_on(label, kind)
|
|
236
|
+
share {"label": label, "kind": kind, "since": now()} as "lampson:busy"
|
|
237
|
+
|
|
238
|
+
-- apagar y esperar a que el agente suelte la terminal (máx. 1 s) antes de volver a imprimir
|
|
239
|
+
task busy_off()
|
|
240
|
+
share nothing as "lampson:busy"
|
|
241
|
+
let waited be 0
|
|
242
|
+
let holding be true
|
|
243
|
+
while holding and waited < 100
|
|
244
|
+
observe "lampson:spinner:holding" as hd
|
|
245
|
+
set holding to hd == true
|
|
246
|
+
when holding
|
|
247
|
+
sleep(0.01)
|
|
248
|
+
set waited to waited + 1
|
|
249
|
+
|
|
152
250
|
-- tag = "" para el agente principal; los subagentes en background NO pasan por acá (escriben su log)
|
|
153
251
|
task on_event(kind, data, tag)
|
|
154
252
|
trace.event(sid, kind, data, tag)
|
|
155
|
-
when kind == "
|
|
253
|
+
when kind == "busy"
|
|
254
|
+
busy_on(data["label"], data["kind"])
|
|
255
|
+
otherwise when kind == "idle"
|
|
256
|
+
busy_off()
|
|
257
|
+
otherwise when kind == "assistant"
|
|
258
|
+
-- ● marca el turno del asistente (primera línea con contenido)
|
|
259
|
+
let lines be split(md.render(data, COLOR), "\n")
|
|
260
|
+
let marked be false
|
|
261
|
+
let out_lines be []
|
|
262
|
+
each l in lines
|
|
263
|
+
when not marked and trim(l) != "" and starts_with(l, " ")
|
|
264
|
+
set out_lines to append(out_lines, c("36;1", "● ") + slice(l, 2, length(l)))
|
|
265
|
+
set marked to true
|
|
266
|
+
otherwise
|
|
267
|
+
set out_lines to append(out_lines, l)
|
|
156
268
|
print("")
|
|
157
|
-
print(
|
|
269
|
+
print(join(out_lines, "\n"))
|
|
158
270
|
print("")
|
|
159
271
|
otherwise when kind == "inbox"
|
|
160
272
|
print(" " + cyan("✉ " + first_line(data, 140)))
|
|
161
273
|
otherwise when kind == "tool_call"
|
|
162
|
-
|
|
274
|
+
let desc be permission.describe_call(data["name"], data["args"])
|
|
275
|
+
when is_instant(data["name"])
|
|
276
|
+
set pending_call to desc
|
|
277
|
+
otherwise
|
|
278
|
+
set pending_call to ""
|
|
279
|
+
print(" " + yellow("▸ " + desc))
|
|
163
280
|
otherwise when kind == "tool_result"
|
|
164
281
|
let out be data["output"]
|
|
165
282
|
let name be data["call"]["name"]
|
|
@@ -167,11 +284,17 @@ task on_event(kind, data, tag)
|
|
|
167
284
|
let d be diff_of(name, out)
|
|
168
285
|
let path be when d != nothing then text(data["call"]["args"]["path"]) otherwise ""
|
|
169
286
|
set turn_outputs to append(turn_outputs, {"name": name, "args": data["call"]["args"], "output": out, "diff": d, "path": path})
|
|
287
|
+
let lead be when pending_call != "" then " " + yellow("▸ " + pending_call) + " " otherwise " "
|
|
288
|
+
set pending_call to ""
|
|
170
289
|
when d != nothing
|
|
171
|
-
|
|
290
|
+
print_diff_with(lead, path, d)
|
|
172
291
|
otherwise
|
|
173
292
|
let mark be when bad then red("✗") otherwise green("✓")
|
|
174
|
-
|
|
293
|
+
let s be summarize(name, out)
|
|
294
|
+
-- una sola línea → cortar al ancho; multilínea (bash) se deja
|
|
295
|
+
when not contains(s, "\n")
|
|
296
|
+
set s to ui.cut(s, ui.cols() - ui.width(lead) - 4)
|
|
297
|
+
print(lead + mark + " " + (when bad then red(s) otherwise dim(s)))
|
|
175
298
|
otherwise when kind == "tool_denied"
|
|
176
299
|
set data to data
|
|
177
300
|
otherwise when kind == "error"
|
|
@@ -180,11 +303,13 @@ task on_event(kind, data, tag)
|
|
|
180
303
|
print(" " + yellow("el proveedor rechazó el nombre del modelo (" + cfg["model"] + "): /model sin argumentos lista los válidos, /model <nombre> lo cambia"))
|
|
181
304
|
otherwise when kind == "compact"
|
|
182
305
|
print(" " + dim("⧗ compactando contexto (~" + text(data["before"]) + " tokens)"))
|
|
306
|
+
busy_on("compactando contexto", "llm")
|
|
183
307
|
flush()
|
|
184
308
|
|
|
185
309
|
-- Human in the loop con el `approve` nativo de Synsema: prompt [approve] … (y/n) en TTY; sin TTY deniega
|
|
186
310
|
-- (fail-closed, un agente no puede auto-aprobarse); `within` acota la espera.
|
|
187
311
|
task ask_user(name, args, why)
|
|
312
|
+
busy_off()
|
|
188
313
|
print("")
|
|
189
314
|
print(" " + yellow("⚠ requiere tu aprobación · " + why))
|
|
190
315
|
print(" " + permission.describe_call(name, args))
|
|
@@ -223,6 +348,8 @@ let COMMANDS be [
|
|
|
223
348
|
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
224
349
|
["/lamps", "[on <nombre> | off <nombre> | run <lámpara> <tool> [json] | remove <nombre>]", "lámparas (plugins de tools): listar, encender o apagar (global: lampson/lamps/ · proyecto: .lampson/lamps/)"],
|
|
225
350
|
["/lsp", "[add <typescript|python|rust|go|css|html> [--project] | add <nombre> <comando…> --ext .x=lang | remove <nombre>]", "language servers (navegación semántica: symbols/definition/references/hover); arrancan en la primera consulta"],
|
|
351
|
+
["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…): lámpara, comando o corrida del agente; corren mientras lampson esté abierto (o con lampson --daemon start, sin nada abierto)"],
|
|
352
|
+
["/approve", "<id> yes|no", "responder una aprobación pendiente de una tarea programada que corre en background"],
|
|
226
353
|
["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
|
|
227
354
|
["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
|
|
228
355
|
["/trace", "[n]", "traza legible de esta sesión (pasos, tools, tiempos, tokens, errores): .lampson/trace/<sesión>.log"],
|
|
@@ -282,6 +409,16 @@ task complete_args(cmd, head, last)
|
|
|
282
409
|
when trim(head) == "add"
|
|
283
410
|
give sort_by(keys(lsp.PRESETS), (x) => x)
|
|
284
411
|
give ["--project", "--ext"]
|
|
412
|
+
when cmd == "/approve"
|
|
413
|
+
when first
|
|
414
|
+
give apply((a) => a["id"], approvals.pending())
|
|
415
|
+
give ["yes", "no"]
|
|
416
|
+
when cmd == "/schedule"
|
|
417
|
+
when first
|
|
418
|
+
give ["add", "run", "on", "off", "remove", "log"]
|
|
419
|
+
when starts_with(trim(head), "add")
|
|
420
|
+
give []
|
|
421
|
+
give apply((s) => s["id"], schedule.summary())
|
|
285
422
|
when cmd == "/agent"
|
|
286
423
|
give ["build", "plan", "review", "explore", "worker"]
|
|
287
424
|
when cmd == "/model"
|
|
@@ -551,6 +688,47 @@ task opts_for(p, mode)
|
|
|
551
688
|
task lampson_subagent(spec_json)
|
|
552
689
|
give agents.run_child_json(spec_json)
|
|
553
690
|
|
|
691
|
+
-- Tareas programadas TAMBIÉN en la terminal: `cron_every` no corre mientras el hilo principal está en el teclado,
|
|
692
|
+
-- pero un `agent` sí (hilo real, como el Spinner). Cada 30 s llama al tick (heartbeat + corridas vencidas); una
|
|
693
|
+
-- corrida `prompt` transcurre al lado del chat sin tocar su sesión (blackboard lampson:sched:session) y si pide
|
|
694
|
+
-- permiso lo dice por consola: /approve <id> yes|no (o link/webhook si están configurados).
|
|
695
|
+
task lampson_sched_tick()
|
|
696
|
+
give sched_run.tick()
|
|
697
|
+
|
|
698
|
+
agent Sched
|
|
699
|
+
require net
|
|
700
|
+
require time
|
|
701
|
+
require exec
|
|
702
|
+
require random
|
|
703
|
+
require env("LAMPSON_*")
|
|
704
|
+
require env("OS")
|
|
705
|
+
require secret("LAMPSON_*")
|
|
706
|
+
require file("workspace")
|
|
707
|
+
require file("workspace/*")
|
|
708
|
+
require file.read("skills")
|
|
709
|
+
require file.read("skills/*")
|
|
710
|
+
require file.read("lamps")
|
|
711
|
+
require file.read("lamps/*")
|
|
712
|
+
require file("memory")
|
|
713
|
+
require file("memory/*")
|
|
714
|
+
require file(".lampson")
|
|
715
|
+
require file(".lampson/*")
|
|
716
|
+
let alive be true
|
|
717
|
+
let waited be 25
|
|
718
|
+
while alive
|
|
719
|
+
observe "lampson:spinner:stop" as st
|
|
720
|
+
when st == true
|
|
721
|
+
set alive to false
|
|
722
|
+
otherwise when waited >= 30
|
|
723
|
+
set waited to 0
|
|
724
|
+
try
|
|
725
|
+
lampson_sched_tick()
|
|
726
|
+
recover err
|
|
727
|
+
print("[lampson] scheduler: " + text(err))
|
|
728
|
+
otherwise
|
|
729
|
+
sleep(1)
|
|
730
|
+
set waited to waited + 1
|
|
731
|
+
|
|
554
732
|
-- !comando del usuario en un pseudo-terminal: la salida se ve EN VIVO y, si el proceso se queda esperando
|
|
555
733
|
-- (prompt y/N, contraseña, REPL, `npm init`…), lo que escribas se le manda como teclas + Enter.
|
|
556
734
|
-- Enter vacío = seguir esperando · ^C = cortar. Devuelve la salida (sin ANSI) para el contexto del agente.
|
|
@@ -641,6 +819,10 @@ when sid == ""
|
|
|
641
819
|
share {"id": sid} as "lampson:session"
|
|
642
820
|
|
|
643
821
|
banner(env_info["cwd"], cfg, profile, opts["permission_mode"], sid)
|
|
822
|
+
share nothing as "lampson:busy"
|
|
823
|
+
share false as "lampson:spinner:stop"
|
|
824
|
+
spawn Spinner with esc = ESC, color = COLOR
|
|
825
|
+
spawn Sched
|
|
644
826
|
flush()
|
|
645
827
|
|
|
646
828
|
let total_usage be {"input": 0, "output": 0}
|
|
@@ -881,6 +1063,71 @@ while running
|
|
|
881
1063
|
each s in ss
|
|
882
1064
|
print(" " + (when s["status"] == "ready" then green("● ") otherwise (when s["status"] == "error" or s["status"] == "exited" then red("○ ") otherwise dim("○ "))) + pad(s["name"], 12) + dim(pad(s["scope"], 8) + pad(s["status"], 8) + join(s["extensions"], " ") + " $ " + s["command"]) + (when s["error"] != nothing then red(" " + text(s["error"])) otherwise ""))
|
|
883
1065
|
print(" " + dim("idle = arranca en la primera consulta del agente"))
|
|
1066
|
+
otherwise when input == "/schedule" or starts_with(input, "/schedule ")
|
|
1067
|
+
-- tareas programadas (lib/schedule.syn). Crear: JSON, o pedírselo al agente ("todos los días a las 9 corré…").
|
|
1068
|
+
-- Las corre el daemon (web.syn con `lampson --daemon start`); `/schedule run` las corre acá mismo, con
|
|
1069
|
+
-- el menú de aprobación de la terminal si la tarea es `ask`.
|
|
1070
|
+
let srest be trim(slice(input, 9, length(input)))
|
|
1071
|
+
let stoks be where(split(srest, " "), (x) => x != "")
|
|
1072
|
+
let sact be when length(stoks) > 0 then stoks[0] otherwise ""
|
|
1073
|
+
let sid_arg be when length(stoks) > 1 then stoks[1] otherwise ""
|
|
1074
|
+
when sact == "add"
|
|
1075
|
+
let sjson be trim(slice(srest, 3, length(srest)))
|
|
1076
|
+
when sjson == ""
|
|
1077
|
+
print(" uso: /schedule add {\"name\": \"…\", \"when\": \"daily 09:00\", \"action\": {\"type\": \"bash\", \"command\": \"npm test\"}, \"permission\": \"ask\"}")
|
|
1078
|
+
print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type: lamp {lamp, tool, args} · bash {command} · prompt {prompt, agent}"))
|
|
1079
|
+
print(" " + dim("o pedíselo al agente en lenguaje natural: «todos los días a las 9 revisá los tests y avisame»"))
|
|
1080
|
+
otherwise
|
|
1081
|
+
try
|
|
1082
|
+
let st be schedule.add(json_decode(sjson))
|
|
1083
|
+
print(" " + green("⏰ programada «" + st["name"] + "» (" + st["id"] + "): " + schedule.describe_plan(st["plan"]) + " · próxima " + schedule.fmt_local(st["next_run"])))
|
|
1084
|
+
recover err
|
|
1085
|
+
print(" " + red(text(err)))
|
|
1086
|
+
otherwise when sact == "run" and sid_arg != ""
|
|
1087
|
+
let st be schedule.get(sid_arg)
|
|
1088
|
+
when st == nothing
|
|
1089
|
+
print(" " + red("no existe la tarea " + sid_arg))
|
|
1090
|
+
otherwise
|
|
1091
|
+
print(" " + yellow("⏰ corriendo «" + st["name"] + "» · " + schedule.describe_action(st["action"])))
|
|
1092
|
+
flush()
|
|
1093
|
+
let sr be sched_run.run(st, ask_user, on_event)
|
|
1094
|
+
print(" " + (when sr["last_status"] == "ok" then green("✓ ") otherwise red("✗ ")) + sr["last_status"] + " · " + first_line(sr["last_summary"], 160))
|
|
1095
|
+
print(" " + dim("log: " + schedule.LOG_DIR + "/" + st["id"] + ".log"))
|
|
1096
|
+
otherwise when (sact == "on" or sact == "off") and sid_arg != ""
|
|
1097
|
+
try
|
|
1098
|
+
print(" " + schedule.set_enabled(sid_arg, sact == "on"))
|
|
1099
|
+
recover err
|
|
1100
|
+
print(" " + red(text(err)))
|
|
1101
|
+
otherwise when sact == "remove" and sid_arg != ""
|
|
1102
|
+
try
|
|
1103
|
+
print(" " + schedule.remove(sid_arg))
|
|
1104
|
+
recover err
|
|
1105
|
+
print(" " + red(text(err)))
|
|
1106
|
+
otherwise when sact == "log" and sid_arg != ""
|
|
1107
|
+
let tl be schedule.log_tail(sid_arg, 60)
|
|
1108
|
+
print(when tl == "" then " sin corridas todavía" otherwise tl)
|
|
1109
|
+
otherwise
|
|
1110
|
+
let ss be schedule.summary()
|
|
1111
|
+
let age be schedule.daemon_age()
|
|
1112
|
+
when length(ss) == 0
|
|
1113
|
+
print(" sin tareas programadas. Pedísela al agente («cada 6 horas corré los tests y avisame») o: " + cyan("/schedule add <json>"))
|
|
1114
|
+
each s in ss
|
|
1115
|
+
print(" " + (when s["running"] then yellow("● ") otherwise (when s["enabled"] then green("● ") otherwise dim("○ "))) + pad(s["id"], 22) + dim(pad(s["plan"], 30)) + s["action_text"])
|
|
1116
|
+
print(" " + dim("permisos " + s["permission"] + " · próxima " + s["next_local"] + (when s["last_run"] != nothing then " · última " + s["last_status"] + ": " + first_line(s["last_summary"], 80) otherwise "")))
|
|
1117
|
+
let pend be approvals.pending()
|
|
1118
|
+
each ap in pend
|
|
1119
|
+
print(" " + yellow("⚠ aprobación pendiente " + ap["id"] + ": " + first_line(ap["message"], 100)) + dim(" → /approve " + ap["id"] + " yes|no"))
|
|
1120
|
+
print(" " + dim("corren solas mientras lampson esté abierto (tick cada " + text(schedule.TICK_SECONDS) + " s) · con todo cerrado: lampson --daemon start · run/on/off/remove/log <id>"))
|
|
1121
|
+
otherwise when starts_with(input, "/approve ")
|
|
1122
|
+
let atoks be where(split(trim(slice(input, 9, length(input))), " "), (x) => x != "")
|
|
1123
|
+
when length(atoks) < 2 or not contains(["yes", "no", "si", "sí", "y", "n"], lower(atoks[1]))
|
|
1124
|
+
print(" uso: /approve <id> yes|no (pendientes: /schedule)")
|
|
1125
|
+
otherwise
|
|
1126
|
+
let yes be contains(["yes", "si", "sí", "y"], lower(atoks[1]))
|
|
1127
|
+
when approvals.answer(atoks[0], yes)
|
|
1128
|
+
print(" " + (when yes then green("✓ permitido ") otherwise red("✗ denegado ")) + atoks[0])
|
|
1129
|
+
otherwise
|
|
1130
|
+
print(" " + red("no hay una aprobación pendiente con id " + atoks[0]))
|
|
884
1131
|
otherwise when input == "/lamps" or starts_with(input, "/lamps ")
|
|
885
1132
|
let lrest be trim(slice(input, 6, length(input)))
|
|
886
1133
|
when starts_with(lrest, "run ")
|
|
@@ -1005,23 +1252,31 @@ while running
|
|
|
1005
1252
|
-- catálogo fresco por turno: si el turno anterior conectó/quitó un server MCP, sus tools
|
|
1006
1253
|
-- entran/salen acá (con los mismos servers el catálogo es idéntico → el prompt cache no se corta)
|
|
1007
1254
|
set opts to opts_for(profile, mode)
|
|
1255
|
+
let t0 be now()
|
|
1008
1256
|
let result be loop.run_turn(cfg, messages, opts, on_event)
|
|
1009
1257
|
trace.turn_end(sid, result)
|
|
1010
1258
|
set messages to result["messages"]
|
|
1011
1259
|
set total_usage to {"input": total_usage["input"] + result["usage"]["input"], "output": total_usage["output"] + result["usage"]["output"]}
|
|
1012
|
-
let summary be text(result["steps"]) + (when result["steps"] == 1 then " paso" otherwise " pasos") + " · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens"
|
|
1260
|
+
let summary be text(result["steps"]) + (when result["steps"] == 1 then " paso" otherwise " pasos") + " · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens · " + ui.fmt_duration(now() - t0)
|
|
1013
1261
|
when result["stopped"] == "max_steps"
|
|
1014
1262
|
set summary to summary + " · " + red("límite de " + text(opts["max_steps"]) + " pasos por turno")
|
|
1015
1263
|
print(dim(" ─── " + summary))
|
|
1016
1264
|
print(" " + yellow("El agente paró por el límite de pasos, no porque terminó. Escribí «seguí» para que continúe donde quedó, o subí LAMPSON_MAX_STEPS."))
|
|
1017
1265
|
otherwise when result["stopped"] != "done"
|
|
1018
|
-
|
|
1266
|
+
-- el detalle del error ya salió en la línea ‼; acá solo la causa corta
|
|
1267
|
+
let why be result["stopped"]
|
|
1268
|
+
when starts_with(why, "error: network")
|
|
1269
|
+
set why to "error de red (ver ‼ arriba)"
|
|
1270
|
+
otherwise when starts_with(why, "error:")
|
|
1271
|
+
set why to first_line(why, 60)
|
|
1272
|
+
set summary to summary + " · " + red("detenido: " + why)
|
|
1019
1273
|
print(dim(" ─── " + summary))
|
|
1020
1274
|
otherwise
|
|
1021
1275
|
print(dim(" ─── " + summary))
|
|
1022
1276
|
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
1023
1277
|
flush()
|
|
1024
1278
|
-- los procesos gestionados (servidores) y los subagentes en background mueren con lampson
|
|
1279
|
+
share true as "lampson:spinner:stop"
|
|
1025
1280
|
agents.stop_all()
|
|
1026
1281
|
mcp.stop_all()
|
|
1027
1282
|
lsp.stop_all()
|
package/lampson.ps1
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# lampson --workspace C:\otro\proyecto # elegir la ubicación explícitamente (también -Workspace)
|
|
6
6
|
# lampson --agent plan # perfil inicial: build | plan | review | explore
|
|
7
7
|
# lampson --yolo | --strict | --ask # permisos para comandos peligrosos (--dangerously-skip-permissions = --yolo)
|
|
8
|
+
# lampson --daemon start|stop|status|logs|restart # proceso residente (web + tareas programadas + aprobaciones)
|
|
8
9
|
# lampson --update # actualizar Lampson (git pull) y salir
|
|
9
10
|
# lampson --help
|
|
10
11
|
#
|
|
@@ -17,12 +18,13 @@ $caller = (Get-Location).Path
|
|
|
17
18
|
$mount = Join-Path $here "workspace"
|
|
18
19
|
|
|
19
20
|
# --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
|
|
20
|
-
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""
|
|
21
|
+
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $Daemon = ""
|
|
21
22
|
$i = 0
|
|
22
23
|
while ($i -lt $args.Count) {
|
|
23
24
|
$a = [string]$args[$i]
|
|
24
25
|
switch -Regex ($a.ToLower()) {
|
|
25
26
|
'^--?(web|w)$' { $Web = $true }
|
|
27
|
+
'^--?(daemon|d)$' { $i++; $Daemon = if ($i -lt $args.Count) { ([string]$args[$i]).ToLower() } else { "status" } }
|
|
26
28
|
'^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
|
|
27
29
|
'^--?(agent|a)$' { $i++; $Agent = [string]$args[$i] }
|
|
28
30
|
'^--?(yolo|y|dangerously-skip-permissions)$' { $Perm = "yolo" }
|
|
@@ -30,7 +32,7 @@ while ($i -lt $args.Count) {
|
|
|
30
32
|
'^--?(ask)$' { $Perm = "ask" }
|
|
31
33
|
'^--?(permission|p)$' { $i++; $Perm = ([string]$args[$i]).ToLower() }
|
|
32
34
|
'^--?(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
|
|
35
|
+
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First 10 | ForEach-Object { $_.TrimStart('#',' ') }; exit 0 }
|
|
34
36
|
default { if ($Workspace -eq "" -and -not $a.StartsWith("-")) { $Workspace = $a } else { Write-Error "argumento desconocido: $a (probá lampson --help)"; exit 1 } }
|
|
35
37
|
}
|
|
36
38
|
$i++
|
|
@@ -49,6 +51,12 @@ if ($Workspace -ne "") {
|
|
|
49
51
|
if (Test-Path -LiteralPath $mount) {
|
|
50
52
|
$item = Get-Item -LiteralPath $mount -Force
|
|
51
53
|
if ($item.LinkType -ne "Junction") { Write-Error "./workspace existe y no es una junction; movelo antes de montar otro proyecto"; exit 1 }
|
|
54
|
+
# un daemon/web corriendo sobre OTRO proyecto (latido reciente del scheduler) quedaría apuntando a este: aviso
|
|
55
|
+
$prev = $item.Target; if ($prev -is [array]) { $prev = $prev[0] }
|
|
56
|
+
$hb = Join-Path $here ".lampson\schedules.heartbeat"
|
|
57
|
+
if ($prev -and $prev.TrimEnd('\').ToLower() -ne $target.TrimEnd('\').ToLower() -and (Test-Path -LiteralPath $hb) -and ((Get-Date) - (Get-Item -LiteralPath $hb).LastWriteTime).TotalSeconds -lt 90) {
|
|
58
|
+
Write-Host "AVISO: hay un lampson (web/daemon) corriendo sobre $prev; al montar $target sus tareas programadas quedan en pausa. Paralo o reinicialo desde su carpeta: lampson --daemon restart" -ForegroundColor Yellow
|
|
59
|
+
}
|
|
52
60
|
$item.Delete()
|
|
53
61
|
}
|
|
54
62
|
New-Item -ItemType Junction -Path $mount -Target $target | Out-Null
|
|
@@ -76,7 +84,13 @@ try {
|
|
|
76
84
|
$env:LAMPSON_WORKSPACE = $target
|
|
77
85
|
if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
|
|
78
86
|
if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
|
|
79
|
-
if ($
|
|
87
|
+
if ($Daemon -ne "") {
|
|
88
|
+
# proceso residente: web.syn en background (synsema daemon, sin config del sistema; para producción,
|
|
89
|
+
# systemd/NSSM con `synsema serve web.syn`). Ejecuta las tareas programadas y atiende aprobaciones por link.
|
|
90
|
+
if ($Daemon -notmatch '^(start|stop|status|logs|restart)$') { Write-Error "uso: lampson --daemon start|stop|status|logs|restart"; exit 1 }
|
|
91
|
+
if ($Daemon -eq "start" -or $Daemon -eq "restart") { Write-Host "Lampson daemon · workspace: $target · http://127.0.0.1:8080" }
|
|
92
|
+
synsema daemon $Daemon web.syn
|
|
93
|
+
} elseif ($Web) {
|
|
80
94
|
Write-Host "Lampson web · workspace: $target"
|
|
81
95
|
Write-Host "abrí http://127.0.0.1:8080 (Ctrl+C para parar)"
|
|
82
96
|
synsema serve web.syn
|
package/lampson.sh
CHANGED
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
# cd /mi/proyecto && lampson # REPL (con lampson/ en el PATH)
|
|
4
4
|
# cd /mi/proyecto && lampson --web # servidor web en http://127.0.0.1:8080
|
|
5
5
|
# lampson --workspace /otro/proyecto [--web] [--agent plan]
|
|
6
|
+
# lampson --daemon start|stop|status|logs|restart # proceso residente (web + tareas programadas + aprobaciones)
|
|
6
7
|
set -euo pipefail
|
|
7
8
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
9
|
caller="$(pwd)"
|
|
9
|
-
ws=""; web=0
|
|
10
|
+
ws=""; web=0; daemon=""
|
|
10
11
|
while [ $# -gt 0 ]; do
|
|
11
12
|
case "$1" in
|
|
12
13
|
--web) web=1 ;;
|
|
14
|
+
--daemon) shift; daemon="${1:-status}" ;;
|
|
13
15
|
--workspace) shift; ws="$1" ;;
|
|
14
16
|
--agent) shift; export LAMPSON_AGENT="$1" ;;
|
|
15
17
|
--yolo|--dangerously-skip-permissions) export LAMPSON_PERMISSION=yolo ;;
|
|
16
18
|
--strict) export LAMPSON_PERMISSION=strict ;;
|
|
17
19
|
--ask) export LAMPSON_PERMISSION=ask ;;
|
|
18
20
|
--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 ;;
|
|
21
|
+
*) echo "uso: lampson [--web] [--daemon start|stop|status|logs|restart] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
|
|
20
22
|
esac
|
|
21
23
|
shift
|
|
22
24
|
done
|
|
@@ -25,6 +27,13 @@ if [ -n "$ws" ]; then
|
|
|
25
27
|
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
26
28
|
ws="$(cd "$ws" && pwd)"
|
|
27
29
|
if [ -e "$here/workspace" ] && [ ! -L "$here/workspace" ]; then echo "./workspace existe y no es un symlink" >&2; exit 1; fi
|
|
30
|
+
# un daemon/web corriendo sobre OTRO proyecto (latido reciente del scheduler) quedaría apuntando a este: aviso
|
|
31
|
+
if [ -L "$here/workspace" ] && [ -f "$here/.lampson/schedules.heartbeat" ]; then
|
|
32
|
+
prev="$(readlink -f "$here/workspace")"
|
|
33
|
+
if [ "$prev" != "$ws" ] && [ $(( $(date +%s) - $(stat -c %Y "$here/.lampson/schedules.heartbeat") )) -lt 90 ]; then
|
|
34
|
+
echo "AVISO: hay un lampson (web/daemon) corriendo sobre $prev; al montar $ws sus tareas programadas quedan en pausa. Paralo o reinicialo desde su carpeta: lampson --daemon restart" >&2
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
28
37
|
rm -f "$here/workspace"; ln -s "$ws" "$here/workspace"
|
|
29
38
|
elif [ ! -e "$here/workspace" ]; then
|
|
30
39
|
echo "no hay workspace: corré lampson desde el directorio del proyecto, o --workspace /ruta" >&2; exit 1
|
|
@@ -39,4 +48,9 @@ for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/sk
|
|
|
39
48
|
done
|
|
40
49
|
echo "workspace -> $LAMPSON_WORKSPACE"
|
|
41
50
|
cd "$here"
|
|
42
|
-
if [ "$
|
|
51
|
+
if [ -n "$daemon" ]; then
|
|
52
|
+
# proceso residente en background (synsema daemon: sin arranque al boot ni reinicio; para producción, systemd
|
|
53
|
+
# con `synsema serve web.syn`). Ejecuta las tareas programadas y atiende aprobaciones por link.
|
|
54
|
+
case "$daemon" in start|stop|status|logs|restart) ;; *) echo "uso: lampson --daemon start|stop|status|logs|restart" >&2; exit 1 ;; esac
|
|
55
|
+
exec synsema daemon "$daemon" web.syn
|
|
56
|
+
elif [ "$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
CHANGED
|
@@ -40,7 +40,7 @@ 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", "lamp", "delegate"],
|
|
43
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "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
|
},
|