@ancleto/spec 0.6.33 → 0.6.35
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 +28 -2
- package/agents/orchestrator.md +4 -3
- package/agents/technical-discovery.md +4 -3
- package/agents/technical-seed-writer.md +4 -4
- package/package.json +1 -1
- package/skills/ancleto-archive/SKILL.md +3 -2
- package/skills/ancleto-technical-discovery/SKILL.md +2 -1
- package/skills/ancleto-technical-discovery/references/generation-pipeline.md +6 -1
- package/skills/ancleto-technical-discovery/references/validation-checklist.md +1 -0
- package/src/cli/index.js +178 -14
package/README.md
CHANGED
|
@@ -200,6 +200,32 @@ ancleto init --lang es # fija español para ese proyecto
|
|
|
200
200
|
|
|
201
201
|
Con `auto`, el orquestador detecta el idioma en tus primeros hasta 3 mensajes y lo persiste en `.ancletorc` → `language`. Podés volver a `auto` o fijar otro código cuando quieras (`auto | es | en | pt`). Lo que **nunca** se traduce: los keywords (`Requirement`, `Scenario`, `SHALL`, `WHEN`/`THEN`/`AND`) y los nombres de archivos y directorios.
|
|
202
202
|
|
|
203
|
+
### Exclusiones del discovery
|
|
204
|
+
|
|
205
|
+
En la instalación interactiva elegís qué excluir del seed técnico con un menú (tests, assets pesados, docs, migraciones/seeds, lockfiles). Excluir reduce el tamaño del pack y estabiliza la detección de cambios:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
ancleto init --exclude "**/*.test.*,**/*.png,docs" # sin menú, directo
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Se guarda en `.ancletorc` → `discovery.exclude` y se preserva en `update`/`upgrade`.
|
|
212
|
+
|
|
213
|
+
### Frescura del seed (impact)
|
|
214
|
+
|
|
215
|
+
`ancleto discovery --check` no solo dice si el seed está `STALE`: dice **qué tan grave** es el cambio con `impact`:
|
|
216
|
+
|
|
217
|
+
| `impact` | Significado | Qué pasa |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `none` | Sin cambios | El agente trabaja normal |
|
|
220
|
+
| `minor` | Cambios fuera de áreas materiales (un componente, un estilo) | El agente trabaja y lo anota en el resumen; **no ofrece regenerar** |
|
|
221
|
+
| `material` | Cambió un config de runtime, un entry point o apareció un directorio raíz nuevo | El agente **ofrece regenerar** (solo con tu aprobación) |
|
|
222
|
+
|
|
223
|
+
La excepción: si el agente detecta un **hueco concreto** (info que el seed debería tener y no tiene, como una paleta nueva sin unidad), puede ofrecer regenerar aunque el `impact` sea `minor` — siempre con tu aprobación. Nunca regenera solo.
|
|
224
|
+
|
|
225
|
+
### Regeneración parcial
|
|
226
|
+
|
|
227
|
+
`--check` también devuelve `affectedDocs`: los documentos del seed cuyas áreas cambiaron. Cuando aprobás una regeneración sobre un reporte `minor`, el seed-writer **reescribe solo esos documentos** y deja el resto intacto (un solo pack igual, pero sin reescribir los 8 documentos + dossiers). El mapeo vive en `seed-map.json` dentro del directorio del seed y lo mantiene la skill al generar; sin ese archivo el chequeo degrada a lista vacía y la regeneración es completa.
|
|
228
|
+
|
|
203
229
|
---
|
|
204
230
|
|
|
205
231
|
## Uso rápido
|
|
@@ -237,10 +263,10 @@ ancleto discovery # Genera .discovery-map.json y empaqueta el repo
|
|
|
237
263
|
## Referencia de comandos CLI
|
|
238
264
|
|
|
239
265
|
```bash
|
|
240
|
-
ancleto init [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--with-azure]
|
|
266
|
+
ancleto init [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--with-azure]
|
|
241
267
|
# Configura el proyecto (interactivo en TTY)
|
|
242
268
|
|
|
243
|
-
ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--lang <codigo>] [--no-mcp]
|
|
269
|
+
ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--lang <codigo>] [--exclude <globs>] [--no-mcp]
|
|
244
270
|
# Instala agentes, skills y templates
|
|
245
271
|
|
|
246
272
|
ancleto update # Re-instala la última versión sobre lo existente
|
package/agents/orchestrator.md
CHANGED
|
@@ -65,12 +65,13 @@ The orchestrator never executes `ancleto discovery --check`. When the technical
|
|
|
65
65
|
| --------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
66
66
|
| `READY` | use the focused answer returned by `@technical-discovery` |
|
|
67
67
|
| `PARTIAL` | explain which required documents are unavailable and offer to complete them |
|
|
68
|
-
| `STALE`
|
|
68
|
+
| `STALE` (minor) | use the answer with a one-line freshness note; do not offer regeneration — the seed is still usable |
|
|
69
|
+
| `STALE` (material) | warn and offer regeneration only when a concrete gap (info the seed should have but lacks) or the changed area affects the work |
|
|
69
70
|
| `MISSING` | explain that broad repository context is unavailable and offer to generate the seed |
|
|
70
71
|
|
|
71
72
|
For `MISSING` and `PARTIAL`, never generate automatically. Ask for explicit user approval before invoking `@technical-seed-writer`. If approved, pass the complete check report returned by `@technical-discovery` and the action `generate` or `complete`. If declined, continue only when the request can be handled safely without broad repository context; otherwise report the limitation and stop.
|
|
72
73
|
|
|
73
|
-
For `STALE`, delegate `regenerate` to `@technical-seed-writer` only after explicit user approval.
|
|
74
|
+
For `STALE`, delegate `regenerate` to `@technical-seed-writer` only after explicit user approval. When `impact` is `minor`, keep working from the seed and mention the drift in the summary. A concrete gap found while working — needed info the seed does not cover — may justify offering regeneration even with `minor` impact, always with approval. When the report lists `affectedDocs`, pass them so an approved regeneration stays scoped to those documents.
|
|
74
75
|
|
|
75
76
|
`@technical-discovery` owns read-only repository orientation and seed-state inspection. `@technical-seed-writer` owns all seed generation, completion, and regeneration. Never substitute an unavailable seed with a manual repository scan.
|
|
76
77
|
|
|
@@ -269,7 +270,7 @@ If the runtime is read-only or plan-only:
|
|
|
269
270
|
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`.
|
|
270
271
|
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.
|
|
271
272
|
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
|
|
272
|
-
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;
|
|
273
|
+
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` with `impact: material`, warn and offer regeneration via **`@technical-seed-writer`** with explicit user approval — never automatic; with `impact: minor`, continue the archive normally and note the drift in the summary. This closure check protects future sessions from a silently aging seed. Then delegate to **`@documenter`** only after explicit user approval to archive
|
|
273
274
|
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.
|
|
274
275
|
10. **Report**: Return final status or blocking findings to the user after automatic storage, an explicit decline, or User-Approved Record completes.
|
|
275
276
|
|
|
@@ -76,7 +76,8 @@ Inspect seed state only when `docs/technical-discovery/` is the selected source.
|
|
|
76
76
|
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
77
77
|
| `READY` | answer from the seed |
|
|
78
78
|
| `PARTIAL` | use an available document only when it safely answers the focused question; otherwise report the missing documents and request completion |
|
|
79
|
-
| `STALE`
|
|
79
|
+
| `STALE` with `impact: minor` | answer noting the freshness drift; do not request regeneration — the seed is still usable |
|
|
80
|
+
| `STALE` with `impact: material` | answer with an explicit freshness warning when the evidence is sufficient; request regeneration only when stale evidence could materially affect the answer |
|
|
80
81
|
| `MISSING` | report that broad repository context is unavailable and request generation |
|
|
81
82
|
|
|
82
83
|
You **report** states and recommend actions. You never resolve them: generating or regenerating
|
|
@@ -117,7 +118,7 @@ Any request to create, modify, or implement — including writing aspec artifact
|
|
|
117
118
|
| Situation | Action |
|
|
118
119
|
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
120
|
| request to create / modify / implement | return the context and escalate the request to the `@orchestrator` |
|
|
120
|
-
| seed missing, partial, or stale
|
|
121
|
+
| seed missing, partial, or stale with material impact | report the state and emit the corresponding `SEED_ACTION_REQUIRED` marker for `@orchestrator` to delegate `@technical-seed-writer` |
|
|
121
122
|
|
|
122
123
|
When you were delegated, do not address the developer as if they had called you: your output is context for the calling agent.
|
|
123
124
|
|
|
@@ -126,7 +127,7 @@ When you were delegated, do not address the developer as if they had called you:
|
|
|
126
127
|
- **Answer in Spanish.** Paths, file names, and identifiers stay as they are.
|
|
127
128
|
- Cite the paths that support each claim.
|
|
128
129
|
- State which source you used and whether seed inspection was required.
|
|
129
|
-
- When seed inspection was required, return the resolved state and the complete unchanged check report if a seed action is needed.
|
|
130
|
+
- When seed inspection was required, return the resolved state with its `impact` and `affectedDocs`, and the complete unchanged check report if a seed action is needed.
|
|
130
131
|
- Declare — when they apply — the stale nodes, any direct code read, and any gap left by the budget.
|
|
131
132
|
- If the seed state prevented the answer, state that no repository sweep was performed and
|
|
132
133
|
include the required `SEED_ACTION_REQUIRED` marker.
|
|
@@ -45,11 +45,11 @@ action was explicitly requested, run `ancleto discovery --check` once and read i
|
|
|
45
45
|
1. For `READY`, do not rewrite the seed; report that it is already available.
|
|
46
46
|
2. For `MISSING`, `PARTIAL`, or an accepted `STALE` regeneration, execute the skill's single global Repomix command exactly once. It must use the resolved `config.exclude` values and
|
|
47
47
|
must not run `--include`, an uncompressed preliminary pack, or another compressed pack.
|
|
48
|
-
3. Read that pack and
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
3. Read that pack and write the seed documents under `config.outputDir`, including `units/_map.md`. When the passed report has `impact: minor` with a non-empty `affectedDocs` list, rewrite ONLY those documents and leave every other seed document untouched. Otherwise (full regeneration), write every required seed document. Use the same pack for any optional dossier.
|
|
49
|
+
4. Write `seed-map.json` in `config.outputDir` mapping every seed document to the source areas it draws evidence from. Areas are first-level directories (or root file names), the same convention the CLI uses. Example: `{ "version": 1, "generatedAt": "...", "docs": { "overview.md": ["src", "package.json"] } }`.
|
|
50
|
+
5. Run `ancleto discovery --check` once to validate the written seed. This is state-only: do
|
|
51
51
|
not generate another pack after it.
|
|
52
|
-
|
|
52
|
+
6. Return the result as specified in `## Output`.
|
|
53
53
|
|
|
54
54
|
If the pack fails, writing fails, or the final state is not usable, report the exact blocker;
|
|
55
55
|
do not claim the seed was generated.
|
package/package.json
CHANGED
|
@@ -46,9 +46,10 @@ Check for delta specs at `aspec/changes/<name>/specs/`; if none, proceed without
|
|
|
46
46
|
|
|
47
47
|
### 3. Check technical seed freshness
|
|
48
48
|
|
|
49
|
-
If the `ancleto` CLI is available in this project, run `ancleto discovery --check` (read-only) and read its JSON `state`.
|
|
49
|
+
If the `ancleto` CLI is available in this project, run `ancleto discovery --check` (read-only) and read its JSON `state` and `impact`.
|
|
50
50
|
|
|
51
|
-
- **`STALE`**: warn that the technical seed is out of date
|
|
51
|
+
- **`STALE` with `impact: material`**: warn that the technical seed is out of date in material areas, and offer to regenerate it now (follow the `ancleto-technical-discovery` skill) or to continue archiving with the stale seed.
|
|
52
|
+
- **`STALE` with `impact: minor`**: continue without offering — the seed is still usable; record the drift in the summary.
|
|
52
53
|
- **Any other state**: continue without comment.
|
|
53
54
|
- Never regenerate automatically — regenerate only with the user's explicit approval.
|
|
54
55
|
- If the user declines, continue the archive normally and record the stale seed in the summary.
|
|
@@ -41,7 +41,8 @@ further or create one subagent per unit: generation happens in one sequential ex
|
|
|
41
41
|
| --- | --- |
|
|
42
42
|
| `READY` | Read `index.md` and at most two documents it routes to. |
|
|
43
43
|
| `PARTIAL` | Complete only the missing documents. |
|
|
44
|
-
| `STALE` |
|
|
44
|
+
| `STALE` with `impact: minor` | Answer from the seed and note the freshness drift; do not regenerate or run another pack for it. |
|
|
45
|
+
| `STALE` with `impact: material` | Regenerate only the affected focused dossier; update a transversal decision when it changed. |
|
|
45
46
|
| `MISSING` | Generate the concise seed. |
|
|
46
47
|
|
|
47
48
|
Never replace a missing seed with a manual repository sweep.
|
|
@@ -51,6 +51,11 @@ do not split it or create child documents.
|
|
|
51
51
|
|
|
52
52
|
Run `ancleto discovery --check` once more and apply `validation-checklist.md`. This is a
|
|
53
53
|
state-only validation; it must not be followed by another Repomix command. For stale nodes,
|
|
54
|
-
regenerate only the affected documents using one new global generation on a later request
|
|
54
|
+
regenerate only the affected documents using one new global generation on a later request:
|
|
55
|
+
when the check report lists `affectedDocs`, those are the only documents to rewrite.
|
|
55
56
|
If a unit has no dossier, update its row in `units/_map.md` and only the root documents whose
|
|
56
57
|
content changed.
|
|
58
|
+
|
|
59
|
+
After any generation, write `seed-map.json` in `config.outputDir` mapping every seed document
|
|
60
|
+
to the source areas (first-level directories or root file names) it draws evidence from, so a
|
|
61
|
+
later `--check` can scope the next regeneration.
|
|
@@ -9,4 +9,5 @@
|
|
|
9
9
|
- [ ] `decisions.md` concentrates rules, risks, and coupling without duplicating the overview.
|
|
10
10
|
- [ ] `inventory.md` declares directories or globs, not file lists.
|
|
11
11
|
- [ ] `unknowns.md` records what could not be verified.
|
|
12
|
+
- [ ] `seed-map.json` exists and maps each seed document to its source areas.
|
|
12
13
|
- [ ] No secret values are present.
|
package/src/cli/index.js
CHANGED
|
@@ -33,12 +33,13 @@ Uso:
|
|
|
33
33
|
ancleto install --with-engram Ademas configura el MCP externo engram (memoria opcional)
|
|
34
34
|
ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
|
|
35
35
|
ancleto install --lang <codigo> auto | es | en | pt (idioma de los artifacts; auto = idioma de la conversacion)
|
|
36
|
+
ancleto install --exclude <globs> Paths excluidos del discovery, coma-separados (ej: "**/*.png,docs")
|
|
36
37
|
ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
37
38
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
38
39
|
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
39
40
|
usa --global para forzar el alcance global
|
|
40
41
|
ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
|
|
41
|
-
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>]
|
|
42
|
+
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>]
|
|
42
43
|
Crea .ancletorc en el repositorio actual
|
|
43
44
|
(interactivo en TTY: banner + menu; Azure desactivado por defecto)
|
|
44
45
|
ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
|
|
@@ -639,6 +640,47 @@ function langLabel(code) {
|
|
|
639
640
|
return { auto: 'automatico (idioma de la conversacion)', es: 'espanol', en: 'ingles', pt: 'portugues' }[code] || code
|
|
640
641
|
}
|
|
641
642
|
|
|
643
|
+
const EXCLUDE_PRESETS = [
|
|
644
|
+
{ key: 'tests', label: 'tests (*.test.*, *.spec.*, __tests__)', recommended: true, globs: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**'] },
|
|
645
|
+
{ key: 'assets', label: 'assets pesados (*.png, *.jpg, *.svg, *.ico, public/)', recommended: true, globs: ['**/*.png', '**/*.jpg', '**/*.svg', '**/*.ico', 'public'] },
|
|
646
|
+
{ key: 'docs', label: 'documentacion (docs/)', recommended: false, globs: ['docs'] },
|
|
647
|
+
{ key: 'data', label: 'migraciones y seeds', recommended: false, globs: ['**/migrations/**', '**/seeds/**'] },
|
|
648
|
+
{ key: 'lockfiles', label: 'lockfiles (package-lock, yarn.lock, pnpm-lock)', recommended: true, globs: ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'] }
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
function parseExcludeFlag(value) {
|
|
652
|
+
return String(value)
|
|
653
|
+
.split(',')
|
|
654
|
+
.map((s) => s.trim())
|
|
655
|
+
.filter(Boolean)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// null = flag ausente; [] = limpiar exclusiones; [...] = globs.
|
|
659
|
+
function scanExcludeFlag(args) {
|
|
660
|
+
const i = args.indexOf('--exclude')
|
|
661
|
+
if (i < 0) return null
|
|
662
|
+
const next = args[i + 1]
|
|
663
|
+
if (!next || next.startsWith('--')) return []
|
|
664
|
+
return parseExcludeFlag(next)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Une los grupos elegidos con los globs propios del proyecto (los que no son de un preset).
|
|
668
|
+
function applyExcludePresets(existing, keys) {
|
|
669
|
+
const presetGlobs = new Set(EXCLUDE_PRESETS.flatMap((p) => p.globs))
|
|
670
|
+
const custom = (existing || []).filter((g) => !presetGlobs.has(g))
|
|
671
|
+
const chosen = EXCLUDE_PRESETS.filter((p) => keys.includes(p.key)).flatMap((p) => p.globs)
|
|
672
|
+
return [...custom, ...chosen]
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function askExcludePresets(existing) {
|
|
676
|
+
const presetGlobs = new Set(EXCLUDE_PRESETS.flatMap((p) => p.globs))
|
|
677
|
+
const hasPreset = (existing || []).some((g) => presetGlobs.has(g))
|
|
678
|
+
const preselected = EXCLUDE_PRESETS.map((p, i) => (hasPreset ? (p.globs.some((g) => (existing || []).includes(g)) ? i : -1) : p.recommended ? i : -1)).filter((i) => i >= 0)
|
|
679
|
+
const { cancelled, indices } = await selectMultiple('Que excluir del discovery (ademas de node_modules, dist, build)', EXCLUDE_PRESETS.map((p) => p.label), preselected)
|
|
680
|
+
if (cancelled) return existing || []
|
|
681
|
+
return applyExcludePresets(existing, indices.map((i) => EXCLUDE_PRESETS[i].key))
|
|
682
|
+
}
|
|
683
|
+
|
|
642
684
|
async function resolveAgent(args, existing, allowAsk = Boolean(process.stdin.isTTY)) {
|
|
643
685
|
const flag = scanAgentFlag(args)
|
|
644
686
|
if (flag) return flag
|
|
@@ -838,9 +880,12 @@ async function install(args) {
|
|
|
838
880
|
|
|
839
881
|
let agent = agentFlag || (existingRc?.agent && SUPPORTED_AGENTS.includes(existingRc.agent) ? existingRc.agent : null)
|
|
840
882
|
let language = scanLangFlag(args) || (existingRc?.language && SUPPORTED_LANGS.includes(existingRc.language) ? existingRc.language : null)
|
|
883
|
+
const excludeFlag = scanExcludeFlag(args)
|
|
884
|
+
const existingDiscovery = existingRc?.discovery || { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
885
|
+
let excludeChoice = excludeFlag
|
|
841
886
|
|
|
842
887
|
let bannerShown = false
|
|
843
|
-
const isInteractive = Boolean(process.stdout.isTTY) && ((projectDir && !agent) || !tier || !language)
|
|
888
|
+
const isInteractive = Boolean(process.stdout.isTTY) && ((projectDir && !agent) || !tier || !language || (projectDir && excludeChoice === null && (existingDiscovery.exclude || []).length === 0))
|
|
844
889
|
if (isInteractive) {
|
|
845
890
|
await showBanner()
|
|
846
891
|
bannerShown = true
|
|
@@ -855,6 +900,9 @@ async function install(args) {
|
|
|
855
900
|
const picked = await selectOption('Idioma de los artifacts', SUPPORTED_LANGS.map(langLabel), 0)
|
|
856
901
|
language = SUPPORTED_LANGS[SUPPORTED_LANGS.map(langLabel).indexOf(picked)] || 'auto'
|
|
857
902
|
}
|
|
903
|
+
if (projectDir && excludeChoice === null && (existingDiscovery.exclude || []).length === 0) {
|
|
904
|
+
excludeChoice = await askExcludePresets(existingDiscovery.exclude || [])
|
|
905
|
+
}
|
|
858
906
|
stopBanner()
|
|
859
907
|
}
|
|
860
908
|
if (!language) language = 'auto'
|
|
@@ -886,6 +934,7 @@ async function install(args) {
|
|
|
886
934
|
await writeManifest(projectDir, {
|
|
887
935
|
agent,
|
|
888
936
|
language,
|
|
937
|
+
...(excludeChoice !== null ? { discovery: { ...existingDiscovery, exclude: excludeChoice } } : {}),
|
|
889
938
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
890
939
|
installedPaths: {
|
|
891
940
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
@@ -969,12 +1018,14 @@ async function initProject(args) {
|
|
|
969
1018
|
const langFlag = scanLangFlag(args)
|
|
970
1019
|
const existing = await readAncletorc(projectDir)
|
|
971
1020
|
const azure = existing?.azure ?? { enabled: false }
|
|
972
|
-
|
|
1021
|
+
let discovery = existing?.discovery ?? { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
973
1022
|
const tierFile = join(projectDir, '.opencode', '.ancleto-tier')
|
|
1023
|
+
const excludeFlag = scanExcludeFlag(args)
|
|
1024
|
+
let excludeChoice = excludeFlag
|
|
974
1025
|
|
|
975
1026
|
let agent, tier, language
|
|
976
1027
|
language = langFlag || (existing?.language && SUPPORTED_LANGS.includes(existing.language) ? existing.language : null)
|
|
977
|
-
const isInteractive = Boolean(process.stdout.isTTY) && (!agentFlag || !tierFlag || !language)
|
|
1028
|
+
const isInteractive = Boolean(process.stdout.isTTY) && (!agentFlag || !tierFlag || !language || (excludeChoice === null && (discovery.exclude || []).length === 0))
|
|
978
1029
|
if (isInteractive) {
|
|
979
1030
|
await showBanner()
|
|
980
1031
|
const agentIdx = Math.max(0, SUPPORTED_AGENTS.indexOf(existing?.agent))
|
|
@@ -986,6 +1037,9 @@ async function initProject(args) {
|
|
|
986
1037
|
const picked = await selectOption('Idioma de los artifacts', SUPPORTED_LANGS.map(langLabel), 0)
|
|
987
1038
|
language = SUPPORTED_LANGS[SUPPORTED_LANGS.map(langLabel).indexOf(picked)] || 'auto'
|
|
988
1039
|
}
|
|
1040
|
+
if (excludeChoice === null && (discovery.exclude || []).length === 0) {
|
|
1041
|
+
excludeChoice = await askExcludePresets(discovery.exclude || [])
|
|
1042
|
+
}
|
|
989
1043
|
if (withAzure) {
|
|
990
1044
|
azure.enabled = true
|
|
991
1045
|
} else {
|
|
@@ -998,6 +1052,7 @@ async function initProject(args) {
|
|
|
998
1052
|
if (!language) language = 'auto'
|
|
999
1053
|
if (withAzure) azure.enabled = true
|
|
1000
1054
|
}
|
|
1055
|
+
if (excludeChoice !== null) discovery = { ...discovery, exclude: excludeChoice }
|
|
1001
1056
|
|
|
1002
1057
|
if (tier) {
|
|
1003
1058
|
await mkdir(join(projectDir, '.opencode'), { recursive: true })
|
|
@@ -1072,9 +1127,11 @@ async function computeSources() {
|
|
|
1072
1127
|
|
|
1073
1128
|
async function hashSources(sources) {
|
|
1074
1129
|
const h = createHash('sha256')
|
|
1130
|
+
const fileHashes = {}
|
|
1075
1131
|
for (const rel of sources) {
|
|
1076
1132
|
try {
|
|
1077
1133
|
const content = await readFile(join(process.cwd(), rel))
|
|
1134
|
+
fileHashes[rel] = createHash('sha256').update(content).digest('hex').slice(0, 16)
|
|
1078
1135
|
h.update(rel)
|
|
1079
1136
|
h.update('\0')
|
|
1080
1137
|
h.update(String(content.length))
|
|
@@ -1083,7 +1140,93 @@ async function hashSources(sources) {
|
|
|
1083
1140
|
h.update('\n')
|
|
1084
1141
|
} catch {}
|
|
1085
1142
|
}
|
|
1086
|
-
return h.digest('hex')
|
|
1143
|
+
return { hash: h.digest('hex'), fileHashes }
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Archivos cuyo cambio altera la arquitectura (configs de runtime, entry points).
|
|
1147
|
+
const MATERIAL_FILE_RES = [
|
|
1148
|
+
/^package\.json$/,
|
|
1149
|
+
/^tsconfig(\..+)?\.json$/,
|
|
1150
|
+
/^jsconfig\.json$/,
|
|
1151
|
+
/^(vite|webpack|rollup|next|nuxt|astro|svelte|tailwind)\.config\.[cm]?[jt]s$/,
|
|
1152
|
+
/^docker-compose\.ya?ml$/,
|
|
1153
|
+
/^Dockerfile$/,
|
|
1154
|
+
/^index\.html$/,
|
|
1155
|
+
/^src\/(main|index)\.[cm]?[jt]sx?$/
|
|
1156
|
+
]
|
|
1157
|
+
|
|
1158
|
+
function isMaterialFile(rel) {
|
|
1159
|
+
return MATERIAL_FILE_RES.some((re) => re.test(rel))
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Area = directorio de primer nivel; los archivos raiz son su propia area.
|
|
1163
|
+
function areaOf(rel) {
|
|
1164
|
+
const i = rel.indexOf('/')
|
|
1165
|
+
return i === -1 ? rel : rel.slice(0, i)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function computeImpact(stored, sources, fileHashes) {
|
|
1169
|
+
const storedList = new Set(stored.sources || [])
|
|
1170
|
+
const currentList = new Set(sources)
|
|
1171
|
+
const storedFiles = stored.fileHashes || null
|
|
1172
|
+
const changedAreas = new Set()
|
|
1173
|
+
const materialReasons = []
|
|
1174
|
+
const notes = []
|
|
1175
|
+
|
|
1176
|
+
for (const rel of sources) {
|
|
1177
|
+
if (!storedList.has(rel)) {
|
|
1178
|
+
changedAreas.add(areaOf(rel))
|
|
1179
|
+
if (isMaterialFile(rel)) materialReasons.push(`archivo material nuevo: ${rel}`)
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
for (const rel of storedList) {
|
|
1183
|
+
if (!currentList.has(rel)) {
|
|
1184
|
+
changedAreas.add(areaOf(rel))
|
|
1185
|
+
if (isMaterialFile(rel)) materialReasons.push(`archivo material eliminado: ${rel}`)
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (storedFiles) {
|
|
1189
|
+
for (const rel of sources) {
|
|
1190
|
+
if (storedFiles[rel] && storedFiles[rel] !== fileHashes[rel]) {
|
|
1191
|
+
changedAreas.add(areaOf(rel))
|
|
1192
|
+
if (isMaterialFile(rel)) materialReasons.push(`archivo material modificado: ${rel}`)
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const storedAreas = new Set([...storedList].map(areaOf))
|
|
1198
|
+
const currentAreas = new Set(sources.map(areaOf))
|
|
1199
|
+
for (const a of currentAreas) if (!storedAreas.has(a)) materialReasons.push(`area raiz nueva: ${a}`)
|
|
1200
|
+
for (const a of storedAreas) if (!currentAreas.has(a)) materialReasons.push(`area raiz eliminada: ${a}`)
|
|
1201
|
+
|
|
1202
|
+
if (!storedFiles && changedAreas.size === 0) {
|
|
1203
|
+
notes.push('estado previo sin hashes por archivo: el cambio de contenido no se puede atribuir a un area')
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const impact = materialReasons.length ? 'material' : changedAreas.size || notes.length ? 'minor' : 'none'
|
|
1207
|
+
return { impact, changedAreas: [...changedAreas].sort(), materialReasons, notes }
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// seed-map.json lo escribe la skill al generar: cada documento → areas de
|
|
1211
|
+
// primer nivel (o archivos raiz) de las que saco evidencia. Misma convencion que areaOf.
|
|
1212
|
+
async function readSeedMap(outputDir) {
|
|
1213
|
+
try {
|
|
1214
|
+
const raw = await readFile(join(outputDir, 'seed-map.json'), 'utf8')
|
|
1215
|
+
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''))
|
|
1216
|
+
if (!parsed || typeof parsed.docs !== 'object' || parsed.docs === null) return null
|
|
1217
|
+
return parsed.docs
|
|
1218
|
+
} catch {
|
|
1219
|
+
return null
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function affectedDocsFor(seedMap, changedAreas) {
|
|
1224
|
+
if (!seedMap || !changedAreas.length) return []
|
|
1225
|
+
const out = new Set()
|
|
1226
|
+
for (const [doc, areas] of Object.entries(seedMap)) {
|
|
1227
|
+
if (Array.isArray(areas) && areas.some((a) => changedAreas.includes(a))) out.add(doc)
|
|
1228
|
+
}
|
|
1229
|
+
return [...out].sort()
|
|
1087
1230
|
}
|
|
1088
1231
|
|
|
1089
1232
|
function statePath(outputDir) {
|
|
@@ -1108,7 +1251,7 @@ async function checkDiscovery() {
|
|
|
1108
1251
|
const { outputDir } = config
|
|
1109
1252
|
const tier = readProjectTier(process.cwd())
|
|
1110
1253
|
const present = await presentDocs(outputDir)
|
|
1111
|
-
let state, action, message, missingDocs
|
|
1254
|
+
let state, action, message, missingDocs, impact = 'none', changedAreas = [], materialReasons = [], notes = [], affectedDocs = [], seedMapPresent = false
|
|
1112
1255
|
if (present.length === 0) {
|
|
1113
1256
|
state = 'MISSING'
|
|
1114
1257
|
action = 'generate'
|
|
@@ -1121,35 +1264,55 @@ async function checkDiscovery() {
|
|
|
1121
1264
|
message = `Faltan ${missingDocs.length} documentos del seed.`
|
|
1122
1265
|
} else {
|
|
1123
1266
|
const sources = await computeSources()
|
|
1124
|
-
const hash = await hashSources(sources)
|
|
1125
|
-
let
|
|
1267
|
+
const { hash, fileHashes } = await hashSources(sources)
|
|
1268
|
+
let stored = null
|
|
1126
1269
|
const sp = statePath(outputDir)
|
|
1127
1270
|
if (await exists(sp)) {
|
|
1128
1271
|
try {
|
|
1129
|
-
|
|
1272
|
+
stored = JSON.parse((await readFile(sp, 'utf8')).replace(/^\uFEFF/, ''))
|
|
1130
1273
|
} catch {}
|
|
1131
1274
|
}
|
|
1132
|
-
if (!
|
|
1275
|
+
if (!stored?.hash) {
|
|
1133
1276
|
state = 'STALE'
|
|
1277
|
+
impact = 'material'
|
|
1134
1278
|
action = 'regenerate'
|
|
1135
1279
|
message = 'Seed completo pero sin estado registrado — frescura desconocida.'
|
|
1136
|
-
} else if (
|
|
1280
|
+
} else if (stored.hash === hash) {
|
|
1137
1281
|
state = 'READY'
|
|
1138
1282
|
action = 'continue'
|
|
1139
1283
|
message = 'El seed esta al dia.'
|
|
1140
1284
|
} else {
|
|
1285
|
+
const diff = computeImpact(stored, sources, fileHashes)
|
|
1286
|
+
impact = diff.impact
|
|
1287
|
+
changedAreas = diff.changedAreas
|
|
1288
|
+
materialReasons = diff.materialReasons
|
|
1289
|
+
notes = diff.notes
|
|
1290
|
+
const seedMap = await readSeedMap(outputDir)
|
|
1291
|
+
seedMapPresent = seedMap !== null
|
|
1292
|
+
affectedDocs = affectedDocsFor(seedMap, changedAreas)
|
|
1141
1293
|
state = 'STALE'
|
|
1142
|
-
|
|
1143
|
-
|
|
1294
|
+
if (impact === 'material') {
|
|
1295
|
+
action = 'regenerate'
|
|
1296
|
+
message = 'El repositorio cambio en areas materiales (config/entry points/estructura).'
|
|
1297
|
+
} else {
|
|
1298
|
+
action = 'continue'
|
|
1299
|
+
message = 'Cambios menores desde el ultimo pack; el seed sigue siendo util.'
|
|
1300
|
+
}
|
|
1144
1301
|
}
|
|
1145
1302
|
missingDocs = []
|
|
1146
1303
|
}
|
|
1147
1304
|
console.log(JSON.stringify({
|
|
1148
1305
|
schemaVersion: 2,
|
|
1149
1306
|
state,
|
|
1307
|
+
impact,
|
|
1150
1308
|
recommendedAction: action,
|
|
1151
1309
|
message,
|
|
1152
1310
|
missingDocs,
|
|
1311
|
+
changedAreas,
|
|
1312
|
+
materialReasons,
|
|
1313
|
+
notes,
|
|
1314
|
+
affectedDocs,
|
|
1315
|
+
seedMapPresent,
|
|
1153
1316
|
config: {
|
|
1154
1317
|
outputDir,
|
|
1155
1318
|
exclude: config.exclude,
|
|
@@ -1194,12 +1357,13 @@ async function packDiscovery(flags) {
|
|
|
1194
1357
|
process.exit(1)
|
|
1195
1358
|
}
|
|
1196
1359
|
const sources = await computeSources()
|
|
1197
|
-
const hash = await hashSources(sources)
|
|
1360
|
+
const { hash, fileHashes } = await hashSources(sources)
|
|
1198
1361
|
await mkdir(outputDir, { recursive: true })
|
|
1199
1362
|
await writeFile(statePath(outputDir), JSON.stringify({
|
|
1200
1363
|
version: 1,
|
|
1201
1364
|
generatedAt: new Date().toISOString(),
|
|
1202
1365
|
sources,
|
|
1366
|
+
fileHashes,
|
|
1203
1367
|
hash,
|
|
1204
1368
|
packTokens: tokens
|
|
1205
1369
|
}, null, 2) + '\n')
|