@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 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.4.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"
@@ -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
- export function discoverImplements(root) {
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 (!SKIP_DIRS.has(name)) walk(full);
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")) });
@@ -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.
@@ -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.4.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/",