@ancleto/spec 0.7.2 → 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.
Files changed (3) hide show
  1. package/README.md +27 -7
  2. package/package.json +1 -1
  3. package/src/cli/index.js +251 -18
package/README.md CHANGED
@@ -138,7 +138,7 @@ del host (el CLI no deduplica ni elige un "ganador").
138
138
  | `opencode` (default) | `.opencode/skills/<n>/SKILL.md` | `.opencode/agents/<n>.md` | `.opencode/commands/<n>.md` |
139
139
  | `claude` | `.claude/skills/<n>/SKILL.md` | `.claude/agents/<n>.md` (adaptado) | `.claude/commands/<n>.md` |
140
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` | — (no soportado) | — (no soportado) |
141
+ | `antigravity` | `.agents/skills/<n>/SKILL.md` | `.agents/agents/<n>.md` (adaptado) | `.agents/skills/<n>/SKILL.md` (como skill) |
142
142
  | `cursor` | `.cursor/skills/<n>/SKILL.md` | — | — |
143
143
  | `roo` | `.roo/skills/<n>/SKILL.md` | — | — |
144
144
 
@@ -146,16 +146,36 @@ Cada host recibe sus skills en **su** directorio nativo. **`.agents/skills` es u
146
146
  compartido** —lo leen opencode, Cursor, VS Code y Antigravity—, no un destino universal: Claude lee
147
147
  `.claude/skills` y Roo, `.roo/skills`. Por eso el CLI no escribe todas las skills en `.agents/skills`.
148
148
 
149
- Los assets que un host no soporta (por ejemplo `agents`/`commands` de `antigravity`, `cursor` y `roo`)
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`)
150
165
  **no se escriben** y el instalador avisa por `stderr`
151
- (`skip agents: not supported by host 'antigravity'`) sin abortar. La instalación es **aditiva y no
166
+ (`skip agents: not supported by host 'cursor'`) sin abortar. La instalación es **aditiva y no
152
167
  destructiva**: instalar con otro agente escribe en su base y preserva los layouts previos, y el
153
168
  manifiesto `.ancletorc.installedPaths` acumula la unión de los destinos realmente escritos.
154
169
 
155
- La **configuración MCP específica de host** está fuera de alcance: el CLI no genera ni transforma
156
- `.mcp.json`, `mcp_config.json` ni `.vscode/mcp.json`, y no pisa config MCP preexistente. `claude` se
157
- soporta con adaptación de frontmatter obligatoria para `agents` (se omiten `model`/`tools` sin
158
- equivalencia verificada y las claves `mode`/`color`/`temperature`/`permission`), sin MCP propio.
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.
159
179
 
160
180
  ---
161
181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.js CHANGED
@@ -40,12 +40,14 @@ Uso:
40
40
  nombre) y el adapter de frontmatter del host. NO arbitra el
41
41
  descubrimiento entre hosts: si dos leen el mismo directorio, eso es
42
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.
43
45
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
44
46
  Parado en un proyecto con .ancletorc opera sobre ese proyecto;
45
47
  usa --global para forzar el alcance global
46
48
  ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
47
- ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>]
48
- 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
49
51
  (interactivo en TTY: banner + menu; Azure desactivado por defecto)
50
52
  ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
51
53
  ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
@@ -151,12 +153,28 @@ async function writeProjectsRegistry(reg) {
151
153
  return file
152
154
  }
153
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
+
154
172
  async function registerProject(projectDir, { agent, tier } = {}) {
155
173
  const abs = resolve(projectDir)
156
174
  const rc = await readAncletorc(abs)
157
175
  const tierFile = join(abs, '.opencode', '.ancleto-tier')
158
176
  const localTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
159
- const hasScopedAgents = await exists(join(abs, '.opencode', 'agents'))
177
+ const hasScopedAgents = await hasLocalAgents(abs, rc, agent)
160
178
  const entry = {
161
179
  path: abs.replace(/\\/g, '/'),
162
180
  version: await packageVersion(),
@@ -231,7 +249,7 @@ async function projectStatus(entry) {
231
249
  if (!rc) return { ...entry, exists: false, reason: 'sin .ancletorc' }
232
250
  const tierFile = join(abs, '.opencode', '.ancleto-tier')
233
251
  const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
234
- const scoped = await exists(join(abs, '.opencode', 'agents'))
252
+ const scoped = await hasLocalAgents(abs, rc, entry.agent)
235
253
  return {
236
254
  ...entry,
237
255
  exists: true,
@@ -729,6 +747,47 @@ async function mergeMcp(configDir, mcpMap) {
729
747
  return { file: targets.join(', '), added: [...new Set(added)] }
730
748
  }
731
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
+
732
791
  async function copyAssets(dest) {
733
792
  await mkdir(dest, { recursive: true })
734
793
  for (const d of ASSETS) {
@@ -755,9 +814,17 @@ const AGENT_TARGETS = {
755
814
  agents: { dir: '.github/agents', ext: '.agent.md' },
756
815
  commands: { dir: '.github/prompts', ext: '.prompt.md' }
757
816
  },
758
- // Antigravity, Cursor y Roo: solo skills (D3/D7). .agents/skills es punto de
759
- // lectura compartido (D2), no destino universal.
760
- antigravity: { skills: { dir: '.agents/skills' }, agents: null, commands: null },
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.
761
828
  cursor: { skills: { dir: '.cursor/skills' }, agents: null, commands: null },
762
829
  roo: { skills: { dir: '.roo/skills' }, agents: null, commands: null }
763
830
  }
@@ -788,14 +855,119 @@ function serializeFrontmatter(parsed) {
788
855
  return `${parsed.open}${inner}${parsed.close}${parsed.nl}${parsed.body}`
789
856
  }
790
857
 
791
- // D4/D5: hosts cuyo formato de agents difiere del origen requieren adaptacion
792
- // (hoy Claude y VS Code). El resto es identidad (preservacion verbatim). Nunca se
793
- // inventan equivalencias de model/tools.
794
- const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode'])
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'])
795
862
  const AGENT_ADAPTER_DROP = new Set(['mode', 'color', 'temperature', 'permission', 'model', 'tools'])
796
863
 
797
- function adaptFrontmatter(content, host, assetKind) {
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) {
798
969
  if (assetKind !== 'agents' || !AGENT_ADAPTER_HOSTS.has(host)) return content
970
+ if (host === 'antigravity') return adaptAntigravityFrontmatter(content, name)
799
971
  const parsed = parseFrontmatter(content)
800
972
  if (!parsed.hasFrontmatter) return content
801
973
  parsed.entries = parsed.entries.filter((e) => !(e.key && AGENT_ADAPTER_DROP.has(e.key)))
@@ -829,7 +1001,30 @@ async function installAssetFiles(cat, projectDir, spec, host) {
829
1001
  if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
830
1002
  const base = e.name.slice(0, -MD_EXT.length)
831
1003
  const content = await readFile(join(ROOT, cat, e.name), 'utf8')
832
- await writeFile(join(destDir, `${base}${spec.ext}`), adaptFrontmatter(content, host, cat))
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))
833
1028
  }
834
1029
  }
835
1030
 
@@ -852,6 +1047,12 @@ async function installAgentAssets(projectDir, agent) {
852
1047
  warnUnsupportedAsset(cat, agent)
853
1048
  continue
854
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)
1054
+ continue
1055
+ }
855
1056
  await installAssetFiles(cat, projectDir, spec, agent)
856
1057
  written[cat].push(spec.dir)
857
1058
  }
@@ -877,6 +1078,16 @@ function installedExtFor(cat, dirRel) {
877
1078
  return null
878
1079
  }
879
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)
1089
+ }
1090
+
880
1091
  const CHANGES_ROOT = 'aspec'
881
1092
 
882
1093
  const DEFAULT_ASPEC_CONFIG = `# aspec project configuration
@@ -1115,7 +1326,7 @@ async function install(args) {
1115
1326
  }
1116
1327
  }
1117
1328
 
1118
- const res = await mergeMcp(target, mcpMap)
1329
+ const res = await setupHostMcp(target, mcpMap, { projectDir, agent })
1119
1330
  const loc = projectDir
1120
1331
  ? `${projectDir} (layout ${agent} + templates en la raiz)`
1121
1332
  : `${target} (disponible en todos tus proyectos)`
@@ -1158,6 +1369,7 @@ async function upgradeCmd(args) {
1158
1369
  async function initProject(args) {
1159
1370
  const projectDir = process.cwd()
1160
1371
  const withAzure = args.includes('--with-azure')
1372
+ const withMcp = !args.includes('--no-mcp')
1161
1373
  const agentFlag = scanAgentFlag(args)
1162
1374
  const tierFlag = scanTierFlag(args)
1163
1375
  const langFlag = scanLangFlag(args)
@@ -1237,7 +1449,12 @@ async function initProject(args) {
1237
1449
  await scaffoldAspec(projectDir)
1238
1450
  await refreshWorkingContext(projectDir)
1239
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 })
1240
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}`)
1241
1458
  console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''} (Idioma: ${language})`)
1242
1459
  }
1243
1460
 
@@ -1693,6 +1910,8 @@ async function checkCommand() {
1693
1910
  process.exit(1)
1694
1911
  }
1695
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)
1696
1915
  let missing = 0
1697
1916
  let orphans = 0
1698
1917
 
@@ -1716,7 +1935,18 @@ async function checkCommand() {
1716
1935
  // La convencion de nombre del host (p. ej. vscode: .agent.md/.prompt.md)
1717
1936
  // se deriva del mismo modelo de destino; en skills no cambia el nombre.
1718
1937
  const ext = installedExtFor(cat, dirRel)
1719
- const expected = (await readdir(join(ROOT, cat))).sort().map((f) => (ext ? f.replace(/\.md$/, ext) : f))
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
+ }
1720
1950
  const actual = (await readdir(destDir)).sort()
1721
1951
  const missingFiles = expected.filter((f) => !actual.includes(f))
1722
1952
  const orphanFiles = actual.filter((f) => !expected.includes(f))
@@ -1749,10 +1979,13 @@ async function checkTierOrphan(cwd) {
1749
1979
  if (!(await exists(tierFile))) return warnings
1750
1980
  const tier = (await readFile(tierFile, 'utf8')).trim()
1751
1981
  if (!TIERS[tier]) return warnings
1752
- const localAgents = join(cwd, '.opencode', 'agents')
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)
1753
1985
  let count = 0
1754
- if (await exists(localAgents)) {
1755
- count = (await readdir(localAgents)).filter((f) => f.endsWith('.md')).length
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
1756
1989
  }
1757
1990
  if (count === 0) {
1758
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.`)