@dforce2055/dai 0.4.0 → 0.6.0
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/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +12 -0
- package/README.md +2 -1
- package/VERSION +1 -1
- package/cli/dai.mjs +78 -3
- package/cli/lib/implements.mjs +7 -2
- package/cli/lib/semver.mjs +15 -0
- package/docs/adr/0011-archive-gate-de-aprobacion.md +68 -0
- package/docs/adr/0012-upgrade-self-update-del-cli.md +59 -0
- package/docs/adr/README.md +1 -0
- package/governance/commit-convention.md +4 -0
- package/governance/human-authorship.md +61 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,40 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.6.0] — 2026-07-12
|
|
7
|
+
|
|
8
|
+
Self-update del CLI y blindaje de autoría del repo.
|
|
9
|
+
|
|
10
|
+
### Agregado
|
|
11
|
+
- `dai upgrade` (alias `update`): actualiza el CLI global a la última publicada
|
|
12
|
+
(`npm i -g …@latest`), con `--check` y `--dry-run`. No toca el repo: reporta el
|
|
13
|
+
drift del scaffold pero deja el `dai sync` explícito del mantenedor (ADR-0012).
|
|
14
|
+
|
|
15
|
+
### Interno
|
|
16
|
+
- Guard de autoría: rechaza commits autorados/co-autorados por agentes de IA
|
|
17
|
+
(check de CI `authorship` + hook local + `governance/human-authorship.md`).
|
|
18
|
+
- Eliminado `.mailmap` (ya no cumplía función).
|
|
19
|
+
- **111 tests** (+1: `planUpgrade`).
|
|
20
|
+
|
|
21
|
+
## [0.5.0] — 2026-07-10
|
|
22
|
+
|
|
23
|
+
**`archive` en el flujo** ([ADR-0011](docs/adr/0011-archive-gate-de-aprobacion.md)): cerrar el CÓMO
|
|
24
|
+
del lado de las specs canónicas, atado a la aprobación de la PR.
|
|
25
|
+
|
|
26
|
+
### Agregado
|
|
27
|
+
- **`dai archive [<change>]`**: funde los delta specs del change en las specs canónicas
|
|
28
|
+
(`openspec/specs/`) y lo archiva. Lo corre el **aprobador** de la PR, en la branch, al aprobar
|
|
29
|
+
(el fold viaja en la PR → elude la base protegida). Detecta el change activo por su `implements.yaml`
|
|
30
|
+
o le pasás el nombre; envuelve `openspec archive --yes` (mecánico → comando, no skill). Flag `--skip-specs`.
|
|
31
|
+
|
|
32
|
+
### Cambiado
|
|
33
|
+
- **`dai check` y `dai ls` saltean `openspec/changes/archive/`**: un change shippeado ya no genera
|
|
34
|
+
⚠️ de drift falso ni aparece en el listado. `discoverImplements` acepta `includeArchived` (default
|
|
35
|
+
`true`); `check`/`ls` lo pasan `false`. `stamp`/`done` mantienen el default (lo necesitan post-merge).
|
|
36
|
+
|
|
37
|
+
### Interno
|
|
38
|
+
- **110 tests** (+1 desde 0.4.0: filtro `includeArchived`).
|
|
39
|
+
|
|
6
40
|
## [0.4.0] — 2026-07-10
|
|
7
41
|
|
|
8
42
|
**Versionado y upgrade** ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)): mantené tu repo al
|
|
@@ -147,6 +181,8 @@ ClickUp y Jira Cloud.
|
|
|
147
181
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
148
182
|
`files` de npm sin tests ni secretos.
|
|
149
183
|
|
|
184
|
+
[0.6.0]: https://github.com/dforce2055/dai/releases/tag/v0.6.0
|
|
185
|
+
[0.5.0]: https://github.com/dforce2055/dai/releases/tag/v0.5.0
|
|
150
186
|
[0.4.0]: https://github.com/dforce2055/dai/releases/tag/v0.4.0
|
|
151
187
|
[0.3.1]: https://github.com/dforce2055/dai/releases/tag/v0.3.1
|
|
152
188
|
[0.3.0]: https://github.com/dforce2055/dai/releases/tag/v0.3.0
|
package/CONTRIBUTING.md
CHANGED
|
@@ -16,6 +16,13 @@ git clone <tu-fork> dai && cd dai
|
|
|
16
16
|
npm test # 48 tests, sin `npm install` (no hay deps)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Opcional, para que los hooks del repo te avisen antes de commitear (cero
|
|
20
|
+
dependencias, sin husky):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git config core.hooksPath .githooks # valida convención de commits + autoría humana
|
|
24
|
+
```
|
|
25
|
+
|
|
19
26
|
## Reglas de oro
|
|
20
27
|
|
|
21
28
|
1. **Cero dependencias de runtime.** El CLI corre en redes cerradas sin `npm install`
|
|
@@ -29,6 +36,11 @@ npm test # 48 tests, sin `npm install` (no hay deps)
|
|
|
29
36
|
necesita un ADR (ver abajo).
|
|
30
37
|
4. **Secretos nunca en el repo.** Tokens solo en `.env` (gitignored) o el secret
|
|
31
38
|
store del CI. git usa **SSH**, las APIs usan **tokens scopeados** (ADR-0007).
|
|
39
|
+
5. **Autoría humana.** El código de `dai` lo **autora y revisa una persona**. No se
|
|
40
|
+
aceptan commits cuyo *author*, *committer* o `Co-authored-by` sea un agente o bot
|
|
41
|
+
(Cursor, Copilot, Claude, …). Un agente puede ayudarte, pero el cambio lo firmas
|
|
42
|
+
tú: con tu identidad y sin el trailer del agente. Un check de CI lo bloquea en cada
|
|
43
|
+
PR (ver [`governance/human-authorship.md`](governance/human-authorship.md)).
|
|
32
44
|
|
|
33
45
|
## Flujo (la propia metodología)
|
|
34
46
|
|
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ flowchart TD
|
|
|
127
127
|
|
|
128
128
|
| # | Fase | Cómo | Quién |
|
|
129
129
|
|---|---|---|---|
|
|
130
|
-
| 1 | **Instalar el CLI** | `npm i -g @dforce2055/dai` (o `npm link` en dev) · `dai --version` | dev |
|
|
130
|
+
| 1 | **Instalar el CLI** | `npm i -g @dforce2055/dai` (o `npm link` en dev) · `dai --version` · actualizar después: `dai upgrade` | dev |
|
|
131
131
|
| 2 | **Bootstrap del repo** | `dai init` → `.dai` + Claude/Copilot/Cursor + config + PR template | dev/lead |
|
|
132
132
|
| 3a | **Definir el QUÉ** | `/grill-user-story` (una US) · `/grill-epic` (algo grande) · `/doc-to-backlog` (un doc) — te interrogan hasta una US testeable | PO / analista |
|
|
133
133
|
| 3b | **Publicar la US** | la skill la sube al tracker vía **MCP**, o con **`dai publish <us.md>`** (crea el issue vía token, sin MCP) → devuelve el key | PO / IA |
|
|
@@ -167,6 +167,7 @@ flowchart TD
|
|
|
167
167
|
| `dai pr [--assignee u] [--base b] [--draft] [--yes]` | crea TU PR/MR precargada: pregunta la branch base (default `main`), muestra el texto y confirma antes de publicar |
|
|
168
168
|
| `dai stamp` | estampa la cobertura inversa en el tracker (branch + commit-ancla) |
|
|
169
169
|
| `dai done [--base main] [--force]` | cierra la US: vuelve a la base, `fetch --prune` + `pull`, y borra la branch local **si está mergeada** (chequeo estricto; `--force` la borra igual). Redes: no estar en la base, sin cambios sueltos, sin commits sin pushear |
|
|
170
|
+
| `dai archive [<change>] [--skip-specs]` | **funde los delta specs del change en las specs canónicas** (`openspec/specs/`) y lo archiva. Lo corre el **aprobador** de la PR (gate de aprobación, [ADR-0011](docs/adr/0011-archive-gate-de-aprobacion.md)); detecta el change activo o le pasás el nombre. Envuelve `openspec archive` |
|
|
170
171
|
| `dai forge comment <ref> --body-file <f>` · `dai forge pr <ref>` | comentar / leer una PR/MR (GitHub/GitLab) |
|
|
171
172
|
| `dai ac-hash <us.md>` | calcula el hash de los criterios de aceptación de una US |
|
|
172
173
|
| `dai doctor` · `dai docs <dest>` · `dai version` | diagnóstico del entorno (incluye **version-drift** del scaffold) · copiar la doc · versión (`dai version` avisa si tu repo quedó atrás) |
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.6.0
|
package/cli/dai.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { parsePrRef, getPR, postComment } from "./lib/forge-api.mjs";
|
|
|
28
28
|
import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
|
|
29
29
|
import { dirsEqual } from "./lib/fsutil.mjs";
|
|
30
30
|
import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
|
|
31
|
-
import { versionDrift } from "./lib/semver.mjs";
|
|
31
|
+
import { versionDrift, planUpgrade } from "./lib/semver.mjs";
|
|
32
32
|
import { skillToPrompt, skillToCursor, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore } from "./lib/bootstrap.mjs";
|
|
33
33
|
|
|
34
34
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -71,7 +71,7 @@ function cmdAcHash(arg) {
|
|
|
71
71
|
// ── ls ──────────────────────────────────────────────────────────────────────
|
|
72
72
|
function cmdLs(opts) {
|
|
73
73
|
const root = opts.root || process.cwd();
|
|
74
|
-
const found = discoverImplements(root);
|
|
74
|
+
const found = discoverImplements(root, { includeArchived: false });
|
|
75
75
|
const rows = [];
|
|
76
76
|
for (const f of found) {
|
|
77
77
|
for (const im of f.implements || []) {
|
|
@@ -170,7 +170,7 @@ function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return
|
|
|
170
170
|
async function cmdCheck() {
|
|
171
171
|
loadEnv();
|
|
172
172
|
const adapter = getAdapter(process.env);
|
|
173
|
-
const found = discoverImplements(process.cwd());
|
|
173
|
+
const found = discoverImplements(process.cwd(), { includeArchived: false });
|
|
174
174
|
let worst = 0, n = 0;
|
|
175
175
|
const atrasadas = [];
|
|
176
176
|
for (const f of found) for (const im of f.implements || []) {
|
|
@@ -682,6 +682,39 @@ function cmdDocs(dest) {
|
|
|
682
682
|
ok(`documentación copiada a ${dest}`);
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
+
// ── archive: funde los delta specs del change en los specs canónicos y lo archiva ─
|
|
686
|
+
// Lo corre el APROBADOR de la PR, en la branch, al aprobar: es el gate de aprobación
|
|
687
|
+
// (el fold va atado a la aprobación, no a la autoría). Envuelve `openspec archive
|
|
688
|
+
// <change> --yes` — mecánico, va al CLI y no a una skill (ADR-0002). El fold queda
|
|
689
|
+
// sin commitear para que el aprobador lo revise y lo incluya en la PR antes de mergear.
|
|
690
|
+
function cmdArchive(changeArg, opts) {
|
|
691
|
+
const repo = process.cwd();
|
|
692
|
+
if (!existsSync(join(repo, "openspec"))) fail("no hay OpenSpec en este repo (falta openspec/). Nada que archivar.", 2);
|
|
693
|
+
|
|
694
|
+
let change = changeArg;
|
|
695
|
+
if (!change) {
|
|
696
|
+
// Detectar el change ACTIVO (no archivado) por su implements.yaml.
|
|
697
|
+
const active = discoverImplements(repo, { includeArchived: false })
|
|
698
|
+
.filter((f) => /[/\\]openspec[/\\]changes[/\\]/.test(f.path))
|
|
699
|
+
.map((f) => basename(dirname(f.path)));
|
|
700
|
+
const uniq = [...new Set(active)];
|
|
701
|
+
if (uniq.length === 0) fail("no encontré un change activo para archivar (¿ya está archivado, o falta `dai link-us`?).", 2);
|
|
702
|
+
if (uniq.length > 1) fail(`hay varios changes activos: ${uniq.join(", ")}.\n Pasá cuál: dai archive <change>`, 2);
|
|
703
|
+
change = uniq[0];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
info(`archivando '${change}' — funde los delta specs en openspec/specs/ y mueve el change a archive/…`);
|
|
707
|
+
const args = ["archive", change, "--yes"];
|
|
708
|
+
if (opts.skipSpecs) args.push("--skip-specs");
|
|
709
|
+
try {
|
|
710
|
+
execFileSync(npmBin("openspec"), args, { stdio: "inherit", cwd: repo });
|
|
711
|
+
} catch (e) {
|
|
712
|
+
fail(`openspec archive falló (¿tasks incompletas? ¿change inexistente?): ${String(e.message).split("\n")[0]}`, 1);
|
|
713
|
+
}
|
|
714
|
+
process.stdout.write("\n");
|
|
715
|
+
ok(`'${change}' archivado. Revisá los cambios (specs fundidas + change en archive/) y commitealos en la PR antes de mergear.`);
|
|
716
|
+
}
|
|
717
|
+
|
|
685
718
|
// ── sync: refresca las copias scaffoldeadas a la versión del CLI (ADR-0010) ───
|
|
686
719
|
// Las copias (skills, constitución, templates, PR template) son un CACHÉ derivable
|
|
687
720
|
// del CLI: `dai sync` las re-genera a la versión instalada, aditivo (no pisa la
|
|
@@ -789,6 +822,43 @@ function reportDrift(repo = process.cwd()) {
|
|
|
789
822
|
return status;
|
|
790
823
|
}
|
|
791
824
|
|
|
825
|
+
// ── upgrade: self-update del CLI global (ADR-0012) ────────────────────────────
|
|
826
|
+
// Actualiza el paquete npm global de dai a la última publicada. NO toca el repo:
|
|
827
|
+
// si hay drift del scaffold, solo lo reporta (el `dai sync` queda explícito, del
|
|
828
|
+
// mantenedor). El nombre sale de package.json → robusto si cambia el scope.
|
|
829
|
+
function cmdUpgrade(opts) {
|
|
830
|
+
const name = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).name;
|
|
831
|
+
const currentV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
|
|
832
|
+
const manual = C.cy(`npm i -g ${name}@latest`);
|
|
833
|
+
info(`dai upgrade — CLI v${currentV} (${name})`);
|
|
834
|
+
|
|
835
|
+
let latestV;
|
|
836
|
+
try {
|
|
837
|
+
latestV = execFileSync(npmBin("npm"), ["view", name, "version"], { encoding: "utf8" }).trim();
|
|
838
|
+
} catch {
|
|
839
|
+
fail(`no pude consultar el registry (¿sin red?). Actualizá a mano: ${manual}`, 1);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const plan = planUpgrade(currentV, latestV);
|
|
843
|
+
if (plan.action === "unknown") { warn(`no pude comparar versiones (actual '${currentV}', última '${latestV}')`); return; }
|
|
844
|
+
if (plan.action === "up-to-date") { ok(`ya estás en la última (v${currentV})`); reportDrift(); return; }
|
|
845
|
+
if (plan.action === "ahead") { info(`tu CLI (v${currentV}) es más nuevo que el registry (v${latestV}) — nada que actualizar`); reportDrift(); return; }
|
|
846
|
+
|
|
847
|
+
// plan.action === "upgrade"
|
|
848
|
+
if (opts.check) { process.stdout.write(`${C.b(C.y("⬆️ hay update"))} — v${plan.from} → v${plan.to}. Corré ${C.cy("dai upgrade")}\n`); return; }
|
|
849
|
+
if (opts.dryRun) { info(`[dry-run] correría: npm i -g ${name}@latest (v${plan.from} → v${plan.to})`); return; }
|
|
850
|
+
|
|
851
|
+
info(`actualizando v${plan.from} → v${plan.to} …`);
|
|
852
|
+
try {
|
|
853
|
+
execFileSync(npmBin("npm"), ["install", "-g", `${name}@latest`], { stdio: "inherit" });
|
|
854
|
+
} catch {
|
|
855
|
+
fail(`el install falló. Probá a mano: ${manual}`, 1);
|
|
856
|
+
}
|
|
857
|
+
ok(`CLI actualizado a v${plan.to}`);
|
|
858
|
+
const st = reportDrift();
|
|
859
|
+
if (st === null) process.stdout.write(" (No estás en un repo dai — entrá al repo y, si hace falta, corré `dai sync`.)\n");
|
|
860
|
+
}
|
|
861
|
+
|
|
792
862
|
// ── doctor: diagnóstico ───────────────────────────────────────────────────────
|
|
793
863
|
function cmdDoctor() {
|
|
794
864
|
loadEnv();
|
|
@@ -864,9 +934,12 @@ switch (cmd) {
|
|
|
864
934
|
case "publish": cmdPublish(pos[0]).catch((e) => fail(String(e.message))); break;
|
|
865
935
|
case "pr": cmdPr(opts).catch((e) => fail(String(e.message))); break;
|
|
866
936
|
case "done": cmdDone(opts); break;
|
|
937
|
+
case "archive": cmdArchive(pos[0], opts); break;
|
|
867
938
|
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break;
|
|
868
939
|
case "init": cmdInit(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
869
940
|
case "sync": cmdSync(pos[0], opts); break;
|
|
941
|
+
case "upgrade":
|
|
942
|
+
case "update": cmdUpgrade(opts); break;
|
|
870
943
|
case "docs": cmdDocs(pos[0]); break;
|
|
871
944
|
case "doctor": cmdDoctor(); break;
|
|
872
945
|
case "version": cmdVersion(); break;
|
|
@@ -882,6 +955,7 @@ switch (cmd) {
|
|
|
882
955
|
" check compara vs la US viva → atrasado (ADR-0003)\n" +
|
|
883
956
|
" stamp estampa la cobertura en el tracker (ADR-0005)\n" +
|
|
884
957
|
" done [--base main] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local (si está mergeada)\n" +
|
|
958
|
+
" archive [<change>] [--skip-specs] funde los delta specs del change en las specs canónicas y lo archiva (lo corre el aprobador en la PR)\n" +
|
|
885
959
|
" pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
886
960
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
|
|
887
961
|
"Instalación:\n" +
|
|
@@ -891,6 +965,7 @@ switch (cmd) {
|
|
|
891
965
|
" ej: --for claude,cursor · --for copilot · --for all\n" +
|
|
892
966
|
" --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
|
|
893
967
|
" sync [<repo>] [--dry-run] [--for <asistentes>] refresca skills/constitución/templates a la versión del CLI (aditivo; no toca .env ni OpenSpec)\n" +
|
|
968
|
+
" upgrade [--check] [--dry-run] (alias: update) actualiza el CLI global a la última (npm i -g …@latest) y avisa si el repo quedó atrasado (ADR-0012)\n" +
|
|
894
969
|
" docs <destino> documentación conceptual → <destino>\n" +
|
|
895
970
|
" doctor diagnóstico del entorno\n\n" +
|
|
896
971
|
" (config: .env — ver .env.example)\n"
|
package/cli/lib/implements.mjs
CHANGED
|
@@ -71,7 +71,10 @@ export function parseImplements(text) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Camina el árbol desde root y devuelve { path, ...parsed } por cada implements.yaml.
|
|
74
|
-
|
|
74
|
+
// `includeArchived: false` saltea `openspec/changes/archive/` — los changes shippeados
|
|
75
|
+
// no deben aparecer en `check`/`ls` (ADR-0010). Por defecto los incluye (stamp/done los
|
|
76
|
+
// necesitan post-merge).
|
|
77
|
+
export function discoverImplements(root, { includeArchived = true } = {}) {
|
|
75
78
|
const found = [];
|
|
76
79
|
const walk = (dir) => {
|
|
77
80
|
let entries;
|
|
@@ -81,7 +84,9 @@ export function discoverImplements(root) {
|
|
|
81
84
|
let st;
|
|
82
85
|
try { st = statSync(full); } catch { continue; }
|
|
83
86
|
if (st.isDirectory()) {
|
|
84
|
-
if (
|
|
87
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
88
|
+
if (!includeArchived && name === "archive") continue;
|
|
89
|
+
walk(full);
|
|
85
90
|
} else if (name === "implements.yaml") {
|
|
86
91
|
try {
|
|
87
92
|
found.push({ path: full, ...parseImplements(readFileSync(full, "utf8")) });
|
package/cli/lib/semver.mjs
CHANGED
|
@@ -28,3 +28,18 @@ export function versionDrift(repoV, cliV) {
|
|
|
28
28
|
if (cmp < 0) return "cli-behind";
|
|
29
29
|
return c.major > r.major ? "major-behind" : "minor-behind";
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
// Plan de `dai upgrade` (ADR-0012): compara el CLI instalado (currentV) con la
|
|
33
|
+
// última publicada en el registry (latestV). Núcleo puro; el I/O (npm view /
|
|
34
|
+
// npm i -g) vive en el comando.
|
|
35
|
+
// up-to-date → iguales → { action, version }
|
|
36
|
+
// ahead → el CLI local es más nuevo → { action, current, latest }
|
|
37
|
+
// upgrade → hay una versión más nueva → { action, from, to }
|
|
38
|
+
// unknown → alguna versión no parsea → { action }
|
|
39
|
+
export function planUpgrade(currentV, latestV) {
|
|
40
|
+
const cmp = compareVersions(currentV, latestV);
|
|
41
|
+
if (cmp === null) return { action: "unknown" };
|
|
42
|
+
if (cmp === 0) return { action: "up-to-date", version: currentV };
|
|
43
|
+
if (cmp > 0) return { action: "ahead", current: currentV, latest: latestV };
|
|
44
|
+
return { action: "upgrade", from: currentV, to: latestV };
|
|
45
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ADR-0011 — `archive` es un gate de aprobación (`dai archive`)
|
|
2
|
+
|
|
3
|
+
- **Estado:** aceptado
|
|
4
|
+
- **Fecha:** 2026-07-10
|
|
5
|
+
- **Decide:** lead / arquitecto de la metodología
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
OpenSpec tiene `archive`: cuando un change está completo, **funde sus delta specs** en las
|
|
10
|
+
specs canónicas (`openspec/specs/<capability>/spec.md`) y **mueve el change** a
|
|
11
|
+
`openspec/changes/archive/`. Pero en el flujo dai (`link-us → propose → apply/tdd → check
|
|
12
|
+
→ pr → dai-review → merge → stamp → done`) el `archive` **no tenía lugar**: ni cuándo, ni
|
|
13
|
+
quién. Sin eso, las specs vivas del repo quedan desincronizadas de lo shippeado.
|
|
14
|
+
|
|
15
|
+
Dos preguntas de fondo: **¿quién lo corre?** y **¿en qué momento?**
|
|
16
|
+
|
|
17
|
+
## Decisión
|
|
18
|
+
|
|
19
|
+
### `archive` = el acto de aceptar el change en las specs canónicas → lo hace **quien aprueba la PR**
|
|
20
|
+
|
|
21
|
+
Fundir un change en las specs oficiales es **bendecir** ese cambio. Por eso lo ata a la
|
|
22
|
+
**aprobación**, no a la autoría: lo corre **quien aprueba la PR**, no el autor. Es un
|
|
23
|
+
**gate de aprobación**, no un paso de cierre del autor (un autor no debería fundir sus
|
|
24
|
+
propios cambios en la spec oficial de un equipo).
|
|
25
|
+
|
|
26
|
+
Ownership por **dial de ceremonia** (N1/N2/N3):
|
|
27
|
+
|
|
28
|
+
- **N1/N2 — `dai archive` (comando):** el aprobador lo corre **en la branch de la PR** al
|
|
29
|
+
aprobar → funde los deltas + mueve el change + lo deja **sin commitear** para revisarlo →
|
|
30
|
+
lo incluye en la PR → mergea normal. **Resuelve solo el problema de base protegida**: el
|
|
31
|
+
fold viaja dentro de la PR, no hay push directo a `main`.
|
|
32
|
+
- **N3 — workflow de CI opt-in** (roadmap): el merge aprobado dispara el `archive` solo.
|
|
33
|
+
|
|
34
|
+
### `dai archive` es un **comando**, no una skill
|
|
35
|
+
|
|
36
|
+
Archivar es **mecánico** (detectar el change + `openspec archive <change> --yes`): sin
|
|
37
|
+
juicio → va al CLI, como `dai stamp`/`check`/`done` (ADR-0002). `dai-review` es skill
|
|
38
|
+
porque revisar código sí requiere juicio; archivar no. `dai archive` **envuelve**
|
|
39
|
+
`openspec archive`, detectando el change activo por su `implements.yaml` (reusa
|
|
40
|
+
`discoverImplements`); si hay varios, pide el nombre.
|
|
41
|
+
|
|
42
|
+
### `dai check` y `dai ls` saltean `openspec/changes/archive/`
|
|
43
|
+
|
|
44
|
+
Un change archivado está **shippeado**: no debe aparecer en `check` (daría un ⚠️ de drift
|
|
45
|
+
falso si el PO edita esa US después) ni en `ls`. `discoverImplements` acepta
|
|
46
|
+
`includeArchived` (default `true`); `check`/`ls` lo pasan en `false`. **`dai stamp`/`done`/
|
|
47
|
+
`archive` mantienen el default** — sí necesitan encontrar el `implements.yaml` (p. ej.
|
|
48
|
+
`stamp` post-merge, cuando el change ya se archivó en la branch).
|
|
49
|
+
|
|
50
|
+
## Consecuencias
|
|
51
|
+
|
|
52
|
+
- ✅ El `archive` queda **atado a la aprobación** — las specs canónicas reflejan solo lo bendecido.
|
|
53
|
+
- ✅ La variante `dai archive` (N1/N2) **elude la base protegida** sin tokens ni CI: el fold entra por la PR.
|
|
54
|
+
- ✅ `check`/`ls` dejan de reportar ruido de changes shippeados.
|
|
55
|
+
- ✅ Coherente con el ADN: es una **herramienta opcional** (no obliga), mecánica → CLI.
|
|
56
|
+
- ⚠️ Edge: en PRs desde **fork**, el aprobador puede no tener push a la branch del autor →
|
|
57
|
+
ahí cae al workflow de CI, o lo corre el autor.
|
|
58
|
+
- ⚠️ El **nudge** (que `/dai-review` recuerde archivar al aprobar) y el **workflow de CI**
|
|
59
|
+
quedan como follow-up (roadmap), igual que documentar el paso en `SCRUM-CON-IA`.
|
|
60
|
+
|
|
61
|
+
## Alternativas consideradas
|
|
62
|
+
|
|
63
|
+
- **Que lo corra el autor, en el PR** — descartado: fundir en la spec oficial es un acto de
|
|
64
|
+
aprobación; el autor no debería bendecir su propio cambio. Va atado a quien aprueba.
|
|
65
|
+
- **Post-merge sobre la base (commit directo)** — descartado como default: choca con branch
|
|
66
|
+
protection (push directo a `main`). Queda para el workflow de CI (con follow-up PR o App token).
|
|
67
|
+
- **Una skill `/dai-archive`** — descartado: archivar es mecánico, no necesita un LLM (ADR-0002).
|
|
68
|
+
El comando puede invocarse desde el asistente igual, sin ser skill.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ADR-0012 — `dai upgrade` (self-update del CLI)
|
|
2
|
+
|
|
3
|
+
- **Estado:** aceptado
|
|
4
|
+
- **Fecha:** 2026-07-12
|
|
5
|
+
- **Decide:** lead / arquitecto de la metodología
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
dai tiene **dos cosas versionadas e independientes**: el **CLI** (el paquete npm
|
|
10
|
+
global, una copia por dev) y el **scaffold del repo** (`.dai/`, skills, `.dai/VERSION`,
|
|
11
|
+
compartido y commiteado). El drift entre ambos ya se **detecta** (`versionDrift`,
|
|
12
|
+
ADR-0010) y `dai doctor`/`dai version` imprimen el estado.
|
|
13
|
+
|
|
14
|
+
Pero actualizar el **CLI** era **manual**: cuando el detector veía el CLI atrasado,
|
|
15
|
+
imprimía el texto `npm i -g @dforce2055/dai` para que el dev lo copiara y corriera a
|
|
16
|
+
mano. No había comando. Fricción repetida, y un detector que empujaba a algo que no
|
|
17
|
+
existía como acción de dai.
|
|
18
|
+
|
|
19
|
+
Escenario típico: un dev vuelve el lunes con el CLI en v0.2.0 y ya salió v0.5.0.
|
|
20
|
+
Tiene que actualizar su CLI (y después traer el repo con `git pull`, que el
|
|
21
|
+
mantenedor ya sincronizó).
|
|
22
|
+
|
|
23
|
+
## Decisión
|
|
24
|
+
|
|
25
|
+
Agregamos **`dai upgrade`** (alias `update`): **self-update del CLI global** a la
|
|
26
|
+
última versión publicada en el registry.
|
|
27
|
+
|
|
28
|
+
- **Scope: solo el CLI.** Corre `npm i -g <name>@latest` (el `<name>` sale de
|
|
29
|
+
`package.json`, robusto si cambia el scope). `--check` solo informa; `--dry-run`
|
|
30
|
+
muestra el comando sin correrlo.
|
|
31
|
+
- **NO toca el repo.** Tras actualizar, **reporta** el drift del scaffold (`reportDrift`)
|
|
32
|
+
pero **no corre `dai sync`**. El `sync` queda como acto **explícito del mantenedor**:
|
|
33
|
+
un dev con CLI viejo que sincronizara **degradaría** el `.dai/` del repo, y sincronizar
|
|
34
|
+
desde cada máquina genera commits en conflicto.
|
|
35
|
+
- **El núcleo es puro y testeado** (`planUpgrade` en `lib/semver.mjs`): decide
|
|
36
|
+
`up-to-date | ahead | upgrade`. El I/O (`npm view` / `npm i -g`) es la cáscara del
|
|
37
|
+
comando, no se unit-testea (mismo criterio que el `fetch`).
|
|
38
|
+
|
|
39
|
+
## Consecuencias
|
|
40
|
+
|
|
41
|
+
- **Más fácil:** un comando cierra el loop de "CLI atrasado" en vez de copiar un hint.
|
|
42
|
+
La separación de roles queda nítida: **`upgrade` = tu CLI** (self-update),
|
|
43
|
+
**`sync` = el repo** (mantenedor, commiteado).
|
|
44
|
+
- **Se acepta pagar:** depende del **registry** (es online; en redes cerradas —ADR-0006—
|
|
45
|
+
falla con mensaje claro y fallback al `npm i -g` manual). Asume **npm** como package
|
|
46
|
+
manager del global (no pnpm/yarn/volta/asdf); si el install falla, deriva al comando
|
|
47
|
+
manual. No auto-sincroniza el repo (a propósito).
|
|
48
|
+
|
|
49
|
+
## Alternativas consideradas
|
|
50
|
+
|
|
51
|
+
- **Auto-correr `dai sync` tras el upgrade** — descartado: un dev degradaría el repo con
|
|
52
|
+
un CLI viejo y sincronizar desde cada máquina genera conflictos. El `sync` es del
|
|
53
|
+
mantenedor.
|
|
54
|
+
- **Pinnear dai como devDependency por-repo** (sin global → sin drift, alineado en
|
|
55
|
+
`npm install`) — descartado como default: dai se diseñó **global y zero-dep para redes
|
|
56
|
+
cerradas** (ADR-0006), y no todos los repos son npm. Queda como opción del consumidor,
|
|
57
|
+
no del método.
|
|
58
|
+
- **No hacer nada** (dejar el hint manual) — descartado: fricción repetida sobre una
|
|
59
|
+
acción que el propio detector ya recomendaba.
|
package/docs/adr/README.md
CHANGED
|
@@ -16,6 +16,7 @@ decisión cambia, se escribe un ADR nuevo que supersede al viejo. Molde en
|
|
|
16
16
|
| [0008](0008-estrategia-de-i18n.md) | Estrategia de i18n: fuente única (español) + traducciones derivadas, `DAI_LANG` en el CLI, por fases | propuesto |
|
|
17
17
|
| [0009](0009-adaptador-cursor.md) | Adaptador nativo para Cursor (skills + rules) con `dai init`/`install`/`doctor` | propuesto |
|
|
18
18
|
| [0010](0010-versionado-y-upgrade.md) | Versionado y upgrade: compatibilidad por semver, `doctor` version-drift, `dai sync` aditivo | propuesto |
|
|
19
|
+
| [0011](0011-archive-gate-de-aprobacion.md) | `archive` es un gate de aprobación: `dai archive` (comando) lo corre el aprobador; `check`/`ls` saltean `archive/` | aceptado |
|
|
19
20
|
|
|
20
21
|
> Estas son las decisiones que cierran las "Decisiones abiertas" de
|
|
21
22
|
> [`METODOLOGIA.md §7`](../METODOLOGIA.md) y las enmiendas al
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
|
|
36
36
|
Se dejan pasar sin validar: `Merge …`, `Revert …`, `fixup!`/`squash!` y commits de bots (`🤖…`).
|
|
37
37
|
|
|
38
|
+
> Esto es solo la validación de **formato**. La **autoría** es otra regla: en el repo
|
|
39
|
+
> de dai no se aceptan commits autorados ni co-autorados por un agente
|
|
40
|
+
> (ver [`human-authorship.md`](human-authorship.md)).
|
|
41
|
+
|
|
38
42
|
## Relación con la trazabilidad
|
|
39
43
|
|
|
40
44
|
El link formal QUÉ↔CÓMO vive en el `implements.yaml` (no en el mensaje del commit). Pero si
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Autoría humana — quién firma el código de dai
|
|
2
|
+
|
|
3
|
+
> El código de `dai` lo **autora y revisa una persona**. Un agente puede ayudarte a
|
|
4
|
+
> escribirlo, pero el commit lo firmas tú: con tu identidad, y sin dejar al agente
|
|
5
|
+
> como co-autor. No es una postura sobre cómo trabajas — es una regla de contribución
|
|
6
|
+
> de **este repo**, y un check la blinda, sin depender de que nadie "se acuerde"
|
|
7
|
+
> (mismo espíritu que [`ci-rules.md`](ci-rules.md) y [`commit-convention.md`](commit-convention.md)).
|
|
8
|
+
|
|
9
|
+
## La regla
|
|
10
|
+
|
|
11
|
+
Ningún commit que entre a `dai` puede tener un **agente o bot** como:
|
|
12
|
+
|
|
13
|
+
- **author** — quien escribió el cambio,
|
|
14
|
+
- **committer** — quien lo registró, ni
|
|
15
|
+
- **`Co-authored-by:`** — un co-autor en el mensaje.
|
|
16
|
+
|
|
17
|
+
Concretamente se rechazan identidades de asistentes de código (Cursor, GitHub
|
|
18
|
+
Copilot, Claude, Devin, Codeium, Windsurf, … y cualquier `…[bot]`). La lista es
|
|
19
|
+
editable en [`scripts/no-agent-authors.sh`](../scripts/no-agent-authors.sh).
|
|
20
|
+
|
|
21
|
+
## Por qué
|
|
22
|
+
|
|
23
|
+
- **La persona firma ([Art. 5](../docs/MANIFIESTO.md#art-5)).** El código que entra a
|
|
24
|
+
`dai` es una decisión de una persona que lo entiende y se hace responsable. Un
|
|
25
|
+
commit firmado por un agente diluye ese "alguien responde por esto".
|
|
26
|
+
- **No vibe coding ([Art. 7](../docs/MANIFIESTO.md#art-7)).** El aporte no es "lo que
|
|
27
|
+
salió del chat": es un cambio que revisaste, entendiste y hiciste tuyo.
|
|
28
|
+
- **Higiene del historial.** El grafo de *Contributors* del repo se arma con
|
|
29
|
+
author/committer/co-author. Un trailer de agente mete al bot como contribuidor —
|
|
30
|
+
y sacarlo después obliga a reescribir historia.
|
|
31
|
+
|
|
32
|
+
## Distinción importante (no es scope de la metodología)
|
|
33
|
+
|
|
34
|
+
Esto **no** dice cómo debes usar agentes en *tus* proyectos. La metodología es
|
|
35
|
+
agnóstica de la herramienta de abajo ([Art. 2](../docs/MANIFIESTO.md#art-2)): usa los
|
|
36
|
+
agentes como quieras. Esta regla gobierna **la contribución al repo de dai**, igual
|
|
37
|
+
que el naming de ramas o la convención de commits. Por eso vive en `governance/` y
|
|
38
|
+
**no** se publica como parte de las plantillas de la metodología.
|
|
39
|
+
|
|
40
|
+
## Cómo cumplirla
|
|
41
|
+
|
|
42
|
+
Si un agente te ayudó con un cambio: **revísalo, apropiate de él y commitealo con tu
|
|
43
|
+
identidad**, sin el trailer `Co-authored-by`. Configura tu identidad una vez:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git config user.name "Tu Nombre"
|
|
47
|
+
git config user.email "tu@email"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Cómo se blinda
|
|
51
|
+
|
|
52
|
+
Dos capas, un solo script ([`scripts/no-agent-authors.sh`](../scripts/no-agent-authors.sh),
|
|
53
|
+
cero dependencias):
|
|
54
|
+
|
|
55
|
+
| Capa | Qué | Cuándo |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| **Hook local** | [`.githooks/commit-msg`](../.githooks/commit-msg) valida el commit en curso (`--pending`). | Al commitear, si activaste `git config core.hooksPath .githooks`. Es opt-in y salteable — feedback temprano, no barrera. |
|
|
58
|
+
| **CI (el gate)** | El job `authorship` de [`ci.yml`](../.github/workflows/ci.yml) valida todos los commits del PR (`--range`). | En cada PR. Como *required check* **bloquea el merge**. No es salteable. |
|
|
59
|
+
|
|
60
|
+
El hook avisa temprano; el CI es la barrera real. El commit siempre es de una persona
|
|
61
|
+
antes de que un mantenedor lo firme ([`CONTRIBUTING.md`](../CONTRIBUTING.md)).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Metodología de desarrollo asistido por IA — CLI de acciones deterministas (trazabilidad QUÉ↔CÓMO).",
|
|
5
5
|
"repository": { "type": "git", "url": "git+https://github.com/dforce2055/dai.git" },
|
|
6
6
|
"homepage": "https://dforce2055.github.io/dai/",
|