@ancleto/spec 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -1
- package/package.json +1 -1
- package/src/cli/index.js +405 -47
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- [Características principales](#características-principales)
|
|
24
24
|
- [Requisitos](#requisitos)
|
|
25
25
|
- [Instalación](#instalación)
|
|
26
|
+
- [Hosts soportados y layouts por agente](#hosts-soportados-y-layouts-por-agente)
|
|
26
27
|
- [Guía de .gitignore](#guía-de-gitignore)
|
|
27
28
|
- [Configuración y tiers de costo](#configuración-y-tiers-de-costo)
|
|
28
29
|
- [Uso rápido](#uso-rápido)
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
|
|
40
41
|
**Ancleto** (alias de CLI: `aspec`) es un orquestador ligero para desarrollo de software asistido por IA bajo el paradigma **SDD (Spec-Driven Development)**.
|
|
41
42
|
|
|
42
|
-
Vive dentro de tu entorno de desarrollo (OpenCode, Cursor, VS Code, Roo, etc.) y automatiza tres cosas que normalmente hacés a mano:
|
|
43
|
+
Vive dentro de tu entorno de desarrollo (OpenCode, Claude, Cursor, VS Code, Roo, etc.) y automatiza tres cosas que normalmente hacés a mano:
|
|
43
44
|
|
|
44
45
|
- **Especificar** — escritura guiada de especificaciones y planes de implementación.
|
|
45
46
|
- **Descubrir** — relevamiento topológico del repositorio y empaquetado de contexto.
|
|
@@ -125,6 +126,59 @@ Para medir el consumo **real** por sesión (no estimado): `ancleto stats` lee la
|
|
|
125
126
|
|
|
126
127
|
---
|
|
127
128
|
|
|
129
|
+
## Hosts soportados y layouts por agente
|
|
130
|
+
|
|
131
|
+
`--agent <nombre>` selecciona **el layout nativo a materializar** (el directorio y la convención de
|
|
132
|
+
nombre de cada asset) y el **pipeline de adaptación de frontmatter** de ese host. No arbitra qué
|
|
133
|
+
descubre cada host: si dos hosts leen el mismo directorio, el descubrimiento compartido es un efecto
|
|
134
|
+
del host (el CLI no deduplica ni elige un "ganador").
|
|
135
|
+
|
|
136
|
+
| Host (`--agent`) | skills | agents | commands |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| `opencode` (default) | `.opencode/skills/<n>/SKILL.md` | `.opencode/agents/<n>.md` | `.opencode/commands/<n>.md` |
|
|
139
|
+
| `claude` | `.claude/skills/<n>/SKILL.md` | `.claude/agents/<n>.md` (adaptado) | `.claude/commands/<n>.md` |
|
|
140
|
+
| `vscode` (VS Code / Copilot) | `.github/skills/<n>/SKILL.md` | `.github/agents/<n>.agent.md` | `.github/prompts/<n>.prompt.md` |
|
|
141
|
+
| `antigravity` | `.agents/skills/<n>/SKILL.md` | `.agents/agents/<n>.md` (adaptado) | `.agents/skills/<n>/SKILL.md` (como skill) |
|
|
142
|
+
| `cursor` | `.cursor/skills/<n>/SKILL.md` | — | — |
|
|
143
|
+
| `roo` | `.roo/skills/<n>/SKILL.md` | — | — |
|
|
144
|
+
|
|
145
|
+
Cada host recibe sus skills en **su** directorio nativo. **`.agents/skills` es un punto de lectura
|
|
146
|
+
compartido** —lo leen opencode, Cursor, VS Code y Antigravity—, no un destino universal: Claude lee
|
|
147
|
+
`.claude/skills` y Roo, `.roo/skills`. Por eso el CLI no escribe todas las skills en `.agents/skills`.
|
|
148
|
+
|
|
149
|
+
**Antigravity** no tiene directorio de commands de proyecto; sus `/cleto-*` se materializan como
|
|
150
|
+
**command-skills** en `.agents/skills/<n>/SKILL.md` (los Workflows están deprecados a favor de Agent
|
|
151
|
+
Skills), por eso `installedPaths.commands` queda vacío para este host. Sus `agents` se escriben en
|
|
152
|
+
`.agents/agents/<n>.md` con el frontmatter adaptado: `name` inyectado, `tools` como **lista** de ids
|
|
153
|
+
verificados de Antigravity (`read`→`view_file`, `edit`→`replace_file_content`, `grep`→`grep_search`,
|
|
154
|
+
`bash`→`run_command`, `todowrite`→`manage_task`), `mode`→`mainAgent`/`subagent`,
|
|
155
|
+
`commandExecutionPolicy: sandbox` y `model: inherit` (el tier no reescribe modelos de Antigravity). Una
|
|
156
|
+
tool de opencode sin id verificado se **omite con aviso** (`skip tool '<k>': no verified Antigravity id
|
|
157
|
+
for agent '<n>'`): un id inexistente cuelga el subagent. Los ids que sólo existen en el SDK
|
|
158
|
+
(`find_file`, `edit_file`, `search_web`, `read_url_content`, …), los de comunidad (`write_to_file`,
|
|
159
|
+
`call_mcp_tool`, `multi_replace_file_content`, …) y los de delegación (`invoke_subagent`,
|
|
160
|
+
`start_subagent`, `define_subagent`) **no** se emiten; el uso de MCP se expresa por
|
|
161
|
+
`mcpServers`/`.agents/mcp_config.json`, nunca como tool id. El campo `skill` no es una tool: se cubre
|
|
162
|
+
con el surfaceo automático de skills de Antigravity.
|
|
163
|
+
|
|
164
|
+
Los assets que un host no soporta (por ejemplo `agents`/`commands` de `cursor` y `roo`)
|
|
165
|
+
**no se escriben** y el instalador avisa por `stderr`
|
|
166
|
+
(`skip agents: not supported by host 'cursor'`) sin abortar. La instalación es **aditiva y no
|
|
167
|
+
destructiva**: instalar con otro agente escribe en su base y preserva los layouts previos, y el
|
|
168
|
+
manifiesto `.ancletorc.installedPaths` acumula la unión de los destinos realmente escritos.
|
|
169
|
+
|
|
170
|
+
La **configuración MCP específica de host** está fuera de alcance con **una excepción**: para
|
|
171
|
+
`agent: antigravity` el CLI genera o mergea (no destructivo) el MCP de workspace
|
|
172
|
+
`.agents/mcp_config.json` con esquema `{ "mcpServers": { "<n>": { "command", "args", "env" } } }`,
|
|
173
|
+
incluyendo el servidor de memoria `ancleto-memory`; preserva `mcpServers` preexistentes y no pisa
|
|
174
|
+
homónimos. `.mcp.json` (Claude) y `.vscode/mcp.json` (VS Code) siguen fuera de alcance, al igual que el
|
|
175
|
+
MCP global de Antigravity. `claude` se soporta con adaptación de frontmatter obligatoria para `agents`
|
|
176
|
+
(se omiten `model`/`tools` sin equivalencia verificada y las claves
|
|
177
|
+
`mode`/`color`/`temperature`/`permission`), sin MCP propio. Tanto `init` como `install --project`
|
|
178
|
+
configuran el MCP del host en la misma ejecución; `--no-mcp` lo omite.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
128
182
|
## Guía de .gitignore
|
|
129
183
|
|
|
130
184
|
El framework **no gestiona tu `.gitignore`**: es una decisión del proyecto. Como referencia, esta es la combinación que funciona bien con Ancleto:
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -35,13 +35,19 @@ Uso:
|
|
|
35
35
|
ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
|
|
36
36
|
ancleto install --lang <codigo> auto | es | en | pt (idioma de los artifacts; auto = idioma de la conversacion)
|
|
37
37
|
ancleto install --exclude <globs> Paths excluidos del discovery, coma-separados (ej: "**/*.png,docs")
|
|
38
|
-
ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
38
|
+
ancleto install --agent <nombre> opencode | claude | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
39
|
+
Elige el layout nativo a materializar (directorio + convencion de
|
|
40
|
+
nombre) y el adapter de frontmatter del host. NO arbitra el
|
|
41
|
+
descubrimiento entre hosts: si dos leen el mismo directorio, eso es
|
|
42
|
+
efecto del host (no se deduplica ni se elige "ganador").
|
|
43
|
+
antigravity instala skills, agents adaptados y commands como skills
|
|
44
|
+
(.agents/*) y mergea el MCP de workspace .agents/mcp_config.json.
|
|
39
45
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
40
46
|
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
41
47
|
usa --global para forzar el alcance global
|
|
42
48
|
ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
|
|
43
|
-
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>]
|
|
44
|
-
Crea .ancletorc en el repositorio actual
|
|
49
|
+
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--no-mcp]
|
|
50
|
+
Crea .ancletorc en el repositorio actual y configura el MCP del host
|
|
45
51
|
(interactivo en TTY: banner + menu; Azure desactivado por defecto)
|
|
46
52
|
ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
|
|
47
53
|
ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
|
|
@@ -147,12 +153,28 @@ async function writeProjectsRegistry(reg) {
|
|
|
147
153
|
return file
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
// D9: directorios locales de agents derivados del host/manifiesto, no de
|
|
157
|
+
// `.opencode/agents` fijo. Un proyecto antigravity usa `.agents/agents`.
|
|
158
|
+
function localAgentDirs(rc, agent) {
|
|
159
|
+
const dirs = new Set(rc?.installedPaths?.agents || [])
|
|
160
|
+
const spec = AGENT_TARGETS[agent || rc?.agent]?.agents
|
|
161
|
+
if (spec?.dir) dirs.add(spec.dir)
|
|
162
|
+
return [...dirs]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function hasLocalAgents(baseDir, rc, agent) {
|
|
166
|
+
for (const d of localAgentDirs(rc, agent)) {
|
|
167
|
+
if (await exists(join(baseDir, d))) return true
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
150
172
|
async function registerProject(projectDir, { agent, tier } = {}) {
|
|
151
173
|
const abs = resolve(projectDir)
|
|
152
174
|
const rc = await readAncletorc(abs)
|
|
153
175
|
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
154
176
|
const localTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
155
|
-
const hasScopedAgents = await
|
|
177
|
+
const hasScopedAgents = await hasLocalAgents(abs, rc, agent)
|
|
156
178
|
const entry = {
|
|
157
179
|
path: abs.replace(/\\/g, '/'),
|
|
158
180
|
version: await packageVersion(),
|
|
@@ -227,7 +249,7 @@ async function projectStatus(entry) {
|
|
|
227
249
|
if (!rc) return { ...entry, exists: false, reason: 'sin .ancletorc' }
|
|
228
250
|
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
229
251
|
const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
230
|
-
const scoped = await
|
|
252
|
+
const scoped = await hasLocalAgents(abs, rc, entry.agent)
|
|
231
253
|
return {
|
|
232
254
|
...entry,
|
|
233
255
|
exists: true,
|
|
@@ -591,13 +613,13 @@ async function applyTier(agentsDir, tier, models = null) {
|
|
|
591
613
|
}
|
|
592
614
|
}
|
|
593
615
|
|
|
594
|
-
const SUPPORTED_AGENTS = ['opencode', 'vscode', 'antigravity', 'cursor', 'roo']
|
|
616
|
+
const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo']
|
|
595
617
|
const DEFAULT_AGENT = 'opencode'
|
|
596
618
|
|
|
597
619
|
function askAgent() {
|
|
598
620
|
return new Promise((resolve) => {
|
|
599
621
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
600
|
-
rl.question(
|
|
622
|
+
rl.question(`Agente/IDE [${SUPPORTED_AGENTS.join('/')}] (default: ${DEFAULT_AGENT}): `, (a) => {
|
|
601
623
|
rl.close()
|
|
602
624
|
const t = a.trim().toLowerCase()
|
|
603
625
|
resolve(SUPPORTED_AGENTS.includes(t) ? t : DEFAULT_AGENT)
|
|
@@ -725,6 +747,47 @@ async function mergeMcp(configDir, mcpMap) {
|
|
|
725
747
|
return { file: targets.join(', '), added: [...new Set(added)] }
|
|
726
748
|
}
|
|
727
749
|
|
|
750
|
+
// D6: MCP de workspace de Antigravity (`.agents/mcp_config.json`). Merge NO
|
|
751
|
+
// destructivo: se preservan `mcpServers` y demas claves top-level, no se pisa un
|
|
752
|
+
// servidor homonimo, y un JSON invalido se avisa sin sobrescribir. Esquema del host
|
|
753
|
+
// `{ command, args, env }` (se descartan `type`/`enabled` de opencode).
|
|
754
|
+
async function mergeAntigravityMcp(projectDir, mcpMap) {
|
|
755
|
+
if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
|
|
756
|
+
const file = join(projectDir, '.agents', 'mcp_config.json')
|
|
757
|
+
let cfg = {}
|
|
758
|
+
if (await exists(file)) {
|
|
759
|
+
try {
|
|
760
|
+
cfg = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
|
|
761
|
+
} catch {
|
|
762
|
+
console.warn(`ancleto: no se pudo leer ${basename(file)} como JSON; MCP de Antigravity no se modifico`)
|
|
763
|
+
return { file, added: [] }
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
cfg.mcpServers = cfg.mcpServers || {}
|
|
767
|
+
const added = []
|
|
768
|
+
for (const [name, def] of Object.entries(mcpMap)) {
|
|
769
|
+
if (cfg.mcpServers[name]) continue
|
|
770
|
+
const command = Array.isArray(def.command) ? def.command : [def.command]
|
|
771
|
+
const server = { command: command[0], args: command.slice(1) }
|
|
772
|
+
if (def.env) server.env = def.env
|
|
773
|
+
cfg.mcpServers[name] = server
|
|
774
|
+
added.push(name)
|
|
775
|
+
}
|
|
776
|
+
await mkdir(dirname(file), { recursive: true })
|
|
777
|
+
await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
|
|
778
|
+
return { file, added }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// D6: dueno unico del MCP de host. Antigravity mergea `.agents/mcp_config.json`;
|
|
782
|
+
// el resto conserva `mergeMcp` sobre opencode.json (targetDir = .opencode del
|
|
783
|
+
// proyecto o config global).
|
|
784
|
+
async function setupHostMcp(targetDir, mcpMap, { projectDir = null, agent = null } = {}) {
|
|
785
|
+
if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
|
|
786
|
+
if (agent === 'antigravity' && projectDir) return mergeAntigravityMcp(projectDir, mcpMap)
|
|
787
|
+
await mkdir(targetDir, { recursive: true })
|
|
788
|
+
return mergeMcp(targetDir, mcpMap)
|
|
789
|
+
}
|
|
790
|
+
|
|
728
791
|
async function copyAssets(dest) {
|
|
729
792
|
await mkdir(dest, { recursive: true })
|
|
730
793
|
for (const d of ASSETS) {
|
|
@@ -732,32 +795,297 @@ async function copyAssets(dest) {
|
|
|
732
795
|
}
|
|
733
796
|
}
|
|
734
797
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
798
|
+
// D1: destino nativo por host. Cada asset declara su directorio y su convencion
|
|
799
|
+
// de nombre (ext); null = asset no soportado por ese host. Fuente unica de rutas.
|
|
800
|
+
const MD_EXT = '.md'
|
|
801
|
+
const AGENT_TARGETS = {
|
|
802
|
+
opencode: {
|
|
803
|
+
skills: { dir: '.opencode/skills' },
|
|
804
|
+
agents: { dir: '.opencode/agents', ext: MD_EXT },
|
|
805
|
+
commands: { dir: '.opencode/commands', ext: MD_EXT }
|
|
806
|
+
},
|
|
807
|
+
claude: {
|
|
808
|
+
skills: { dir: '.claude/skills' },
|
|
809
|
+
agents: { dir: '.claude/agents', ext: MD_EXT },
|
|
810
|
+
commands: { dir: '.claude/commands', ext: MD_EXT }
|
|
811
|
+
},
|
|
812
|
+
vscode: {
|
|
813
|
+
skills: { dir: '.github/skills' },
|
|
814
|
+
agents: { dir: '.github/agents', ext: '.agent.md' },
|
|
815
|
+
commands: { dir: '.github/prompts', ext: '.prompt.md' }
|
|
816
|
+
},
|
|
817
|
+
// Antigravity: agents adaptados en `.agents/agents` y commands materializados
|
|
818
|
+
// como skills dentro de `.agents/skills` (Antigravity no tiene dir de commands;
|
|
819
|
+
// los Workflows estan deprecados a favor de Agent Skills). `package: 'skill-dir'`
|
|
820
|
+
// marca que el asset se empaqueta como `<dir>/<n>/SKILL.md`, no como archivo plano.
|
|
821
|
+
antigravity: {
|
|
822
|
+
skills: { dir: '.agents/skills' },
|
|
823
|
+
agents: { dir: '.agents/agents', ext: MD_EXT },
|
|
824
|
+
commands: { dir: '.agents/skills', package: 'skill-dir' }
|
|
825
|
+
},
|
|
826
|
+
// Cursor y Roo: solo skills (D3/D7). .agents/skills es punto de lectura
|
|
827
|
+
// compartido (D2), no destino universal.
|
|
828
|
+
cursor: { skills: { dir: '.cursor/skills' }, agents: null, commands: null },
|
|
829
|
+
roo: { skills: { dir: '.roo/skills' }, agents: null, commands: null }
|
|
741
830
|
}
|
|
742
831
|
|
|
743
832
|
const ANCLETO_SKILLS = ['ancleto-new', 'ancleto-propose', 'ancleto-apply', 'ancleto-verify', 'ancleto-archive', 'ancleto-bulk-archive', 'ancleto-continue', 'ancleto-explore', 'ancleto-ff', 'ancleto-onboard', 'ancleto-workflow']
|
|
744
833
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
834
|
+
// Frontmatter: parser propio sin dependencias. Cada entrada conserva su clave y
|
|
835
|
+
// sus lineas crudas (incluye block scalars y mapas anidados), preservando orden.
|
|
836
|
+
function parseFrontmatter(content) {
|
|
837
|
+
const m = /^(---\r?\n)([\s\S]*?)(\r?\n---)(\r?\n?)([\s\S]*)$/.exec(content)
|
|
838
|
+
if (!m) return { hasFrontmatter: false, content }
|
|
839
|
+
const entries = []
|
|
840
|
+
for (const line of m[2].split(/\r?\n/)) {
|
|
841
|
+
if (/^[A-Za-z0-9_-]+:/.test(line)) {
|
|
842
|
+
entries.push({ key: line.slice(0, line.indexOf(':')), lines: [line] })
|
|
843
|
+
} else if (entries.length) {
|
|
844
|
+
entries[entries.length - 1].lines.push(line)
|
|
845
|
+
} else {
|
|
846
|
+
entries.push({ key: null, lines: [line] })
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return { hasFrontmatter: true, open: m[1], entries, close: m[3], nl: m[4], body: m[5] }
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function serializeFrontmatter(parsed) {
|
|
853
|
+
if (!parsed.hasFrontmatter) return parsed.content
|
|
854
|
+
const inner = parsed.entries.flatMap((e) => e.lines).join('\n')
|
|
855
|
+
return `${parsed.open}${inner}${parsed.close}${parsed.nl}${parsed.body}`
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// D4/D5: hosts cuyo formato de agents difiere del origen requieren adaptacion.
|
|
859
|
+
// Claude/VS Code eliminan las claves de opencode; Antigravity usa una
|
|
860
|
+
// transformacion propia (D2). El resto es identidad (preservacion verbatim).
|
|
861
|
+
const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode', 'antigravity'])
|
|
862
|
+
const AGENT_ADAPTER_DROP = new Set(['mode', 'color', 'temperature', 'permission', 'model', 'tools'])
|
|
863
|
+
|
|
864
|
+
// D3: tabla unica (fijada contra la tabla oficial de frontmatter de Custom
|
|
865
|
+
// Subagents) de ids de Antigravity. Conjunto CERRADO: solo se emiten estos ids.
|
|
866
|
+
// Los ids solo-SDK (`find_file`, `edit_file`, `search_web`, `read_url_content`,
|
|
867
|
+
// ...), los de comunidad (`write_to_file`, `call_mcp_tool`,
|
|
868
|
+
// `multi_replace_file_content`, ...) y los de delegacion (`invoke_subagent`,
|
|
869
|
+
// `start_subagent`, `define_subagent`) NO se emiten: no estan confirmados en el
|
|
870
|
+
// frontmatter y un id inexistente cuelga el subagent (Known Issue). Una clave no
|
|
871
|
+
// listada aqui se omite con aviso a stderr, nunca en silencio (D5).
|
|
872
|
+
const ANTIGRAVITY_TOOL_MAP = {
|
|
873
|
+
read: 'view_file',
|
|
874
|
+
edit: 'replace_file_content',
|
|
875
|
+
grep: 'grep_search',
|
|
876
|
+
bash: 'run_command',
|
|
877
|
+
todowrite: 'manage_task'
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Hijos directos del mapa `tools:` con su valor crudo (p. ej. `read: true`).
|
|
881
|
+
function parseToolFlags(entry) {
|
|
882
|
+
const flags = []
|
|
883
|
+
if (!entry) return flags
|
|
884
|
+
let childIndent = null
|
|
885
|
+
for (const line of entry.lines.slice(1)) {
|
|
886
|
+
const m = /^(\s+)([A-Za-z0-9_-]+):\s*(.*?)\s*$/.exec(line)
|
|
887
|
+
if (!m) continue
|
|
888
|
+
if (childIndent === null) childIndent = m[1].length
|
|
889
|
+
if (m[1].length !== childIndent) continue
|
|
890
|
+
flags.push({ key: m[2], value: m[3] })
|
|
891
|
+
}
|
|
892
|
+
return flags
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function frontmatterValue(entry, fallback = null) {
|
|
896
|
+
if (!entry) return fallback
|
|
897
|
+
const m = /^[A-Za-z0-9_-]+:\s*(.*?)\s*$/.exec(entry.lines[0] || '')
|
|
898
|
+
return m ? m[1] : fallback
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// D2/D3/D5/D8: adapta el frontmatter de un agent de opencode al de Antigravity.
|
|
902
|
+
// `name` (requerido por el host) se inyecta desde el nombre del archivo. `tools` se
|
|
903
|
+
// emite siempre como lista (el default del host es la lista vacia). Nunca se inventa
|
|
904
|
+
// un id: una clave sin id verificado se omite con aviso (un id inexistente cuelga el
|
|
905
|
+
// subagent).
|
|
906
|
+
function adaptAntigravityFrontmatter(content, name) {
|
|
907
|
+
const parsed = parseFrontmatter(content)
|
|
908
|
+
if (!parsed.hasFrontmatter) return content
|
|
909
|
+
const byKey = (k) => parsed.entries.find((e) => e.key === k)
|
|
910
|
+
// Claves que el adaptador gestiona: las dropeadas (AGENT_ADAPTER_DROP), las que
|
|
911
|
+
// re-emite (name/tools/mainAgent/subagent/model/commandExecutionPolicy) y los campos
|
|
912
|
+
// que preserva del origen (description/skills/mcpServers). Se filtran TODAS del
|
|
913
|
+
// conjunto conservado para que ninguna pueda quedar duplicada, y se re-emiten una
|
|
914
|
+
// sola vez en orden determinista, sin importar lo que declare la fuente.
|
|
915
|
+
const managed = new Set([
|
|
916
|
+
...AGENT_ADAPTER_DROP,
|
|
917
|
+
'name',
|
|
918
|
+
'description',
|
|
919
|
+
'mainAgent',
|
|
920
|
+
'subagent',
|
|
921
|
+
'commandExecutionPolicy',
|
|
922
|
+
'mcpServers',
|
|
923
|
+
'skills'
|
|
924
|
+
])
|
|
925
|
+
// Se conservan las claves extra (no gestionadas); los campos preservados se
|
|
926
|
+
// re-emiten abajo desde su primera ocurrencia en el origen.
|
|
927
|
+
const kept = parsed.entries.filter((e) => e.key && !managed.has(e.key))
|
|
928
|
+
|
|
929
|
+
const toolIds = []
|
|
930
|
+
for (const { key, value } of parseToolFlags(byKey('tools'))) {
|
|
931
|
+
if (value === 'false') continue
|
|
932
|
+
if (key === 'skill') continue // no es tool: se cubre por `skills`/surfaceo automatico (OQ2)
|
|
933
|
+
const id = ANTIGRAVITY_TOOL_MAP[key]
|
|
934
|
+
if (id) {
|
|
935
|
+
toolIds.push(id)
|
|
936
|
+
continue
|
|
937
|
+
}
|
|
938
|
+
// D3/D5: regla dura. Un id fuera del frontmatter confirmado (exista o no en el
|
|
939
|
+
// SDK) no se emite: un id inexistente cuelga el subagent. Se omite con aviso,
|
|
940
|
+
// nunca en silencio. El uso de MCP NO se infiere desde claves desconocidas del
|
|
941
|
+
// mapa `tools`: se expresa por `mcpServers`/`.agents/mcp_config.json` (D6).
|
|
942
|
+
console.error(`skip tool '${key}': no verified Antigravity id for agent '${name}'`)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const mode = frontmatterValue(byKey('mode'))
|
|
946
|
+
const mainAgent = mode === 'subagent' ? 'false' : 'true'
|
|
947
|
+
const subagent = mode === 'primary' ? 'false' : 'true'
|
|
948
|
+
|
|
949
|
+
// Orden determinista: name, description, tools, mainAgent, subagent, model,
|
|
950
|
+
// commandExecutionPolicy, mcpServers, skills, y luego las claves extra conservadas.
|
|
951
|
+
const out = [{ key: 'name', lines: [`name: ${name}`] }]
|
|
952
|
+
const description = byKey('description')
|
|
953
|
+
if (description) out.push(description)
|
|
954
|
+
out.push({ key: 'tools', lines: [`tools: [${toolIds.join(', ')}]`] })
|
|
955
|
+
out.push({ key: 'mainAgent', lines: [`mainAgent: ${mainAgent}`] })
|
|
956
|
+
out.push({ key: 'subagent', lines: [`subagent: ${subagent}`] })
|
|
957
|
+
out.push({ key: 'model', lines: [`model: inherit`] })
|
|
958
|
+
out.push({ key: 'commandExecutionPolicy', lines: [`commandExecutionPolicy: sandbox`] })
|
|
959
|
+
const mcpServers = byKey('mcpServers')
|
|
960
|
+
if (mcpServers) out.push(mcpServers)
|
|
961
|
+
const skills = byKey('skills')
|
|
962
|
+
if (skills) out.push(skills)
|
|
963
|
+
out.push(...kept)
|
|
964
|
+
parsed.entries = out
|
|
965
|
+
return serializeFrontmatter(parsed)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function adaptFrontmatter(content, host, assetKind, name) {
|
|
969
|
+
if (assetKind !== 'agents' || !AGENT_ADAPTER_HOSTS.has(host)) return content
|
|
970
|
+
if (host === 'antigravity') return adaptAntigravityFrontmatter(content, name)
|
|
971
|
+
const parsed = parseFrontmatter(content)
|
|
972
|
+
if (!parsed.hasFrontmatter) return content
|
|
973
|
+
parsed.entries = parsed.entries.filter((e) => !(e.key && AGENT_ADAPTER_DROP.has(e.key)))
|
|
974
|
+
return serializeFrontmatter(parsed)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// D12: asset no soportado -> no se escribe y se avisa a stderr (no bloqueante).
|
|
978
|
+
function warnUnsupportedAsset(asset, agent) {
|
|
979
|
+
console.error(`skip ${asset}: not supported by host '${agent}'`)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function copyDirTransformed(srcDir, destDir, host, assetKind) {
|
|
983
|
+
await mkdir(destDir, { recursive: true })
|
|
984
|
+
for (const e of await readdir(srcDir, { withFileTypes: true })) {
|
|
985
|
+
const src = join(srcDir, e.name)
|
|
986
|
+
const dest = join(destDir, e.name)
|
|
987
|
+
if (e.isDirectory()) {
|
|
988
|
+
await copyDirTransformed(src, dest, host, assetKind)
|
|
989
|
+
} else if (e.isFile() && e.name.endsWith(MD_EXT)) {
|
|
990
|
+
await writeFile(dest, adaptFrontmatter(await readFile(src, 'utf8'), host, assetKind))
|
|
991
|
+
} else if (e.isFile()) {
|
|
992
|
+
await cp(src, dest)
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function installAssetFiles(cat, projectDir, spec, host) {
|
|
998
|
+
const destDir = join(projectDir, spec.dir)
|
|
999
|
+
await mkdir(destDir, { recursive: true })
|
|
1000
|
+
for (const e of await readdir(join(ROOT, cat), { withFileTypes: true })) {
|
|
1001
|
+
if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
|
|
1002
|
+
const base = e.name.slice(0, -MD_EXT.length)
|
|
1003
|
+
const content = await readFile(join(ROOT, cat, e.name), 'utf8')
|
|
1004
|
+
await writeFile(join(destDir, `${base}${spec.ext}`), adaptFrontmatter(content, host, cat, base))
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// D4: un command de ancleto convertido en skill (`name` + `description` del origen,
|
|
1009
|
+
// body verbatim). Antigravity no tiene dir de commands: sus `/cleto-*` viven como
|
|
1010
|
+
// command-skills dentro de `.agents/skills`.
|
|
1011
|
+
function commandToSkill(content, name) {
|
|
1012
|
+
const parsed = parseFrontmatter(content)
|
|
1013
|
+
const desc = parsed.hasFrontmatter ? parsed.entries.find((e) => e.key === 'description') : null
|
|
1014
|
+
const head = ['---', `name: ${name}`, ...(desc ? desc.lines : []), '---'].join('\n')
|
|
1015
|
+
const body = parsed.hasFrontmatter ? parsed.body : content
|
|
1016
|
+
return `${head}\n${body}`
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Materializa `commands/*.md` como `.agents/skills/<n>/SKILL.md` (asset empaquetado).
|
|
1020
|
+
async function installCommandSkills(projectDir, spec) {
|
|
1021
|
+
const destBase = join(projectDir, spec.dir)
|
|
1022
|
+
for (const e of await readdir(join(ROOT, 'commands'), { withFileTypes: true })) {
|
|
1023
|
+
if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
|
|
1024
|
+
const base = e.name.slice(0, -MD_EXT.length)
|
|
1025
|
+
const content = await readFile(join(ROOT, 'commands', e.name), 'utf8')
|
|
1026
|
+
await mkdir(join(destBase, base), { recursive: true })
|
|
1027
|
+
await writeFile(join(destBase, base, 'SKILL.md'), commandToSkill(content, base))
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Dueno unico de la instalacion de assets por proyecto: materializa skills,
|
|
1032
|
+
// agents y commands en el destino nativo del host y devuelve las rutas escritas.
|
|
1033
|
+
async function installAgentAssets(projectDir, agent) {
|
|
1034
|
+
const model = AGENT_TARGETS[agent] || AGENT_TARGETS[DEFAULT_AGENT]
|
|
1035
|
+
const written = { agents: [], commands: [], skills: [] }
|
|
1036
|
+
|
|
1037
|
+
if (model.skills) {
|
|
1038
|
+
await copyDirTransformed(join(ROOT, 'skills'), join(projectDir, model.skills.dir), agent, 'skills')
|
|
1039
|
+
written.skills.push(model.skills.dir)
|
|
1040
|
+
} else {
|
|
1041
|
+
warnUnsupportedAsset('skills', agent)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
for (const cat of ['agents', 'commands']) {
|
|
1045
|
+
const spec = model[cat]
|
|
1046
|
+
if (!spec) {
|
|
1047
|
+
warnUnsupportedAsset(cat, agent)
|
|
1048
|
+
continue
|
|
1049
|
+
}
|
|
1050
|
+
// D4: un asset empaquetado (commands de Antigravity) vive dentro del layout de
|
|
1051
|
+
// skills; no se registra un directorio de commands propio en installedPaths.
|
|
1052
|
+
if (spec.package === 'skill-dir') {
|
|
1053
|
+
await installCommandSkills(projectDir, spec)
|
|
753
1054
|
continue
|
|
754
1055
|
}
|
|
755
|
-
await
|
|
1056
|
+
await installAssetFiles(cat, projectDir, spec, agent)
|
|
1057
|
+
written[cat].push(spec.dir)
|
|
1058
|
+
}
|
|
1059
|
+
return written
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// D10: installedPaths es la union de destinos realmente escritos; un asset
|
|
1063
|
+
// omitido (null) no deja rastro. No se borran rutas previas (no destructivo).
|
|
1064
|
+
function unionInstalledPaths(existing, next) {
|
|
1065
|
+
const out = {}
|
|
1066
|
+
for (const k of ['templates', 'agents', 'commands', 'skills']) {
|
|
1067
|
+
out[k] = [...new Set([...(existing?.[k] || []), ...(next?.[k] || [])])]
|
|
756
1068
|
}
|
|
757
|
-
|
|
758
|
-
|
|
1069
|
+
return out
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Convencion de nombre esperada en un directorio instalado (para `check`).
|
|
1073
|
+
function installedExtFor(cat, dirRel) {
|
|
1074
|
+
for (const t of Object.values(AGENT_TARGETS)) {
|
|
1075
|
+
const spec = t[cat]
|
|
1076
|
+
if (spec && spec.dir === dirRel && spec.ext) return spec.ext
|
|
759
1077
|
}
|
|
760
|
-
return
|
|
1078
|
+
return null
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// D9: hosts realmente instalados derivados del manifiesto (union de destinos
|
|
1082
|
+
// escritos), no de un unico `rc.agent`. Un proyecto multi-host debe sumar los
|
|
1083
|
+
// esperados de todos los hosts para no reportar huerfanos falsos.
|
|
1084
|
+
function installedHostsFromPaths(ip) {
|
|
1085
|
+
const dirs = new Set([...(ip?.skills || []), ...(ip?.agents || []), ...(ip?.commands || [])])
|
|
1086
|
+
return Object.entries(AGENT_TARGETS)
|
|
1087
|
+
.filter(([, t]) => ['skills', 'agents', 'commands'].some((cat) => t[cat]?.dir && dirs.has(t[cat].dir)))
|
|
1088
|
+
.map(([host]) => host)
|
|
761
1089
|
}
|
|
762
1090
|
|
|
763
1091
|
const CHANGES_ROOT = 'aspec'
|
|
@@ -864,9 +1192,9 @@ function findInsertAnchor(local, source, name) {
|
|
|
864
1192
|
return local.length
|
|
865
1193
|
}
|
|
866
1194
|
|
|
1195
|
+
// Solo mergea los templates raiz (AGENTS.md/PRODUCT.md). Los assets por host
|
|
1196
|
+
// los materializa installAgentAssets (dueno unico).
|
|
867
1197
|
async function copyTemplates(projectDir) {
|
|
868
|
-
const dest = join(projectDir, '.opencode')
|
|
869
|
-
await copyAssets(dest)
|
|
870
1198
|
for (const t of TEMPLATES) {
|
|
871
1199
|
const target = join(projectDir, t)
|
|
872
1200
|
const source = await readFile(join(ROOT, 'templates', t), 'utf8')
|
|
@@ -965,18 +1293,16 @@ async function install(args) {
|
|
|
965
1293
|
await migrateLegacyOpenspec(projectDir)
|
|
966
1294
|
await scaffoldAspec(projectDir)
|
|
967
1295
|
await refreshWorkingContext(projectDir)
|
|
968
|
-
const
|
|
1296
|
+
const written = await installAgentAssets(projectDir, agent)
|
|
969
1297
|
await writeManifest(projectDir, {
|
|
970
1298
|
agent,
|
|
971
1299
|
language,
|
|
972
1300
|
...(excludeChoice !== null ? { discovery: { ...existingDiscovery, exclude: excludeChoice } } : {}),
|
|
973
1301
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
974
|
-
installedPaths: {
|
|
1302
|
+
installedPaths: unionInstalledPaths(existingRc?.installedPaths, {
|
|
975
1303
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
skills: [agentSkillsDir]
|
|
979
|
-
}
|
|
1304
|
+
...written
|
|
1305
|
+
})
|
|
980
1306
|
})
|
|
981
1307
|
} else {
|
|
982
1308
|
await copyAssets(target)
|
|
@@ -984,6 +1310,9 @@ async function install(args) {
|
|
|
984
1310
|
|
|
985
1311
|
const models = tierModels(tier, TIERS, gratisModelChoice)
|
|
986
1312
|
await applyTier(join(target, 'agents'), tier, models)
|
|
1313
|
+
// El estado de tier vive en el base de opencode; puede no existir si el host
|
|
1314
|
+
// resuelto ubica sus assets en otro directorio (p. ej. cursor -> .cursor/).
|
|
1315
|
+
await mkdir(target, { recursive: true })
|
|
987
1316
|
await writeFile(tierStatePath(target), tier + '\n')
|
|
988
1317
|
|
|
989
1318
|
if (projectDir) await registerProject(projectDir, { agent, tier })
|
|
@@ -997,9 +1326,9 @@ async function install(args) {
|
|
|
997
1326
|
}
|
|
998
1327
|
}
|
|
999
1328
|
|
|
1000
|
-
const res = await
|
|
1329
|
+
const res = await setupHostMcp(target, mcpMap, { projectDir, agent })
|
|
1001
1330
|
const loc = projectDir
|
|
1002
|
-
? `${projectDir} (
|
|
1331
|
+
? `${projectDir} (layout ${agent} + templates en la raiz)`
|
|
1003
1332
|
: `${target} (disponible en todos tus proyectos)`
|
|
1004
1333
|
console.log(`ancleto: instalado en ${loc}`)
|
|
1005
1334
|
console.log(`ancleto: tier de costo de agents: ${tier}`)
|
|
@@ -1020,20 +1349,19 @@ async function upgradeCmd(args) {
|
|
|
1020
1349
|
await migrateLegacyOpenspec(projectDir)
|
|
1021
1350
|
const agent = await resolveAgent(args, rc.agent)
|
|
1022
1351
|
await copyTemplates(projectDir)
|
|
1023
|
-
const
|
|
1352
|
+
const written = await installAgentAssets(projectDir, agent)
|
|
1024
1353
|
const wentContext = await refreshWorkingContext(projectDir)
|
|
1025
1354
|
const manifest = await writeManifest(projectDir, {
|
|
1026
1355
|
agent,
|
|
1027
|
-
installedPaths: {
|
|
1356
|
+
installedPaths: unionInstalledPaths(rc.installedPaths, {
|
|
1028
1357
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
skills: [agentSkillsDir]
|
|
1032
|
-
}
|
|
1358
|
+
...written
|
|
1359
|
+
})
|
|
1033
1360
|
})
|
|
1361
|
+
const skillsDir = written.skills[0] || '.opencode/skills'
|
|
1034
1362
|
console.log(`ancleto: upgrade completo (v${manifest.version}, agente: ${agent})`)
|
|
1035
1363
|
console.log(' ✔ templates re-aplicados (bloques LOCKED actualizados, EXTENSIBLE intacto)')
|
|
1036
|
-
console.log(` ✔ skills actualizadas en ${
|
|
1364
|
+
console.log(` ✔ skills actualizadas en ${skillsDir} (${ANCLETO_SKILLS.length} skills)`)
|
|
1037
1365
|
console.log(wentContext ? ' ✔ working-context.md regenerado' : ' ✔ working-context.md sin reglas activas (vacio)')
|
|
1038
1366
|
console.log(' ✔ manifiesto .ancletorc actualizado')
|
|
1039
1367
|
}
|
|
@@ -1041,6 +1369,7 @@ async function upgradeCmd(args) {
|
|
|
1041
1369
|
async function initProject(args) {
|
|
1042
1370
|
const projectDir = process.cwd()
|
|
1043
1371
|
const withAzure = args.includes('--with-azure')
|
|
1372
|
+
const withMcp = !args.includes('--no-mcp')
|
|
1044
1373
|
const agentFlag = scanAgentFlag(args)
|
|
1045
1374
|
const tierFlag = scanTierFlag(args)
|
|
1046
1375
|
const langFlag = scanLangFlag(args)
|
|
@@ -1100,12 +1429,17 @@ async function initProject(args) {
|
|
|
1100
1429
|
const persisted = isKnownGratisModel(existing?.gratisModel) ? existing.gratisModel : null
|
|
1101
1430
|
gratisModelChoice = persisted || gratisModel()
|
|
1102
1431
|
}
|
|
1432
|
+
const written = await installAgentAssets(projectDir, agent)
|
|
1103
1433
|
const manifest = await writeManifest(projectDir, {
|
|
1104
1434
|
azure,
|
|
1105
1435
|
discovery,
|
|
1106
1436
|
agent,
|
|
1107
1437
|
language,
|
|
1108
|
-
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {})
|
|
1438
|
+
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
1439
|
+
installedPaths: unionInstalledPaths(existing?.installedPaths, {
|
|
1440
|
+
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1441
|
+
...written
|
|
1442
|
+
})
|
|
1109
1443
|
})
|
|
1110
1444
|
await copyTemplates(projectDir)
|
|
1111
1445
|
if (tier) {
|
|
@@ -1115,7 +1449,12 @@ async function initProject(args) {
|
|
|
1115
1449
|
await scaffoldAspec(projectDir)
|
|
1116
1450
|
await refreshWorkingContext(projectDir)
|
|
1117
1451
|
await registerProject(projectDir, { agent, tier: tier || null })
|
|
1452
|
+
// D7: init configura el MCP del host igual que install (mismo dueno, sin rama
|
|
1453
|
+
// especial por host); --no-mcp conserva el escape.
|
|
1454
|
+
const mcpMap = withMcp ? buildDefaultMcp({}) : {}
|
|
1455
|
+
const mcpRes = await setupHostMcp(join(projectDir, '.opencode'), mcpMap, { projectDir, agent })
|
|
1118
1456
|
if (azure.enabled) console.log(AZURE_MCP_NOTICE)
|
|
1457
|
+
if (mcpRes.added.length) console.log(`ancleto: MCP configurados: ${mcpRes.added.join(', ')} en ${mcpRes.file}`)
|
|
1119
1458
|
console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''} (Idioma: ${language})`)
|
|
1120
1459
|
}
|
|
1121
1460
|
|
|
@@ -1571,6 +1910,8 @@ async function checkCommand() {
|
|
|
1571
1910
|
process.exit(1)
|
|
1572
1911
|
}
|
|
1573
1912
|
const ip = rc.installedPaths
|
|
1913
|
+
// D9: la union de hosts instalados se lee del manifiesto, no de un unico `rc.agent`.
|
|
1914
|
+
const installedHosts = installedHostsFromPaths(ip)
|
|
1574
1915
|
let missing = 0
|
|
1575
1916
|
let orphans = 0
|
|
1576
1917
|
|
|
@@ -1591,7 +1932,21 @@ async function checkCommand() {
|
|
|
1591
1932
|
missing++
|
|
1592
1933
|
continue
|
|
1593
1934
|
}
|
|
1594
|
-
|
|
1935
|
+
// La convencion de nombre del host (p. ej. vscode: .agent.md/.prompt.md)
|
|
1936
|
+
// se deriva del mismo modelo de destino; en skills no cambia el nombre.
|
|
1937
|
+
const ext = installedExtFor(cat, dirRel)
|
|
1938
|
+
let expected = (await readdir(join(ROOT, cat))).sort().map((f) => (ext ? f.replace(/\.md$/, ext) : f))
|
|
1939
|
+
// D9: los command-skills de Antigravity viven dentro de `.agents/skills`; el
|
|
1940
|
+
// conjunto esperado de ese dir suma el catalogo de skills y los commands de
|
|
1941
|
+
// cualquier host instalado que empaquete commands ahi (union, no rc.agent).
|
|
1942
|
+
const packagesCommandsHere = cat === 'skills' && installedHosts.some((h) => {
|
|
1943
|
+
const cs = AGENT_TARGETS[h].commands
|
|
1944
|
+
return cs?.package === 'skill-dir' && cs.dir === dirRel
|
|
1945
|
+
})
|
|
1946
|
+
if (packagesCommandsHere) {
|
|
1947
|
+
const cmds = (await readdir(join(ROOT, 'commands'))).filter((f) => f.endsWith(MD_EXT)).map((f) => f.slice(0, -MD_EXT.length))
|
|
1948
|
+
expected = [...new Set([...expected, ...cmds])].sort()
|
|
1949
|
+
}
|
|
1595
1950
|
const actual = (await readdir(destDir)).sort()
|
|
1596
1951
|
const missingFiles = expected.filter((f) => !actual.includes(f))
|
|
1597
1952
|
const orphanFiles = actual.filter((f) => !expected.includes(f))
|
|
@@ -1624,10 +1979,13 @@ async function checkTierOrphan(cwd) {
|
|
|
1624
1979
|
if (!(await exists(tierFile))) return warnings
|
|
1625
1980
|
const tier = (await readFile(tierFile, 'utf8')).trim()
|
|
1626
1981
|
if (!TIERS[tier]) return warnings
|
|
1627
|
-
|
|
1982
|
+
// D9: se cuentan los agents locales del host realmente instalados, no
|
|
1983
|
+
// `.opencode/agents` fijo (un proyecto antigravity usa `.agents/agents`).
|
|
1984
|
+
const rc = await readAncletorc(cwd)
|
|
1628
1985
|
let count = 0
|
|
1629
|
-
|
|
1630
|
-
|
|
1986
|
+
for (const d of localAgentDirs(rc)) {
|
|
1987
|
+
const p = join(cwd, d)
|
|
1988
|
+
if (await exists(p)) count += (await readdir(p)).filter((f) => f.endsWith(MD_EXT)).length
|
|
1631
1989
|
}
|
|
1632
1990
|
if (count === 0) {
|
|
1633
1991
|
warnings.push(`tier "${tier}" sin agentes locales donde aplicarlo — este proyecto usa los agentes globales. Corre 'ancleto install --project .' para que el tier aplique, o borra .opencode/.ancleto-tier.`)
|