@dforce2055/dai 0.7.0 → 0.8.1
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/.env.example +9 -3
- package/CHANGELOG.md +133 -0
- package/README.md +45 -14
- package/VERSION +1 -1
- package/cli/dai.mjs +180 -72
- package/cli/lib/args.mjs +13 -2
- package/cli/lib/bootstrap.mjs +69 -19
- package/cli/lib/http.mjs +67 -0
- package/cli/lib/jira-fields.mjs +143 -0
- package/cli/lib/pm-jira.mjs +71 -12
- package/docs/adr/0014-copilot-agent-skills.md +99 -0
- package/docs/adr/0015-jira-corporativo.md +104 -0
- package/docs/adr/README.md +4 -0
- package/package.json +1 -1
- package/skills/dai-review/SKILL.md +1 -1
- package/skills/doc-to-backlog/SKILL.md +1 -1
- package/skills/grill-epic/SKILL.md +13 -5
- package/skills/grill-intent/SKILL.md +1 -1
- package/skills/grill-user-story/SKILL.md +16 -3
- package/skills/link-us/SKILL.md +1 -1
- package/skills/tdd/SKILL.md +1 -1
- package/templates/jira-fields.example.json +49 -0
- package/templates/skill.md +8 -4
package/cli/dai.mjs
CHANGED
|
@@ -27,10 +27,12 @@ import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.
|
|
|
27
27
|
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
|
-
import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
|
|
30
|
+
import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
|
|
31
31
|
import { versionDrift, planUpgrade } from "./lib/semver.mjs";
|
|
32
32
|
import { parseSource } from "./lib/skills-source.mjs";
|
|
33
|
-
import {
|
|
33
|
+
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles } from "./lib/bootstrap.mjs";
|
|
34
|
+
import { parseFieldsFile, parseFieldOverrides, resolveJiraFields } from "./lib/jira-fields.mjs";
|
|
35
|
+
import { assertProjectKey } from "./lib/pm-jira.mjs";
|
|
34
36
|
|
|
35
37
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
36
38
|
|
|
@@ -48,14 +50,22 @@ const C = { y: (m) => paint("33", m), r: (m) => paint("31", m), cy: (m) => paint
|
|
|
48
50
|
const ROOT = join(HERE, ".."); // raíz del paquete dai (cli/ está adentro)
|
|
49
51
|
const CLAUDE_SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
|
|
50
52
|
const CURSOR_SKILLS_DIR = process.env.CURSOR_SKILLS_DIR || join(homedir(), ".cursor", "skills");
|
|
53
|
+
// Copilot lee las skills personales de ~/.copilot/skills (NO de ~/.claude/skills, que
|
|
54
|
+
// solo mira VS Code). Por eso una skill "instalada global" no le aparecía (ADR-0014).
|
|
55
|
+
const COPILOT_SKILLS_DIR = process.env.COPILOT_SKILLS_DIR || join(homedir(), ".copilot", "skills");
|
|
51
56
|
function git(args, opts = {}) {
|
|
52
57
|
// stderr en 'pipe' (no 'inherit') para no filtrar errores de git a la salida;
|
|
53
58
|
// quedan en e.stderr para quien quiera inspeccionarlos.
|
|
54
59
|
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...opts }).trim();
|
|
55
60
|
}
|
|
56
|
-
// En Windows los binarios instalados por npm (npm, openspec) son shims `.cmd
|
|
57
|
-
//
|
|
58
|
-
|
|
61
|
+
// En Windows los binarios instalados por npm (npm, openspec) son shims `.cmd`. Desde
|
|
62
|
+
// Node 18.20 / 20.12 / 21.7 (fix de CVE-2024-27980) execFileSync se NIEGA a lanzar un
|
|
63
|
+
// `.cmd` salvo con shell:true — si no, tira EINVAL, que dai confundía con "sin red".
|
|
64
|
+
// Por eso van con shell en Windows. git/gh/glab son `.exe`: se resuelven normal, sin shell.
|
|
65
|
+
function runNpmTool(name, args, opts = {}) {
|
|
66
|
+
const win = process.platform === "win32";
|
|
67
|
+
return execFileSync(win ? `${name}.cmd` : name, args, { shell: win, ...opts });
|
|
68
|
+
}
|
|
59
69
|
function trackerUrl(id) {
|
|
60
70
|
const tpl = process.env.DAI_TRACKER_URL_TEMPLATE;
|
|
61
71
|
return tpl ? tpl.replace("{id}", id) : id;
|
|
@@ -238,16 +248,45 @@ async function cmdForge(sub, ref, opts) {
|
|
|
238
248
|
}
|
|
239
249
|
|
|
240
250
|
// ── publish: crea la US en el tracker desde un .md (fallback del MCP) ──────────
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
|
|
252
|
+
// Los campos propios que exige el proyecto, declarados en .dai/jira-fields.json
|
|
253
|
+
// (o donde apunte DAI_JIRA_FIELDS_FILE). Sin archivo → {}: un Jira sin campos
|
|
254
|
+
// obligatorios publica igual que siempre.
|
|
255
|
+
function loadJiraFieldsSpec() {
|
|
256
|
+
const path = process.env.DAI_JIRA_FIELDS_FILE || join(".dai", "jira-fields.json");
|
|
257
|
+
if (!existsSync(path)) return {};
|
|
258
|
+
return parseFieldsFile(readFileSync(path, "utf8"), path);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function cmdPublish(file, opts = {}) {
|
|
262
|
+
if (!file) fail("uso: dai publish <archivo-us.md> [--parent KEY] [--issuetype T] [--field alias=valor]", 1);
|
|
243
263
|
loadEnv();
|
|
244
264
|
const md = readFileSync(file, "utf8");
|
|
245
265
|
const title = extractTitle(md);
|
|
246
266
|
if (!title) fail("no pude extraer el título de la US (falta un '# Título').", 1);
|
|
247
267
|
const adapter = getAdapter(process.env);
|
|
248
268
|
if (typeof adapter.createUS !== "function") fail(`el backend '${adapter.kind}' no soporta crear US.`, 1);
|
|
249
|
-
|
|
269
|
+
|
|
270
|
+
const parent = typeof opts.parent === "string" ? opts.parent : undefined;
|
|
271
|
+
const issuetype = typeof opts.issuetype === "string" ? opts.issuetype : undefined;
|
|
272
|
+
const project = typeof opts.project === "string" ? opts.project : undefined;
|
|
273
|
+
|
|
274
|
+
// Los campos propios son cosa de Jira; los otros backends no los usan.
|
|
275
|
+
let fields;
|
|
276
|
+
if (adapter.kind === "jira") {
|
|
277
|
+
const type = issuetype || process.env.DAI_JIRA_ISSUETYPE || "Story";
|
|
278
|
+
fields = resolveJiraFields({
|
|
279
|
+
spec: loadJiraFieldsSpec(),
|
|
280
|
+
issuetype: type,
|
|
281
|
+
overrides: parseFieldOverrides(asList(opts.field)),
|
|
282
|
+
});
|
|
283
|
+
} else if (opts.field !== undefined) {
|
|
284
|
+
fail(`--field es solo para jira (DAI_PM=${adapter.kind}).`, 2);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const r = await adapter.createUS({ title, descriptionMarkdown: md, parent, issuetype, project, fields });
|
|
250
288
|
ok(`US publicada en ${adapter.kind}: ${r.id}${r.url ? ` → ${r.url}` : ""}`);
|
|
289
|
+
if (parent) info(`colgada de ${parent}`);
|
|
251
290
|
info(`Próximo paso (el dev abre el CÓMO): dai link-us ${r.id}`);
|
|
252
291
|
}
|
|
253
292
|
|
|
@@ -389,8 +428,16 @@ async function cmdPr(opts) {
|
|
|
389
428
|
writeFileSync(bodyFile, body);
|
|
390
429
|
try {
|
|
391
430
|
info(`Publicando la branch ${branch}…`);
|
|
392
|
-
|
|
393
|
-
|
|
431
|
+
// stdin heredado + GIT_TERMINAL_PROMPT=1: la PRIMERA vez contra un remoto HTTPS
|
|
432
|
+
// corporativo, git/credential-manager necesita poder pedir la credencial. Con stdin
|
|
433
|
+
// ignorado (el default de git()) el login no completaba y el push fallaba en seco.
|
|
434
|
+
git(["push", "-u", "origin", branch], { stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, GIT_TERMINAL_PROMPT: "1" } });
|
|
435
|
+
} catch (e) {
|
|
436
|
+
const err = String(e.stderr || e.message || "").trim();
|
|
437
|
+
if (err) process.stderr.write(" " + err.split("\n").join("\n ") + "\n");
|
|
438
|
+
process.stdout.write(` Si es la primera vez contra este remoto, autenticá pusheando a mano una vez:\n git push -u origin ${branch}\n y volvé a correr: dai pr\n`);
|
|
439
|
+
fail(`no pude pushear la branch '${branch}'.`, 1);
|
|
440
|
+
}
|
|
394
441
|
|
|
395
442
|
const cmd = tool === "gh"
|
|
396
443
|
? ["pr", "create", "--title", title, "--body-file", bodyFile, "--base", base,
|
|
@@ -405,12 +452,20 @@ async function cmdPr(opts) {
|
|
|
405
452
|
try { rmSync(bodyFile); } catch { /* noop */ }
|
|
406
453
|
} catch (e) {
|
|
407
454
|
const msg = String(e.stderr || e.message || "");
|
|
408
|
-
|
|
455
|
+
const manual = ` Comando listo para correr a mano:\n ${tool} ${cmd.map((c) => /\s/.test(c) ? `'${c}'` : c).join(" ")}\n`;
|
|
456
|
+
if (e.code === "ENOENT") {
|
|
457
|
+
// El binario del forge no está instalado (el caso más común detrás de "no salió la MR").
|
|
458
|
+
const doc = tool === "glab" ? "https://gitlab.com/gitlab-org/cli/-/releases" : "https://cli.github.com";
|
|
459
|
+
warn(`'${tool}' no está instalado. El body quedó en ${bodyFile}.`);
|
|
460
|
+
process.stdout.write(` Instalá ${tool} (${doc}) y autenticá con \`${tool} auth login\`${tool === "glab" ? " --hostname " + (parseRemote(remote)?.host || "tu-gitlab") : ""}, o creá la PR a mano.\n${manual}`);
|
|
461
|
+
} else if (/no history in common|no commits between|not found.*base|base.*not found/i.test(msg)) {
|
|
409
462
|
warn(`la branch no comparte historia con '${base}' en el remoto (o '${base}' no existe allá).`);
|
|
410
463
|
process.stdout.write(` Suele pasar cuando el repo local y el remoto son distintos. Empuja la base primero:\n git push origin ${base}\n y vuelve a correr: dai pr\n`);
|
|
411
464
|
} else {
|
|
412
|
-
warn(`no pude crear la PR con ${tool}
|
|
413
|
-
|
|
465
|
+
warn(`no pude crear la PR con ${tool}. El body quedó en ${bodyFile}.`);
|
|
466
|
+
// El stderr real de gh/glab (auth vencida, host no configurado, flag desconocido…).
|
|
467
|
+
if (msg.trim()) process.stdout.write(` ${tool} dijo:\n ${msg.trim().split("\n").join("\n ")}\n`);
|
|
468
|
+
process.stdout.write(manual);
|
|
414
469
|
}
|
|
415
470
|
}
|
|
416
471
|
}
|
|
@@ -423,12 +478,9 @@ async function cmdInstall(opts) {
|
|
|
423
478
|
let want;
|
|
424
479
|
try { want = parseAssistants(typeof opts.for === "string" ? opts.for : "all"); }
|
|
425
480
|
catch (e) { fail(`--for ${e.message}`); }
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const wantClaude = want.claude, wantCursor = want.cursor;
|
|
430
|
-
if (!wantClaude && !wantCursor) fail("nada para instalar: pasá --for claude|cursor|all.");
|
|
431
|
-
const installFor = [wantClaude && "claude", wantCursor && "cursor"].filter(Boolean).join("+");
|
|
481
|
+
const wantClaude = want.claude, wantCursor = want.cursor, wantCopilot = want.copilot;
|
|
482
|
+
if (!wantClaude && !wantCursor && !wantCopilot) fail("nada para instalar: pasá --for claude|copilot|cursor|all.");
|
|
483
|
+
const installFor = [wantClaude && "claude", wantCopilot && "copilot", wantCursor && "cursor"].filter(Boolean).join("+");
|
|
432
484
|
const interactive = process.stdin.isTTY && !opts.global && opts.local === undefined;
|
|
433
485
|
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
434
486
|
|
|
@@ -439,17 +491,21 @@ async function cmdInstall(opts) {
|
|
|
439
491
|
return a === "l" ? { scope: "local", repo: process.cwd() } : a === "s" ? { scope: "skip" } : { scope: "global" };
|
|
440
492
|
};
|
|
441
493
|
|
|
494
|
+
// Cada asistente tiene SU dir: Claude ~/.claude/skills · Copilot ~/.copilot/skills
|
|
495
|
+
// (ADR-0014) · Cursor ~/.cursor/skills. Local, el de Copilot es .github/skills.
|
|
496
|
+
const LOCAL_DIR = { claude: [".claude", "skills"], copilot: [".github", "skills"], cursor: [".cursor", "skills"] };
|
|
497
|
+
const GLOBAL_DIR = { claude: CLAUDE_SKILLS_DIR, copilot: COPILOT_SKILLS_DIR, cursor: CURSOR_SKILLS_DIR };
|
|
498
|
+
|
|
442
499
|
const installOne = async ({ name, scope, repo, kind }) => {
|
|
443
500
|
const src = join(skillsSrc, name);
|
|
444
|
-
const targetDir = scope === "local"
|
|
445
|
-
? join(repo, kind === "cursor" ? ".cursor" : ".claude", "skills")
|
|
446
|
-
: kind === "cursor" ? CURSOR_SKILLS_DIR : CLAUDE_SKILLS_DIR;
|
|
501
|
+
const targetDir = scope === "local" ? join(repo, ...LOCAL_DIR[kind]) : GLOBAL_DIR[kind];
|
|
447
502
|
const target = join(targetDir, name);
|
|
448
|
-
const label =
|
|
503
|
+
const label = `${name} (${kind})`;
|
|
449
504
|
|
|
450
505
|
let sameAsSource = false;
|
|
451
506
|
if (existsSync(target)) {
|
|
452
|
-
if (kind
|
|
507
|
+
if (kind !== "cursor") {
|
|
508
|
+
// claude y copilot son copia cruda del SKILL.md: comparación directa.
|
|
453
509
|
sameAsSource = dirsEqual(src, target);
|
|
454
510
|
} else {
|
|
455
511
|
const tempRoot = mkdtempSync(join(tmpdir(), "dai-cursor-skill-"));
|
|
@@ -486,6 +542,7 @@ async function cmdInstall(opts) {
|
|
|
486
542
|
const { scope, repo } = await scopeFor(name);
|
|
487
543
|
if (scope === "skip") { warn(`salto ${name}`); continue; }
|
|
488
544
|
if (wantClaude) await installOne({ name, scope, repo, kind: "claude" });
|
|
545
|
+
if (wantCopilot) await installOne({ name, scope, repo, kind: "copilot" });
|
|
489
546
|
if (wantCursor) await installOne({ name, scope, repo, kind: "cursor" });
|
|
490
547
|
}
|
|
491
548
|
if (rl) rl.close();
|
|
@@ -559,8 +616,9 @@ function cmdInstallFrom(opts) {
|
|
|
559
616
|
rmSync(t, { recursive: true, force: true }); cpSync(srcDir, t, { recursive: true }); writeFileSync(join(t, "SKILL.md"), skillToCursor(md));
|
|
560
617
|
}
|
|
561
618
|
if (want.copilot) {
|
|
562
|
-
|
|
563
|
-
|
|
619
|
+
// Copilot lee SKILL.md nativo (Agent Skills, ADR-0014): copia cruda, con templates/.
|
|
620
|
+
const t = join(repo, ".github", "skills", name);
|
|
621
|
+
rmSync(t, { recursive: true, force: true }); mkdirSync(dirname(t), { recursive: true }); cpSync(srcDir, t, { recursive: true });
|
|
564
622
|
}
|
|
565
623
|
ok(name); installed++;
|
|
566
624
|
}
|
|
@@ -692,12 +750,20 @@ async function cmdInit(repo, opts) {
|
|
|
692
750
|
ok(`Claude: .claude/skills/ (${skills.length}) + CLAUDE.md${cCur ? " (bloque dai, aditivo)" : ""} → conoce el método y las skills`);
|
|
693
751
|
}
|
|
694
752
|
if (wantCopilot) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
753
|
+
// Copilot lee SKILL.md nativo (Agent Skills, ADR-0014): copia cruda, sin convertir.
|
|
754
|
+
// Así viajan también los templates/ que la conversión a .prompt.md perdía.
|
|
755
|
+
const dir = join(repo, ".github", "skills");
|
|
756
|
+
mkdirSync(dir, { recursive: true });
|
|
757
|
+
for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
|
|
698
758
|
const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
|
|
699
759
|
writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
|
|
700
|
-
ok(`Copilot: .github/
|
|
760
|
+
ok(`Copilot: .github/skills/ (${skills.length}) + copilot-instructions.md${ciCur ? " (bloque dai, aditivo)" : ""}`);
|
|
761
|
+
// Los .prompt.md de versiones previas duplicarían cada /comando con una copia
|
|
762
|
+
// vieja y sin templates. Solo borramos los que generó dai, nunca los del equipo.
|
|
763
|
+
const pdir = join(repo, ".github", "prompts");
|
|
764
|
+
const stale = existsSync(pdir) ? stalePromptFiles(skills).filter((f) => existsSync(join(pdir, f))) : [];
|
|
765
|
+
for (const f of stale) rmSync(join(pdir, f), { force: true });
|
|
766
|
+
if (stale.length) info(` quité ${stale.length} .prompt.md viejo(s) de .github/prompts/ — ahora son skills`);
|
|
701
767
|
}
|
|
702
768
|
if (wantCursor) {
|
|
703
769
|
const dir = join(repo, ".cursor", "skills");
|
|
@@ -719,7 +785,7 @@ async function cmdInit(repo, opts) {
|
|
|
719
785
|
// así que sí lo inicializamos nosotros — mapeando --for a sus tools. (Antes se
|
|
720
786
|
// dejaba a medias porque se intentaba correr su modo interactivo anidado.)
|
|
721
787
|
process.stdout.write("\n");
|
|
722
|
-
const openspecPresent = () => { try {
|
|
788
|
+
const openspecPresent = () => { try { runNpmTool("openspec", ["--version"], { stdio: "ignore" }); return true; } catch { return false; } };
|
|
723
789
|
const osTools = [want.claude && "claude", want.copilot && "github-copilot", want.cursor && "cursor"]
|
|
724
790
|
.filter(Boolean).join(",") || "claude,github-copilot,cursor";
|
|
725
791
|
const osHint = "para sumarlo después: npm i -g @fission-ai/openspec@latest && openspec init --tools " + osTools;
|
|
@@ -730,13 +796,13 @@ async function cmdInit(repo, opts) {
|
|
|
730
796
|
} else if (installOpenspec) {
|
|
731
797
|
let cliOk = openspecPresent();
|
|
732
798
|
if (!cliOk) {
|
|
733
|
-
try { info("OpenSpec: instalando el CLI (npm i -g @fission-ai/openspec)…");
|
|
799
|
+
try { info("OpenSpec: instalando el CLI (npm i -g @fission-ai/openspec)…"); runNpmTool("npm", ["install", "-g", "@fission-ai/openspec@latest"], { stdio: "inherit" }); cliOk = openspecPresent(); }
|
|
734
800
|
catch { cliOk = false; }
|
|
735
801
|
}
|
|
736
802
|
if (cliOk) {
|
|
737
803
|
try {
|
|
738
804
|
info(`OpenSpec: inicializando en el repo (--tools ${osTools})…`);
|
|
739
|
-
|
|
805
|
+
runNpmTool("openspec", ["init", "--tools", osTools, "--force"], { stdio: "inherit", cwd: repo === "." ? process.cwd() : repo });
|
|
740
806
|
ok("OpenSpec: instalado e inicializado — genera design/tasks con /opsx:*");
|
|
741
807
|
} catch {
|
|
742
808
|
warn("OpenSpec: el CLI está pero falló `openspec init`. Ejecuta a mano en el repo:");
|
|
@@ -792,7 +858,7 @@ function cmdArchive(changeArg, opts) {
|
|
|
792
858
|
const args = ["archive", change, "--yes"];
|
|
793
859
|
if (opts.skipSpecs) args.push("--skip-specs");
|
|
794
860
|
try {
|
|
795
|
-
|
|
861
|
+
runNpmTool("openspec", args, { stdio: "inherit", cwd: repo });
|
|
796
862
|
} catch (e) {
|
|
797
863
|
fail(`openspec archive falló (¿tasks incompletas? ¿change inexistente?): ${String(e.message).split("\n")[0]}`, 1);
|
|
798
864
|
}
|
|
@@ -817,11 +883,13 @@ function cmdSync(repo, opts) {
|
|
|
817
883
|
} else {
|
|
818
884
|
want = {
|
|
819
885
|
claude: existsSync(join(repo, ".claude", "skills")),
|
|
820
|
-
|
|
886
|
+
// .github/skills es el layout nativo (ADR-0014); .github/prompts es el viejo, que
|
|
887
|
+
// seguimos detectando para migrar repos que quedaron en el formato anterior.
|
|
888
|
+
copilot: existsSync(join(repo, ".github", "skills")) || existsSync(join(repo, ".github", "prompts")),
|
|
821
889
|
cursor: existsSync(join(repo, ".cursor", "skills")),
|
|
822
890
|
};
|
|
823
891
|
if (!want.claude && !want.copilot && !want.cursor)
|
|
824
|
-
fail("no detecté asistentes instalados (.claude
|
|
892
|
+
fail("no detecté asistentes instalados (.claude/skills · .github/skills · .cursor/skills). Pasá --for.", 2);
|
|
825
893
|
}
|
|
826
894
|
|
|
827
895
|
const cliV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
|
|
@@ -856,11 +924,16 @@ function cmdSync(repo, opts) {
|
|
|
856
924
|
const cPath = join(repo, "CLAUDE.md"), cCur = existsSync(cPath) ? readFileSync(cPath, "utf8") : "";
|
|
857
925
|
writeFileSync(cPath, upsertBlock(cCur, constitution("claude")));
|
|
858
926
|
});
|
|
859
|
-
if (want.copilot) step(`Copilot: .github/
|
|
860
|
-
|
|
861
|
-
|
|
927
|
+
if (want.copilot) step(`Copilot: .github/skills/ (${skills.length}) + copilot-instructions.md`, () => {
|
|
928
|
+
// Copilot lee SKILL.md nativo (ADR-0014): copia cruda, con templates/ — igual que `dai init`.
|
|
929
|
+
const dir = join(repo, ".github", "skills"); mkdirSync(dir, { recursive: true });
|
|
930
|
+
for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
|
|
862
931
|
const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
|
|
863
932
|
writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
|
|
933
|
+
// Limpia los .prompt.md viejos de dai (ahora son skills nativas) para no duplicar cada /comando.
|
|
934
|
+
const pdir = join(repo, ".github", "prompts");
|
|
935
|
+
const stale = existsSync(pdir) ? stalePromptFiles(skills).filter((f) => existsSync(join(pdir, f))) : [];
|
|
936
|
+
for (const f of stale) rmSync(join(pdir, f), { force: true });
|
|
864
937
|
});
|
|
865
938
|
if (want.cursor) step(`Cursor: .cursor/skills/ (${skills.length}) + .cursor/rules/dai-constitution.mdc`, () => {
|
|
866
939
|
const dir = join(repo, ".cursor", "skills"); mkdirSync(dir, { recursive: true });
|
|
@@ -919,7 +992,7 @@ function cmdUpgrade(opts) {
|
|
|
919
992
|
|
|
920
993
|
let latestV;
|
|
921
994
|
try {
|
|
922
|
-
latestV =
|
|
995
|
+
latestV = runNpmTool("npm", ["view", name, "version"], { encoding: "utf8" }).trim();
|
|
923
996
|
} catch {
|
|
924
997
|
fail(`no pude consultar el registry (¿sin red?). Actualizá a mano: ${manual}`, 1);
|
|
925
998
|
}
|
|
@@ -935,7 +1008,7 @@ function cmdUpgrade(opts) {
|
|
|
935
1008
|
|
|
936
1009
|
info(`actualizando v${plan.from} → v${plan.to} …`);
|
|
937
1010
|
try {
|
|
938
|
-
|
|
1011
|
+
runNpmTool("npm", ["install", "-g", `${name}@latest`], { stdio: "inherit" });
|
|
939
1012
|
} catch {
|
|
940
1013
|
fail(`el install falló. Probá a mano: ${manual}`, 1);
|
|
941
1014
|
}
|
|
@@ -949,35 +1022,52 @@ function cmdDoctor() {
|
|
|
949
1022
|
loadEnv();
|
|
950
1023
|
info(`dai doctor — versión v${readFileSync(join(ROOT, "VERSION"), "utf8").trim()}`);
|
|
951
1024
|
|
|
952
|
-
// Una skill sirve si está en el repo actual (
|
|
953
|
-
//
|
|
1025
|
+
// Una skill sirve si está en el repo actual (la puso `dai init`) o global (la puso
|
|
1026
|
+
// `dai install`). Reportamos dónde — y SOLO de los asistentes que este repo usa: antes
|
|
1027
|
+
// se listaban los tres siempre, así que quien configuró uno veía 14 warnings de los
|
|
1028
|
+
// otros dos y leía "está todo roto" cuando estaba todo bien.
|
|
954
1029
|
const skillsRoot = join(ROOT, "skills");
|
|
955
1030
|
const skillNames = readdirSync(skillsRoot).filter((n) => statSync(join(skillsRoot, n)).isDirectory());
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1031
|
+
const cwd = process.cwd();
|
|
1032
|
+
const ASSISTANTS = [
|
|
1033
|
+
{ kind: "claude", label: "Claude", local: join(cwd, ".claude", "skills"), global: CLAUDE_SKILLS_DIR, home: "~/.claude/skills" },
|
|
1034
|
+
{ kind: "copilot", label: "Copilot", local: join(cwd, ".github", "skills"), global: COPILOT_SKILLS_DIR, home: "~/.copilot/skills" },
|
|
1035
|
+
{ kind: "cursor", label: "Cursor", local: join(cwd, ".cursor", "skills"), global: CURSOR_SKILLS_DIR, home: "~/.cursor/skills" },
|
|
1036
|
+
];
|
|
1037
|
+
const present = (a) => skillNames.some((n) => existsSync(join(a.local, n)) || existsSync(join(a.global, n)));
|
|
1038
|
+
const active = ASSISTANTS.filter(present);
|
|
1039
|
+
|
|
1040
|
+
if (active.length === 0) {
|
|
1041
|
+
warn("no encontré skills de dai para ningún asistente.");
|
|
1042
|
+
process.stdout.write(" En un repo: dai init --for claude|copilot|cursor|all\n");
|
|
1043
|
+
process.stdout.write(" O global: dai install --for all\n");
|
|
964
1044
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1045
|
+
for (const a of active) {
|
|
1046
|
+
info(`skills ${a.label} (repo local / global ${a.home}):`);
|
|
1047
|
+
for (const name of skillNames) {
|
|
1048
|
+
const local = existsSync(join(a.local, name)), global = existsSync(join(a.global, name));
|
|
1049
|
+
if (local && global) ok(`${name} (local + global)`);
|
|
1050
|
+
else if (local) ok(`${name} (local, este repo)`);
|
|
1051
|
+
else if (global) ok(`${name} (global)`);
|
|
1052
|
+
else warn(`${name} — falta para ${a.label} (dai init --for ${a.kind}, o dai install --for ${a.kind})`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (active.some((a) => a.kind === "copilot")) {
|
|
1056
|
+
const ci = join(cwd, ".github", "copilot-instructions.md");
|
|
1057
|
+
existsSync(ci) ? ok("constitución Copilot (.github/copilot-instructions.md)") : warn("falta la constitución de Copilot (dai init --for copilot)");
|
|
1058
|
+
// Los .prompt.md viejos duplican cada /comando con una copia sin templates.
|
|
1059
|
+
const pdir = join(cwd, ".github", "prompts");
|
|
1060
|
+
const stale = existsSync(pdir) ? stalePromptFiles(skillNames).filter((f) => existsSync(join(pdir, f))) : [];
|
|
1061
|
+
if (stale.length) warn(`${stale.length} .prompt.md viejo(s) en .github/prompts/ — duplican las skills. Corré \`dai init --for copilot\` para limpiarlos.`);
|
|
1062
|
+
}
|
|
1063
|
+
if (active.some((a) => a.kind === "cursor")) {
|
|
1064
|
+
const localRule = join(cwd, ".cursor", "rules", "dai-constitution.mdc");
|
|
1065
|
+
const globalRule = join(homedir(), ".cursor", "rules", "dai-constitution.mdc");
|
|
1066
|
+
if (existsSync(localRule) && existsSync(globalRule)) ok("constitución Cursor (local + global)");
|
|
1067
|
+
else if (existsSync(localRule)) ok("constitución Cursor (local)");
|
|
1068
|
+
else if (existsSync(globalRule)) ok("constitución Cursor (global)");
|
|
1069
|
+
else warn("falta constitución Cursor (dai-constitution.mdc)");
|
|
974
1070
|
}
|
|
975
|
-
const localRule = join(process.cwd(), ".cursor", "rules", "dai-constitution.mdc");
|
|
976
|
-
const globalRule = join(homedir(), ".cursor", "rules", "dai-constitution.mdc");
|
|
977
|
-
if (existsSync(localRule) && existsSync(globalRule)) ok("constitución Cursor (local + global)");
|
|
978
|
-
else if (existsSync(localRule)) ok("constitución Cursor (local)");
|
|
979
|
-
else if (existsSync(globalRule)) ok("constitución Cursor (global)");
|
|
980
|
-
else warn("falta constitución Cursor (dai-constitution.mdc)");
|
|
981
1071
|
|
|
982
1072
|
info("adaptador de PM:");
|
|
983
1073
|
const pm = process.env.DAI_PM || "md";
|
|
@@ -985,9 +1075,24 @@ function cmdDoctor() {
|
|
|
985
1075
|
if (pm === "jira") {
|
|
986
1076
|
if (!process.env.DAI_JIRA_BASE_URL) warn("falta DAI_JIRA_BASE_URL en el .env");
|
|
987
1077
|
if (!process.env.DAI_JIRA_EMAIL) warn("falta DAI_JIRA_EMAIL en el .env");
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1078
|
+
// Ojo: solo miramos que el token ESTÉ, no que sirva — uno vencido pasa este chequeo
|
|
1079
|
+
// y recién falla al publicar. Verificarlo de verdad es pegarle a la red.
|
|
1080
|
+
if (!process.env.DAI_JIRA_TOKEN) warn("falta DAI_JIRA_TOKEN en el .env"); else ok("token de Jira presente (no verificado: eso lo dice `dai publish`)");
|
|
1081
|
+
if (!process.env.DAI_JIRA_PROJECT) warn("DAI_JIRA_PROJECT vacío — solo hace falta para `dai publish` (crear issues)");
|
|
1082
|
+
else {
|
|
1083
|
+
try { ok(`proyecto=${assertProjectKey(process.env.DAI_JIRA_PROJECT)} (para dai publish)`); }
|
|
1084
|
+
catch (e) { warn(String(e.message)); }
|
|
1085
|
+
}
|
|
1086
|
+
// Campos propios del proyecto: que el archivo parsee ANTES de necesitarlo.
|
|
1087
|
+
const fpath = process.env.DAI_JIRA_FIELDS_FILE || join(".dai", "jira-fields.json");
|
|
1088
|
+
if (!existsSync(fpath)) info(`sin campos propios declarados (${fpath} no existe — normal si tu Jira no los exige)`);
|
|
1089
|
+
else {
|
|
1090
|
+
try {
|
|
1091
|
+
const spec = parseFieldsFile(readFileSync(fpath, "utf8"), fpath);
|
|
1092
|
+
const types = Object.keys(spec);
|
|
1093
|
+
ok(`campos propios: ${fpath} — issuetypes: ${types.length ? types.join(", ") : "(ninguno)"}`);
|
|
1094
|
+
} catch (e) { warn(String(e.message)); }
|
|
1095
|
+
}
|
|
991
1096
|
}
|
|
992
1097
|
if (pm === "clickup") {
|
|
993
1098
|
if (!process.env.DAI_CLICKUP_TOKEN) warn("falta DAI_CLICKUP_TOKEN en el .env"); else ok("token de ClickUp presente");
|
|
@@ -1016,8 +1121,9 @@ switch (cmd) {
|
|
|
1016
1121
|
case "check": cmdCheck().catch((e) => fail(String(e.message))); break;
|
|
1017
1122
|
case "stamp": cmdStamp().catch((e) => fail(String(e.message))); break;
|
|
1018
1123
|
case "forge": cmdForge(pos[0], pos[1], opts).catch((e) => fail(String(e.message))); break;
|
|
1019
|
-
case "publish": cmdPublish(pos[0]).catch((e) => fail(String(e.message))); break;
|
|
1020
|
-
case "pr":
|
|
1124
|
+
case "publish": cmdPublish(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
1125
|
+
case "pr":
|
|
1126
|
+
case "mr": cmdPr(opts).catch((e) => fail(String(e.message))); break; // `mr` = alias para GitLab (merge request)
|
|
1021
1127
|
case "done": cmdDone(opts); break;
|
|
1022
1128
|
case "archive": cmdArchive(pos[0], opts); break;
|
|
1023
1129
|
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break; // alias de `dai skills install`
|
|
@@ -1039,13 +1145,15 @@ switch (cmd) {
|
|
|
1039
1145
|
" ac-hash <us.md> calcula el ac_hash (ADR-0001)\n" +
|
|
1040
1146
|
" ls [--json] lista lo que implementa el repo (ADR-0005)\n" +
|
|
1041
1147
|
" publish <us.md> crea la US en el tracker (Jira/ClickUp/md) y devuelve el key\n" +
|
|
1148
|
+
" [--parent KEY] la cuelga de su épica · [--issuetype T] p. ej. Epic\n" +
|
|
1149
|
+
" [--field alias=valor] campos propios que exige tu Jira (.dai/jira-fields.json); repetible\n" +
|
|
1042
1150
|
" link-us <KEY> [--us <md>] crea branch + implements.yaml; sin --us trae la US del tracker (ADR-0004)\n" +
|
|
1043
1151
|
" link-us <KEY> --resync re-estampa el ac_hash contra la US viva (tras un ⚠️ de check)\n" +
|
|
1044
1152
|
" check compara vs la US viva → atrasado (ADR-0003)\n" +
|
|
1045
1153
|
" stamp estampa la cobertura en el tracker (ADR-0005)\n" +
|
|
1046
1154
|
" done [--base main] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local (si está mergeada)\n" +
|
|
1047
1155
|
" 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" +
|
|
1048
|
-
" pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1156
|
+
" pr (alias mr) [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1049
1157
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
|
|
1050
1158
|
"Instalación:\n" +
|
|
1051
1159
|
" skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
|
package/cli/lib/args.mjs
CHANGED
|
@@ -28,6 +28,17 @@ export function parseAssistants(str) {
|
|
|
28
28
|
return want;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Un flag REPETIDO acumula en lista en vez de pisarse (`--field a=1 --field b=2`).
|
|
32
|
+
// Antes ganaba el último en silencio, que para --field sería perder datos sin avisar.
|
|
33
|
+
const push = (opts, key, val) => {
|
|
34
|
+
if (!(key in opts)) { opts[key] = val; return; }
|
|
35
|
+
if (Array.isArray(opts[key])) opts[key].push(val);
|
|
36
|
+
else opts[key] = [opts[key], val];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Normaliza un flag que puede venir 0, 1 o N veces → siempre lista.
|
|
40
|
+
export const asList = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
|
|
41
|
+
|
|
31
42
|
export function parseFlags(argv) {
|
|
32
43
|
const opts = {};
|
|
33
44
|
const pos = [];
|
|
@@ -36,8 +47,8 @@ export function parseFlags(argv) {
|
|
|
36
47
|
if (a.startsWith("--")) {
|
|
37
48
|
const key = camel(a.slice(2));
|
|
38
49
|
const next = argv[i + 1];
|
|
39
|
-
if (next !== undefined && !next.startsWith("--")) { opts
|
|
40
|
-
else opts
|
|
50
|
+
if (next !== undefined && !next.startsWith("--")) { push(opts, key, next); i++; }
|
|
51
|
+
else push(opts, key, true); // flag booleano
|
|
41
52
|
} else {
|
|
42
53
|
pos.push(a);
|
|
43
54
|
}
|
package/cli/lib/bootstrap.mjs
CHANGED
|
@@ -4,40 +4,78 @@
|
|
|
4
4
|
|
|
5
5
|
const REPO_URL = "https://github.com/dforce2055/dai";
|
|
6
6
|
|
|
7
|
+
// Un escalar YAML "plano" (sin comillas) no puede contener `: ` ni ` #`, ni empezar con
|
|
8
|
+
// un indicador: YAML lo leería como un mapa, un comentario o una estructura.
|
|
9
|
+
//
|
|
10
|
+
// Esto importa porque el parser de acá es un regex, no YAML — se traga cualquier cosa.
|
|
11
|
+
// Mientras dai CONVERTÍA el SKILL.md para Copilot y Cursor, el `JSON.stringify` de la
|
|
12
|
+
// conversión citaba el valor y tapaba el problema sin querer. Al entregar el SKILL.md
|
|
13
|
+
// crudo (ADR-0014) lo lee un parser YAML de verdad, y ahí una descripción con un `: `
|
|
14
|
+
// suelto tira abajo la skill entera. Le pasó a `doc-to-backlog` y a `grill-epic`.
|
|
15
|
+
const YAML_INDICATORS = /^[-?:,[\]{}#&*!|>'"%@`]/;
|
|
16
|
+
export function yamlScalarIssue(raw) {
|
|
17
|
+
const v = String(raw ?? "").trim();
|
|
18
|
+
if (v === "") return "está vacío";
|
|
19
|
+
const quoted = (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"));
|
|
20
|
+
if (quoted && v.length >= 2) return null; // citado: YAML acepta lo que sea adentro
|
|
21
|
+
if (v.includes(": ")) return "contiene ': ' sin comillas (YAML lo lee como un mapa)";
|
|
22
|
+
if (v.includes(" #")) return "contiene ' #' sin comillas (YAML lo lee como un comentario)";
|
|
23
|
+
if (YAML_INDICATORS.test(v)) return `empieza con '${v[0]}', que YAML reserva`;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// El valor crudo de una clave del frontmatter, tal cual está escrito (con comillas si
|
|
28
|
+
// las tiene). Es lo que hay que validar: `parseFrontmatter` ya las saca.
|
|
29
|
+
export function rawFrontmatterValue(md, key) {
|
|
30
|
+
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
const line = m[1].match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
33
|
+
return line ? line[1].trim() : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Saca las comillas de un escalar YAML citado. Sin comillas, lo devuelve tal cual.
|
|
37
|
+
function unquoteScalar(s) {
|
|
38
|
+
if (s == null) return null;
|
|
39
|
+
const v = s.trim();
|
|
40
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
|
|
41
|
+
try { return JSON.parse(v); } catch { return v.slice(1, -1); }
|
|
42
|
+
}
|
|
43
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
46
|
+
|
|
7
47
|
// Parsea el frontmatter YAML de un SKILL.md → { name, description, body }.
|
|
48
|
+
// Devuelve los valores YA sin comillas, para que quien los reserialice (skillToCursor)
|
|
49
|
+
// no los cite dos veces.
|
|
8
50
|
export function parseFrontmatter(md) {
|
|
9
51
|
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
10
52
|
if (!m) return { name: null, description: null, body: md.trim() };
|
|
11
53
|
const fm = m[1];
|
|
12
|
-
const name = (fm.match(/^name:\s*(.+)$/m) || [])[1]
|
|
13
|
-
const description = (fm.match(/^description:\s*(.+)$/m) || [])[1]
|
|
54
|
+
const name = unquoteScalar((fm.match(/^name:\s*(.+)$/m) || [])[1]) || null;
|
|
55
|
+
const description = unquoteScalar((fm.match(/^description:\s*(.+)$/m) || [])[1]) || null;
|
|
14
56
|
return { name, description, body: m[2].trim() };
|
|
15
57
|
}
|
|
16
58
|
|
|
17
|
-
// Valida el contrato MÍNIMO de un SKILL.md para que dai lo ingiera y
|
|
18
|
-
//
|
|
19
|
-
//
|
|
59
|
+
// Valida el contrato MÍNIMO de un SKILL.md para que dai lo ingiera y los asistentes lo
|
|
60
|
+
// carguen: frontmatter con `name` y `description`, y que ambos sean YAML válido — si no,
|
|
61
|
+
// el asistente descarta la skill entera. Devuelve null si está OK, o el motivo.
|
|
20
62
|
// NO valida el contenido de la skill — eso es criterio del equipo (ADR-0013).
|
|
21
63
|
export function validateSkill(md) {
|
|
22
64
|
const { name, description } = parseFrontmatter(md);
|
|
23
65
|
if (!name && !description) return "sin frontmatter (falta name y description)";
|
|
24
66
|
if (!name) return "falta 'name' en el frontmatter";
|
|
25
67
|
if (!description) return "falta 'description' en el frontmatter";
|
|
68
|
+
for (const key of ["name", "description"]) {
|
|
69
|
+
const issue = yamlScalarIssue(rawFrontmatterValue(md, key));
|
|
70
|
+
if (issue) return `'${key}' no es YAML válido: ${issue} — citá el valor con comillas dobles`;
|
|
71
|
+
}
|
|
26
72
|
return null;
|
|
27
73
|
}
|
|
28
74
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const fm = [
|
|
34
|
-
"---",
|
|
35
|
-
"mode: agent",
|
|
36
|
-
description ? `description: ${JSON.stringify(description)}` : null,
|
|
37
|
-
"---",
|
|
38
|
-
].filter((x) => x !== null).join("\n");
|
|
39
|
-
return `${fm}\n\n${body}\n`;
|
|
40
|
-
}
|
|
75
|
+
// Los archivos de Copilot que dai generaba ANTES de que Copilot adoptara Agent Skills
|
|
76
|
+
// (ADR-0014). `dai init` los borra al reencontrarlos, para que no queden duplicando
|
|
77
|
+
// cada `/comando` con una copia vieja y sin templates.
|
|
78
|
+
export const stalePromptFiles = (skills) => skills.map((n) => `${n}.prompt.md`);
|
|
41
79
|
|
|
42
80
|
// Transforma un SKILL.md (Claude) en un SKILL.md de Cursor.
|
|
43
81
|
// Conserva name/description/body y ajusta solo el frontmatter.
|
|
@@ -60,7 +98,17 @@ export function envFor(pm) {
|
|
|
60
98
|
return head + "DAI_PM=clickup\nDAI_CLICKUP_TOKEN=\nDAI_CLICKUP_LIST_ID=\nDAI_TRACKER_URL_TEMPLATE=https://app.clickup.com/t/{id}\n";
|
|
61
99
|
}
|
|
62
100
|
if (pm === "jira") {
|
|
63
|
-
return head +
|
|
101
|
+
return head +
|
|
102
|
+
"DAI_PM=jira\n" +
|
|
103
|
+
"DAI_JIRA_BASE_URL=\n" +
|
|
104
|
+
"DAI_JIRA_EMAIL=\n" +
|
|
105
|
+
"DAI_JIRA_TOKEN=\n" +
|
|
106
|
+
"# La clave del PROYECTO (p. ej. PROJ), no la de un ticket (PROJ-123).\n" +
|
|
107
|
+
"DAI_JIRA_PROJECT=\n" +
|
|
108
|
+
"DAI_JIRA_ISSUETYPE=Story\n" +
|
|
109
|
+
"# Campos propios que tu Jira exige al crear. Si el archivo no existe, se ignora.\n" +
|
|
110
|
+
"DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json\n" +
|
|
111
|
+
"DAI_TRACKER_URL_TEMPLATE=\n";
|
|
64
112
|
}
|
|
65
113
|
return head + "DAI_PM=md\nDAI_MD_US_DIR=.dai/us\n";
|
|
66
114
|
}
|
|
@@ -124,7 +172,7 @@ export function reconcileGitignore(text, want) {
|
|
|
124
172
|
// copilot-instructions.md (Copilot). Mismo núcleo, distinto encabezado.
|
|
125
173
|
export function constitution(kind) {
|
|
126
174
|
const head = kind === "copilot"
|
|
127
|
-
? "# Instrucciones de Copilot para este repo\n\nEste repo sigue la metodología **dai**. Aplica estas reglas en todo lo que generes.\n\n> **Superficie:**
|
|
175
|
+
? "# Instrucciones de Copilot para este repo\n\nEste repo sigue la metodología **dai**. Aplica estas reglas en todo lo que generes.\n\n> **Superficie:** las skills de dai (`.github/skills/`) se invocan con `/nombre-skill`, o\n> cuando el agente las detecta por su descripción. Funcionan en el Copilot CLI, en la app,\n> y en modo agente de VS Code / JetBrains (ADR-0014). El CLI `dai` corre en cualquier\n> terminal."
|
|
128
176
|
: kind === "cursor"
|
|
129
177
|
? "# Constitución del proyecto (dai)\n\nEste repo sigue la metodología **dai**. Estas reglas gobiernan todo el trabajo.\n\n> **Superficie:** las skills de dai (`.cursor/skills/`) se invocan con `/nombre-skill`\n> o cuando el agente las detecta por descripción. El CLI `dai` corre en cualquier terminal."
|
|
130
178
|
: "# Constitución del proyecto (dai)\n\nEste repo sigue la metodología **dai**. Estas reglas gobiernan todo el trabajo.";
|
|
@@ -145,6 +193,8 @@ export function constitution(kind) {
|
|
|
145
193
|
- **Verifica el comportamiento, no solo que compile:** que pase el chequeo estático o el build no prueba que funcione; ejercita el flujo real antes de darlo por hecho.
|
|
146
194
|
- **La IA confirma antes de construir:** el asistente declara que entendió esta constitución y la va a obedecer antes de generar código.
|
|
147
195
|
- **Secretos:** en \`.env\` (nunca commiteados). git por **SSH**, APIs por **token scopeado**.
|
|
196
|
+
- **No bajes la seguridad para avanzar:** si una llamada falla por el certificado, declara la CA (\`NODE_EXTRA_CA_CERTS\`). **Nunca** \`NODE_TLS_REJECT_UNAUTHORIZED=0\`, \`verify=False\`, \`-k\` ni equivalentes: apagan la verificación de toda la conexión, y por ahí viajan los tokens.
|
|
197
|
+
- **Si el CLI no llega, para y dilo:** cuando \`dai\` no cubre un caso, repórtalo — no improvises una llamada a la API por fuera. El atajo publica igual, pero rompe el link QUÉ↔CÓMO en silencio y nadie se entera hasta que la trazabilidad ya está mal.
|
|
148
198
|
- **Docs vivas:** una constitución o arquitectura desactualizada es un defecto, no documentación.
|
|
149
199
|
- Separa el QUÉ (funcional) del CÓMO (técnico); no mezcles.
|
|
150
200
|
|