@ancleto/spec 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,7 +77,7 @@ Ancleto resuelve esto centralizando las reglas de negocio en un motor de memoria
77
77
 
78
78
  **Wizard interactivo** — Banner animado y menús navegables con flechas en `init` e `install`, sin librerías pesadas.
79
79
 
80
- **Hosts e IDEs (7)** — opencode, claude, vscode, antigravity, cursor, roo y copilot, cada uno con su layout nativo (Copilot: prompts en `.github/prompts` + MCP en `copilot-mcp.json`).
80
+ **Hosts e IDEs (8)** — opencode, claude, vscode, antigravity, cursor, roo, copilot y commandcode, cada uno con su layout nativo (Copilot: prompts en `.github/prompts` + MCP en `copilot-mcp.json`; Command Code: `.commandcode/*` + MCP en `.mcp.json`).
81
81
 
82
82
  **Perfil de test automation** — `--profile test`: tester ampliado (planning/generation/healing/coverage), comandos `cleto-test-*` y estructura `testspec/` para proyectos Playwright.
83
83
 
@@ -151,6 +151,7 @@ del host (el CLI no deduplica ni elige un "ganador").
151
151
  | `cursor` | `.cursor/skills/<n>/SKILL.md` | — | — |
152
152
  | `roo` | `.roo/skills/<n>/SKILL.md` | — | — |
153
153
  | `copilot` | — | `.github/prompts/<n>.prompt.md` | `.github/prompts/<n>.prompt.md` |
154
+ | `commandcode` | `.commandcode/skills/<n>/SKILL.md` | `.commandcode/agents/<n>.md` (adaptado) | `.commandcode/commands/<n>.md` |
154
155
 
155
156
  Cada host recibe sus skills en **su** directorio nativo. **`.agents/skills` es un punto de lectura
156
157
  compartido** —lo leen opencode, Cursor, VS Code y Antigravity—, no un destino universal: Claude lee
@@ -172,18 +173,36 @@ for agent '<n>'`): un id inexistente cuelga el subagent. Los ids que sólo exist
172
173
  de MCP se expresa por `mcpServers`/`.agents/mcp_config.json`, nunca como tool id. El campo `skill` no
173
174
  es una tool: se cubre con el surfaceo automático de skills de Antigravity.
174
175
 
176
+ **Command Code** (CLI `cmd`) usa su layout nativo por asset bajo `.commandcode/`: skills en
177
+ `.commandcode/skills/<n>/SKILL.md` (Agent Skills, verbatim), agents en `.commandcode/agents/<n>.md`
178
+ (frontmatter adaptado) y commands en `.commandcode/commands/<n>.md` (el body es el prompt, copia
179
+ verbatim). Sus agents adaptan el frontmatter de opencode: `name` inyectado, `description` preservado,
180
+ `mode`/`color`/`temperature`/`permission` eliminados, `model` **omitido** (el host hereda el modelo de
181
+ sesión; elegilo con `/model`, el tier no lo reescribe) y `tools` como **lista** de ids verificados
182
+ (`read`→`read_file`, `write`→`write_file`, `edit`→`edit_file`, `bash`→`shell_command`, `grep`→`grep`,
183
+ `glob`→`glob`, `webfetch`→`web_fetch`, `websearch`→`web_search`, `todowrite`→`todo_write`). Las tools
184
+ de memoria (`searchMemory`/`recordRule`/`recordDecision`) se emiten con la convención MCP de Command
185
+ Code (`mcp__ancleto-memory__<tool>`). Una clave sin id verificado se omite con aviso
186
+ (`skip tool '<k>': no verified Command Code id for agent '<n>'`). A diferencia de otros hosts, en
187
+ Command Code `tools` **omitido** significa "ninguna tool": si el agent de origen no declara `tools`,
188
+ se emite `tools: "*"` (el default de opencode es "todas"). El body de los commands invoca "the Skill
189
+ tool" (no se transforma); en Command Code el equivalente es `/skill:<name>`.
190
+
175
191
  Los assets que un host no soporta (por ejemplo `agents`/`commands` de `cursor` y `roo`)
176
192
  **no se escriben** y el instalador avisa por `stderr`
177
193
  (`skip agents: not supported by host 'cursor'`) sin abortar. La instalación es **aditiva y no
178
194
  destructiva**: instalar con otro agente escribe en su base y preserva los layouts previos, y el
179
195
  manifiesto `.ancletorc.installedPaths` acumula la unión de los destinos realmente escritos.
180
196
 
181
- La **configuración MCP específica de host** está fuera de alcance con **una excepción**: para
197
+ La **configuración MCP específica de host** está fuera de alcance con **dos excepciones**: para
182
198
  `agent: antigravity` el CLI genera o mergea (no destructivo) el MCP de workspace
183
- `.agents/mcp_config.json` con esquema `{ "mcpServers": { "<n>": { "command", "args", "env" } } }`,
184
- incluyendo el servidor de memoria `ancleto-memory`; preserva `mcpServers` preexistentes y no pisa
185
- homónimos. `.mcp.json` (Claude) y `.vscode/mcp.json` (VS Code) siguen fuera de alcance, al igual que el
186
- MCP global de Antigravity. `claude` se soporta con adaptación de frontmatter obligatoria para `agents`
199
+ `.agents/mcp_config.json` con esquema `{ "mcpServers": { "<n>": { "command", "args", "env" } } }`, y
200
+ para `agent: commandcode` el MCP de proyecto `.mcp.json` con esquema
201
+ `{ "mcpServers": { "<n>": { "transport": "stdio", "command", "args", "env" } } }`; en ambos casos
202
+ incluye el servidor de memoria `ancleto-memory`, preserva `mcpServers` preexistentes y no pisa
203
+ homónimos. `.mcp.json` sólo lo gestiona `commandcode` (para `claude` sigue fuera de alcance), al igual
204
+ que `.vscode/mcp.json` (VS Code) y el MCP global de Antigravity. `claude` se soporta con adaptación de
205
+ frontmatter obligatoria para `agents`
187
206
  (se omiten `model`/`tools` sin equivalencia verificada y las claves
188
207
  `mode`/`color`/`temperature`/`permission`), sin MCP propio. **Copilot** instala agents y commands
189
208
  como prompt files en `.github/prompts/*.prompt.md` (mismo adapter, sin `model:`) y su MCP en
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Update a change's artifacts when a definition changed
3
+ ---
4
+
5
+ Invoke the `ancleto-update` skill with the Skill tool and follow it exactly; do not improvise steps.
6
+
7
+ **Input**: the argument after `/cleto-update` is a change name (kebab-case) and what changed (a definition to apply, or a spec that already changed). If the change name is omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
8
+
9
+ This is the artifact-update skill, not the CLI `ancleto update` (which reinstalls the package).
10
+
11
+ Work Item grounding (when the request references a Work Item) is owned by `@context-resolver`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: ancleto-update
3
+ description: Update the artifacts of an existing change when a definition changed (a main spec, the scope, or a design decision). Merges in place or writes a rev2, preserving unmentioned content. No external binaries required.
4
+ license: MIT
5
+ compatibility: No external binaries required. Works with any agent runtime.
6
+ metadata:
7
+ author: ancleto
8
+ version: '1.0'
9
+ ---
10
+
11
+ # aspec Update
12
+
13
+ **Artifacts language**: write artifact content in the user's conversation language (or the project's configured `language` in `.ancletorc`). Keywords (`Requirement`, `Scenario`, `SHALL`, `WHEN`/`THEN`/`AND`, `ADDED/MODIFIED/REMOVED/RENAMED Requirements`) and file/directory names are literal and MUST NOT be translated.
14
+
15
+ Update the artifacts of an existing change when a definition changed. This skill edits the change's own artifacts with a non-destructive merge; it does not implement code.
16
+
17
+ **Name disambiguation**: this skill (`ancleto-update`, command `/cleto-update`) updates change artifacts. It is NOT the CLI `ancleto update`, which reinstalls the package over an existing install. Never confuse the two.
18
+
19
+ **Input**: a change name (e.g., `add-auth`) and what changed — either a definition the user describes, or a spec that already changed. If the change name is omitted, infer it from conversation context; if vague or ambiguous you MUST list the directories under `aspec/changes/` (excluding `archive/`) and ask the user to select.
20
+
21
+ **IMPORTANT**: Do NOT guess or auto-select a change. If the name resolves inside `aspec/changes/archive/`, STOP and report it — never edit an archived change.
22
+
23
+ ## Steps
24
+
25
+ ### 1. Select the change
26
+
27
+ - A name is provided → use it.
28
+ - Omitted → infer from context. If ambiguous, list `aspec/changes/` (excluding `archive/`) and ask.
29
+ - If it resolves to `aspec/changes/archive/<name>/`, report and stop.
30
+
31
+ ### 2. Read the artifacts
32
+
33
+ Read every present artifact — they constrain what you write:
34
+
35
+ - `proposal.md` — what & why
36
+ - `specs/<capability>/spec.md` — delta requirements
37
+ - `design.md` — how
38
+ - `tasks.md` — implementation checklist
39
+
40
+ If `context.md` exists, read it as background (Work Item title, description, acceptance criteria); it is NOT an artifact and MUST NOT be copied into output.
41
+
42
+ ### 3. Consolidate what changed
43
+
44
+ Two sources, and they are **additive** (neither replaces the other):
45
+
46
+ - **Drift (delta ↔ main)**: for each delta spec at `aspec/changes/<name>/specs/<capability>/spec.md`, compare against the main spec at `aspec/specs/<capability>/spec.md`. Report requirements present in the delta but absent or changed in the main spec (and vice versa).
47
+ - **User-described change**: the definition change the user states.
48
+
49
+ If neither source yields anything actionable, report and stop.
50
+
51
+ ### 4. Classify by owner and apply a non-destructive merge
52
+
53
+ Each change has exactly one owning artifact:
54
+
55
+ | What changed | Owning artifact |
56
+ |---|---|
57
+ | Requirement / specified behavior | `specs/<capability>/spec.md` (`## MODIFIED Requirements`) |
58
+ | Design decision | `design.md` |
59
+ | Scope | `proposal.md` |
60
+ | New work | `tasks.md` |
61
+
62
+ Apply the **minimal** edit to each owning artifact and **preserve everything not mentioned**:
63
+
64
+ - **specs/**: use `## MODIFIED Requirements`, adding or editing scenarios only. Do NOT copy existing scenarios; preserve requirements the delta does not mention.
65
+ - **tasks.md**: add new tasks at the end of their phase; preserve `- [x]` marks; do NOT reorder existing tasks.
66
+ - **proposal.md / design.md**: edit the affected section; leave the rest intact.
67
+
68
+ The merge MUST be idempotent: running twice with the same change yields the same result.
69
+
70
+ ### 5. Choose the write mode (ask the user)
71
+
72
+ Ask the user which write mode to use, and do NOT pick one yourself:
73
+
74
+ - **in-place**: edit the owning artifact in place, then append an entry recording the applied change to a `## Change Log` section in `proposal.md` (create the section if missing).
75
+ - **rev2**: write only the affected artifacts under `aspec/changes/<name>/rev2/`, mirroring their relative paths (`rev2/proposal.md`, `rev2/specs/<capability>/spec.md`, …), leaving the originals untouched; also write `rev2/README.md` with the reason, the date, and the source of the change.
76
+
77
+ ### 6. Report and offer verify
78
+
79
+ Summarize the artifacts updated and the drift resolved. If the change altered specified behavior, offer `ancleto-verify`.
80
+
81
+ ## Guardrails
82
+
83
+ - **Merge, not overwrite**: preserve all content the change does not mention; be idempotent.
84
+ - **Scope**: only the change's own artifacts. NEVER edit `aspec/changes/archive/`, the main specs under `aspec/specs/` (that is `ancleto-sync-specs`), or implementation code (that is `ancleto-apply`).
85
+ - **Ask, don't guess**: if the change, the owning artifact, or the write mode is unclear, ask.
86
+ - **Never auto-select** the change; exclude `archive/` from the candidate list.
87
+ - Never conflate this skill with the CLI `ancleto update`.
@@ -15,12 +15,13 @@ Map of the spec-driven lifecycle. It does not execute work — it routes: given
15
15
  ## The Lifecycle
16
16
 
17
17
  ```
18
- explore → new/propose/ff → continue* → apply → verify → archive
18
+ explore → new/propose/ff → continue* → update? → apply → verify → archive
19
19
  ↑ ↓
20
20
  onboard bulk-archive (batches)
21
21
  ```
22
22
 
23
23
  `*` continue runs once per missing artifact until the set is complete.
24
+ `?` update runs only when a definition changes (a main spec, the scope, or a design decision).
24
25
 
25
26
  ## Routing Rules
26
27
 
@@ -32,6 +33,7 @@ explore → new/propose/ff → continue* → apply → verify → archive
32
33
  | Start structured work | `ancleto-new` (step by step) or `ancleto-propose` (all artifacts at once) |
33
34
  | Full draft when the path is clear | `ancleto-ff` |
34
35
  | Resume a change missing artifacts | `ancleto-continue` (repeat until complete) |
36
+ | Update artifacts after a definition changed | `ancleto-update` |
35
37
  | Implement tasks from ready artifacts | `ancleto-apply` |
36
38
  | Check implementation before closing | `ancleto-verify` |
37
39
  | Close one finished change (sync its specs) | `ancleto-archive` |
@@ -48,6 +50,7 @@ explore → new/propose/ff → continue* → apply → verify → archive
48
50
  | `specs/` or `design.md` missing | `ancleto-continue` |
49
51
  | `tasks.md` missing | `ancleto-continue` |
50
52
  | All artifacts present, tasks unchecked | `ancleto-apply` |
53
+ | A definition changed (main spec, scope, design) | `ancleto-update` |
51
54
  | All tasks checked | `ancleto-verify`, then `ancleto-archive` |
52
55
  | Several changes complete | `ancleto-bulk-archive` |
53
56
 
package/src/cli/index.js CHANGED
@@ -36,13 +36,15 @@ Uso:
36
36
  ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
37
37
  ancleto install --lang <codigo> auto | es | en | pt (idioma de los artifacts; auto = idioma de la conversacion)
38
38
  ancleto install --exclude <globs> Paths excluidos del discovery, coma-separados (ej: "**/*.png,docs")
39
- ancleto install --agent <nombre> opencode | claude | vscode | antigravity | cursor | roo | copilot (wizard si no esta guardado)
39
+ ancleto install --agent <nombre> opencode | claude | vscode | antigravity | cursor | roo | copilot | commandcode (wizard si no esta guardado)
40
40
  Elige el layout nativo a materializar (directorio + convencion de
41
41
  nombre) y el adapter de frontmatter del host. NO arbitra el
42
42
  descubrimiento entre hosts: si dos leen el mismo directorio, eso es
43
43
  efecto del host (no se deduplica ni se elige "ganador").
44
44
  antigravity instala skills, agents adaptados y commands como skills
45
45
  (.agents/*) y mergea el MCP de workspace .agents/mcp_config.json.
46
+ commandcode instala skills, agents adaptados y commands en
47
+ .commandcode/* y mergea el MCP de proyecto .mcp.json.
46
48
  ancleto install --profile <perfil> general | test (perfil de test automation con Playwright; persiste en .ancletorc)
47
49
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
48
50
  Parado en un proyecto con .ancletorc opera sobre ese proyecto;
@@ -612,7 +614,7 @@ async function applyTier(agentsDir, tier, models = null) {
612
614
  }
613
615
  }
614
616
 
615
- const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo', 'copilot']
617
+ const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo', 'copilot', 'commandcode']
616
618
  const DEFAULT_AGENT = 'opencode'
617
619
 
618
620
  function askAgent() {
@@ -855,12 +857,48 @@ async function mergeCopilotMcp(projectDir, mcpMap) {
855
857
  return { file, added }
856
858
  }
857
859
 
860
+ // Command Code: MCP de proyecto en `.mcp.json` (scope `project`, versionado con
861
+ // el repo). Esquema del host: `{ mcpServers: { <name>: { transport: 'stdio',
862
+ // command, args, env? } } }` (se descartan `type`/`enabled` de opencode). Merge NO
863
+ // destructivo: se preservan `mcpServers` y demas claves top-level, no se pisa un
864
+ // servidor homonimo, y un JSON invalido se avisa sin sobrescribir.
865
+ function toCommandCodeServer(def) {
866
+ const command = Array.isArray(def.command) ? def.command : [def.command]
867
+ const server = { transport: 'stdio', command: command[0], args: command.slice(1) }
868
+ if (def.env) server.env = def.env
869
+ return server
870
+ }
871
+
872
+ async function mergeCommandCodeMcp(projectDir, mcpMap) {
873
+ if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
874
+ const file = join(projectDir, '.mcp.json')
875
+ let cfg = {}
876
+ if (await exists(file)) {
877
+ try {
878
+ cfg = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
879
+ } catch {
880
+ console.warn('ancleto: no se pudo leer .mcp.json como JSON; MCP de Command Code no se modifico')
881
+ return { file, added: [] }
882
+ }
883
+ }
884
+ cfg.mcpServers = cfg.mcpServers || {}
885
+ const added = []
886
+ for (const [name, def] of Object.entries(mcpMap)) {
887
+ if (cfg.mcpServers[name]) continue
888
+ cfg.mcpServers[name] = toCommandCodeServer(def)
889
+ added.push(name)
890
+ }
891
+ await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
892
+ return { file, added }
893
+ }
894
+
858
895
  // D6: dueno unico del MCP de host. Antigravity mergea `.agents/mcp_config.json`;
859
- // el resto conserva `mergeMcp` sobre opencode.json (targetDir = .opencode del
860
- // proyecto o config global).
896
+ // Command Code mergea `.mcp.json`; el resto conserva `mergeMcp` sobre opencode.json
897
+ // (targetDir = .opencode del proyecto o config global).
861
898
  async function setupHostMcp(targetDir, mcpMap, { projectDir = null, agent = null } = {}) {
862
899
  if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
863
900
  if (agent === 'antigravity' && projectDir) return mergeAntigravityMcp(projectDir, mcpMap)
901
+ if (agent === 'commandcode' && projectDir) return mergeCommandCodeMcp(projectDir, mcpMap)
864
902
  await mkdir(targetDir, { recursive: true })
865
903
  return mergeMcp(targetDir, mcpMap)
866
904
  }
@@ -895,6 +933,39 @@ async function refreshCopilotMcp(projectDir) {
895
933
  return { file, fixed }
896
934
  }
897
935
 
936
+ // Regenera entradas rotas y agrega ausentes en `.mcp.json` de Command Code (para
937
+ // upgrade). Mismo criterio que `refreshCopilotMcp`, con el esquema del host.
938
+ async function refreshCommandCodeMcp(projectDir) {
939
+ const file = join(projectDir, '.mcp.json')
940
+ let cfg = {}
941
+ if (await exists(file)) {
942
+ try {
943
+ cfg = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
944
+ } catch {
945
+ cfg = {}
946
+ }
947
+ }
948
+ cfg.mcpServers = cfg.mcpServers || {}
949
+ const fresh = buildDefaultMcp({ withEngram: Boolean(cfg.mcpServers.engram) })
950
+ const fixed = []
951
+ for (const [name, def] of Object.entries(cfg.mcpServers)) {
952
+ if (!fresh[name]) continue
953
+ const asArray = { command: [def.command, ...(Array.isArray(def.args) ? def.args : [])] }
954
+ if (mcpCommandBroken(asArray)) {
955
+ Object.assign(cfg.mcpServers[name], toCommandCodeServer(fresh[name]))
956
+ fixed.push(name)
957
+ }
958
+ }
959
+ for (const [name, def] of Object.entries(fresh)) {
960
+ if (!cfg.mcpServers[name]) {
961
+ cfg.mcpServers[name] = toCommandCodeServer(def)
962
+ fixed.push(`${name} (agregado)`)
963
+ }
964
+ }
965
+ await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
966
+ return { file, fixed }
967
+ }
968
+
898
969
  async function copyAssets(dest) {
899
970
  await mkdir(dest, { recursive: true })
900
971
  for (const d of ASSETS) {
@@ -940,6 +1011,16 @@ const AGENT_TARGETS = {
940
1011
  skills: null,
941
1012
  agents: { dir: '.github/prompts', ext: '.prompt.md' },
942
1013
  commands: { dir: '.github/prompts', ext: '.prompt.md' }
1014
+ },
1015
+ // Command Code: layout nativo por asset bajo `.commandcode/`. Los tres assets
1016
+ // son nativos: skills (Agent Skills, identity), agents (frontmatter adaptado,
1017
+ // ver `adaptCommandCodeFrontmatter`) y commands (custom slash commands, el body
1018
+ // es el prompt, copia verbatim). El MCP de proyecto vive en `.mcp.json`
1019
+ // (ver `mergeCommandCodeMcp`).
1020
+ commandcode: {
1021
+ skills: { dir: '.commandcode/skills' },
1022
+ agents: { dir: '.commandcode/agents', ext: MD_EXT },
1023
+ commands: { dir: '.commandcode/commands', ext: MD_EXT }
943
1024
  }
944
1025
  }
945
1026
 
@@ -956,6 +1037,18 @@ normal = flujo completo, minimo = pasos agrupados y económicos, gratis = modelo
956
1037
  gratis disponible en tu cuenta.
957
1038
  `
958
1039
 
1040
+ // Solo Command Code: los tiers tampoco cambian modelos (el agent no fija `model`,
1041
+ // hereda el de la sesión); la nota lo recuerda al empezar.
1042
+ const COMMANDCODE_MODEL_NOTE = `
1043
+ ## Nota de modelo (solo Command Code)
1044
+
1045
+ Elegí el modelo con \`/model\` antes de empezar: los tiers de ancleto
1046
+ (normal|minimo|gratis) no cambian modelos en Command Code, solo el nivel de
1047
+ esfuerzo (normal = flujo completo, minimo = pasos agrupados y económicos,
1048
+ gratis = modelo gratis disponible en tu cuenta). Los subagentes heredan el
1049
+ modelo de la sesión porque su frontmatter no fija \`model\`.
1050
+ `
1051
+
959
1052
  // D12: asset no soportado -> no se escribe y se avisa a stderr (no bloqueante).
960
1053
  function warnUnsupportedAsset(asset, agent) {
961
1054
  console.error(`skip ${asset}: not supported by host '${agent}'`)
@@ -985,7 +1078,11 @@ async function installAssetFiles(cat, projectDir, spec, host, srcDir = null) {
985
1078
  const base = e.name.slice(0, -MD_EXT.length)
986
1079
  const content = await readFile(join(origin, e.name), 'utf8')
987
1080
  const adapted = adaptFrontmatter(content, host, cat, base)
988
- const body = (host === 'copilot' && cat === 'agents' && base === 'orchestrator') ? adapted + COPILOT_MODEL_NOTE : adapted
1081
+ let body = adapted
1082
+ if (cat === 'agents' && base === 'orchestrator') {
1083
+ if (host === 'copilot') body += COPILOT_MODEL_NOTE
1084
+ else if (host === 'commandcode') body += COMMANDCODE_MODEL_NOTE
1085
+ }
989
1086
  await writeFile(join(destDir, `${base}${spec.ext}`), body)
990
1087
  }
991
1088
  }
@@ -1490,6 +1587,12 @@ async function upgradeCmd(args) {
1490
1587
  console.log(fixed.length
1491
1588
  ? ` ✔ MCP regenerados en ${file}: ${fixed.join(', ')}`
1492
1589
  : ` ✔ MCP sanos en ${file}, sin cambios`)
1590
+ } else if (agent === 'commandcode') {
1591
+ const { file, fixed } = await refreshCommandCodeMcp(projectDir)
1592
+ console.log(` ✔ skills actualizadas en ${skillsDir} (${ANCLETO_SKILLS.length} skills)`)
1593
+ console.log(fixed.length
1594
+ ? ` ✔ MCP regenerados en ${file}: ${fixed.join(', ')}`
1595
+ : ` ✔ MCP sanos en ${file}, sin cambios`)
1493
1596
  } else {
1494
1597
  console.log(` ✔ skills actualizadas en ${skillsDir} (${ANCLETO_SKILLS.length} skills)`)
1495
1598
  }
@@ -2265,8 +2368,9 @@ async function checkAgentsFrontmatter(cwd, dirRel, actual, expected, fileExt) {
2265
2368
  source = source.replace(/^model: .*$/m, `model: ${tierModelsMap[name]}`)
2266
2369
  }
2267
2370
  let expectedContent = adaptFrontmatter(source, host, 'agents', name)
2268
- if (host === 'copilot' && name === 'orchestrator') {
2269
- expectedContent += COPILOT_MODEL_NOTE
2371
+ if (name === 'orchestrator') {
2372
+ if (host === 'copilot') expectedContent += COPILOT_MODEL_NOTE
2373
+ else if (host === 'commandcode') expectedContent += COMMANDCODE_MODEL_NOTE
2270
2374
  }
2271
2375
  const installed = await readFile(join(cwd, dirRel, f), 'utf8')
2272
2376
  if (installed !== expectedContent) {
@@ -12,12 +12,12 @@ export const AGENT_ADAPTER_DROP = new Set(['mode', 'color', 'temperature', 'perm
12
12
  // Hosts conocidos por el modulo. Cualquier host fuera de este conjunto cae en la
13
13
  // rama `unknown` (no aborta; emite aviso a stderr y retorna el contenido sin
14
14
  // cambios).
15
- const KNOWN_HOSTS = new Set(['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo', 'copilot'])
15
+ const KNOWN_HOSTS = new Set(['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo', 'copilot', 'commandcode'])
16
16
 
17
17
  // Hosts para los que existe una transformacion documentada de agents
18
18
  // (distinta de identidad). Cursor y Roo viven fuera: no hay formato de frontmatter
19
19
  // propio, así que la adaptación es passthrough con aviso.
20
- const TRANSFORM_AGENTS = new Set(['claude', 'vscode', 'antigravity', 'copilot'])
20
+ const TRANSFORM_AGENTS = new Set(['claude', 'vscode', 'antigravity', 'copilot', 'commandcode'])
21
21
 
22
22
  // Tabla unica (fijada contra la tabla oficial de frontmatter de Custom
23
23
  // Subagents) de ids de Antigravity. Conjunto CERRADO: solo se emiten estos ids.
@@ -35,6 +35,31 @@ export const ANTIGRAVITY_TOOL_MAP = {
35
35
  todowrite: 'manage_task'
36
36
  }
37
37
 
38
+ // Tabla unica (fijada contra la documentacion oficial de Command Code) de ids de
39
+ // tools. Conjunto CERRADO: solo se emiten estos ids. Toda clave del mapa `tools`
40
+ // de opencode sin id aqui se omite con aviso a stderr (nunca en silencio).
41
+ export const COMMANDCODE_TOOL_MAP = {
42
+ read: 'read_file',
43
+ write: 'write_file',
44
+ edit: 'edit_file',
45
+ bash: 'shell_command',
46
+ grep: 'grep',
47
+ glob: 'glob',
48
+ webfetch: 'web_fetch',
49
+ websearch: 'web_search',
50
+ todowrite: 'todo_write'
51
+ }
52
+
53
+ // Tools de la memoria propia del framework. Command Code expone los servers MCP
54
+ // con la convencion documentada `mcp__<server>__<tool>`; `ancleto-memory` es el
55
+ // server que registra `buildDefaultMcp`. Estas claves del mapa `tools` de opencode
56
+ // son sus tools, asi que se emiten con el nombre MCP completo.
57
+ export const COMMANDCODE_MCP_TOOL_MAP = {
58
+ searchMemory: 'mcp__ancleto-memory__searchMemory',
59
+ recordRule: 'mcp__ancleto-memory__recordRule',
60
+ recordDecision: 'mcp__ancleto-memory__recordDecision'
61
+ }
62
+
38
63
  // Parser propio sin dependencias. Cada entrada conserva su clave y sus lineas
39
64
  // crudas (incluye block scalars y mapas anidados), preservando orden.
40
65
  export function parseFrontmatter(content) {
@@ -147,6 +172,45 @@ function adaptAntigravityFrontmatter(content, name) {
147
172
  return serializeFrontmatter(parsed)
148
173
  }
149
174
 
175
+ // Adapta el frontmatter de un agent de opencode al de Command Code. `name` (el
176
+ // host lo requiere; default filename) se inyecta desde el nombre del archivo.
177
+ // `description` se preserva. `mode`/`color`/`temperature`/`permission` se eliminan
178
+ // y `model` se omite: Command Code hereda el modelo de sesion cuando no se declara
179
+ // (nunca se mapea un id de `opencode-go/*`). `tools` sale como lista de ids
180
+ // verificados (o `"*"` si el origen no declara tools: el default de opencode es
181
+ // "todas" y el de Command Code es "ninguna"). Una clave sin id verificado se omite
182
+ // con aviso a stderr (nunca en silencio).
183
+ function adaptCommandCodeFrontmatter(content, name) {
184
+ const parsed = parseFrontmatter(content)
185
+ if (!parsed.hasFrontmatter) return content
186
+ const byKey = (k) => parsed.entries.find((e) => e.key === k)
187
+
188
+ const managed = new Set([...AGENT_ADAPTER_DROP, 'name', 'description', 'tools'])
189
+ const kept = parsed.entries.filter((e) => e.key && !managed.has(e.key))
190
+
191
+ const toolsEntry = byKey('tools')
192
+ const toolIds = []
193
+ if (toolsEntry) {
194
+ for (const { key, value } of parseToolFlags(toolsEntry)) {
195
+ if (value === 'false') continue
196
+ const id = COMMANDCODE_TOOL_MAP[key] || COMMANDCODE_MCP_TOOL_MAP[key]
197
+ if (id) {
198
+ toolIds.push(id)
199
+ continue
200
+ }
201
+ console.error(`skip tool '${key}': no verified Command Code id for agent '${name}'`)
202
+ }
203
+ }
204
+
205
+ const out = [{ key: 'name', lines: [`name: ${name}`] }]
206
+ const description = byKey('description')
207
+ if (description) out.push(description)
208
+ out.push(toolsEntry ? { key: 'tools', lines: [`tools: [${toolIds.join(', ')}]`] } : { key: 'tools', lines: ['tools: "*"'] })
209
+ out.push(...kept)
210
+ parsed.entries = out
211
+ return serializeFrontmatter(parsed)
212
+ }
213
+
150
214
  // Elimina las claves gestionadas por el adaptador (mode/color/temperature/
151
215
  // permission/model/tools) preservando el resto y el body. Para claude, vscode y
152
216
  // copilot. Si no hay frontmatter, retorna el contenido sin cambios.
@@ -166,6 +230,8 @@ function dropManagedKeys(content) {
166
230
  // opencode → identity (passthrough)
167
231
  // antigravity + agents → adaptAntigravityFrontmatter(content, name)
168
232
  // antigravity + (skills/cmds) → identity
233
+ // commandcode + agents → adaptCommandCodeFrontmatter(content, name)
234
+ // commandcode + (skills/cmds) → identity
169
235
  // claude/vscode/copilot + ags → dropManagedKeys(content)
170
236
  // claude/vscode/copilot + sk → identity
171
237
  // cursor/roo → identity + aviso stderr (sin formato documentado)
@@ -182,5 +248,6 @@ export function adaptFrontmatter(content, host, assetKind, name) {
182
248
  if (assetKind !== 'agents') return content
183
249
  if (!TRANSFORM_AGENTS.has(host)) return content // opencode u otro conocido sin transform
184
250
  if (host === 'antigravity') return adaptAntigravityFrontmatter(content, name)
251
+ if (host === 'commandcode') return adaptCommandCodeFrontmatter(content, name)
185
252
  return dropManagedKeys(content)
186
253
  }