lampson 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +382 -382
- package/bin/lampson.js +8 -2
- package/chat.syn +799 -799
- package/lampson.cmd +4 -4
- package/lampson.ps1 +88 -88
- package/lampson.sh +42 -42
- package/lib/agents.syn +471 -471
- package/lib/lamps.syn +386 -386
- package/lib/lsp.syn +503 -503
- package/lib/mcp.syn +403 -403
- package/lib/permission.syn +154 -154
- package/lib/prompt.syn +75 -75
- package/lib/session.syn +111 -111
- package/lib/skills.syn +179 -179
- package/lib/tools/bash.syn +105 -105
- package/lib/tools/edit.syn +32 -32
- package/lib/tools/memo.syn +148 -148
- package/lib/tools/process.syn +46 -46
- package/lib/tools.syn +198 -198
- package/lib/tree.syn +59 -59
- package/package.json +1 -1
- package/public/index.html +1268 -1268
- package/skills/lampson/SKILL.md +117 -117
- package/web.syn +468 -468
package/lib/session.syn
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
-- lib/session.syn — persistencia de sesiones (historial canónico como JSON en .lampson/sessions/)
|
|
2
|
-
--
|
|
3
|
-
-- Aquí: un JSON por sesión, reescrito atómicamente en cada turno (write_file es atómico).
|
|
4
|
-
-- Suficiente para v1 y trivialmente inspeccionable. Migrar a `db("./.lampson/sessions.db")` si crece.
|
|
5
|
-
--
|
|
6
|
-
-- MIGA: no existe append_file → releer + reescribir. write_file crea los directorios padres.
|
|
7
|
-
|
|
8
|
-
use "./tools/memo.syn" as memo
|
|
9
|
-
|
|
10
|
-
let DIR be ".lampson/sessions"
|
|
11
|
-
|
|
12
|
-
export task new_id()
|
|
13
|
-
require time
|
|
14
|
-
give format_time(now(), "%Y%m%d-%H%M%S")
|
|
15
|
-
|
|
16
|
-
task path_of(id)
|
|
17
|
-
give DIR + "/" + id + ".json"
|
|
18
|
-
|
|
19
|
-
export task save(id, messages, meta)
|
|
20
|
-
require file(".lampson")
|
|
21
|
-
require file(".lampson/*")
|
|
22
|
-
require time
|
|
23
|
-
require env("LAMPSON_*")
|
|
24
|
-
-- cada sesión queda marcada con el proyecto (slug del workspace) para que list() no mezcle proyectos
|
|
25
|
-
set meta["project"] to memo.slug()
|
|
26
|
-
-- y con la corrida de lampson que la guardó (blackboard "lampson:run", lo comparte el entry al arrancar):
|
|
27
|
-
-- reanudar una sesión de OTRA corrida dispara la nota de procesos muertos (proc.resume_note)
|
|
28
|
-
observe "lampson:run" as run_info
|
|
29
|
-
when run_info != nothing
|
|
30
|
-
set meta["run"] to run_info["id"]
|
|
31
|
-
let doc be {"id": id, "updated": format_time(now()), "meta": meta, "messages": messages}
|
|
32
|
-
write_file(path_of(id), json_encode(doc))
|
|
33
|
-
give id
|
|
34
|
-
|
|
35
|
-
export task load(id)
|
|
36
|
-
require file.read(".lampson")
|
|
37
|
-
require file.read(".lampson/*")
|
|
38
|
-
let doc be json_decode(read_file(path_of(id)))
|
|
39
|
-
give doc
|
|
40
|
-
|
|
41
|
-
-- ¿el doc cargado fue guardado por OTRA corrida de lampson? (sin marca = sesión vieja → sí)
|
|
42
|
-
export task from_other_run(doc)
|
|
43
|
-
observe "lampson:run" as run_info
|
|
44
|
-
when run_info == nothing
|
|
45
|
-
give false
|
|
46
|
-
when not contains(doc["meta"], "run")
|
|
47
|
-
give true
|
|
48
|
-
give doc["meta"]["run"] != run_info["id"]
|
|
49
|
-
|
|
50
|
-
export task exists(id)
|
|
51
|
-
require file.read(".lampson")
|
|
52
|
-
require file.read(".lampson/*")
|
|
53
|
-
try
|
|
54
|
-
read_file(path_of(id), 1, 1)
|
|
55
|
-
give true
|
|
56
|
-
recover err
|
|
57
|
-
give false
|
|
58
|
-
|
|
59
|
-
-- [{id, updated, title}] del proyecto ACTUAL, ordenado por id descendente (los ids son timestamps).
|
|
60
|
-
-- Sesiones viejas sin marca de proyecto se listan igual (anteriores a esta marca).
|
|
61
|
-
export task list()
|
|
62
|
-
require file.read(".lampson")
|
|
63
|
-
require file.read(".lampson/*")
|
|
64
|
-
require env("LAMPSON_*")
|
|
65
|
-
let mine be memo.slug()
|
|
66
|
-
let out be []
|
|
67
|
-
try
|
|
68
|
-
let entries be list_dir(DIR)
|
|
69
|
-
each e in entries
|
|
70
|
-
when not e["is_dir"]
|
|
71
|
-
let id be replace_text(e["name"], ".json", "")
|
|
72
|
-
try
|
|
73
|
-
let d be load(id)
|
|
74
|
-
let title be when contains(d["meta"], "title") then d["meta"]["title"] otherwise ""
|
|
75
|
-
let project be when contains(d["meta"], "project") then d["meta"]["project"] otherwise mine
|
|
76
|
-
when project == mine
|
|
77
|
-
set out to append(out, {"id": id, "updated": d["updated"], "title": title})
|
|
78
|
-
recover err
|
|
79
|
-
set out to out
|
|
80
|
-
recover err
|
|
81
|
-
give []
|
|
82
|
-
give sort_by(out, (s) => 0 - number(replace_text(replace_text(s["id"], "-", ""), "test", "")))
|
|
83
|
-
|
|
84
|
-
-- Borrar una sesión (no hay delete_file en el runtime: se usa el shell del sistema sobre la ruta fija).
|
|
85
|
-
export task delete(id)
|
|
86
|
-
require exec
|
|
87
|
-
require env("OS")
|
|
88
|
-
require file.read(".lampson")
|
|
89
|
-
require file.read(".lampson/*")
|
|
90
|
-
when not matches(id, "[a-zA-Z0-9_-]{1,40}")
|
|
91
|
-
raise("invalid session id")
|
|
92
|
-
when not exists(id)
|
|
93
|
-
give "no existe " + id
|
|
94
|
-
-- la sesión y su traza (.lampson/trace/<id>.log + .last) se van juntas
|
|
95
|
-
let files be [path_of(id), ".lampson/trace/" + id + ".log", ".lampson/trace/" + id + ".last"]
|
|
96
|
-
each f in files
|
|
97
|
-
when env("OS", "") == "Windows_NT"
|
|
98
|
-
run("cmd", ["/c", "del", "/q", replace_text(f, "/", "\\")], 10)
|
|
99
|
-
otherwise
|
|
100
|
-
run("rm", ["-f", f], 10)
|
|
101
|
-
give "borrada " + id
|
|
102
|
-
|
|
103
|
-
-- Título = primer mensaje de usuario recortado.
|
|
104
|
-
export task title_of(messages)
|
|
105
|
-
each m in messages
|
|
106
|
-
when m["role"] == "user"
|
|
107
|
-
let t be replace_text(m["content"], "\n", " ")
|
|
108
|
-
when length(t) > 60
|
|
109
|
-
give slice(t, 0, 60) + "…"
|
|
110
|
-
give t
|
|
111
|
-
give "(sin título)"
|
|
1
|
+
-- lib/session.syn — persistencia de sesiones (historial canónico como JSON en .lampson/sessions/)
|
|
2
|
+
--
|
|
3
|
+
-- Aquí: un JSON por sesión, reescrito atómicamente en cada turno (write_file es atómico).
|
|
4
|
+
-- Suficiente para v1 y trivialmente inspeccionable. Migrar a `db("./.lampson/sessions.db")` si crece.
|
|
5
|
+
--
|
|
6
|
+
-- MIGA: no existe append_file → releer + reescribir. write_file crea los directorios padres.
|
|
7
|
+
|
|
8
|
+
use "./tools/memo.syn" as memo
|
|
9
|
+
|
|
10
|
+
let DIR be ".lampson/sessions"
|
|
11
|
+
|
|
12
|
+
export task new_id()
|
|
13
|
+
require time
|
|
14
|
+
give format_time(now(), "%Y%m%d-%H%M%S")
|
|
15
|
+
|
|
16
|
+
task path_of(id)
|
|
17
|
+
give DIR + "/" + id + ".json"
|
|
18
|
+
|
|
19
|
+
export task save(id, messages, meta)
|
|
20
|
+
require file(".lampson")
|
|
21
|
+
require file(".lampson/*")
|
|
22
|
+
require time
|
|
23
|
+
require env("LAMPSON_*")
|
|
24
|
+
-- cada sesión queda marcada con el proyecto (slug del workspace) para que list() no mezcle proyectos
|
|
25
|
+
set meta["project"] to memo.slug()
|
|
26
|
+
-- y con la corrida de lampson que la guardó (blackboard "lampson:run", lo comparte el entry al arrancar):
|
|
27
|
+
-- reanudar una sesión de OTRA corrida dispara la nota de procesos muertos (proc.resume_note)
|
|
28
|
+
observe "lampson:run" as run_info
|
|
29
|
+
when run_info != nothing
|
|
30
|
+
set meta["run"] to run_info["id"]
|
|
31
|
+
let doc be {"id": id, "updated": format_time(now()), "meta": meta, "messages": messages}
|
|
32
|
+
write_file(path_of(id), json_encode(doc))
|
|
33
|
+
give id
|
|
34
|
+
|
|
35
|
+
export task load(id)
|
|
36
|
+
require file.read(".lampson")
|
|
37
|
+
require file.read(".lampson/*")
|
|
38
|
+
let doc be json_decode(read_file(path_of(id)))
|
|
39
|
+
give doc
|
|
40
|
+
|
|
41
|
+
-- ¿el doc cargado fue guardado por OTRA corrida de lampson? (sin marca = sesión vieja → sí)
|
|
42
|
+
export task from_other_run(doc)
|
|
43
|
+
observe "lampson:run" as run_info
|
|
44
|
+
when run_info == nothing
|
|
45
|
+
give false
|
|
46
|
+
when not contains(doc["meta"], "run")
|
|
47
|
+
give true
|
|
48
|
+
give doc["meta"]["run"] != run_info["id"]
|
|
49
|
+
|
|
50
|
+
export task exists(id)
|
|
51
|
+
require file.read(".lampson")
|
|
52
|
+
require file.read(".lampson/*")
|
|
53
|
+
try
|
|
54
|
+
read_file(path_of(id), 1, 1)
|
|
55
|
+
give true
|
|
56
|
+
recover err
|
|
57
|
+
give false
|
|
58
|
+
|
|
59
|
+
-- [{id, updated, title}] del proyecto ACTUAL, ordenado por id descendente (los ids son timestamps).
|
|
60
|
+
-- Sesiones viejas sin marca de proyecto se listan igual (anteriores a esta marca).
|
|
61
|
+
export task list()
|
|
62
|
+
require file.read(".lampson")
|
|
63
|
+
require file.read(".lampson/*")
|
|
64
|
+
require env("LAMPSON_*")
|
|
65
|
+
let mine be memo.slug()
|
|
66
|
+
let out be []
|
|
67
|
+
try
|
|
68
|
+
let entries be list_dir(DIR)
|
|
69
|
+
each e in entries
|
|
70
|
+
when not e["is_dir"]
|
|
71
|
+
let id be replace_text(e["name"], ".json", "")
|
|
72
|
+
try
|
|
73
|
+
let d be load(id)
|
|
74
|
+
let title be when contains(d["meta"], "title") then d["meta"]["title"] otherwise ""
|
|
75
|
+
let project be when contains(d["meta"], "project") then d["meta"]["project"] otherwise mine
|
|
76
|
+
when project == mine
|
|
77
|
+
set out to append(out, {"id": id, "updated": d["updated"], "title": title})
|
|
78
|
+
recover err
|
|
79
|
+
set out to out
|
|
80
|
+
recover err
|
|
81
|
+
give []
|
|
82
|
+
give sort_by(out, (s) => 0 - number(replace_text(replace_text(s["id"], "-", ""), "test", "")))
|
|
83
|
+
|
|
84
|
+
-- Borrar una sesión (no hay delete_file en el runtime: se usa el shell del sistema sobre la ruta fija).
|
|
85
|
+
export task delete(id)
|
|
86
|
+
require exec
|
|
87
|
+
require env("OS")
|
|
88
|
+
require file.read(".lampson")
|
|
89
|
+
require file.read(".lampson/*")
|
|
90
|
+
when not matches(id, "[a-zA-Z0-9_-]{1,40}")
|
|
91
|
+
raise("invalid session id")
|
|
92
|
+
when not exists(id)
|
|
93
|
+
give "no existe " + id
|
|
94
|
+
-- la sesión y su traza (.lampson/trace/<id>.log + .last) se van juntas
|
|
95
|
+
let files be [path_of(id), ".lampson/trace/" + id + ".log", ".lampson/trace/" + id + ".last"]
|
|
96
|
+
each f in files
|
|
97
|
+
when env("OS", "") == "Windows_NT"
|
|
98
|
+
run("cmd", ["/c", "del", "/q", replace_text(f, "/", "\\")], 10)
|
|
99
|
+
otherwise
|
|
100
|
+
run("rm", ["-f", f], 10)
|
|
101
|
+
give "borrada " + id
|
|
102
|
+
|
|
103
|
+
-- Título = primer mensaje de usuario recortado.
|
|
104
|
+
export task title_of(messages)
|
|
105
|
+
each m in messages
|
|
106
|
+
when m["role"] == "user"
|
|
107
|
+
let t be replace_text(m["content"], "\n", " ")
|
|
108
|
+
when length(t) > 60
|
|
109
|
+
give slice(t, 0, 60) + "…"
|
|
110
|
+
give t
|
|
111
|
+
give "(sin título)"
|
package/lib/skills.syn
CHANGED
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
-- lib/skills.syn — skills: procedimientos en Markdown que el modelo carga bajo demanda
|
|
2
|
-
--
|
|
3
|
-
-- Una skill = carpeta con SKILL.md y frontmatter YAML mínimo:
|
|
4
|
-
-- ---
|
|
5
|
-
-- name: deploy
|
|
6
|
-
-- description: Cómo desplegar este proyecto (una línea; es lo que ve el modelo en el índice)
|
|
7
|
-
-- ---
|
|
8
|
-
-- ...instrucciones...
|
|
9
|
-
--
|
|
10
|
-
-- Se buscan en (prioridad de abajo hacia arriba, el nombre repetido gana el más específico):
|
|
11
|
-
-- .lampson/skills-global/<name>/SKILL.md -- ~/.agents/skills (junction que crea lampson.ps1/sh; `npx skills add -g`)
|
|
12
|
-
-- .lampson/skills-claude/<name>/SKILL.md -- ~/.claude/skills (idem; las de Claude Code también sirven acá)
|
|
13
|
-
-- skills/<name>/SKILL.md -- del harness (enseñan a usar lampson y Synsema)
|
|
14
|
-
-- workspace/.claude/skills/<name>/SKILL.md -- del proyecto, instaladas para Claude Code
|
|
15
|
-
-- workspace/.agents/skills/<name>/SKILL.md -- del proyecto, canónico de `npx skills add owner/repo` (skills.sh)
|
|
16
|
-
-- workspace/skills/<name>/SKILL.md -- del proyecto (committeables)
|
|
17
|
-
-- workspace/.lampson/skills/<name>/SKILL.md -- locales al proyecto (no committeadas)
|
|
18
|
-
--
|
|
19
|
-
-- Skills externas (2026-08-27): el estándar "Agent Skills" (agentskills.io) es el mismo formato que usamos
|
|
20
|
-
-- (SKILL.md + frontmatter name/description), así que NO hay instalador propio: `npx skills add owner/repo
|
|
21
|
-
-- --skill x` deja la skill en ./.agents/skills (o ~/.agents/skills con -g) y Lampson la ve en el próximo
|
|
22
|
-
-- arranque. Las carpetas de HOME no se pueden declarar como capability con ruta dinámica → se MONTAN
|
|
23
|
-
-- bajo .lampson/ (mismo truco que workspace/).
|
|
24
|
-
--
|
|
25
|
-
-- El system prompt lleva SOLO el índice (name + description); el contenido entra al contexto
|
|
26
|
-
-- cuando el modelo llama a la tool `skill(name)`. Así el prompt no crece con cada skill.
|
|
27
|
-
|
|
28
|
-
let ROOTS be [
|
|
29
|
-
{"dir": ".lampson/skills-global", "source": "global"},
|
|
30
|
-
{"dir": ".lampson/skills-claude", "source": "global"},
|
|
31
|
-
{"dir": "skills", "source": "harness"},
|
|
32
|
-
{"dir": "workspace/.claude/skills", "source": "project"},
|
|
33
|
-
{"dir": "workspace/.agents/skills", "source": "project"},
|
|
34
|
-
{"dir": "workspace/skills", "source": "project"},
|
|
35
|
-
{"dir": "workspace/.lampson/skills", "source": "local"}
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
task frontmatter(md)
|
|
39
|
-
-- devuelve {name, description, body}; sin frontmatter → name vacío
|
|
40
|
-
let out be {"name": "", "description": "", "body": md}
|
|
41
|
-
when not starts_with(md, "---")
|
|
42
|
-
give out
|
|
43
|
-
let rest be slice(md, 3, length(md))
|
|
44
|
-
let parts be split(rest, "\n---")
|
|
45
|
-
when length(parts) < 2
|
|
46
|
-
give out
|
|
47
|
-
let head be parts[0]
|
|
48
|
-
let body be join(slice(parts, 1, length(parts)), "\n---")
|
|
49
|
-
each line in split(head, "\n")
|
|
50
|
-
let l be trim(line)
|
|
51
|
-
when starts_with(l, "name:")
|
|
52
|
-
set out["name"] to trim(slice(l, 5, length(l)))
|
|
53
|
-
otherwise when starts_with(l, "description:")
|
|
54
|
-
set out["description"] to trim(slice(l, 12, length(l)))
|
|
55
|
-
set out["body"] to trim(body)
|
|
56
|
-
give out
|
|
57
|
-
|
|
58
|
-
task scan_root(root, acc)
|
|
59
|
-
let out be acc
|
|
60
|
-
try
|
|
61
|
-
each e in list_dir(root["dir"])
|
|
62
|
-
when e["is_dir"]
|
|
63
|
-
let path be root["dir"] + "/" + e["name"] + "/SKILL.md"
|
|
64
|
-
try
|
|
65
|
-
let fm be frontmatter(read_file(path))
|
|
66
|
-
let name be when fm["name"] == "" then e["name"] otherwise fm["name"]
|
|
67
|
-
set out[name] to {"name": name, "description": fm["description"], "path": path, "source": root["source"]}
|
|
68
|
-
recover err
|
|
69
|
-
set out to out
|
|
70
|
-
recover err
|
|
71
|
-
give out
|
|
72
|
-
give out
|
|
73
|
-
|
|
74
|
-
-- índice: map name → {name, description, path, source}
|
|
75
|
-
export task index()
|
|
76
|
-
require file.read("skills")
|
|
77
|
-
require file.read("skills/*")
|
|
78
|
-
require file.read("workspace")
|
|
79
|
-
require file.read("workspace/*")
|
|
80
|
-
require file.read(".lampson")
|
|
81
|
-
require file.read(".lampson/*")
|
|
82
|
-
let acc be {}
|
|
83
|
-
each r in ROOTS
|
|
84
|
-
set acc to scan_root(r, acc)
|
|
85
|
-
give acc
|
|
86
|
-
|
|
87
|
-
-- sección del system prompt
|
|
88
|
-
export task prompt_section(idx)
|
|
89
|
-
let names be sort_by(keys(idx), (n) => n)
|
|
90
|
-
when length(names) == 0
|
|
91
|
-
give ""
|
|
92
|
-
let lines be ["", "# Skills (load one with the `skill` tool when relevant — read it BEFORE doing that kind of task)"]
|
|
93
|
-
each n in names
|
|
94
|
-
let s be idx[n]
|
|
95
|
-
set lines to append(lines, "- " + n + " (" + s["source"] + "): " + s["description"])
|
|
96
|
-
give join(lines, "\n")
|
|
97
|
-
|
|
98
|
-
export task load(name)
|
|
99
|
-
require file.read("skills")
|
|
100
|
-
require file.read("skills/*")
|
|
101
|
-
require file.read("workspace")
|
|
102
|
-
require file.read("workspace/*")
|
|
103
|
-
require file.read(".lampson")
|
|
104
|
-
require file.read(".lampson/*")
|
|
105
|
-
let idx be index()
|
|
106
|
-
when not contains(idx, name)
|
|
107
|
-
raise("unknown skill '" + name + "'. Available: " + join(sort_by(keys(idx), (n) => n), ", "))
|
|
108
|
-
let s be idx[name]
|
|
109
|
-
let fm be frontmatter(read_file(s["path"]))
|
|
110
|
-
-- archivos hermanos de la skill (scripts, templates) para que el modelo sepa que existen
|
|
111
|
-
let dir be slice(s["path"], 0, length(s["path"]) - length("/SKILL.md"))
|
|
112
|
-
let extras be []
|
|
113
|
-
each e in list_dir(dir)
|
|
114
|
-
when e["name"] != "SKILL.md"
|
|
115
|
-
set extras to append(extras, dir + "/" + e["name"] + (when e["is_dir"] then "/" otherwise ""))
|
|
116
|
-
let head be "# skill: " + name + " (" + s["source"] + ", " + s["path"] + ")\n"
|
|
117
|
-
when length(extras) > 0
|
|
118
|
-
set head to head + "Files in this skill: " + join(extras, ", ") + "\n"
|
|
119
|
-
give head + "\n" + fm["body"]
|
|
120
|
-
|
|
121
|
-
-- ---------- instalación de skills externas (skills.sh / cualquier repo con SKILL.md) ----------
|
|
122
|
-
-- No reinventamos el instalador: `npx skills add <owner/repo> --skill <name>` (vercel-labs/skills) escribe
|
|
123
|
-
-- en ./.agents/skills (scope project) o ~/.agents/skills (-g, scope global — sirve para TODOS los proyectos,
|
|
124
|
-
-- que es lo que uno quiere con una skill de Go, de Next, de diseño…). `-a codex -y` = no interactivo
|
|
125
|
-
-- (codex ya usa la carpeta canónica, así que no crea symlinks extra; verificado 2026-08-27). Es una acción
|
|
126
|
-
-- con efectos fuera del workspace → permission.syn la marca "ask" SIEMPRE (human in the loop), incluso en yolo.
|
|
127
|
-
task shell_exe()
|
|
128
|
-
require env("LAMPSON_*")
|
|
129
|
-
require env("OS")
|
|
130
|
-
let is_win be env("OS", "") == "Windows_NT"
|
|
131
|
-
give env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
|
|
132
|
-
|
|
133
|
-
task valid_slug(s)
|
|
134
|
-
when s == nothing or s == ""
|
|
135
|
-
give false
|
|
136
|
-
give matches(s, "[a-zA-Z0-9._/@:-]{1,120}")
|
|
137
|
-
|
|
138
|
-
-- la junction/symlink .lampson/skills-global → ~/.agents/skills la crea lampson.ps1/sh al arrancar, pero si
|
|
139
|
-
-- la carpeta HOME no existía todavía (primera instalación global) hay que crear ambas ahora
|
|
140
|
-
task ensure_global_mount()
|
|
141
|
-
require exec
|
|
142
|
-
require env("LAMPSON_*")
|
|
143
|
-
require env("OS")
|
|
144
|
-
let is_win be env("OS", "") == "Windows_NT"
|
|
145
|
-
let win be "src=\"$USERPROFILE/.agents/skills\"; mkdir -p \"$src\"; [ -e .lampson/skills-global ] || cmd //c mklink //J \".lampson\\\\skills-global\" \"$(cygpath -w \"$src\")\" >/dev/null"
|
|
146
|
-
let unix be "src=\"$HOME/.agents/skills\"; mkdir -p \"$src\"; [ -e .lampson/skills-global ] || ln -s \"$src\" .lampson/skills-global"
|
|
147
|
-
run(shell_exe(), ["-c", when is_win then win otherwise unix], 30, {"cwd": "."})
|
|
148
|
-
|
|
149
|
-
task tail_out(s)
|
|
150
|
-
let t be strip_ansi(replace_text(s, "\r", ""))
|
|
151
|
-
when length(t) > 3000
|
|
152
|
-
give slice(t, length(t) - 3000, length(t))
|
|
153
|
-
give t
|
|
154
|
-
|
|
155
|
-
export task install(source, name, scope)
|
|
156
|
-
require exec
|
|
157
|
-
require time
|
|
158
|
-
require env("LAMPSON_*")
|
|
159
|
-
require env("OS")
|
|
160
|
-
require file.read("skills")
|
|
161
|
-
require file.read("skills/*")
|
|
162
|
-
require file.read("workspace")
|
|
163
|
-
require file.read("workspace/*")
|
|
164
|
-
require file.read(".lampson")
|
|
165
|
-
require file.read(".lampson/*")
|
|
166
|
-
when not valid_slug(source)
|
|
167
|
-
raise("invalid source '" + text(source) + "' (expected owner/repo or a github URL)")
|
|
168
|
-
when not valid_slug(name)
|
|
169
|
-
raise("invalid skill name '" + text(name) + "'")
|
|
170
|
-
let global be scope != "project"
|
|
171
|
-
let argv be ["-y", "skills", "add", source, "--skill", name, "-a", "codex", "-y"]
|
|
172
|
-
when global
|
|
173
|
-
set argv to append(argv, "-g")
|
|
174
|
-
ensure_global_mount()
|
|
175
|
-
let r be run(shell_exe(), ["-c", "CI=1 npx " + join(argv, " ") + " 2>&1"], 240, {"cwd": when global then "." otherwise "workspace"})
|
|
176
|
-
let idx be index()
|
|
177
|
-
when contains(idx, name)
|
|
178
|
-
give "installed skill '" + name + "' (" + (when global then "global: ~/.agents/skills, available in every project" otherwise "project: ./.agents/skills") + "). It is in the skills index now — call skill(name=\"" + name + "\") to load it.\n" + tail_out(r["stdout"])
|
|
179
|
-
give "npx skills finished (exit " + text(r["exit_code"]) + ") but '" + name + "' is not in the skills index. Output:\n" + tail_out(r["stdout"])
|
|
1
|
+
-- lib/skills.syn — skills: procedimientos en Markdown que el modelo carga bajo demanda
|
|
2
|
+
--
|
|
3
|
+
-- Una skill = carpeta con SKILL.md y frontmatter YAML mínimo:
|
|
4
|
+
-- ---
|
|
5
|
+
-- name: deploy
|
|
6
|
+
-- description: Cómo desplegar este proyecto (una línea; es lo que ve el modelo en el índice)
|
|
7
|
+
-- ---
|
|
8
|
+
-- ...instrucciones...
|
|
9
|
+
--
|
|
10
|
+
-- Se buscan en (prioridad de abajo hacia arriba, el nombre repetido gana el más específico):
|
|
11
|
+
-- .lampson/skills-global/<name>/SKILL.md -- ~/.agents/skills (junction que crea lampson.ps1/sh; `npx skills add -g`)
|
|
12
|
+
-- .lampson/skills-claude/<name>/SKILL.md -- ~/.claude/skills (idem; las de Claude Code también sirven acá)
|
|
13
|
+
-- skills/<name>/SKILL.md -- del harness (enseñan a usar lampson y Synsema)
|
|
14
|
+
-- workspace/.claude/skills/<name>/SKILL.md -- del proyecto, instaladas para Claude Code
|
|
15
|
+
-- workspace/.agents/skills/<name>/SKILL.md -- del proyecto, canónico de `npx skills add owner/repo` (skills.sh)
|
|
16
|
+
-- workspace/skills/<name>/SKILL.md -- del proyecto (committeables)
|
|
17
|
+
-- workspace/.lampson/skills/<name>/SKILL.md -- locales al proyecto (no committeadas)
|
|
18
|
+
--
|
|
19
|
+
-- Skills externas (2026-08-27): el estándar "Agent Skills" (agentskills.io) es el mismo formato que usamos
|
|
20
|
+
-- (SKILL.md + frontmatter name/description), así que NO hay instalador propio: `npx skills add owner/repo
|
|
21
|
+
-- --skill x` deja la skill en ./.agents/skills (o ~/.agents/skills con -g) y Lampson la ve en el próximo
|
|
22
|
+
-- arranque. Las carpetas de HOME no se pueden declarar como capability con ruta dinámica → se MONTAN
|
|
23
|
+
-- bajo .lampson/ (mismo truco que workspace/).
|
|
24
|
+
--
|
|
25
|
+
-- El system prompt lleva SOLO el índice (name + description); el contenido entra al contexto
|
|
26
|
+
-- cuando el modelo llama a la tool `skill(name)`. Así el prompt no crece con cada skill.
|
|
27
|
+
|
|
28
|
+
let ROOTS be [
|
|
29
|
+
{"dir": ".lampson/skills-global", "source": "global"},
|
|
30
|
+
{"dir": ".lampson/skills-claude", "source": "global"},
|
|
31
|
+
{"dir": "skills", "source": "harness"},
|
|
32
|
+
{"dir": "workspace/.claude/skills", "source": "project"},
|
|
33
|
+
{"dir": "workspace/.agents/skills", "source": "project"},
|
|
34
|
+
{"dir": "workspace/skills", "source": "project"},
|
|
35
|
+
{"dir": "workspace/.lampson/skills", "source": "local"}
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
task frontmatter(md)
|
|
39
|
+
-- devuelve {name, description, body}; sin frontmatter → name vacío
|
|
40
|
+
let out be {"name": "", "description": "", "body": md}
|
|
41
|
+
when not starts_with(md, "---")
|
|
42
|
+
give out
|
|
43
|
+
let rest be slice(md, 3, length(md))
|
|
44
|
+
let parts be split(rest, "\n---")
|
|
45
|
+
when length(parts) < 2
|
|
46
|
+
give out
|
|
47
|
+
let head be parts[0]
|
|
48
|
+
let body be join(slice(parts, 1, length(parts)), "\n---")
|
|
49
|
+
each line in split(head, "\n")
|
|
50
|
+
let l be trim(line)
|
|
51
|
+
when starts_with(l, "name:")
|
|
52
|
+
set out["name"] to trim(slice(l, 5, length(l)))
|
|
53
|
+
otherwise when starts_with(l, "description:")
|
|
54
|
+
set out["description"] to trim(slice(l, 12, length(l)))
|
|
55
|
+
set out["body"] to trim(body)
|
|
56
|
+
give out
|
|
57
|
+
|
|
58
|
+
task scan_root(root, acc)
|
|
59
|
+
let out be acc
|
|
60
|
+
try
|
|
61
|
+
each e in list_dir(root["dir"])
|
|
62
|
+
when e["is_dir"]
|
|
63
|
+
let path be root["dir"] + "/" + e["name"] + "/SKILL.md"
|
|
64
|
+
try
|
|
65
|
+
let fm be frontmatter(read_file(path))
|
|
66
|
+
let name be when fm["name"] == "" then e["name"] otherwise fm["name"]
|
|
67
|
+
set out[name] to {"name": name, "description": fm["description"], "path": path, "source": root["source"]}
|
|
68
|
+
recover err
|
|
69
|
+
set out to out
|
|
70
|
+
recover err
|
|
71
|
+
give out
|
|
72
|
+
give out
|
|
73
|
+
|
|
74
|
+
-- índice: map name → {name, description, path, source}
|
|
75
|
+
export task index()
|
|
76
|
+
require file.read("skills")
|
|
77
|
+
require file.read("skills/*")
|
|
78
|
+
require file.read("workspace")
|
|
79
|
+
require file.read("workspace/*")
|
|
80
|
+
require file.read(".lampson")
|
|
81
|
+
require file.read(".lampson/*")
|
|
82
|
+
let acc be {}
|
|
83
|
+
each r in ROOTS
|
|
84
|
+
set acc to scan_root(r, acc)
|
|
85
|
+
give acc
|
|
86
|
+
|
|
87
|
+
-- sección del system prompt
|
|
88
|
+
export task prompt_section(idx)
|
|
89
|
+
let names be sort_by(keys(idx), (n) => n)
|
|
90
|
+
when length(names) == 0
|
|
91
|
+
give ""
|
|
92
|
+
let lines be ["", "# Skills (load one with the `skill` tool when relevant — read it BEFORE doing that kind of task)"]
|
|
93
|
+
each n in names
|
|
94
|
+
let s be idx[n]
|
|
95
|
+
set lines to append(lines, "- " + n + " (" + s["source"] + "): " + s["description"])
|
|
96
|
+
give join(lines, "\n")
|
|
97
|
+
|
|
98
|
+
export task load(name)
|
|
99
|
+
require file.read("skills")
|
|
100
|
+
require file.read("skills/*")
|
|
101
|
+
require file.read("workspace")
|
|
102
|
+
require file.read("workspace/*")
|
|
103
|
+
require file.read(".lampson")
|
|
104
|
+
require file.read(".lampson/*")
|
|
105
|
+
let idx be index()
|
|
106
|
+
when not contains(idx, name)
|
|
107
|
+
raise("unknown skill '" + name + "'. Available: " + join(sort_by(keys(idx), (n) => n), ", "))
|
|
108
|
+
let s be idx[name]
|
|
109
|
+
let fm be frontmatter(read_file(s["path"]))
|
|
110
|
+
-- archivos hermanos de la skill (scripts, templates) para que el modelo sepa que existen
|
|
111
|
+
let dir be slice(s["path"], 0, length(s["path"]) - length("/SKILL.md"))
|
|
112
|
+
let extras be []
|
|
113
|
+
each e in list_dir(dir)
|
|
114
|
+
when e["name"] != "SKILL.md"
|
|
115
|
+
set extras to append(extras, dir + "/" + e["name"] + (when e["is_dir"] then "/" otherwise ""))
|
|
116
|
+
let head be "# skill: " + name + " (" + s["source"] + ", " + s["path"] + ")\n"
|
|
117
|
+
when length(extras) > 0
|
|
118
|
+
set head to head + "Files in this skill: " + join(extras, ", ") + "\n"
|
|
119
|
+
give head + "\n" + fm["body"]
|
|
120
|
+
|
|
121
|
+
-- ---------- instalación de skills externas (skills.sh / cualquier repo con SKILL.md) ----------
|
|
122
|
+
-- No reinventamos el instalador: `npx skills add <owner/repo> --skill <name>` (vercel-labs/skills) escribe
|
|
123
|
+
-- en ./.agents/skills (scope project) o ~/.agents/skills (-g, scope global — sirve para TODOS los proyectos,
|
|
124
|
+
-- que es lo que uno quiere con una skill de Go, de Next, de diseño…). `-a codex -y` = no interactivo
|
|
125
|
+
-- (codex ya usa la carpeta canónica, así que no crea symlinks extra; verificado 2026-08-27). Es una acción
|
|
126
|
+
-- con efectos fuera del workspace → permission.syn la marca "ask" SIEMPRE (human in the loop), incluso en yolo.
|
|
127
|
+
task shell_exe()
|
|
128
|
+
require env("LAMPSON_*")
|
|
129
|
+
require env("OS")
|
|
130
|
+
let is_win be env("OS", "") == "Windows_NT"
|
|
131
|
+
give env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
|
|
132
|
+
|
|
133
|
+
task valid_slug(s)
|
|
134
|
+
when s == nothing or s == ""
|
|
135
|
+
give false
|
|
136
|
+
give matches(s, "[a-zA-Z0-9._/@:-]{1,120}")
|
|
137
|
+
|
|
138
|
+
-- la junction/symlink .lampson/skills-global → ~/.agents/skills la crea lampson.ps1/sh al arrancar, pero si
|
|
139
|
+
-- la carpeta HOME no existía todavía (primera instalación global) hay que crear ambas ahora
|
|
140
|
+
task ensure_global_mount()
|
|
141
|
+
require exec
|
|
142
|
+
require env("LAMPSON_*")
|
|
143
|
+
require env("OS")
|
|
144
|
+
let is_win be env("OS", "") == "Windows_NT"
|
|
145
|
+
let win be "src=\"$USERPROFILE/.agents/skills\"; mkdir -p \"$src\"; [ -e .lampson/skills-global ] || cmd //c mklink //J \".lampson\\\\skills-global\" \"$(cygpath -w \"$src\")\" >/dev/null"
|
|
146
|
+
let unix be "src=\"$HOME/.agents/skills\"; mkdir -p \"$src\"; [ -e .lampson/skills-global ] || ln -s \"$src\" .lampson/skills-global"
|
|
147
|
+
run(shell_exe(), ["-c", when is_win then win otherwise unix], 30, {"cwd": "."})
|
|
148
|
+
|
|
149
|
+
task tail_out(s)
|
|
150
|
+
let t be strip_ansi(replace_text(s, "\r", ""))
|
|
151
|
+
when length(t) > 3000
|
|
152
|
+
give slice(t, length(t) - 3000, length(t))
|
|
153
|
+
give t
|
|
154
|
+
|
|
155
|
+
export task install(source, name, scope)
|
|
156
|
+
require exec
|
|
157
|
+
require time
|
|
158
|
+
require env("LAMPSON_*")
|
|
159
|
+
require env("OS")
|
|
160
|
+
require file.read("skills")
|
|
161
|
+
require file.read("skills/*")
|
|
162
|
+
require file.read("workspace")
|
|
163
|
+
require file.read("workspace/*")
|
|
164
|
+
require file.read(".lampson")
|
|
165
|
+
require file.read(".lampson/*")
|
|
166
|
+
when not valid_slug(source)
|
|
167
|
+
raise("invalid source '" + text(source) + "' (expected owner/repo or a github URL)")
|
|
168
|
+
when not valid_slug(name)
|
|
169
|
+
raise("invalid skill name '" + text(name) + "'")
|
|
170
|
+
let global be scope != "project"
|
|
171
|
+
let argv be ["-y", "skills", "add", source, "--skill", name, "-a", "codex", "-y"]
|
|
172
|
+
when global
|
|
173
|
+
set argv to append(argv, "-g")
|
|
174
|
+
ensure_global_mount()
|
|
175
|
+
let r be run(shell_exe(), ["-c", "CI=1 npx " + join(argv, " ") + " 2>&1"], 240, {"cwd": when global then "." otherwise "workspace"})
|
|
176
|
+
let idx be index()
|
|
177
|
+
when contains(idx, name)
|
|
178
|
+
give "installed skill '" + name + "' (" + (when global then "global: ~/.agents/skills, available in every project" otherwise "project: ./.agents/skills") + "). It is in the skills index now — call skill(name=\"" + name + "\") to load it.\n" + tail_out(r["stdout"])
|
|
179
|
+
give "npx skills finished (exit " + text(r["exit_code"]) + ") but '" + name + "' is not in the skills index. Output:\n" + tail_out(r["stdout"])
|