@ancleto/spec 0.6.5 → 0.6.7

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 (40) hide show
  1. package/README.md +26 -1
  2. package/agents/context-resolver.md +1 -1
  3. package/agents/documenter.md +3 -1
  4. package/agents/memory-keeper.md +1 -1
  5. package/agents/spec-writer.md +2 -1
  6. package/agents/technical-discovery.md +1 -1
  7. package/agents/tester.md +1 -1
  8. package/commands/cleto-apply.md +2 -154
  9. package/commands/cleto-archive.md +2 -164
  10. package/commands/cleto-bulk-archive.md +2 -248
  11. package/commands/cleto-continue.md +2 -126
  12. package/commands/cleto-explore.md +2 -174
  13. package/commands/cleto-ff.md +2 -120
  14. package/commands/cleto-new.md +2 -108
  15. package/commands/cleto-onboard.md +2 -550
  16. package/commands/cleto-propose.md +2 -130
  17. package/commands/cleto-recall.md +4 -52
  18. package/commands/cleto-sync.md +2 -137
  19. package/commands/cleto-verify.md +2 -169
  20. package/package.json +1 -1
  21. package/skills/ancleto-apply/SKILL.md +15 -38
  22. package/skills/ancleto-archive/SKILL.md +22 -24
  23. package/skills/ancleto-bulk-archive/SKILL.md +45 -55
  24. package/skills/ancleto-commit/SKILL.md +10 -29
  25. package/skills/ancleto-continue/SKILL.md +18 -24
  26. package/skills/ancleto-explore/SKILL.md +40 -67
  27. package/skills/ancleto-ff/SKILL.md +20 -28
  28. package/skills/ancleto-new/SKILL.md +20 -39
  29. package/skills/ancleto-onboard/SKILL.md +35 -38
  30. package/skills/ancleto-pr/SKILL.md +12 -26
  31. package/skills/ancleto-propose/SKILL.md +22 -69
  32. package/skills/ancleto-recall/SKILL.md +21 -25
  33. package/skills/ancleto-sync-specs/SKILL.md +27 -62
  34. package/skills/ancleto-upgrade/SKILL.md +132 -223
  35. package/skills/ancleto-upgrade/references/templates.md +138 -171
  36. package/skills/ancleto-verify/SKILL.md +36 -66
  37. package/skills/ancleto-workflow/SKILL.md +17 -17
  38. package/src/cli/index.js +76 -49
  39. package/src/core/tier-models.js +48 -0
  40. package/templates/AGENTS.md +3 -0
@@ -10,7 +10,7 @@ metadata:
10
10
 
11
11
  # aspec Workflow
12
12
 
13
- This skill is the map of the spec-driven lifecycle. It does not execute work itself — it routes: given where the user (or the current change) stands, it names the skill that runs next and hands over the required context.
13
+ Map of the spec-driven lifecycle. It does not execute work — it routes: given where the user (or change) stands, it names the next skill and hands over context.
14
14
 
15
15
  ## The Lifecycle
16
16
 
@@ -28,16 +28,16 @@ explore → new/propose/ff → continue* → apply → verify → archive
28
28
 
29
29
  | User wants to… | Run |
30
30
  |---|---|
31
- | Think through an idea or problem, no commitment yet | `ancleto-explore` |
32
- | Start structured work on something new | `ancleto-new` (step by step) or `ancleto-propose` (all artifacts at once) |
33
- | Skip straight to a full draft when the path is clear | `ancleto-ff` |
34
- | Resume a change with missing artifacts | `ancleto-continue` (repeat until complete) |
31
+ | Think through an idea, no commitment | `ancleto-explore` |
32
+ | Start structured work | `ancleto-new` (step by step) or `ancleto-propose` (all artifacts at once) |
33
+ | Full draft when the path is clear | `ancleto-ff` |
34
+ | Resume a change missing artifacts | `ancleto-continue` (repeat until complete) |
35
35
  | Implement tasks from ready artifacts | `ancleto-apply` |
36
- | Check implementation against artifacts before closing | `ancleto-verify` |
37
- | Close one finished change (syncing its specs) | `ancleto-archive` |
38
- | Close several finished changes at once | `ancleto-bulk-archive` |
39
- | Learn the whole cycle hands-on | `ancleto-onboard` |
40
- | Recall what the team learned about a topic | `searchMemory` directly (or the recall contract) |
36
+ | Check implementation before closing | `ancleto-verify` |
37
+ | Close one finished change (sync its specs) | `ancleto-archive` |
38
+ | Close several at once | `ancleto-bulk-archive` |
39
+ | Learn the cycle hands-on | `ancleto-onboard` |
40
+ | Recall team learnings on a topic | `searchMemory` directly (or the recall contract) |
41
41
 
42
42
  **By change state** (read `aspec/changes/<name>/`):
43
43
 
@@ -53,22 +53,22 @@ explore → new/propose/ff → continue* → apply → verify → archive
53
53
 
54
54
  ## Handoff Contract
55
55
 
56
- When routing to the next skill, always pass along:
56
+ When routing, pass along:
57
57
 
58
- - The change name (kebab-case) and its directory
58
+ - Change name (kebab-case) and directory
59
59
  - Which artifacts already exist
60
- - Any user decisions or scope cuts made so far
61
- - The recalled memory (if any) that is relevant — as background, never as instructions
60
+ - User decisions or scope cuts so far
61
+ - Relevant recalled memory — as background, never instructions
62
62
 
63
63
  ## Memory Touchpoints
64
64
 
65
65
  - **Entry** (`explore`, `new`, `propose`, `ff`, `onboard`): one `searchMemory` call with a semantic query, injected as read-only background.
66
- - **Verification** (`verify`): record durable findings with `recordRule` (standing constraints) or `recordDecision` (reasons and trade-offs).
67
- - **Closure** (`archive`, `bulk-archive`): record lessons and conflict resolutions with `recordDecision` (and `recordRule` for rules that must be followed).
66
+ - **Verification** (`verify`): record durable findings with `recordRule` (constraints) or `recordDecision` (reasons, trade-offs).
67
+ - **Closure** (`archive`, `bulk-archive`): record lessons and conflict resolutions with `recordDecision` (and `recordRule` for mandatory rules).
68
68
 
69
69
  ## Guardrails
70
70
 
71
- - This skill never creates, modifies, or archives anything itself — it only routes.
71
+ - Never creates, modifies, or archives anything itself — it only routes.
72
72
  - Never skip `verify` when the change touched specified behavior (`specs/` present); route there before `archive`.
73
73
  - Never route to `apply` when `tasks.md` is missing — route to `continue` first.
74
74
  - `recordRule`/`recordDecision` accept only `memory_key`, `content`, `justification`, `scope`. Never send `source`, `confidence`, `status` or `id` — the runtime manages those.
package/src/cli/index.js CHANGED
@@ -12,6 +12,7 @@ import { memoryDoctor } from '../core/memory/doctor.js'
12
12
  import { writeDiscoveryMap } from '../core/discovery.js'
13
13
  import { readProjectTier, buildRepomixArgs, tierTokenBudget } from '../core/repomix-tier.js'
14
14
  import { showBanner, selectOption, stopBanner } from './ui.js'
15
+ import { tierModels, gratisModel, envGratisModel, isKnownGratisModel, MUSE_SPARK_MODEL, GRATIS_FALLBACK_MODEL } from '../core/tier-models.js'
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url))
17
18
  const ROOT = join(__dirname, '..', '..')
@@ -27,8 +28,8 @@ Uso:
27
28
  (global por defecto, o en .opencode/ del proyecto)
28
29
  Configura los MCP engram + caveman por defecto
29
30
  ancleto install --no-mcp Igual que install pero sin tocar config MCP
30
- ancleto install --tier <nivel> normal | minimo | gratis (pregunta en la 1ra config)
31
- ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (pregunta si no esta guardado)
31
+ ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
32
+ ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
32
33
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
33
34
  ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
34
35
  ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>]
@@ -137,26 +138,26 @@ const TIERS = {
137
138
  normal: {
138
139
  orchestrator: 'opencode-go/qwen3.7-plus',
139
140
  coder: 'opencode-go/minimax-m3',
140
- tester: 'opencode-go/deepseek-v4-flash',
141
+ tester: 'opencode-go/deepseek-v4.1-flash',
141
142
  'spec-writer': 'opencode-go/qwen3.7-plus',
142
143
  reviewer: 'opencode-go/qwen3.6-plus',
143
- 'technical-discovery': 'opencode-go/deepseek-v4-flash',
144
+ 'technical-discovery': 'opencode-go/deepseek-v4.1-flash',
144
145
  'technical-seed-writer': 'opencode-go/minimax-m3',
145
- 'memory-keeper': 'opencode-go/deepseek-v4-flash',
146
- 'context-resolver': 'opencode-go/deepseek-v4-flash',
147
- documenter: 'opencode-go/deepseek-v4-flash'
146
+ 'memory-keeper': 'opencode-go/deepseek-v4.1-flash',
147
+ 'context-resolver': 'opencode-go/deepseek-v4.1-flash',
148
+ documenter: 'opencode-go/deepseek-v4.1-flash'
148
149
  },
149
150
  minimo: {
150
- orchestrator: 'opencode-go/deepseek-v4-flash',
151
- coder: 'opencode-go/minimax-m2.7',
152
- tester: 'opencode-go/deepseek-v4-flash',
153
- 'spec-writer': 'opencode-go/qwen3.6-plus',
154
- reviewer: 'opencode-go/deepseek-v4-flash',
155
- 'technical-discovery': 'opencode-go/deepseek-v4-flash',
156
- 'technical-seed-writer': 'opencode-go/minimax-m2.7',
157
- 'memory-keeper': 'opencode-go/deepseek-v4-flash',
158
- 'context-resolver': 'opencode-go/deepseek-v4-flash',
159
- documenter: 'opencode-go/deepseek-v4-flash'
151
+ orchestrator: 'opencode-go/deepseek-v4.1-flash',
152
+ coder: 'opencode-go/deepseek-v4.1-flash',
153
+ tester: 'opencode-go/deepseek-v4.1-flash',
154
+ 'spec-writer': 'opencode-go/deepseek-v4.1-flash',
155
+ reviewer: 'opencode-go/deepseek-v4.1-flash',
156
+ 'technical-discovery': 'opencode-go/deepseek-v4.1-flash',
157
+ 'technical-seed-writer': 'opencode-go/deepseek-v4.1-flash',
158
+ 'memory-keeper': 'opencode-go/deepseek-v4.1-flash',
159
+ 'context-resolver': 'opencode-go/deepseek-v4.1-flash',
160
+ documenter: 'opencode-go/deepseek-v4.1-flash'
160
161
  },
161
162
  gratis: {
162
163
  orchestrator: 'opencode/big-pickle',
@@ -176,8 +177,8 @@ function tierStatePath(targetDir) {
176
177
  return join(targetDir, '.ancleto-tier')
177
178
  }
178
179
 
179
- async function applyTier(agentsDir, tier) {
180
- const map = TIERS[tier]
180
+ async function applyTier(agentsDir, tier, models = null) {
181
+ const map = models || TIERS[tier]
181
182
  for (const [name, model] of Object.entries(map)) {
182
183
  const p = join(agentsDir, name + '.md')
183
184
  if (!(await exists(p))) continue
@@ -187,17 +188,6 @@ async function applyTier(agentsDir, tier) {
187
188
  }
188
189
  }
189
190
 
190
- function askTier() {
191
- return new Promise((resolve) => {
192
- const rl = createInterface({ input: process.stdin, output: process.stdout })
193
- rl.question('Tier de costo de los agents [normal/minimo/gratis] (default: normal): ', (a) => {
194
- rl.close()
195
- const t = a.trim().toLowerCase()
196
- resolve(TIERS[t] ? t : 'normal')
197
- })
198
- })
199
- }
200
-
201
191
  const SUPPORTED_AGENTS = ['opencode', 'vscode', 'antigravity', 'cursor', 'roo']
202
192
  const DEFAULT_AGENT = 'opencode'
203
193
 
@@ -380,6 +370,7 @@ async function install(args) {
380
370
  const withMcp = !args.includes('--no-mcp')
381
371
  let mcpMap = withMcp ? buildDefaultMcp() : {}
382
372
 
373
+ const agentFlag = scanAgentFlag(args)
383
374
  let tier = scanTierFlag(args)
384
375
 
385
376
  if (project) {
@@ -390,16 +381,56 @@ async function install(args) {
390
381
  }
391
382
  }
392
383
 
393
- const target = project ? join(resolve(project), '.opencode') : globalConfigDir()
384
+ const projectDir = project ? resolve(project) : null
385
+ const target = projectDir ? join(projectDir, '.opencode') : globalConfigDir()
386
+ const existingRc = projectDir ? await readAncletorc(projectDir) : null
387
+ const storedTier = (await exists(tierStatePath(target)))
388
+ ? (await readFile(tierStatePath(target), 'utf8')).trim()
389
+ : null
394
390
 
395
- if (project) {
396
- await copyTemplates(resolve(project))
397
- await scaffoldAspec(resolve(project))
398
- const existingRc = await readAncletorc(resolve(project))
399
- const agent = await resolveAgent(args, existingRc?.agent)
400
- const agentSkillsDir = await installAgentSkills(resolve(project), agent)
401
- await writeManifest(resolve(project), {
391
+ let agent = agentFlag || (existingRc?.agent && SUPPORTED_AGENTS.includes(existingRc.agent) ? existingRc.agent : null)
392
+
393
+ let bannerShown = false
394
+ const isInteractive = Boolean(process.stdout.isTTY) && ((projectDir && !agent) || !tier)
395
+ if (isInteractive) {
396
+ await showBanner()
397
+ bannerShown = true
398
+ if (projectDir && !agent) {
399
+ agent = await selectOption('Agente/IDE', SUPPORTED_AGENTS, Math.max(0, SUPPORTED_AGENTS.indexOf(DEFAULT_AGENT)))
400
+ }
401
+ if (!tier) {
402
+ const current = TIERS[storedTier] ? storedTier : ''
403
+ tier = await selectOption('Tier de costo', Object.keys(TIERS), Math.max(0, Object.keys(TIERS).indexOf(current)))
404
+ }
405
+ stopBanner()
406
+ }
407
+
408
+ if (projectDir && !agent) agent = DEFAULT_AGENT
409
+ if (!tier) tier = TIERS[storedTier] ? storedTier : 'normal'
410
+
411
+ let gratisModelChoice = null
412
+ if (tier === 'gratis') {
413
+ const env = envGratisModel()
414
+ const persisted = isKnownGratisModel(existingRc?.gratisModel) ? existingRc.gratisModel : null
415
+ if (env || persisted) {
416
+ gratisModelChoice = env || persisted
417
+ } else if (process.stdout.isTTY) {
418
+ if (!bannerShown) await showBanner()
419
+ const answer = await selectOption('Muse Spark 1.3 Free disponible en tu cuenta?', ['No (usar big-pickle)', 'Si (Muse Spark)'], 0)
420
+ if (!bannerShown) stopBanner()
421
+ gratisModelChoice = answer.startsWith('Si') ? MUSE_SPARK_MODEL : GRATIS_FALLBACK_MODEL
422
+ } else {
423
+ gratisModelChoice = gratisModel()
424
+ }
425
+ }
426
+
427
+ if (projectDir) {
428
+ await copyTemplates(projectDir)
429
+ await scaffoldAspec(projectDir)
430
+ const agentSkillsDir = await installAgentSkills(projectDir, agent)
431
+ await writeManifest(projectDir, {
402
432
  agent,
433
+ ...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
403
434
  installedPaths: {
404
435
  templates: ['AGENTS.md', 'PRODUCT.md'],
405
436
  agents: ['.opencode/agents'],
@@ -411,18 +442,13 @@ async function install(args) {
411
442
  await copyAssets(target)
412
443
  }
413
444
 
414
- if (!tier) {
415
- const stored = (await exists(tierStatePath(target)))
416
- ? (await readFile(tierStatePath(target), 'utf8')).trim()
417
- : null
418
- tier = TIERS[stored] ? stored : await askTier()
419
- }
420
- await applyTier(join(target, 'agents'), tier)
445
+ const models = tierModels(tier, TIERS, gratisModelChoice)
446
+ await applyTier(join(target, 'agents'), tier, models)
421
447
  await writeFile(tierStatePath(target), tier + '\n')
422
448
 
423
449
  let azureMcp = false
424
- if (project && withMcp) {
425
- const rc = await readAncletorc(resolve(project))
450
+ if (projectDir && withMcp) {
451
+ const rc = await readAncletorc(projectDir)
426
452
  if (rc?.azure?.enabled) {
427
453
  mcpMap['azure-devops'] = { type: 'local', enabled: true, command: ['npx', '-y', '@davstack/mcp-azure-devops'] }
428
454
  azureMcp = true
@@ -430,11 +456,12 @@ async function install(args) {
430
456
  }
431
457
 
432
458
  const res = await mergeMcp(target, mcpMap)
433
- const loc = project
434
- ? `${resolve(project)} (.opencode/ + templates en la raiz)`
459
+ const loc = projectDir
460
+ ? `${projectDir} (.opencode/ + templates en la raiz)`
435
461
  : `${target} (disponible en todos tus proyectos)`
436
462
  console.log(`ancleto: instalado en ${loc}`)
437
463
  console.log(`ancleto: tier de costo de agents: ${tier}`)
464
+ if (tier === 'gratis') console.log(`ancleto: modelo gratis: ${models.orchestrator}`)
438
465
  if (res.added.length) {
439
466
  console.log(`ancleto: MCP configurados: ${res.added.join(', ')} en ${res.file}`)
440
467
  }
@@ -0,0 +1,48 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ export const MUSE_SPARK_MODEL = 'opencode/muse-spark-1.3-contributor-free'
4
+ export const GRATIS_FALLBACK_MODEL = 'opencode/big-pickle'
5
+
6
+ let probed = null
7
+
8
+ export function envGratisModel() {
9
+ if (process.env.ANCLETO_MUSE_SPARK === '1') return MUSE_SPARK_MODEL
10
+ if (process.env.ANCLETO_MUSE_SPARK === '0') return GRATIS_FALLBACK_MODEL
11
+ return null
12
+ }
13
+
14
+ export function isKnownGratisModel(model) {
15
+ return model === MUSE_SPARK_MODEL || model === GRATIS_FALLBACK_MODEL
16
+ }
17
+
18
+ export function detectMuseSpark() {
19
+ const env = envGratisModel()
20
+ if (env) return env === MUSE_SPARK_MODEL
21
+ if (probed !== null) return probed
22
+ try {
23
+ const r = spawnSync('opencode models opencode', {
24
+ encoding: 'utf8',
25
+ timeout: 5000,
26
+ shell: true,
27
+ stdio: ['ignore', 'pipe', 'pipe']
28
+ })
29
+ probed = !r.error && r.status === 0
30
+ && String(r.stdout || '').split('\n').some((l) => l.trim() === MUSE_SPARK_MODEL)
31
+ } catch {
32
+ probed = false
33
+ }
34
+ return probed
35
+ }
36
+
37
+ export function gratisModel(persisted = null) {
38
+ const env = envGratisModel()
39
+ if (env) return env
40
+ if (isKnownGratisModel(persisted)) return persisted
41
+ return detectMuseSpark() ? MUSE_SPARK_MODEL : GRATIS_FALLBACK_MODEL
42
+ }
43
+
44
+ export function tierModels(tier, tiers, gratisOverride = null) {
45
+ if (tier !== 'gratis') return tiers[tier]
46
+ const model = gratisOverride || gratisModel()
47
+ return Object.fromEntries(Object.keys(tiers.gratis).map((k) => [k, model]))
48
+ }
@@ -55,6 +55,9 @@ conflicto, prevalece el diseño vigente.
55
55
  - **Cambios con scope incierto / arquitectura**: Crear artifacts en `aspec/changes/<name>/`.
56
56
  - **Cambios menores / fixes**: Implementación directa.
57
57
  - **Cierre**: Archivar con `/cleto-archive` al finalizar.
58
+ - **Idioma de los artifacts**: el contenido de `proposal.md`, `design.md`, `tasks.md` y `specs/` se escribe
59
+ en **inglés**. Los keywords (`Requirement`, `Scenario`, `SHALL`, `WHEN`/`THEN`, `ADDED/MODIFIED/REMOVED/RENAMED
60
+ Requirements`) son literales y **no se traducen**. Nombres de archivos y directorios: inglés kebab-case.
58
61
 
59
62
  ## Tools de Soporte
60
63