@ancleto/spec 0.6.25 → 0.6.27

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
@@ -23,6 +23,7 @@
23
23
  - [Características principales](#características-principales)
24
24
  - [Requisitos](#requisitos)
25
25
  - [Instalación](#instalación)
26
+ - [Guía de .gitignore](#guía-de-gitignore)
26
27
  - [Configuración y tiers de costo](#configuración-y-tiers-de-costo)
27
28
  - [Uso rápido](#uso-rápido)
28
29
  - [Referencia de comandos CLI](#referencia-de-comandos-cli)
@@ -121,6 +122,20 @@ Cómo bajarlo:
121
122
 
122
123
  ---
123
124
 
125
+ ## Guía de .gitignore
126
+
127
+ El framework **no gestiona tu `.gitignore`**: es una decisión del proyecto. Como referencia, esta es la combinación que funciona bien con Ancleto:
128
+
129
+ | Ruta | Recomendación | Por qué |
130
+ |---|---|---|
131
+ | `aspec/` | **Versionar** | Es la fuente-de-verdad: specs y el historial de changes archivados. Si se ignora, el equipo pierde el por qué de las decisiones y los agentes no tienen contexto estable. |
132
+ | `.ancleto/` | **Ignorar** | Base SQLite local (memoria del repo y working-context). Es de tu máquina; cada integrante la regenera. |
133
+ | `.ancletorc` | **Decisión del equipo** | Versionarlo alinea la configuración del proyecto (agente, tier, discovery). Ignoralo si cada integrante lo personaliza. |
134
+
135
+ > Error común: gitignorear `aspec/` junto con `.ancleto/`. La memoria local es descartable; la fuente-de-verdad, no.
136
+
137
+ ---
138
+
124
139
  ## Configuración y tiers de costo
125
140
 
126
141
  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:
@@ -186,6 +201,9 @@ ancleto upgrade # Re-aplica templates y skills respetando tus personal
186
201
 
187
202
  ancleto check # Verifica la integridad de la instalación (faltantes / huérfanos)
188
203
 
204
+ ancleto specs check [--change <nombre>] [--json]
205
+ # Valida keywords canónicos en aspec/specs y en los deltas de un change
206
+
189
207
  ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
190
208
 
191
209
  ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
@@ -105,12 +105,13 @@ Do not treat archive as a blind file move.
105
105
  - `tasks.md`
106
106
  - `specs/` when the change includes delta specs
107
107
  3. Determine whether the change includes spec deltas that must be reflected in `aspec/specs/`
108
- 4. If needed, update the relevant source-of-truth specs so they reflect the completed change
109
- 5. If the change is not ready for archive, stop and report the inconsistency clearly
110
- 6. If the change is ready, archive it to `aspec/changes/archive/{YYYY-MM-DD}-{change-name}/`
111
- 7. Remove the original active change directory from `aspec/changes/{change-name}/` after a successful archive
112
- 8. If any files or folders remain in the original active change directory, report them as cleanup issues instead of leaving silent residue
113
- 9. Return a short structured summary of the finalization and archive result
108
+ 4. When deltas exist, run `ancleto specs check --change {change-name} --json` before merging. If a delta spec or its target source-of-truth spec is non-canonical (translated keywords), stop and report it instead of merging into it
109
+ 5. If needed, update the relevant source-of-truth specs so they reflect the completed change
110
+ 6. If the change is not ready for archive, stop and report the inconsistency clearly
111
+ 7. If the change is ready, archive it to `aspec/changes/archive/{YYYY-MM-DD}-{change-name}/`
112
+ 8. Remove the original active change directory from `aspec/changes/{change-name}/` after a successful archive
113
+ 9. If any files or folders remain in the original active change directory, report them as cleanup issues instead of leaving silent residue
114
+ 10. Return a short structured summary of the finalization and archive result
114
115
 
115
116
  ## Source-of-Truth Rules
116
117
 
@@ -259,7 +259,7 @@ If the runtime is read-only or plan-only:
259
259
  5. **Testing**: Delegate to **`@tester`** to create or update unit tests and perform final validation against the approved scope, including a non-writing format check and one lint pass after all edits. Include the complete Resolved Context Envelope, the coder's task-owned files, and its reported risks. `@tester` returns the final union of task-owned files and the Validation Ledger. A format failure in a task-owned file is failed verification and must not be fixed silently by `@tester`.
260
260
  6. **Review**: Delegate to **`@reviewer`** when an independent correctness or scope review is appropriate. Include the complete Resolved Context Envelope, the tester's final task-owned file union, and the full Validation Ledger.
261
261
  7. **🛑 FINAL ARCHIVE CHECKPOINT**: If implementation, validation, and any required review are complete with no blocking issues, stop and explicitly ask the user whether to keep iterating on the same change or finalize and archive it
262
- 8. **Finalization / Archive**: Before archiving, verify the delta specs reflect the final implementation — if iteration changed behavior or scope, re-delegate to **`@spec-writer`** to update the delta specs first, so the merge to source-of-truth is accurate. Then delegate to **`@documenter`** only after explicit user approval to archive
262
+ 8. **Finalization / Archive**: Before archiving, verify the delta specs reflect the final implementation — if iteration changed behavior or scope, re-delegate to **`@spec-writer`** to update the delta specs first, so the merge to source-of-truth is accurate. Check the seed freshness before closing: delegate the read-only state check to **`@technical-discovery`** and, when the state is `STALE`, warn and offer regeneration via **`@technical-seed-writer`** with explicit user approval — never automatic; if the user declines, continue the archive normally. This closure check protects future sessions from a silently aging seed. Then delegate to **`@documenter`** only after explicit user approval to archive
263
263
  9. **Memory Record**: After a successful archive, delegate to **`@memory-keeper`** in Automatic Record mode when a concrete, plausible candidate exists, with the factual completed-work summary and validation evidence supporting it. If no candidate exists, delegate Optional Draft with the factual completed-work summary, `no-automatic-candidate` classification, and the reason no candidate was identified. When Automatic Record returns `no-entry-warranted`, use its draft or delegate Optional Draft with that classification and reason if it could not provide one. Before asking, show the final work summary, classification, reason, and exact draft, then stop for explicit user approval. If no safe draft is available, report that outcome and finish without asking or storing. On approval, delegate User-Approved Record with the unchanged draft, the factual summary and validation evidence used to compose it, archived change name, and card context. On rejection, do not store.
264
264
  10. **Report**: Return final status or blocking findings to the user after automatic storage, an explicit decline, or User-Approved Record completes.
265
265
 
@@ -44,8 +44,9 @@ using the `ancleto-workflow` skill and the conventions of:
44
44
  - `design.md`
45
45
  - `tasks.md`
46
46
  - `specs/{capability}/spec.md` as needed
47
- 6. When the delegation says the context was produced without a technical seed, declare that in the generated artifacts.
48
- 7. Return a brief summary with generated paths, scope, and open questions or risks
47
+ 6. Before returning, verify every generated spec uses the canonical keywords (`### Requirement:`, `#### Scenario:`, `**WHEN**`/`**THEN**`, `SHALL`); fix any translated heading or keyword in place — never leave a spec with translated keywords.
48
+ 7. When the delegation says the context was produced without a technical seed, declare that in the generated artifacts.
49
+ 8. Return a brief summary with generated paths, scope, and open questions or risks
49
50
 
50
51
  ## Change Naming
51
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.25",
3
+ "version": "0.6.27",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,6 +31,8 @@ Read `aspec/changes/<name>/tasks.md` (when present); count `- [ ]` (incomplete)
31
31
 
32
32
  Check for delta specs at `aspec/changes/<name>/specs/`; if none, proceed without a sync prompt.
33
33
 
34
+ **Canonical keywords first:** when delta specs exist and the CLI is available, run `ancleto specs check --change <name> --json`. If a delta spec or its target source-of-truth spec is non-canonical (translated keywords), warn that requirement blocks cannot be matched reliably and ask whether to normalize the affected spec first or archive without syncing that capability. Never merge into a non-canonical spec silently.
35
+
34
36
  **If delta specs exist:**
35
37
 
36
38
  - Diff each delta against its main spec at `aspec/specs/<capability>/spec.md`; show a combined summary of adds/modifications/removals/renames.
@@ -42,14 +44,24 @@ Check for delta specs at `aspec/changes/<name>/specs/`; if none, proceed without
42
44
  - `## RENAMED Requirements` → rename using the `FROM:`/`TO:` pair.
43
45
  - Proceed to archive regardless of the choice.
44
46
 
45
- ### 3. Perform the archive
47
+ ### 3. Check technical seed freshness
48
+
49
+ If the `ancleto` CLI is available in this project, run `ancleto discovery --check` (read-only) and read its JSON `state`.
50
+
51
+ - **`STALE`**: warn that the technical seed is out of date for the current code, and offer to regenerate it now (follow the `ancleto-technical-discovery` skill) or to continue archiving with the stale seed.
52
+ - **Any other state**: continue without comment.
53
+ - Never regenerate automatically — regenerate only with the user's explicit approval.
54
+ - If the user declines, continue the archive normally and record the stale seed in the summary.
55
+ - If the CLI is unavailable, skip this step.
56
+
57
+ ### 4. Perform the archive
46
58
 
47
59
  Create `aspec/changes/archive/` if absent. Target: `YYYY-MM-DD-<change-name>` (current date).
48
60
 
49
61
  - **Target exists**: fail, suggesting a rename or different date.
50
62
  - **Otherwise**: delete scaffold-only files (e.g., `aspec/changes/<name>/context.md` — never preserved), then move the change directory to `aspec/changes/archive/YYYY-MM-DD-<name>/`.
51
63
 
52
- ### 4. Capture lessons into persistent memory
64
+ ### 5. Capture lessons into persistent memory
53
65
 
54
66
  Record what this change taught:
55
67
 
@@ -64,7 +76,7 @@ Record what this change taught:
64
76
 
65
77
  Record only durable lessons — decisions whose reasons aren't visible in the result, constraints, verified workarounds. Never workflow meta. If nothing meets the bar, record nothing and say so.
66
78
 
67
- ### 5. Display summary
79
+ ### 6. Display summary
68
80
 
69
81
  ```
70
82
  ## Archive Complete
@@ -75,12 +87,15 @@ Record only durable lessons — decisions whose reasons aren't visible in the re
75
87
  **Memories recorded:** <list, or "None">
76
88
  ```
77
89
 
78
- If warnings applied (incomplete tasks, skipped sync), show an "Archive Complete (with warnings)" variant listing them.
90
+ When the seed was `STALE`, add a `**Seed:**` line: `Regenerated` or `Stale — regeneration declined`.
91
+
92
+ If warnings applied (incomplete tasks, skipped sync, stale seed), show an "Archive Complete (with warnings)" variant listing them.
79
93
 
80
94
  ## Guardrails
81
95
 
82
96
  - Always prompt for change selection if not provided.
83
97
  - Don't block archiving on warnings — inform and confirm.
98
+ - Never regenerate the technical seed automatically: offer it, and continue the archive if the user declines.
84
99
  - If the target archive directory exists, fail instead of overwriting.
85
100
  - Delete scaffold-only files (`context.md`) before moving; never archive them.
86
101
  - `recordRule`/`recordDecision` accept only `memory_key`, `content`, `justification`, `scope`. Never send `source`, `confidence`, `status` or `id` — the runtime manages those.
@@ -26,6 +26,12 @@ Read under `aspec/changes/<name>/`: `tasks.md` (required), plus `proposal.md`, `
26
26
 
27
27
  **Tasks:** parse checkboxes (`- [ ]` incomplete vs `- [x]` complete) and count. Each incomplete → CRITICAL: "Complete task: `<description>`" or "Mark as done if already implemented".
28
28
 
29
+ **Canonical spec keywords:** run `ancleto specs check --change <name> --json` when the CLI is available. For every reported file:
30
+ - Delta spec (`aspec/changes/<name>/specs/...`) → CRITICAL: "Non-canonical spec: `<file>` (missing: `<keywords>`)" + "Normalize `<file>` to the canonical English keywords before archiving".
31
+ - Source-of-truth spec (`aspec/specs/...`) → WARNING with the same recommendation.
32
+
33
+ A non-canonical spec cannot be matched for coverage or merged reliably, so report it before the checks below.
34
+
29
35
  **Spec coverage:** for each `### Requirement:` in `specs/`, search the codebase for related keywords. Unimplemented → CRITICAL: "Requirement not found: `<requirement name>`" + "Implement requirement: `<description>`".
30
36
 
31
37
  ### 3. Correctness
package/src/cli/index.js CHANGED
@@ -4,7 +4,7 @@ import { cp, mkdir, access, writeFile, readFile, readdir, rename } from 'node:fs
4
4
  import { spawnSync } from 'node:child_process'
5
5
  import { createHash } from 'node:crypto'
6
6
  import { createInterface } from 'node:readline'
7
- import { join, dirname, resolve, basename } from 'node:path'
7
+ import { join, dirname, resolve, basename, relative } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { homedir, tmpdir } from 'node:os'
10
10
  import { createMemoryEngine, createReadonlyMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
@@ -40,6 +40,9 @@ Uso:
40
40
  ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
41
41
  ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
42
42
  Empaca el repo con Repomix y guarda estado
43
+ ancleto specs check [--change <name>] [--json]
44
+ Valida keywords canonicos (### Requirement:, WHEN, THEN)
45
+ en aspec/specs y, con --change, en los deltas del change
43
46
  ancleto memory context [--scope X] [--out file]
44
47
  Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
45
48
  ancleto memory list [--type X] [--scope X] [--all] [--json]
@@ -980,6 +983,77 @@ async function checkTierOrphan(cwd) {
980
983
  return warnings
981
984
  }
982
985
 
986
+ // Un spec es canonico cuando usa los keywords literales en ingles. Detecta specs
987
+ // traducidos (p. ej. "#### RF1:" + "**Dado**/**Cuando**/**Entonces**"), que rompen
988
+ // la cobertura por keywords y el merge de deltas.
989
+ function specIssues(content) {
990
+ const missing = []
991
+ if (!/^###\s+Requirement:/m.test(content)) missing.push('### Requirement:')
992
+ if (/^####\s+Scenario:/m.test(content)) {
993
+ if (!/\bWHEN\b/.test(content)) missing.push('WHEN')
994
+ if (!/\bTHEN\b/.test(content)) missing.push('THEN')
995
+ }
996
+ const unexpected = []
997
+ const re = /^####\s+(.+)$/gm
998
+ let m
999
+ while ((m = re.exec(content)) !== null) {
1000
+ if (!/^Scenario\b/.test(m[1])) unexpected.push(`#### ${m[1]}`)
1001
+ }
1002
+ return { missing, unexpected }
1003
+ }
1004
+
1005
+ async function collectSpecFiles(dir) {
1006
+ const out = []
1007
+ if (!(await exists(dir))) return out
1008
+ for (const e of await readdir(dir, { withFileTypes: true })) {
1009
+ const p = join(dir, e.name)
1010
+ if (e.isDirectory()) out.push(...(await collectSpecFiles(p)))
1011
+ else if (e.name === 'spec.md') out.push(p)
1012
+ }
1013
+ return out
1014
+ }
1015
+
1016
+ async function specsCheckCommand(args) {
1017
+ const cwd = process.cwd()
1018
+ const asJson = args.includes('--json')
1019
+ const ci = args.indexOf('--change')
1020
+ const change = ci >= 0 ? args[ci + 1] : null
1021
+ const files = await collectSpecFiles(join(cwd, CHANGES_ROOT, 'specs'))
1022
+ if (change) {
1023
+ const changeDir = join(cwd, CHANGES_ROOT, 'changes', change)
1024
+ if (!(await exists(changeDir))) {
1025
+ console.error(`ancleto: no existe el change "${change}" en ${CHANGES_ROOT}/changes/`)
1026
+ process.exit(1)
1027
+ }
1028
+ files.push(...(await collectSpecFiles(join(changeDir, 'specs'))))
1029
+ }
1030
+ const findings = []
1031
+ for (const f of files) {
1032
+ const { missing, unexpected } = specIssues(await readFile(f, 'utf8'))
1033
+ if (missing.length || unexpected.length) {
1034
+ findings.push({ file: relative(cwd, f).replace(/\\/g, '/'), missing, unexpected })
1035
+ }
1036
+ }
1037
+ const ok = findings.length === 0
1038
+ if (asJson) {
1039
+ console.log(JSON.stringify({ ok, scanned: files.length, nonCanonical: findings }, null, 2))
1040
+ } else if (files.length === 0) {
1041
+ console.log('ancleto: no hay specs para revisar')
1042
+ } else {
1043
+ for (const f of files) {
1044
+ const rel = relative(cwd, f).replace(/\\/g, '/')
1045
+ const found = findings.find((x) => x.file === rel)
1046
+ if (found) {
1047
+ console.log(` ✖ ${rel} (revisar: ${[...found.missing, ...found.unexpected].join(', ')})`)
1048
+ } else {
1049
+ console.log(` ✔ ${rel}`)
1050
+ }
1051
+ }
1052
+ console.log(ok ? `ancleto: ${files.length} spec(s) canonicos` : `ancleto: ${findings.length} de ${files.length} spec(s) sin keywords canonicos`)
1053
+ }
1054
+ process.exit(ok ? 0 : 1)
1055
+ }
1056
+
983
1057
  async function doctorCommand() {
984
1058
  let fatal = false
985
1059
 
@@ -1045,6 +1119,9 @@ switch (cmd) {
1045
1119
  case 'check':
1046
1120
  await checkCommand()
1047
1121
  break
1122
+ case 'specs':
1123
+ await specsCheckCommand(rest)
1124
+ break
1048
1125
  case 'doctor':
1049
1126
  await doctorCommand()
1050
1127
  break