@ancleto/spec 0.6.28 → 0.6.30

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
@@ -26,6 +26,7 @@
26
26
  - [Guía de .gitignore](#guía-de-gitignore)
27
27
  - [Configuración y tiers de costo](#configuración-y-tiers-de-costo)
28
28
  - [Uso rápido](#uso-rápido)
29
+ - [Proyectos con ancleto](#proyectos-con-ancleto)
29
30
  - [Referencia de comandos CLI](#referencia-de-comandos-cli)
30
31
  - [Motor de memoria persistente](#motor-de-memoria-persistente)
31
32
  - [Comandos del ciclo SDD en tu IDE](#comandos-del-ciclo-sdd-en-tu-ide)
@@ -138,6 +139,36 @@ El framework **no gestiona tu `.gitignore`**: es una decisión del proyecto. Com
138
139
 
139
140
  ---
140
141
 
142
+ ## Proyectos con ancleto
143
+
144
+ Cuando instalás ancleto **en un proyecto** (`install --project`, `init` o `update`), el CLI lo anota en un registro global (`~/.config/ancleto/projects.json`). Así podés ver de un vistazo todos los proyectos que lo usan, sin importar desde qué terminal estés:
145
+
146
+ ```bash
147
+ ancleto projects # lista (o `ancleto list --projects`)
148
+ ancleto projects info # estado del proyecto actual
149
+ ancleto projects scan "D:/Repos D/Proyectos" # descubre y registra los que ya existen
150
+ ancleto projects prune # saca del registro los borrados/movidos
151
+ ```
152
+
153
+ La lista muestra versión instalada, origen (por proyecto vs agentes globales), tier y agente:
154
+
155
+ ```
156
+ ancleto: proyectos (2 activos de 3 registrados · CLI v0.6.28)
157
+
158
+ PROYECTO VERSIÓN ORIGEN TIER AGENTE RUTA
159
+ ● App Coffice 0.6.28 scoped minimo opencode D:/Repos D/Proyectos/App Coffice
160
+ ● spec 0.6.28 scoped normal opencode D:/Repos D/Proyectos/ancleto/spec
161
+ ✖ Volatile — muerto — — D:/Repos D/tmp/volatile
162
+ ```
163
+
164
+ - `●` verde: instalación por proyecto · `○` amarillo: usa agentes globales · `✖` rojo: ruta inexistente (candidata a `prune`).
165
+ - Versión en rojo = desactualizada respecto del CLI instalado.
166
+ - `--json` en todos para scripting.
167
+
168
+ El registro es tuyo y local: no viaja con el repo ni se comparte. `ANCLETO_PROJECTS_FILE` permite apuntarlo a otro lado.
169
+
170
+ ---
171
+
141
172
  ## Configuración y tiers de costo
142
173
 
143
174
  En la primera ejecución, un **wizard interactivo** te guía por los pasos de configuración (agente/IDE y nivel de gasto). También podés pasarlos por flags:
@@ -198,6 +229,8 @@ ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--no-mcp]
198
229
  # Instala agentes, skills y templates
199
230
 
200
231
  ancleto update # Re-instala la última versión sobre lo existente
232
+ # Parado en un proyecto con .ancletorc opera sobre ESE proyecto;
233
+ # usá --global para forzar el alcance global
201
234
 
202
235
  ancleto upgrade # Re-aplica templates y skills respetando tus personalizaciones
203
236
 
@@ -209,6 +242,15 @@ ancleto specs check [--change <nombre>] [--json]
209
242
  ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
210
243
  # Tokens por sesión de opencode (rollup de subagentes; --session desglosa por agente)
211
244
 
245
+ ancleto projects [list] [--json]
246
+ # Lista los proyectos que usan ancleto (versión, tier, origen, ruta)
247
+ ancleto projects scan <raíz> [--dry-run] [--json]
248
+ # Descubre y registra proyectos ancleto bajo una raíz
249
+ ancleto projects prune [--dry-run] [--json]
250
+ # Quita del registro los proyectos borrados o movidos
251
+ ancleto projects info [ruta] [--json]
252
+ # Estado de un proyecto (versión, memoria, seed, changes activos)
253
+
212
254
  ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
213
255
 
214
256
  ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.28",
3
+ "version": "0.6.30",
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
@@ -34,6 +34,8 @@ Uso:
34
34
  ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
35
35
  ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
36
36
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
37
+ Parado en un proyecto con .ancletorc opera sobre ese proyecto;
38
+ usa --global para forzar el alcance global
37
39
  ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
38
40
  ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>]
39
41
  Crea .ancletorc en el repositorio actual
@@ -47,6 +49,14 @@ Uso:
47
49
  ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
48
50
  Tokens por sesion de opencode (entrada/salida/razonamiento/cache),
49
51
  con rollup de subagentes y desglose por agente con --session
52
+ ancleto projects [list] [--json] Lista los proyectos que usan ancleto (version, tier, origen, ruta)
53
+ ancleto projects scan <raiz> [--dry-run] [--json]
54
+ Descubre y registra proyectos ancleto bajo una raiz
55
+ ancleto projects prune [--dry-run] [--json]
56
+ Quita del registro los proyectos borrados o movidos
57
+ ancleto projects info [ruta] [--json]
58
+ Estado de un proyecto (version, memoria, seed, changes activos)
59
+ ancleto list --projects [--json] Alias de "ancleto projects list"
50
60
  ancleto memory context [--scope X] [--out file]
51
61
  Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
52
62
  ancleto memory list [--type X] [--scope X] [--all] [--json]
@@ -101,6 +111,301 @@ function globalConfigDir() {
101
111
  : join(homedir(), '.config', 'opencode')
102
112
  }
103
113
 
114
+ // Registro global de proyectos que usan ancleto (no depende de opencode).
115
+ function projectsRegistryPath() {
116
+ if (process.env.ANCLETO_PROJECTS_FILE) return process.env.ANCLETO_PROJECTS_FILE
117
+ const base = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'ancleto') : join(homedir(), '.config', 'ancleto')
118
+ return join(base, 'projects.json')
119
+ }
120
+
121
+ function normKey(p) {
122
+ return normPath(resolve(p))
123
+ }
124
+
125
+ async function readProjectsRegistry() {
126
+ const file = projectsRegistryPath()
127
+ if (!(await exists(file))) return { schemaVersion: 1, projects: {} }
128
+ try {
129
+ const data = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
130
+ return { schemaVersion: 1, projects: data.projects || {} }
131
+ } catch (err) {
132
+ console.warn(`ancleto: registro de proyectos ilegible (${file}): ${err.message}`)
133
+ return { schemaVersion: 1, projects: {} }
134
+ }
135
+ }
136
+
137
+ async function writeProjectsRegistry(reg) {
138
+ const file = projectsRegistryPath()
139
+ await mkdir(dirname(file), { recursive: true })
140
+ await writeFile(file, JSON.stringify(reg, null, 2) + '\n')
141
+ return file
142
+ }
143
+
144
+ async function registerProject(projectDir, { agent, tier } = {}) {
145
+ const abs = resolve(projectDir)
146
+ const rc = await readAncletorc(abs)
147
+ const tierFile = join(abs, '.opencode', '.ancleto-tier')
148
+ const localTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
149
+ const hasScopedAgents = await exists(join(abs, '.opencode', 'agents'))
150
+ const entry = {
151
+ path: abs.replace(/\\/g, '/'),
152
+ version: await packageVersion(),
153
+ agent: agent || rc?.agent || 'opencode',
154
+ tier: tier || localTier || null,
155
+ scoped: hasScopedAgents,
156
+ registeredAt: new Date().toISOString()
157
+ }
158
+ const reg = await readProjectsRegistry()
159
+ const key = normKey(abs)
160
+ entry.registeredAt = reg.projects[key]?.registeredAt || entry.registeredAt
161
+ entry.lastSeen = new Date().toISOString()
162
+ reg.projects[key] = entry
163
+ await writeProjectsRegistry(reg)
164
+ }
165
+
166
+ async function unregisterProject(projectDir) {
167
+ const reg = await readProjectsRegistry()
168
+ const key = normKey(projectDir)
169
+ if (!reg.projects[key]) return false
170
+ delete reg.projects[key]
171
+ await writeProjectsRegistry(reg)
172
+ return true
173
+ }
174
+
175
+ // Color helpers (respetan NO_COLOR y TTY).
176
+ const useColor = () => process.stdout.isTTY && !process.env.NO_COLOR
177
+ function paint(code, s) {
178
+ return useColor() ? `\x1b[${code}m${s}\x1b[0m` : String(s)
179
+ }
180
+ const c = {
181
+ dim: (s) => paint('2', s),
182
+ bold: (s) => paint('1', s),
183
+ green: (s) => paint('32', s),
184
+ yellow: (s) => paint('33', s),
185
+ red: (s) => paint('31', s),
186
+ cyan: (s) => paint('36', s)
187
+ }
188
+ const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '')
189
+ function padVisible(s, width) {
190
+ const len = stripAnsi(s).length
191
+ return s + ' '.repeat(Math.max(0, width - len))
192
+ }
193
+
194
+ // Descubre proyectos ancleto bajo una raiz (busca .ancletorc, con profundidad acotada).
195
+ async function discoverProjectsUnder(root, maxDepth = 3) {
196
+ const found = []
197
+ const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', 'vendor'])
198
+ async function walk(dir, depth) {
199
+ let entries
200
+ try {
201
+ entries = await readdir(dir, { withFileTypes: true })
202
+ } catch {
203
+ return
204
+ }
205
+ if (entries.some((e) => e.isFile() && (e.name === '.ancletorc' || e.name === '.ancletorc'))) found.push(dir)
206
+ if (depth >= maxDepth) return
207
+ for (const e of entries) {
208
+ if (!e.isDirectory() || skip.has(e.name) || e.name.startsWith('.')) continue
209
+ await walk(join(dir, e.name), depth + 1)
210
+ }
211
+ }
212
+ await walk(resolve(root), 0)
213
+ return found
214
+ }
215
+
216
+ // Lee el estado real de un proyecto registrado (o descubre que ya no existe).
217
+ async function projectStatus(entry) {
218
+ const abs = entry.path
219
+ if (!(await exists(abs))) return { ...entry, exists: false }
220
+ const rc = await readAncletorc(abs)
221
+ if (!rc) return { ...entry, exists: false, reason: 'sin .ancletorc' }
222
+ const tierFile = join(abs, '.opencode', '.ancleto-tier')
223
+ const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
224
+ const scoped = await exists(join(abs, '.opencode', 'agents'))
225
+ return {
226
+ ...entry,
227
+ exists: true,
228
+ path: abs.replace(/\\/g, '/'),
229
+ version: rc.version || entry.version || null,
230
+ agent: rc.agent || entry.agent || null,
231
+ tier,
232
+ scoped
233
+ }
234
+ }
235
+
236
+ async function projectsList(reg, { asJson, currentVersion }) {
237
+ const rows = await Promise.all(Object.values(reg.projects).map(projectStatus))
238
+ rows.sort((a, b) => String(a.path).localeCompare(String(b.path)))
239
+ if (asJson) {
240
+ console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, registry: projectsRegistryPath(), count: rows.length, projects: rows }, null, 2))
241
+ return rows
242
+ }
243
+ if (rows.length === 0) {
244
+ console.log('ancleto: no hay proyectos registrados todavia')
245
+ console.log(c.dim(' registra uno con: ancleto install --project <ruta> · o descubre con: ancleto projects scan <raiz>'))
246
+ return rows
247
+ }
248
+ const live = rows.filter((r) => r.exists)
249
+ console.log(`${c.bold('ancleto: proyectos')} ${c.dim(`(${live.length} activos de ${rows.length} registrados · CLI v${currentVersion})`)}`)
250
+ console.log('')
251
+ const header = ['', padVisible('PROYECTO', 22), padVisible('VERSIÓN', 10), padVisible('ORIGEN', 10), padVisible('TIER', 9), padVisible('AGENTE', 10), 'RUTA']
252
+ console.log(c.dim(` ${header.join(' ')}`))
253
+ for (const r of rows) {
254
+ const name = r.path.split('/').filter(Boolean).pop() || r.path
255
+ if (!r.exists) {
256
+ console.log(` ${c.red('✖')} ${padVisible(c.red(name.slice(0, 21)), 22)} ${padVisible(c.dim('—'), 10)} ${padVisible(c.red('muerto'), 10)} ${padVisible('—', 9)} ${padVisible('—', 10)} ${c.dim(r.path)}`)
257
+ continue
258
+ }
259
+ const outdated = r.version && currentVersion && r.version !== currentVersion
260
+ const marker = r.scoped ? c.green('●') : c.yellow('○')
261
+ const ver = outdated ? c.red(r.version) : c.green(r.version || '—')
262
+ const origin = r.scoped ? c.green('scoped') : c.yellow('global')
263
+ console.log(` ${marker} ${padVisible(c.bold(name.slice(0, 21)), 22)} ${padVisible(ver, 10)} ${padVisible(origin, 10)} ${padVisible(r.tier || c.dim('—'), 9)} ${padVisible(r.agent || '—', 10)} ${r.path}`)
264
+ }
265
+ console.log('')
266
+ console.log(c.dim(' ● instalación por proyecto ○ usa agentes globales versión en rojo = desactualizada'))
267
+ return rows
268
+ }
269
+
270
+ async function projectsScan(root, { asJson, dryRun }) {
271
+ if (!root) {
272
+ console.error('ancleto: uso: ancleto projects scan <raiz> [--dry-run] [--json]')
273
+ process.exit(1)
274
+ }
275
+ if (!(await exists(root))) {
276
+ console.error(`ancleto: no existe la raiz "${root}"`)
277
+ process.exit(1)
278
+ }
279
+ const found = await discoverProjectsUnder(root)
280
+ const reg = await readProjectsRegistry()
281
+ const added = []
282
+ const known = []
283
+ for (const dir of found) {
284
+ const key = normKey(dir)
285
+ if (reg.projects[key]) {
286
+ known.push(dir)
287
+ continue
288
+ }
289
+ added.push(dir)
290
+ if (!dryRun) await registerProject(dir)
291
+ }
292
+ if (asJson) {
293
+ console.log(JSON.stringify({ ok: true, root: resolve(root), found: found.length, added: added.map((p) => p.replace(/\\/g, '/')), known: known.map((p) => p.replace(/\\/g, '/')), dryRun: !!dryRun }, null, 2))
294
+ return
295
+ }
296
+ console.log(`${c.bold('ancleto: scan')} ${c.dim(resolve(root).replace(/\\/g, '/'))}`)
297
+ console.log(` ${c.dim('encontrados:')} ${found.length} proyectos con .ancletorc`)
298
+ for (const p of added) console.log(` ${dryRun ? c.yellow('·') : c.green('+')} ${p.replace(/\\/g, '/')}`)
299
+ for (const p of known) console.log(` ${c.dim('=')} ${c.dim(p.replace(/\\/g, '/'))}`)
300
+ if (dryRun) console.log(c.dim('\n --dry-run: no se registro nada'))
301
+ else console.log(c.dim(`\n registrados: ${added.length} nuevos`))
302
+ }
303
+
304
+ async function projectsPrune({ asJson, dryRun }) {
305
+ const reg = await readProjectsRegistry()
306
+ const removed = []
307
+ for (const [key, entry] of Object.entries(reg.projects)) {
308
+ const st = await projectStatus(entry)
309
+ if (!st.exists) {
310
+ removed.push({ key, path: entry.path, reason: st.reason || 'ruta inexistente' })
311
+ if (!dryRun) delete reg.projects[key]
312
+ }
313
+ }
314
+ if (!dryRun && removed.length) await writeProjectsRegistry(reg)
315
+ if (asJson) {
316
+ console.log(JSON.stringify({ ok: true, pruned: removed.length, removed, dryRun: !!dryRun }, null, 2))
317
+ return
318
+ }
319
+ if (removed.length === 0) {
320
+ console.log(c.dim('ancleto: no hay entradas muertas en el registro'))
321
+ return
322
+ }
323
+ console.log(`${c.bold('ancleto: prune')}${dryRun ? ` ${c.dim('(dry-run)')}` : ''}`)
324
+ for (const r of removed) console.log(` ${dryRun ? c.yellow('·') : c.red('−')} ${r.path} ${c.dim(`(${r.reason})`)}`)
325
+ console.log(dryRun ? c.dim(`\n --dry-run: se quitarian ${removed.length}`) : c.dim(`\n removidos: ${removed.length}`))
326
+ }
327
+
328
+ async function projectsInfo(target, { asJson, currentVersion }) {
329
+ const abs = resolve(target || process.cwd())
330
+ const rc = await readAncletorc(abs)
331
+ if (!rc) {
332
+ console.error(`ancleto: "${abs}" no parece un proyecto ancleto (sin .ancletorc)`)
333
+ process.exit(1)
334
+ }
335
+ const st = await projectStatus({ path: abs, ...rc })
336
+ const hasMemory = await exists(join(abs, '.ancleto', 'memory.db'))
337
+ const hasSeedIndex = await exists(join(abs, rc.discovery?.outputDir || 'docs/technical-discovery', 'index.md'))
338
+ const hasWorkingContext = await exists(join(abs, '.ancleto', 'working-context.md'))
339
+ const activeChangesDir = join(abs, CHANGES_ROOT, 'changes')
340
+ let activeChanges = 0
341
+ if (await exists(activeChangesDir)) {
342
+ const entries = await readdir(activeChangesDir, { withFileTypes: true })
343
+ activeChanges = entries.filter((e) => e.isDirectory() && e.name !== 'archive').length
344
+ }
345
+ const info = {
346
+ path: abs.replace(/\\/g, '/'),
347
+ version: st.version,
348
+ outdated: st.version && currentVersion ? st.version !== currentVersion : false,
349
+ agent: st.agent,
350
+ tier: st.tier,
351
+ scoped: st.scoped,
352
+ azureEnabled: !!rc.azure?.enabled,
353
+ memoryDb: hasMemory,
354
+ technicalSeed: hasSeedIndex,
355
+ workingContext: hasWorkingContext,
356
+ activeChanges
357
+ }
358
+ if (asJson) {
359
+ console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, ...info }, null, 2))
360
+ return
361
+ }
362
+ console.log(`${c.bold((abs.split(/[\\/]/).filter(Boolean).pop() || abs))} ${c.dim(info.path)}`)
363
+ console.log('')
364
+ console.log(` versión ${info.outdated ? c.red(`${info.version} → ${currentVersion} disponible`) : c.green(info.version || '—')}`)
365
+ console.log(` instalación ${info.scoped ? c.green('por proyecto (scoped)') : c.yellow('agentes globales')}`)
366
+ console.log(` tier / agente ${info.tier || c.dim('—')} / ${info.agent || '—'}`)
367
+ console.log(` Azure DevOps ${info.azureEnabled ? c.green('habilitado') : c.dim('deshabilitado')}`)
368
+ console.log(` memoria ${info.memoryDb ? c.green('sí (.ancleto/memory.db)') : c.dim('no')}`)
369
+ console.log(` seed técnico ${info.technicalSeed ? c.green('generado') : c.dim('sin generar')}`)
370
+ console.log(` changes ${info.activeChanges > 0 ? c.cyan(`${info.activeChanges} activo(s)`) : c.dim('ninguno')}`)
371
+ if (info.outdated) console.log(c.dim(`\n actualizá con: cd "${info.path}" && ancleto update`))
372
+ }
373
+
374
+ async function projectsCommand(args) {
375
+ const sub = args.find((a) => !a.startsWith('-')) || 'list'
376
+ const flags = args.filter((a) => a.startsWith('-'))
377
+ const asJson = flags.includes('--json')
378
+ const dryRun = flags.includes('--dry-run')
379
+ const currentVersion = await packageVersion()
380
+ switch (sub) {
381
+ case 'list': {
382
+ await projectsList(await readProjectsRegistry(), { asJson, currentVersion })
383
+ break
384
+ }
385
+ case 'scan': {
386
+ const idx = args.indexOf('scan')
387
+ const root = args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null
388
+ await projectsScan(root, { asJson, dryRun })
389
+ break
390
+ }
391
+ case 'prune': {
392
+ await projectsPrune({ asJson, dryRun })
393
+ break
394
+ }
395
+ case 'info': {
396
+ const idx = args.indexOf('info')
397
+ const target = args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null
398
+ await projectsInfo(target, { asJson, currentVersion })
399
+ break
400
+ }
401
+ default:
402
+ console.error(`ancleto: subcomando desconocido: projects ${sub}`)
403
+ console.error('ancleto: uso: ancleto projects [list|scan <raiz>|prune|info <ruta>] [--json] [--dry-run]')
404
+ process.exit(1)
405
+ }
406
+ }
407
+
408
+
104
409
  function binName(base) {
105
410
  return process.platform === 'win32' ? `${base}.exe` : base
106
411
  }
@@ -413,22 +718,28 @@ async function copyTemplates(projectDir) {
413
718
 
414
719
  async function install(args) {
415
720
  const pi = args.indexOf('--project')
416
- const project = pi >= 0 ? args[pi + 1] : null
721
+ const explicitProject = pi >= 0 ? args[pi + 1] : null
417
722
  const withMcp = !args.includes('--no-mcp')
418
723
  let mcpMap = withMcp ? buildDefaultMcp({ withEngram: args.includes('--with-engram') }) : {}
419
724
 
420
725
  const agentFlag = scanAgentFlag(args)
421
726
  let tier = scanTierFlag(args)
422
727
 
423
- if (project) {
424
- const dir = resolve(project)
425
- if (!(await exists(dir))) {
426
- console.error(`ancleto: el directorio no existe: ${dir}`)
728
+ let projectDir
729
+ if (explicitProject) {
730
+ projectDir = resolve(explicitProject)
731
+ if (!(await exists(projectDir))) {
732
+ console.error(`ancleto: el directorio no existe: ${projectDir}`)
427
733
  process.exit(1)
428
734
  }
735
+ } else if (!args.includes('--global') && (await exists(join(process.cwd(), '.ancletorc')))) {
736
+ // Estar parado en un proyecto ancleto implica operar sobre ese proyecto.
737
+ // `--global` fuerza el alcance global cuando el repo tambien tiene .ancletorc.
738
+ projectDir = process.cwd()
739
+ } else {
740
+ projectDir = null
429
741
  }
430
742
 
431
- const projectDir = project ? resolve(project) : null
432
743
  const target = projectDir ? join(projectDir, '.opencode') : globalConfigDir()
433
744
  const existingRc = projectDir ? await readAncletorc(projectDir) : null
434
745
  const storedTier = (await exists(tierStatePath(target)))
@@ -494,6 +805,8 @@ async function install(args) {
494
805
  await applyTier(join(target, 'agents'), tier, models)
495
806
  await writeFile(tierStatePath(target), tier + '\n')
496
807
 
808
+ if (projectDir) await registerProject(projectDir, { agent, tier })
809
+
497
810
  let azureMcp = false
498
811
  if (projectDir && withMcp) {
499
812
  const rc = await readAncletorc(projectDir)
@@ -591,6 +904,7 @@ async function initProject(args) {
591
904
  await copyTemplates(projectDir)
592
905
  await scaffoldAspec(projectDir)
593
906
  await refreshWorkingContext(projectDir)
907
+ await registerProject(projectDir, { agent, tier: tier || null })
594
908
  if (azure.enabled) console.log(AZURE_MCP_NOTICE)
595
909
  console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''}`)
596
910
  }
@@ -1363,6 +1677,17 @@ switch (cmd) {
1363
1677
  case 'stats':
1364
1678
  await statsCommand(rest)
1365
1679
  break
1680
+ case 'projects':
1681
+ await projectsCommand(rest)
1682
+ break
1683
+ case 'list':
1684
+ // Alias conveniente: `ancleto list --projects`
1685
+ if (rest.includes('--projects')) await projectsCommand(rest.filter((a) => a !== '--projects'))
1686
+ else {
1687
+ console.error('ancleto: uso: ancleto list --projects [--json] (o ancleto projects)')
1688
+ process.exit(1)
1689
+ }
1690
+ break
1366
1691
  case 'doctor':
1367
1692
  await doctorCommand()
1368
1693
  break