lampson 0.2.8 → 0.2.10

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 (81) hide show
  1. package/.env.example +10 -1
  2. package/README.md +8 -3
  3. package/bin/lampson.js +1 -1
  4. package/chat.syn +111 -7
  5. package/cli.syn +143 -7
  6. package/edge.syn +26 -0
  7. package/hub.tpl.syn +127 -6
  8. package/lampson.ps1 +43 -2
  9. package/lampson.sh +40 -2
  10. package/lib/agents.syn +27 -6
  11. package/lib/approvals.syn +2 -2
  12. package/lib/auth.syn +274 -0
  13. package/lib/computer.syn +2000 -0
  14. package/lib/computer_judge.syn +410 -0
  15. package/lib/fs.syn +19 -3
  16. package/lib/git.syn +2 -2
  17. package/lib/judge.syn +78 -0
  18. package/lib/line.syn +2 -2
  19. package/lib/loop.syn +36 -4
  20. package/lib/lsp.syn +4 -4
  21. package/lib/mcp.syn +20 -4
  22. package/lib/md.syn +11 -11
  23. package/lib/origin.syn +67 -0
  24. package/lib/permission.syn +21 -2
  25. package/lib/prompt.syn +9 -1
  26. package/lib/provider.syn +70 -20
  27. package/lib/schedule.syn +12 -12
  28. package/lib/session.syn +5 -5
  29. package/lib/settings.syn +65 -1
  30. package/lib/skills.syn +22 -6
  31. package/lib/tools/bash.syn +1 -1
  32. package/lib/tools/common.syn +1 -1
  33. package/lib/tools/fetch.syn +98 -71
  34. package/lib/tools/grep.syn +1 -1
  35. package/lib/tools/memo.syn +3 -3
  36. package/lib/tools/proc.syn +38 -20
  37. package/lib/tools/qr.js +26 -0
  38. package/lib/tools/url.syn +5 -5
  39. package/lib/tools.syn +23 -2
  40. package/lib/trace.syn +2 -2
  41. package/lib/tree.syn +29 -13
  42. package/lib/update.syn +1 -1
  43. package/lib/workspaces.syn +202 -36
  44. package/package.json +3 -2
  45. package/public/apple-touch-icon.png +0 -0
  46. package/public/badge-96.png +0 -0
  47. package/public/css/code.css +48 -0
  48. package/public/css/hub.css +9 -0
  49. package/public/css/login.css +16 -0
  50. package/public/css/mobile.css +95 -0
  51. package/public/css/panel.css +30 -0
  52. package/public/css/tokens.css +1 -1
  53. package/public/hub.html +11 -1
  54. package/public/icon-192.png +0 -0
  55. package/public/icon-512.png +0 -0
  56. package/public/icon-maskable-512.png +0 -0
  57. package/public/icons-src/badge.svg +12 -0
  58. package/public/icons-src/icon-maskable.svg +15 -0
  59. package/public/icons-src/icon.svg +13 -0
  60. package/public/index.html +16 -3
  61. package/public/js/agents.js +1 -1
  62. package/public/js/chat.js +1 -1
  63. package/public/js/config.js +145 -2
  64. package/public/js/core.js +4 -2
  65. package/public/js/devices.js +80 -0
  66. package/public/js/editor.js +232 -0
  67. package/public/js/login.js +78 -0
  68. package/public/js/mobile.js +122 -0
  69. package/public/js/procs.js +1 -1
  70. package/public/js/pwa.js +5 -0
  71. package/public/js/terminal.js +1 -1
  72. package/public/js/tree.js +12 -4
  73. package/public/login.html +43 -0
  74. package/public/manifest.webmanifest +16 -0
  75. package/public/offline.html +24 -0
  76. package/public/sw.js +63 -0
  77. package/public/vendor/highlight.min.js +1343 -0
  78. package/public/vendor/qrcode.js +2297 -0
  79. package/skills/computer-use/SKILL.md +178 -0
  80. package/skills/lampson/SKILL.md +5 -0
  81. package/web.syn +67 -6
@@ -0,0 +1,410 @@
1
+ -- lib/computer_judge.syn — «jev-use» para computer_use: un modelo System One (Jev de TypeSafe, o Laya local) elige
2
+ -- la próxima acción de una lista CERRADA de candidatos, en vez de que el LLM lea el árbol entero y elija un índice
3
+ --
4
+ -- De dónde sale (2026-09-28): la receta jev-use de trycua/cua (libs/cua-driver/examples/jev-use, RFC 4268). Lo que
5
+ -- se copió porque allá está medido:
6
+ -- * el modelo nunca inventa: elige UN id de una tabla que arma el harness; el id se resuelve a la acción ya
7
+ -- armada (elemento + snapshot, o ref de página). El texto a tipear sale del LLM (`text`), nunca del juez.
8
+ -- * MEDIDO ACÁ (2026-09-28, Calculadora de Windows en español, Laya local = checkpoint `english`): el backend
9
+ -- local elige por el ID de la opción, no por su descripción ("press 7" con ids b0…b29 → b7; idéntico con
10
+ -- etiquetas en inglés o en español). Por eso el id ES legible: «button Siete». Y con la interfaz en español el
11
+ -- checkpoint inglés se equivoca con confianza 100% («presionar 7» → «Tres»): la compuerta no protege ahí; el
12
+ -- objetivo tiene que venir con las palabras de la pantalla y el LLM verifica siempre.
13
+ -- * candidatos: solo controles con clase conocida (button, checkbox, radio, popup, menu_item, link, tab,
14
+ -- text_input, toggle, option), habilitados, con tamaño, con etiqueta (y etiqueta ≠ value: así el contenido
15
+ -- tipeado nunca termina en un id), fuera de la barra de título. Ids ESTABLES (clase + etiqueta), nunca índices.
16
+ -- Tope 32 + los reservados `reobserve` y `abstain` (Jev eligió bien 220/220 con 3-7 candidatos; con más de
17
+ -- 12 no está medido — por eso, pasado el tope, se ordena por parecido con el objetivo antes de cortar). El
18
+ -- 24 de cua viene del límite de 26 letras de Cua-S1; una Calculadora ya trae 29 controles útiles.
19
+ -- * riesgo por palabra entera (borrar, enviar, comprar, cerrar): esos controles NO se ofrecen — el LLM los hace
20
+ -- a mano con element=N / ref, y pasan por la aprobación normal. Es una guarda contra accidentes, no autorización.
21
+ -- * el modelo NO decide si la tarea terminó (Jev contesta «listo» en el paso 0): eso lo verifica el LLM.
22
+ -- * cua no pone umbral (lo deja al que llama): acá hay compuerta de probabilidad Y de margen sobre el segundo.
23
+ --
24
+ -- Puro: sin driver ni juez; lo usa lib/computer.syn (acción `act`, y page_select cuando el texto no coincide).
25
+
26
+ -- clase → acción. Roles de Windows UIA, macOS AX (sin el prefijo AX), Linux AT-SPI y ARIA (página), normalizados
27
+ -- (minúsculas, solo letras y dígitos). UIA "Text" es texto estático (AT-SPI "text" es editable, pero en Linux la
28
+ -- ruta nativa todavía no se probó): no entra.
29
+ export let ROLE_CLASS be {
30
+ "button": "button", "splitbutton": "button", "pushbutton": "button", "menubutton": "popup",
31
+ "checkbox": "checkbox", "menuitemcheckbox": "checkbox",
32
+ "radiobutton": "radio", "radio": "radio", "menuitemradio": "radio",
33
+ "switch": "toggle", "togglebutton": "toggle",
34
+ "combobox": "popup", "popupbutton": "popup", "listbox": "popup",
35
+ "menuitem": "menu_item", "menubaritem": "menu_item",
36
+ "hyperlink": "link", "link": "link",
37
+ "tabitem": "tab", "tab": "tab", "pagetab": "tab",
38
+ "edit": "text_input", "textbox": "text_input", "searchbox": "text_input", "textfield": "text_input", "textarea": "text_input", "searchfield": "text_input", "entry": "text_input", "spinbutton": "text_input",
39
+ "option": "option", "listitem": "option", "treeitem": "option"
40
+ }
41
+
42
+ export let CLASS_ACTION be {"button": "press", "checkbox": "toggle", "radio": "select", "toggle": "toggle", "popup": "open_menu", "menu_item": "press", "link": "press", "tab": "press", "text_input": "set_text", "option": "select"}
43
+
44
+ -- palabra entera, sin distinguir mayúsculas; en inglés y en español (las etiquetas vienen en el idioma del usuario)
45
+ export let RISK_WORDS be {
46
+ "destructive": ["delete", "remove", "erase", "trash", "discard", "clear all", "format", "reset", "eliminar", "borrar", "quitar", "vaciar", "descartar", "restablecer", "formatear", "papelera"],
47
+ "send": ["send", "submit", "post", "publish", "share", "reply", "enviar", "publicar", "compartir", "responder"],
48
+ "purchase": ["buy", "purchase", "pay", "checkout", "order", "subscribe", "comprar", "pagar", "suscribirse", "suscribir", "pedido", "finalizar compra"],
49
+ "close_unsaved": ["close", "quit", "exit", "don't save", "discard changes", "cerrar", "salir", "no guardar", "descartar cambios"]
50
+ }
51
+
52
+ export let MAX_CANDIDATES be 32
53
+ export let MAX_HISTORY be 16
54
+ export let MAX_ELEMENTS_IN_STATE be 64
55
+ -- compuerta: probabilidad del elegido y margen sobre el segundo (cua documenta que el llamador debe ponerlas)
56
+ export let MIN_P be 0.6
57
+ export let MIN_MARGIN be 0.15
58
+ export let MAX_STEPS be 6
59
+
60
+ export let RESERVED be {
61
+ "reobserve": "Take no action and obtain a fresh observation of the window, because the current observation looks stale, incomplete, or contradicts the goal's progress.",
62
+ "abstain": "Stop without acting if none of the proposed actions is safe or moves toward the goal."
63
+ }
64
+
65
+ export let INSTRUCTION be "Which single action should run next to move toward the goal?"
66
+
67
+ -- "AXButton" | "Button" | "push button" → "button"
68
+ export task norm_role(r)
69
+ let low be regex_replace(lower(text(r)), "[^a-z0-9]", "")
70
+ when starts_with(low, "ax") and length(low) > 2
71
+ set low to slice(low, 2, length(low))
72
+ give low
73
+
74
+ export task role_class(r)
75
+ let n be norm_role(r)
76
+ when contains(ROLE_CLASS, n)
77
+ give ROLE_CLASS[n]
78
+ give nothing
79
+
80
+ task squash(s)
81
+ give trim(regex_replace(replace(text(s), "’", "'"), "\\s+", " "))
82
+
83
+ -- categoría de riesgo de una etiqueta, o nothing (palabra entera: «Resetear» no es «reset», «Guardar» no es «guardar como»)
84
+ export task risk_of(label)
85
+ let low be " " + regex_replace(lower(squash(label)), "[^a-z0-9áéíóúñü' ]", " ") + " "
86
+ each cat in keys(RISK_WORDS)
87
+ each w in RISK_WORDS[cat]
88
+ when contains(low, " " + w + " ")
89
+ give cat
90
+ give nothing
91
+
92
+ -- id legible de un candidato: «button Guardar», «text_input Nombre de archivo» (el modelo lee el id: ver arriba)
93
+ export task readable_id(cls, label)
94
+ let l be squash(label)
95
+ when length(l) > 48
96
+ set l to trim(slice(l, 0, 48))
97
+ give cls + " " + l
98
+
99
+ -- slug ASCII de 32 como máximo; sin nada ASCII → 8 hex del sha256
100
+ let TRANSLIT be {"á": "a", "à": "a", "ä": "a", "â": "a", "ã": "a", "é": "e", "è": "e", "ë": "e", "ê": "e", "í": "i", "ì": "i", "ï": "i", "î": "i", "ó": "o", "ò": "o", "ö": "o", "ô": "o", "õ": "o", "ú": "u", "ù": "u", "ü": "u", "û": "u", "ñ": "n", "ç": "c"}
101
+
102
+ task ascii_fold(s)
103
+ let out be lower(text(s))
104
+ each k in keys(TRANSLIT)
105
+ set out to replace(out, k, TRANSLIT[k])
106
+ give out
107
+
108
+ export task slug(label)
109
+ let s be regex_replace(ascii_fold(squash(label)), "[^a-z0-9]+", "-")
110
+ set s to regex_replace(s, "^-+|-+$", "")
111
+ when length(s) > 32
112
+ set s to regex_replace(slice(s, 0, 32), "-+$", "")
113
+ when s == ""
114
+ give slice(hex(sha256(text(label))), 2, 10)
115
+ give s
116
+
117
+ task short(s, n)
118
+ let t be squash(s)
119
+ give when length(t) > n then slice(t, 0, n) + "…" otherwise t
120
+
121
+ -- descripción que lee el modelo (las de cua, literales donde se pudo)
122
+ export task describe_candidate(cls, label, state, text_value)
123
+ -- comillas a mano: json_encode escapa lo no ASCII (ú) y el modelo leería «Menú»
124
+ let q be "\"" + replace(short(label, 60), "\"", "'") + "\""
125
+ when cls == "text_input"
126
+ let st be when state == "" then "" otherwise " The field currently reports " + state + "."
127
+ give "Set the text field " + q + " to the text the task supplies" + (when text_value != nothing then " (\"" + replace(short(text_value, 40), "\"", "'") + "\")" otherwise "") + ", replacing its contents." + st
128
+ when cls == "checkbox" or cls == "toggle"
129
+ give "Toggle the " + (when cls == "checkbox" then "checkbox " otherwise "switch ") + q + "." + (when state != "" then " It is currently " + state + "." otherwise "")
130
+ when cls == "radio"
131
+ give "Select the radio option " + q + "." + (when state != "" then " It is currently " + state + "." otherwise "")
132
+ when cls == "popup"
133
+ give "Open the pop-up menu " + q + ". Its options become candidates on the next observation."
134
+ when cls == "option"
135
+ give "Select the option " + q + "." + (when state != "" then " It is currently " + state + "." otherwise "")
136
+ when cls == "menu_item"
137
+ give "Press the menu item labeled " + q + "."
138
+ when cls == "tab"
139
+ give "Switch to the tab labeled " + q + "."
140
+ when cls == "link"
141
+ give "Follow the link labeled " + q + "."
142
+ give "Press the button labeled " + q + "."
143
+
144
+ task num_or(m, k, d)
145
+ when not contains(m, k)
146
+ give d
147
+ when m[k] == nothing
148
+ give d
149
+ try
150
+ give number(text(m[k]))
151
+ recover err
152
+ give d
153
+
154
+ task txt(m, k)
155
+ when not contains(m, k)
156
+ give ""
157
+ when m[k] == nothing
158
+ give ""
159
+ give text(m[k])
160
+
161
+ -- estado sin values (nunca viaja el contenido de un campo): empty / has_text / contains_required_value …
162
+ task native_state(e, cls, want_text)
163
+ when cls == "text_input"
164
+ let v be txt(e, "value")
165
+ when trim(v) == ""
166
+ give "empty"
167
+ when want_text != nothing and trim(v) == trim(text(want_text))
168
+ give "contains_required_value"
169
+ give "has_text"
170
+ when cls == "checkbox" or cls == "toggle"
171
+ when contains(e, "selected")
172
+ give when e["selected"] == true then "checked" otherwise "unchecked"
173
+ let v be lower(txt(e, "value"))
174
+ when v == "on" or v == "true" or v == "1" or v == "checked"
175
+ give "checked"
176
+ when v == "off" or v == "false" or v == "0" or v == "unchecked"
177
+ give "unchecked"
178
+ give ""
179
+ when cls == "radio" or cls == "option"
180
+ when contains(e, "selected")
181
+ give when e["selected"] == true then "selected" otherwise "not selected"
182
+ give ""
183
+ give ""
184
+
185
+ -- ¿cuelga de la barra de título? (Minimizar/Maximizar/Cerrar/menú de sistema no son candidatos)
186
+ task in_title_bar(e, by_index)
187
+ let cur be e
188
+ let hops be 0
189
+ while cur != nothing and hops < 40
190
+ when norm_role(txt(cur, "role")) == "titlebar"
191
+ give true
192
+ let pi be num_or(cur, "parent_index", -1)
193
+ set cur to when pi >= 0 and contains(by_index, text(floor(pi))) then by_index[text(floor(pi))] otherwise nothing
194
+ set hops to hops + 1
195
+ give false
196
+
197
+ -- botones de ventana que no cuelgan de un TitleBar (apps UWP en ApplicationFrameHost: «Minimizar Calculadora»)
198
+ let CHROME_WORDS be ["minimize", "maximize", "restore", "minimizar", "maximizar", "restaurar"]
199
+
200
+ task is_chrome_label(label)
201
+ let first be lower(split(squash(label) + " ", " ")[0])
202
+ give contains(CHROME_WORDS, first)
203
+
204
+ -- palabras del objetivo (3+ letras) que aparecen en la etiqueta: para ordenar cuando hay más candidatos que el tope
205
+ task goal_words(goal)
206
+ let out be []
207
+ each w in split(regex_replace(lower(text(goal)), "[^a-z0-9áéíóúñü]+", " "), " ")
208
+ when length(w) >= 3 and not contains(out, w)
209
+ set out to append(out, w)
210
+ give out
211
+
212
+ task overlap(words, label)
213
+ let low be lower(text(label))
214
+ let n be 0
215
+ each w in words
216
+ when contains(low, w)
217
+ set n to n + 1
218
+ give n
219
+
220
+ -- palabra clave del objetivo para filtrar un snapshot (query): la más larga que no sea un verbo de acción ni un
221
+ -- sustantivo genérico de interfaz. «aplicar el filtro Con control de temperatura» → «temperatura»; «completar el
222
+ -- correo» → «correo». nothing si no queda ninguna.
223
+ let QUERY_STOP be ["completar", "presionar", "escribir", "buscar", "aplicar", "seleccionar", "elegir", "marcar", "abrir", "cerrar", "hacer", "click", "clic", "clickear", "responder", "poner", "ingresar", "llenar", "rellenar", "tocar", "ir", "press", "type", "fill", "open", "select", "choose", "enter", "search", "apply", "tick", "check", "click", "write", "set", "the", "field", "campo", "botón", "boton", "button", "filtro", "filter", "opción", "opcion", "option", "link", "enlace", "menu", "menú", "para", "con", "del", "los", "las", "una", "uno", "que", "sobre", "with", "into", "from", "that", "this"]
224
+
225
+ export task goal_query(goal)
226
+ let best be nothing
227
+ each w in split(regex_replace(lower(text(goal)), "[^a-z0-9áéíóúñü@._-]+", " "), " ")
228
+ when length(w) >= 4 and not contains(QUERY_STOP, w)
229
+ when best == nothing or length(w) > length(best)
230
+ set best to w
231
+ give best
232
+
233
+ -- ids únicos: el segundo con la misma base lleva « (2)» (estable entre observaciones iguales)
234
+ task unique_id(base, seen)
235
+ when not contains(seen, base)
236
+ give base
237
+ let k be 2
238
+ while contains(seen, base + " (" + text(k) + ")")
239
+ set k to k + 1
240
+ give base + " (" + text(k) + ")"
241
+
242
+ -- recorte al tope: si sobran, primero los que comparten palabras con el objetivo (orden estable dentro de cada grupo)
243
+ task cap_ranked(cands, goal)
244
+ when length(cands) <= MAX_CANDIDATES
245
+ give {"cands": cands, "dropped": 0}
246
+ let words be goal_words(goal)
247
+ let scored be []
248
+ let i be 0
249
+ each c in cands
250
+ set scored to append(scored, {"c": c, "s": overlap(words, c["label"]), "i": i})
251
+ set i to i + 1
252
+ let ranked be sort_by(scored, (x) => (0 - x["s"]) * 100000 + x["i"])
253
+ let out be []
254
+ each x in slice(ranked, 0, MAX_CANDIDATES)
255
+ set out to append(out, x["c"])
256
+ -- de vuelta al orden de pantalla: el modelo lee la lista como está en la ventana
257
+ give {"cands": sort_by(out, (c) => c["order"]), "dropped": length(cands) - MAX_CANDIDATES}
258
+
259
+ -- elementos de get_window_state → {cands: [{id, desc, cls, action, label, state, element}], excluded: {motivo: n},
260
+ -- risky: [etiquetas], dropped, elements: [{role_class, label, state}]}. `want_text` = el texto que el LLM pasó
261
+ -- (sin él no hay candidatos set_text).
262
+ export task native_candidates(elements, goal, want_text)
263
+ let by_index be {}
264
+ each e in elements
265
+ when contains(e, "element_index")
266
+ set by_index[text(floor(number(text(e["element_index"]))))] to e
267
+ let cands be []
268
+ let seen be []
269
+ let excluded be {}
270
+ let risky be []
271
+ let summary be []
272
+ let order be 0
273
+ each e in elements
274
+ let cls be role_class(txt(e, "role"))
275
+ let why be nothing
276
+ let label be squash(txt(e, "label"))
277
+ when cls == nothing
278
+ set why to "unknown_role"
279
+ otherwise when contains(e, "enabled") and e["enabled"] == false
280
+ set why to "disabled"
281
+ otherwise when not contains(e, "element_index")
282
+ set why to "no_index"
283
+ otherwise when label == ""
284
+ set why to "unlabelled"
285
+ otherwise when cls != "text_input" and label == squash(txt(e, "value"))
286
+ set why to "label_is_value"
287
+ otherwise when cls == "text_input" and trim(txt(e, "value")) != "" and label == squash(txt(e, "value"))
288
+ set why to "label_is_value"
289
+ otherwise when contains(e, "frame") and e["frame"] != nothing and (num_or(e["frame"], "w", 1) <= 0 or num_or(e["frame"], "h", 1) <= 0)
290
+ set why to "off_screen"
291
+ otherwise when in_title_bar(e, by_index) or is_chrome_label(label)
292
+ set why to "window_chrome"
293
+ otherwise when cls == "text_input" and want_text == nothing
294
+ set why to "no_text_supplied"
295
+ when why == nothing
296
+ let rk be risk_of(label)
297
+ when rk != nothing
298
+ set why to "risk_" + rk
299
+ set risky to append(risky, label)
300
+ when why != nothing
301
+ set excluded[why] to (when contains(excluded, why) then excluded[why] otherwise 0) + 1
302
+ otherwise
303
+ let state be native_state(e, cls, want_text)
304
+ let id be unique_id(readable_id(cls, label), seen)
305
+ set seen to append(seen, id)
306
+ set cands to append(cands, {"id": id, "desc": describe_candidate(cls, label, state, want_text), "cls": cls, "action": CLASS_ACTION[cls], "label": label, "state": state, "element": floor(number(text(e["element_index"]))), "order": order})
307
+ set order to order + 1
308
+ when length(summary) < MAX_ELEMENTS_IN_STATE
309
+ set summary to append(summary, {"role_class": cls, "label": short(label, 60), "state": state})
310
+ let capped be cap_ranked(cands, goal)
311
+ give {"cands": capped["cands"], "excluded": excluded, "risky": risky, "dropped": capped["dropped"], "elements": summary}
312
+
313
+ -- refs de un snapshot de página (semantic_v2: {ref, role, name, actions, visibility}) → la misma forma, con `ref`
314
+ export task page_candidates(refs, goal, want_text)
315
+ let cands be []
316
+ let seen be []
317
+ let excluded be {}
318
+ let risky be []
319
+ let summary be []
320
+ let order be 0
321
+ each r in refs
322
+ let cls be role_class(txt(r, "role"))
323
+ -- en la web un combobox suele ser un campo con autocompletado (el buscador de MercadoLibre): con texto para
324
+ -- escribir va como campo; describirlo como «abrir menú» bajó a Jev al 59% (2026-09-28)
325
+ when cls == "popup" and want_text != nothing and norm_role(txt(r, "role")) == "combobox"
326
+ set cls to "text_input"
327
+ let label be squash(txt(r, "name"))
328
+ let why be nothing
329
+ when cls == nothing
330
+ set why to "unknown_role"
331
+ otherwise when label == ""
332
+ set why to "unlabelled"
333
+ otherwise when txt(r, "ref") == ""
334
+ set why to "no_ref"
335
+ otherwise when cls == "text_input" and want_text == nothing
336
+ set why to "no_text_supplied"
337
+ otherwise when lower(txt(r, "visibility")) == "hidden"
338
+ set why to "hidden"
339
+ when why == nothing
340
+ let rk be risk_of(label)
341
+ when rk != nothing
342
+ set why to "risk_" + rk
343
+ set risky to append(risky, label)
344
+ when why != nothing
345
+ set excluded[why] to (when contains(excluded, why) then excluded[why] otherwise 0) + 1
346
+ otherwise
347
+ let id be unique_id(readable_id(cls, label), seen)
348
+ set seen to append(seen, id)
349
+ set cands to append(cands, {"id": id, "desc": describe_candidate(cls, label, "", want_text), "cls": cls, "action": CLASS_ACTION[cls], "label": label, "state": "", "ref": txt(r, "ref"), "order": order})
350
+ set order to order + 1
351
+ when length(summary) < MAX_ELEMENTS_IN_STATE
352
+ set summary to append(summary, {"role_class": cls, "label": short(label, 60), "state": ""})
353
+ let capped be cap_ranked(cands, goal)
354
+ give {"cands": capped["cands"], "excluded": excluded, "risky": risky, "dropped": capped["dropped"], "elements": summary}
355
+
356
+ -- criteria del `choose`: {id: descripción} con los reservados al final
357
+ export task criteria(cands)
358
+ let m be {}
359
+ each c in cands
360
+ set m[c["id"]] to c["desc"]
361
+ each k in keys(RESERVED)
362
+ set m[k] to RESERVED[k]
363
+ give m
364
+
365
+ -- estado que ve el juez: objetivo, dónde, historial acotado. Sin la lista de elementos: las opciones YA son los
366
+ -- controles, y Laya tiene 512 tokens de contexto (estado + pregunta + opciones) — lo que sobra se trunca por el final
367
+ export task decision_state(goal, where, elements, history)
368
+ let h be history
369
+ when length(h) > MAX_HISTORY
370
+ set h to slice(h, length(h) - MAX_HISTORY, length(h))
371
+ let st be {"goal": short(goal, 600), "where": where}
372
+ when length(h) > 0
373
+ set st["history"] to h
374
+ give st
375
+
376
+ -- compuerta sobre una respuesta `choose` del bloque judge →
377
+ -- {verdict: "act" | "reobserve" | "abstain" | "unsure" | "offline", id, p, margin, top: [{id, p}]}
378
+ export task gate(answer, min_p, min_margin)
379
+ when answer == nothing
380
+ give {"verdict": "offline", "id": nothing, "p": 0, "margin": 0, "top": []}
381
+ when not contains(answer, "available") or answer["available"] != true
382
+ give {"verdict": "offline", "id": nothing, "p": 0, "margin": 0, "top": []}
383
+ let probs be when contains(answer, "probabilities") then answer["probabilities"] otherwise {}
384
+ let pairs be []
385
+ each k in keys(probs)
386
+ set pairs to append(pairs, {"id": k, "p": number(text(probs[k]))})
387
+ let ranked be sort_by(pairs, (x) => 0 - x["p"])
388
+ let top be slice(ranked, 0, when length(ranked) < 4 then length(ranked) otherwise 4)
389
+ when length(ranked) == 0
390
+ give {"verdict": "unsure", "id": nothing, "p": 0, "margin": 0, "top": []}
391
+ let best be ranked[0]
392
+ let second be when length(ranked) > 1 then ranked[1]["p"] otherwise 0
393
+ let margin be best["p"] - second
394
+ -- `none` (la opción de escape de `or nothing`) o el reservado abstain: el juez no ve nada que sirva
395
+ when best["id"] == "none" or best["id"] == "abstain"
396
+ give {"verdict": "abstain", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
397
+ -- empate arriba: no es accionable (cua: «a tied top score is not actionable»)
398
+ when margin <= 0
399
+ give {"verdict": "unsure", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
400
+ when best["p"] < min_p or margin < min_margin
401
+ give {"verdict": "unsure", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
402
+ when best["id"] == "reobserve"
403
+ give {"verdict": "reobserve", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
404
+ give {"verdict": "act", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
405
+
406
+ export task find(cands, id)
407
+ each c in cands
408
+ when c["id"] == id
409
+ give c
410
+ give nothing
package/lib/fs.syn CHANGED
@@ -8,7 +8,7 @@
8
8
  use "./tools/common.syn" as c
9
9
 
10
10
  task sh_quote(s)
11
- give "'" + replace_text(s, "'", "'\\''") + "'"
11
+ give "'" + replace(s, "'", "'\\''") + "'"
12
12
 
13
13
  -- shell POSIX aunque LAMPSON_SHELL apunte a cmd: mv/cp/rm/mkdir son de Git Bash
14
14
  task shell()
@@ -55,7 +55,7 @@ task check_name(name)
55
55
  let n be trim(text(name))
56
56
  when n == "" or n == "." or n == ".." or contains(n, "/") or contains(n, "\\")
57
57
  raise("invalid name \"" + text(name) + "\": one path segment, without / or \\")
58
- when length(find_all(n, "[<>:\"|?*]")) > 0
58
+ when length(regex_find_all(n, "[<>:\"|?*]")) > 0
59
59
  raise("invalid name \"" + n + "\": < > : \" | ? * are not allowed in file names")
60
60
  give n
61
61
 
@@ -102,6 +102,20 @@ export task create(dir, name, content)
102
102
  write_file(c.ROOT + "/" + rel, when content == nothing then "" otherwise text(content))
103
103
  give {"path": rel, "is_dir": false}
104
104
 
105
+ -- guardar desde el editor de la web: pisa SOLO un archivo que ya existe (crear es `create`), dentro del workspace.
106
+ -- "¿cambió en disco desde que lo abrí?" lo comprueba el cliente releyendo antes de guardar (js/editor.js), así el
107
+ -- servidor no guarda estado por archivo.
108
+ export task write(path, content)
109
+ require file("workspace")
110
+ require file("workspace/*")
111
+ let t be target(path)
112
+ must_exist(t)
113
+ when t["info"]["is_dir"]
114
+ raise("\"" + t["rel"] + "\" is a folder")
115
+ let s be when content == nothing then "" otherwise text(content)
116
+ write_file(t["real"], s)
117
+ give {"path": t["rel"], "is_dir": false, "lines": length(split(s, "\n"))}
118
+
105
119
  export task rename(path, name)
106
120
  require exec
107
121
  require env("LAMPSON_*")
@@ -215,6 +229,8 @@ export task apply(b)
215
229
  give mkdir(path, name)
216
230
  when op == "create"
217
231
  give create(path, name, when contains(b, "content") then b["content"] otherwise "")
232
+ when op == "write"
233
+ give write(path, when contains(b, "content") then b["content"] otherwise "")
218
234
  when op == "rename"
219
235
  give rename(path, name)
220
236
  when op == "move"
@@ -223,4 +239,4 @@ export task apply(b)
223
239
  give copy(path, dest)
224
240
  when op == "delete"
225
241
  give remove(path)
226
- raise("unknown op '" + op + "' (mkdir, create, rename, move, copy, delete)")
242
+ raise("unknown op '" + op + "' (mkdir, create, write, rename, move, copy, delete)")
package/lib/git.syn CHANGED
@@ -21,12 +21,12 @@ export task status()
21
21
  let branch be ""
22
22
  let changes be {}
23
23
  let counts be {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}
24
- each line in split(replace_text(out, "\r", ""), "\n")
24
+ each line in split(replace(out, "\r", ""), "\n")
25
25
  when starts_with(line, "## ")
26
26
  set branch to split(split(slice(line, 3, length(line)), "...")[0], " ")[0]
27
27
  otherwise when length(line) > 3
28
28
  let xy be slice(line, 0, 2)
29
- let path be replace_text(trim(slice(line, 3, length(line))), "\"", "")
29
+ let path be replace(trim(slice(line, 3, length(line))), "\"", "")
30
30
  when contains(path, " -> ")
31
31
  set path to split(path, " -> ")[1]
32
32
  let code be trim(xy)
package/lib/judge.syn ADDED
@@ -0,0 +1,78 @@
1
+ -- lib/judge.syn — el juez de decisión de Lampson: un modelo System One que contesta preguntas tipadas con
2
+ -- probabilidades (el bloque `judge` de Synsema: whether / choose / rate), no con texto
3
+ --
4
+ -- Es una credencial/configuración de Lampson, como el proveedor del LLM — no de una tool. Hoy lo usan:
5
+ -- * computer_use: la acción `act` (elige el próximo control de una lista cerrada) y page_select (elige la opción
6
+ -- de un desplegable cuando el texto no coincide) — lib/computer.syn + lib/computer_judge.syn
7
+ -- y cualquier parte de Lampson que necesite una decisión calibrada y barata puede usarlo con `require judge` y
8
+ -- judge.on() como compuerta (clasificar, rutear, validar, decidir si algo aplica…).
9
+ --
10
+ -- Backends (se elige en ⚙ → Juez de decisión, /judge en la terminal):
11
+ -- jev TypeSafe Jev, en la nube: rápido (~0,3 s por decisión), multilingüe, necesita API key (keys.typesafe)
12
+ -- laya Laya local (ModernBERT, ~840 MB en disco): sin red ni costo, ~1-2 s por decisión en CPU; medido
13
+ -- 2026-09-28: con interfaces en español el checkpoint `english` se equivoca con confianza alta
14
+ -- off sin juez
15
+ -- El runtime lee el proveedor del ENTORNO al arrancar el proceso (settings.judge_env): cambiarlo reinicia el
16
+ -- workspace (web) o pide salir y volver a entrar (terminal).
17
+
18
+ use "./settings.syn" as settings
19
+
20
+ export let MODES be ["off", "jev", "laya"]
21
+
22
+ export task norm_mode(v)
23
+ let t be lower(trim(text(when v == nothing then "" otherwise v)))
24
+ when t == "jev" or t == "typesafe"
25
+ give "jev"
26
+ when t == "laya" or t == "local"
27
+ give "laya"
28
+ give "off"
29
+
30
+ -- el modo configurado (config.json / .env), esté vivo o no en este proceso
31
+ export task mode()
32
+ require file.read(".lampson")
33
+ require file.read(".lampson/*")
34
+ require env("LAMPSON_*")
35
+ give norm_mode(settings.judge_setting()["mode"])
36
+
37
+ -- encendido en la configuración Y con un proveedor vivo en este proceso: la compuerta de quien lo use
38
+ export task on()
39
+ require file.read(".lampson")
40
+ require file.read(".lampson/*")
41
+ require env("LAMPSON_*")
42
+ when mode() == "off"
43
+ give false
44
+ give judge_available()
45
+
46
+ -- {mode, model_setting, has_key, live, provider_model, restart, from_env}: lo que ven la UI y /judge
47
+ export task status()
48
+ require file.read(".lampson")
49
+ require file.read(".lampson/*")
50
+ require env("LAMPSON_*")
51
+ let js be settings.judge_setting()
52
+ let m be norm_mode(js["mode"])
53
+ let live be judge_available()
54
+ let pm be judge_model()
55
+ -- el proceso corre otro proveedor que el configurado (o ninguno): hace falta reiniciar
56
+ let restart be false
57
+ when m != "off" and not live
58
+ set restart to true
59
+ when m == "laya" and pm != nothing and not starts_with(text(pm), "laya:")
60
+ set restart to true
61
+ when m == "jev" and pm != nothing and starts_with(text(pm), "laya:")
62
+ set restart to true
63
+ give {"mode": m, "model_setting": text(js["model"]), "has_key": settings.raw_key("typesafe") != "", "live": live, "provider_model": when pm == nothing then "" otherwise text(pm), "restart": restart, "from_env": js["from_env"]}
64
+
65
+ -- guardar el modo (y opcionalmente el modelo / la key de TypeSafe) → {ok, error}
66
+ export task configure(m, model, key)
67
+ require file(".lampson")
68
+ require file(".lampson/*")
69
+ require env("LAMPSON_*")
70
+ let nm be lower(trim(text(m)))
71
+ when not contains(MODES, nm) and nm != "typesafe" and nm != "local"
72
+ give {"ok": false, "error": "judge: off, jev or laya"}
73
+ set nm to norm_mode(nm)
74
+ when key != nothing
75
+ when trim(text(key)) != ""
76
+ settings.set_key("typesafe", trim(text(key)))
77
+ settings.set_judge(nm, model)
78
+ give {"ok": true, "error": nothing}
package/lib/line.syn CHANGED
@@ -37,7 +37,7 @@ task starts(s, p)
37
37
  task candidates(buf, ctx)
38
38
  when not starts(buf, "/") or contains(buf, "\n")
39
39
  give {"kind": "", "items": [], "token": ""}
40
- let sp be capture(buf, "^(/\S*)\s+(.*)$")
40
+ let sp be regex_capture(buf, "^(/\S*)\s+(.*)$")
41
41
  when sp == nothing
42
42
  -- comando a medias: recientes primero cuando es "/" pelado
43
43
  let items be []
@@ -208,7 +208,7 @@ export task read(prompt, color, ctx)
208
208
  set result to nothing
209
209
  set done to true
210
210
  otherwise when ev["type"] == "paste"
211
- set st to insert(st, replace_text(ev["text"], "\r", ""))
211
+ set st to insert(st, replace(ev["text"], "\r", ""))
212
212
  set st to refresh_menu(st, ctx)
213
213
  otherwise when ev["type"] == "resize"
214
214
  ui.set_cols(ev["cols"])