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/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"])