navori 0.2.0 → 0.2.2

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.
Files changed (24) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -17
  3. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +15 -9
  4. package/dist/assets/core/core-assets/lib-skills/formik.md +57 -0
  5. package/dist/assets/core/core-assets/lib-skills/joi-validation.md +73 -0
  6. package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/mongoose.md +3 -2
  7. package/dist/assets/core/core-assets/lib-skills/redux-toolkit.md +59 -0
  8. package/dist/assets/core/core-assets/lib-skills/socketio.md +56 -0
  9. package/dist/assets/core/core-assets/lib-skills/tanstack-query.md +58 -0
  10. package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
  11. package/dist/assets/core/core-assets/managed/idioma-rol.md +1 -1
  12. package/dist/assets/core/core-assets/managed/operaciones-seguras.md +1 -0
  13. package/dist/assets/core/core-assets/presets/background-worker/managed/stack.md +18 -0
  14. package/dist/assets/core/core-assets/presets/background-worker/skills/job-scheduling.md +54 -0
  15. package/dist/assets/core/core-assets/presets/background-worker/skills/queue-consumers.md +55 -0
  16. package/dist/assets/core/core-assets/presets/background-worker/skills/worker-lifecycle.md +57 -0
  17. package/dist/assets/core/core-assets/presets/background-worker.json +39 -0
  18. package/dist/assets/core/core-assets/presets/express-mongoose/managed/stack.md +2 -2
  19. package/dist/assets/core/core-assets/presets/express-mongoose/skills/pr-create.md +18 -18
  20. package/dist/assets/core/core-assets/presets/express-mongoose.json +0 -12
  21. package/dist/assets/core/core-assets/settings/settings-base.json +22 -1
  22. package/dist/index.js +797 -346
  23. package/package.json +5 -4
  24. /package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/zod-validation.md +0 -0
package/dist/index.js CHANGED
@@ -9,8 +9,8 @@ import { fileURLToPath as fileURLToPath2 } from "url";
9
9
  // src/commands/init.ts
10
10
  import { defineCommand as defineCommand2 } from "citty";
11
11
  import * as p2 from "@clack/prompts";
12
- import { resolve as resolve11, join as join10, dirname as dirname6, relative as relative3 } from "path";
13
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, chmodSync as chmodSync2 } from "fs";
12
+ import { resolve as resolve11, join as join11, dirname as dirname6, relative as relative3 } from "path";
13
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, chmodSync as chmodSync2 } from "fs";
14
14
  import { spawnSync as spawnSync2 } from "child_process";
15
15
 
16
16
  // src/lib/config.ts
@@ -175,7 +175,18 @@ var ProjectSchema = z2.object({
175
175
  testsForNewCode: z2.string().optional(),
176
176
  /** Skill ids the user owns under `.claude/skills/<id>.md`. navori never
177
177
  * writes their content — it only indexes them so agents discover them. */
178
- localSkills: z2.array(z2.string()).default([])
178
+ localSkills: z2.array(z2.string()).default([]),
179
+ /** Library-skill ids detected in the repo's deps (socketio, mongoose, …).
180
+ * Cross-preset: a skill is materialized whenever its dependency is present,
181
+ * independent of the active preset. Supersedes the old zod/joiValidation
182
+ * flags. See lib/library-skills.ts. */
183
+ libraries: z2.array(z2.string()).default([]),
184
+ /** Programming language of the repo (ts/js/python/rust/go/unknown), from
185
+ * detection. Drives language-aware baseline blocks — e.g. the TS-only
186
+ * `tipado-fuerte` (any/unknown) is suppressed in python/rust/go. The render
187
+ * derives `typedLanguage` from this; absence is treated as JS/TS for
188
+ * back-compat with configs written before this field existed. */
189
+ codeLanguage: z2.string().optional()
179
190
  }).passthrough();
180
191
  var NavoriConfigSchema = z2.object({
181
192
  $schema: z2.string().optional(),
@@ -186,6 +197,12 @@ var NavoriConfigSchema = z2.object({
186
197
  preset: z2.string().min(1),
187
198
  language: z2.enum(["es", "en"]).default("es"),
188
199
  branchBase: z2.string().default("main"),
200
+ /** Target branch for PRs (`gh pr create --base`). When omitted, PRs target
201
+ * branchBase. Decouples the fork point / protected branch (branchBase) from
202
+ * the PR target — e.g. branch off `main` but open PRs against `develop`.
203
+ * The render derives the effective value (prTarget ?? branchBase) so this
204
+ * stays out of configs that don't need it. */
205
+ prTarget: z2.string().optional(),
189
206
  commits: z2.enum(["conventional", "conventional-es", "free"]).default("conventional-es"),
190
207
  qualityGate: QualityGateSchema.optional(),
191
208
  sdd: SddSchema.optional(),
@@ -207,6 +224,15 @@ function writeConfig(path, input) {
207
224
  const validated = NavoriConfigSchema.parse({ $schema: SCHEMA_URL, ...input });
208
225
  writeFileAtomic(path, JSON.stringify(validated, null, 2) + "\n");
209
226
  }
227
+ function effectiveConfig(config) {
228
+ const codeLanguage = config.project?.codeLanguage;
229
+ const typedLanguage = !(codeLanguage === "python" || codeLanguage === "rust" || codeLanguage === "go");
230
+ return {
231
+ ...config,
232
+ prTarget: config.prTarget ?? config.branchBase,
233
+ project: { ...config.project ?? {}, typedLanguage }
234
+ };
235
+ }
210
236
  var ConfigError = class extends NavoriError {
211
237
  issues;
212
238
  constructor(message, issues) {
@@ -238,8 +264,8 @@ function readConfig(path) {
238
264
  }
239
265
 
240
266
  // src/lib/detect.ts
241
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
242
- import { join as join4, basename as basename2 } from "path";
267
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
268
+ import { join as join5, basename as basename2 } from "path";
243
269
  import { spawnSync } from "child_process";
244
270
 
245
271
  // src/lib/presets.ts
@@ -304,7 +330,16 @@ var PresetExtraFileSchema = z4.object({
304
330
  id: z4.string().min(1),
305
331
  relPath: safeRelPath,
306
332
  /** Where in `.claude/` the file lands (e.g. "skills/medusa-db-migrations.md"). */
307
- destRelPath: safeRelPath
333
+ destRelPath: safeRelPath,
334
+ /**
335
+ * Optional config path (e.g. "project.posture") evaluated with the same
336
+ * truthy semantics as CoreManagedAsset.condition (see resolveCondition). When
337
+ * set, the extra is only materialized — and only listed in the skills index —
338
+ * if the path resolves truthy. Omit for an always-on extra (the default).
339
+ * Lets a preset ship a variant gated on a config flag. (Dependency-driven
340
+ * skills now live in the cross-preset library-skills layer instead.)
341
+ */
342
+ condition: z4.string().optional()
308
343
  });
309
344
  var PresetDefinitionSchema = z4.object({
310
345
  id: z4.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "preset id must be kebab-case"),
@@ -375,9 +410,116 @@ function loadPreset(id, repoRoot) {
375
410
  return { def: result.data, assetRoot: resolved.assetRoot, source: resolved.source };
376
411
  }
377
412
 
378
- // src/lib/claude-infra.ts
379
- import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
413
+ // src/lib/workspace-patterns.ts
414
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
380
415
  import { join as join3 } from "path";
416
+ function collectWorkspacePatterns(cwd) {
417
+ const pnpmYaml = join3(cwd, "pnpm-workspace.yaml");
418
+ if (existsSync3(pnpmYaml)) {
419
+ try {
420
+ return parsePnpmWorkspaceYaml(readFileSync4(pnpmYaml, "utf-8"));
421
+ } catch {
422
+ return [];
423
+ }
424
+ }
425
+ const pkgPath = join3(cwd, "package.json");
426
+ if (existsSync3(pkgPath)) {
427
+ try {
428
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8").replace(/^/, ""));
429
+ return readPackageJsonWorkspaces(pkg.workspaces);
430
+ } catch {
431
+ return [];
432
+ }
433
+ }
434
+ return [];
435
+ }
436
+ function parsePnpmWorkspaceYaml(content) {
437
+ const lines = content.split(/\r?\n/);
438
+ let i = 0;
439
+ while (i < lines.length) {
440
+ const line = lines[i];
441
+ const m = line.match(/^packages\s*:\s*(.*)$/);
442
+ if (m) {
443
+ const inline = m[1].trim();
444
+ if (inline.startsWith("[")) {
445
+ return parseInlineYamlArray(inline);
446
+ }
447
+ const items = [];
448
+ i++;
449
+ while (i < lines.length) {
450
+ const cur = lines[i];
451
+ if (/^\s*#/.test(cur) || /^\s*$/.test(cur)) {
452
+ i++;
453
+ continue;
454
+ }
455
+ const itemMatch = cur.match(/^\s+-\s+(.+?)\s*(?:#.*)?$/);
456
+ if (!itemMatch) break;
457
+ const raw = itemMatch[1].trim();
458
+ const unquoted = stripYamlQuotes(raw);
459
+ if (unquoted && !unquoted.startsWith("!")) items.push(unquoted);
460
+ i++;
461
+ }
462
+ return items;
463
+ }
464
+ i++;
465
+ }
466
+ return [];
467
+ }
468
+ function parseInlineYamlArray(inline) {
469
+ const closed = inline.lastIndexOf("]");
470
+ if (closed < 0) return [];
471
+ const body = inline.slice(1, closed);
472
+ return body.split(",").map((s) => stripYamlQuotes(s.trim())).filter((s) => s && !s.startsWith("!"));
473
+ }
474
+ function stripYamlQuotes(raw) {
475
+ if (raw.length >= 2) {
476
+ const first = raw[0];
477
+ const last = raw[raw.length - 1];
478
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
479
+ return raw.slice(1, -1);
480
+ }
481
+ }
482
+ return raw;
483
+ }
484
+ function readPackageJsonWorkspaces(raw) {
485
+ if (Array.isArray(raw)) {
486
+ return raw.filter((s) => typeof s === "string" && !s.startsWith("!"));
487
+ }
488
+ if (raw && typeof raw === "object" && "packages" in raw) {
489
+ const pkgs = raw.packages;
490
+ if (Array.isArray(pkgs)) {
491
+ return pkgs.filter((s) => typeof s === "string" && !s.startsWith("!"));
492
+ }
493
+ }
494
+ return [];
495
+ }
496
+
497
+ // src/lib/library-skills.ts
498
+ var LIBRARY_SKILLS = [
499
+ { id: "socketio", deps: ["socket.io", "socket.io-client"], label: "Socket.IO realtime" },
500
+ { id: "redux-toolkit", deps: ["@reduxjs/toolkit", "redux"], label: "Redux Toolkit" },
501
+ {
502
+ id: "tanstack-query",
503
+ deps: ["@tanstack/react-query", "vue-query", "solid-query"],
504
+ label: "TanStack Query"
505
+ },
506
+ { id: "formik", deps: ["formik"], label: "Formik forms" },
507
+ { id: "mongoose", deps: ["mongoose", "@nestjs/mongoose"], label: "Mongoose ODM" },
508
+ { id: "zod-validation", deps: ["zod"], label: "Zod validation" },
509
+ { id: "joi-validation", deps: ["joi", "@hapi/joi"], label: "Joi validation" }
510
+ ];
511
+ var BY_ID = new Map(LIBRARY_SKILLS.map((s) => [s.id, s]));
512
+ function detectLibrarySkills(deps) {
513
+ const present = new Set(deps);
514
+ return LIBRARY_SKILLS.filter((s) => s.deps.some((d) => present.has(d))).map((s) => s.id);
515
+ }
516
+ function librarySkillById(id) {
517
+ return BY_ID.get(id) ?? null;
518
+ }
519
+
520
+ // src/lib/claude-infra.ts
521
+ import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
522
+ import { join as join4 } from "path";
381
523
  function safeReaddir(path) {
382
524
  try {
383
525
  return readdirSync2(path);
@@ -391,10 +533,10 @@ function listMarkdownFiles(dir) {
391
533
  function listSkillDirs(dir) {
392
534
  const result = [];
393
535
  for (const entry of safeReaddir(dir)) {
394
- const full = join3(dir, entry);
536
+ const full = join4(dir, entry);
395
537
  try {
396
538
  if (statSync2(full).isDirectory()) {
397
- if (existsSync3(join3(full, "SKILL.md"))) result.push(entry);
539
+ if (existsSync4(join4(full, "SKILL.md"))) result.push(entry);
398
540
  } else if (entry.endsWith(".md")) {
399
541
  result.push(entry);
400
542
  }
@@ -407,7 +549,7 @@ function countFilesIn(dir) {
407
549
  let count = 0;
408
550
  for (const entry of safeReaddir(dir)) {
409
551
  try {
410
- if (statSync2(join3(dir, entry)).isFile()) count++;
552
+ if (statSync2(join4(dir, entry)).isFile()) count++;
411
553
  } catch {
412
554
  }
413
555
  }
@@ -417,27 +559,27 @@ function countSubdirs(dir) {
417
559
  let count = 0;
418
560
  for (const entry of safeReaddir(dir)) {
419
561
  try {
420
- if (statSync2(join3(dir, entry)).isDirectory()) count++;
562
+ if (statSync2(join4(dir, entry)).isDirectory()) count++;
421
563
  } catch {
422
564
  }
423
565
  }
424
566
  return count;
425
567
  }
426
568
  function detectClaudeInfra(cwd) {
427
- const claudeDir = join3(cwd, ".claude");
428
- const agentsDir = join3(claudeDir, "agents");
429
- const skillsDir = join3(claudeDir, "skills");
569
+ const claudeDir = join4(cwd, ".claude");
570
+ const agentsDir = join4(claudeDir, "agents");
571
+ const skillsDir = join4(claudeDir, "skills");
430
572
  const agentFiles = listMarkdownFiles(agentsDir);
431
573
  const skillFiles = listSkillDirs(skillsDir);
432
- const hasSettings = existsSync3(join3(claudeDir, "settings.json"));
433
- const hasLocalSettings = existsSync3(join3(claudeDir, "settings.local.json"));
434
- const hasClaudeMd = existsSync3(join3(cwd, "CLAUDE.md"));
435
- const hasAgentsMd = existsSync3(join3(cwd, "AGENTS.md"));
436
- const hasCheckpointsMd = existsSync3(join3(cwd, "CHECKPOINTS.md"));
437
- const hasFeatureList = existsSync3(join3(cwd, "feature_list.json"));
438
- const hasNavoriConfig = existsSync3(join3(cwd, "navori.config.json"));
439
- const progressFiles = existsSync3(join3(cwd, "progress")) ? countFilesIn(join3(cwd, "progress")) : 0;
440
- const specsDirs = existsSync3(join3(cwd, "specs")) ? countSubdirs(join3(cwd, "specs")) : 0;
574
+ const hasSettings = existsSync4(join4(claudeDir, "settings.json"));
575
+ const hasLocalSettings = existsSync4(join4(claudeDir, "settings.local.json"));
576
+ const hasClaudeMd = existsSync4(join4(cwd, "CLAUDE.md"));
577
+ const hasAgentsMd = existsSync4(join4(cwd, "AGENTS.md"));
578
+ const hasCheckpointsMd = existsSync4(join4(cwd, "CHECKPOINTS.md"));
579
+ const hasFeatureList = existsSync4(join4(cwd, "feature_list.json"));
580
+ const hasNavoriConfig = existsSync4(join4(cwd, "navori.config.json"));
581
+ const progressFiles = existsSync4(join4(cwd, "progress")) ? countFilesIn(join4(cwd, "progress")) : 0;
582
+ const specsDirs = existsSync4(join4(cwd, "specs")) ? countSubdirs(join4(cwd, "specs")) : 0;
441
583
  const present = agentFiles.length > 0 || skillFiles.length > 0 || hasSettings || hasLocalSettings || hasClaudeMd || hasAgentsMd || hasCheckpointsMd || hasFeatureList || progressFiles > 0 || specsDirs > 0;
442
584
  return {
443
585
  present,
@@ -474,6 +616,7 @@ function detectProject(cwd) {
474
616
  const packageManager = detectPackageManager(cwd);
475
617
  const monorepo = detectMonorepo(cwd);
476
618
  const stack = detectStack(cwd, pkg, pyproject, cargo);
619
+ const libraries = detectLibrarySkills(stack.deps);
477
620
  const { preset: suggestedPreset, gap: suggestedPresetGap } = suggestPreset(stack, monorepo);
478
621
  const qualityGate = guessQualityGate(pkg, packageManager, stack);
479
622
  const claudeInfra = detectClaudeInfra(cwd);
@@ -484,6 +627,7 @@ function detectProject(cwd) {
484
627
  packageManager,
485
628
  monorepo,
486
629
  stack,
630
+ libraries,
487
631
  suggestedPreset,
488
632
  suggestedPresetGap,
489
633
  qualityGate,
@@ -495,6 +639,29 @@ function detectProject(cwd) {
495
639
  }
496
640
  };
497
641
  }
642
+ var PLACEHOLDER_NAMES = /* @__PURE__ */ new Set([
643
+ "temp-app",
644
+ "temp",
645
+ "tmp",
646
+ "my-app",
647
+ "myapp",
648
+ "my-project",
649
+ "your-app",
650
+ "your-project",
651
+ "your-app-name",
652
+ "app-name",
653
+ "project-name",
654
+ "changeme",
655
+ "change-me",
656
+ "example-app",
657
+ "sample-app",
658
+ "new-project",
659
+ "untitled",
660
+ "placeholder"
661
+ ]);
662
+ function isPlaceholderName(name) {
663
+ return PLACEHOLDER_NAMES.has(name.trim().toLowerCase());
664
+ }
498
665
  function normalizeName(raw) {
499
666
  if (typeof raw !== "string" || !raw) return null;
500
667
  const cleaned = raw.trim().toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -502,20 +669,20 @@ function normalizeName(raw) {
502
669
  return /^[a-z0-9]/.test(cleaned) ? cleaned : null;
503
670
  }
504
671
  function readPackageJson(cwd) {
505
- const path = join4(cwd, "package.json");
506
- if (!existsSync4(path)) return null;
672
+ const path = join5(cwd, "package.json");
673
+ if (!existsSync5(path)) return null;
507
674
  try {
508
- const raw = readFileSync4(path, "utf-8").replace(/^/, "");
675
+ const raw = readFileSync5(path, "utf-8").replace(/^/, "");
509
676
  return JSON.parse(raw);
510
677
  } catch {
511
678
  return null;
512
679
  }
513
680
  }
514
681
  function readPyproject(cwd) {
515
- const path = join4(cwd, "pyproject.toml");
516
- if (!existsSync4(path)) return null;
682
+ const path = join5(cwd, "pyproject.toml");
683
+ if (!existsSync5(path)) return null;
517
684
  try {
518
- const content = readFileSync4(path, "utf-8");
685
+ const content = readFileSync5(path, "utf-8");
519
686
  const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
520
687
  const deps = [];
521
688
  const depBlock = content.match(/\[(?:tool\.poetry\.)?dependencies\]([\s\S]*?)(?:\n\[|$)/);
@@ -539,10 +706,10 @@ function readPyproject(cwd) {
539
706
  }
540
707
  }
541
708
  function readCargoToml(cwd) {
542
- const path = join4(cwd, "Cargo.toml");
543
- if (!existsSync4(path)) return null;
709
+ const path = join5(cwd, "Cargo.toml");
710
+ if (!existsSync5(path)) return null;
544
711
  try {
545
- const content = readFileSync4(path, "utf-8");
712
+ const content = readFileSync5(path, "utf-8");
546
713
  const match = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
547
714
  return { name: match?.[1] ?? null };
548
715
  } catch {
@@ -588,10 +755,10 @@ function detectBranchBase(cwd) {
588
755
  }
589
756
  function detectExistingEngines(cwd) {
590
757
  const found = [];
591
- if (existsSync4(join4(cwd, ".claude"))) found.push("claude");
592
- if (existsSync4(join4(cwd, "AGENTS.md"))) found.push("agents-md");
593
- if (existsSync4(join4(cwd, ".cursor"))) found.push("cursor");
594
- if (existsSync4(join4(cwd, ".github", "copilot-instructions.md"))) found.push("copilot");
758
+ if (existsSync5(join5(cwd, ".claude"))) found.push("claude");
759
+ if (existsSync5(join5(cwd, "AGENTS.md"))) found.push("agents-md");
760
+ if (existsSync5(join5(cwd, ".cursor"))) found.push("cursor");
761
+ if (existsSync5(join5(cwd, ".github", "copilot-instructions.md"))) found.push("copilot");
595
762
  return found;
596
763
  }
597
764
  function detectPackageManager(cwd) {
@@ -600,42 +767,44 @@ function detectPackageManager(cwd) {
600
767
  const tool = pkg.packageManager.split("@")[0];
601
768
  if (tool === "pnpm" || tool === "npm" || tool === "yarn" || tool === "bun") return tool;
602
769
  }
603
- if (existsSync4(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
604
- if (existsSync4(join4(cwd, "bun.lockb")) || existsSync4(join4(cwd, "bun.lock"))) return "bun";
605
- if (existsSync4(join4(cwd, "yarn.lock"))) return "yarn";
606
- if (existsSync4(join4(cwd, "package-lock.json"))) return "npm";
770
+ if (existsSync5(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
771
+ if (existsSync5(join5(cwd, "bun.lockb")) || existsSync5(join5(cwd, "bun.lock"))) return "bun";
772
+ if (existsSync5(join5(cwd, "yarn.lock"))) return "yarn";
773
+ if (existsSync5(join5(cwd, "package-lock.json"))) return "npm";
607
774
  return null;
608
775
  }
609
776
  function detectPackageManagerSource(cwd) {
610
777
  const pkg = readPackageJson(cwd);
611
778
  if (pkg?.packageManager) return "package.json";
612
- if (existsSync4(join4(cwd, "pnpm-lock.yaml"))) return "pnpm-lock.yaml";
613
- if (existsSync4(join4(cwd, "bun.lockb")) || existsSync4(join4(cwd, "bun.lock"))) return "bun.lock";
614
- if (existsSync4(join4(cwd, "yarn.lock"))) return "yarn.lock";
615
- if (existsSync4(join4(cwd, "package-lock.json"))) return "package-lock.json";
779
+ if (existsSync5(join5(cwd, "pnpm-lock.yaml"))) return "pnpm-lock.yaml";
780
+ if (existsSync5(join5(cwd, "bun.lockb")) || existsSync5(join5(cwd, "bun.lock"))) return "bun.lock";
781
+ if (existsSync5(join5(cwd, "yarn.lock"))) return "yarn.lock";
782
+ if (existsSync5(join5(cwd, "package-lock.json"))) return "package-lock.json";
616
783
  return "unknown";
617
784
  }
618
785
  function detectMonorepo(cwd) {
619
- if (existsSync4(join4(cwd, "pnpm-workspace.yaml"))) {
620
- if (existsSync4(join4(cwd, "turbo.json"))) {
621
- return { tool: "turbo", source: "turbo.json + pnpm-workspace.yaml" };
786
+ if (existsSync5(join5(cwd, "pnpm-workspace.yaml"))) {
787
+ if (collectWorkspacePatterns(cwd).length > 0) {
788
+ if (existsSync5(join5(cwd, "turbo.json"))) {
789
+ return { tool: "turbo", source: "turbo.json + pnpm-workspace.yaml" };
790
+ }
791
+ return { tool: "pnpm", source: "pnpm-workspace.yaml" };
622
792
  }
623
- return { tool: "pnpm", source: "pnpm-workspace.yaml" };
624
793
  }
625
- if (existsSync4(join4(cwd, "turbo.json"))) {
794
+ if (existsSync5(join5(cwd, "turbo.json"))) {
626
795
  return { tool: "turbo", source: "turbo.json" };
627
796
  }
628
- if (existsSync4(join4(cwd, "nx.json"))) {
797
+ if (existsSync5(join5(cwd, "nx.json"))) {
629
798
  return { tool: "nx", source: "nx.json" };
630
799
  }
631
- if (existsSync4(join4(cwd, "rush.json"))) {
800
+ if (existsSync5(join5(cwd, "rush.json"))) {
632
801
  return { tool: "rush", source: "rush.json" };
633
802
  }
634
- if (existsSync4(join4(cwd, "lerna.json"))) {
803
+ if (existsSync5(join5(cwd, "lerna.json"))) {
635
804
  return { tool: "lerna", source: "lerna.json" };
636
805
  }
637
806
  const pkg = readPackageJson(cwd);
638
- if (pkg?.workspaces) {
807
+ if (pkg?.workspaces && collectWorkspacePatterns(cwd).length > 0) {
639
808
  return { tool: "npm", source: "package.json workspaces" };
640
809
  }
641
810
  return null;
@@ -664,6 +833,7 @@ function detectStack(cwd, pkg, pyproject, cargo) {
664
833
  forms: pick(deps, "pydantic") ?? null,
665
834
  state: null,
666
835
  test: pick(deps, "pytest") ?? null,
836
+ worker: pick(deps, "celery", "rq", "dramatiq", "apscheduler") ?? null,
667
837
  deps: Array.from(deps)
668
838
  };
669
839
  }
@@ -675,6 +845,7 @@ function detectStack(cwd, pkg, pyproject, cargo) {
675
845
  forms: null,
676
846
  state: null,
677
847
  test: null,
848
+ worker: null,
678
849
  deps: []
679
850
  };
680
851
  }
@@ -687,10 +858,11 @@ function detectStack(cwd, pkg, pyproject, cargo) {
687
858
  forms: null,
688
859
  state: null,
689
860
  test: null,
861
+ worker: null,
690
862
  deps: []
691
863
  };
692
864
  }
693
- const hasTs = nodeDeps.has("typescript") || existsSync4(join4(cwd, "tsconfig.json"));
865
+ const hasTs = nodeDeps.has("typescript") || existsSync5(join5(cwd, "tsconfig.json"));
694
866
  const framework = (
695
867
  // Application frameworks (they own the project)
696
868
  pick(nodeDeps, "next") ?? pick(nodeDeps, "@nestjs/core") ?? pick(nodeDeps, "@medusajs/medusa") ?? pick(nodeDeps, "@keystone-6/core") ?? pick(nodeDeps, "expo") ?? pick(nodeDeps, "react-native") ?? pick(nodeDeps, "remix") ?? // Meta-frameworks on top of build tools — check before vite/react
@@ -702,6 +874,21 @@ function detectStack(cwd, pkg, pyproject, cargo) {
702
874
  const forms = pick(nodeDeps, "formik") ?? pick(nodeDeps, "react-hook-form") ?? pick(nodeDeps, "@mantine/form") ?? pick(nodeDeps, "vee-validate") ?? null;
703
875
  const state = pick(nodeDeps, "@reduxjs/toolkit") ?? pick(nodeDeps, "redux") ?? pick(nodeDeps, "zustand") ?? pick(nodeDeps, "jotai") ?? pick(nodeDeps, "valtio") ?? pick(nodeDeps, "@tanstack/react-query") ?? pick(nodeDeps, "@apollo/client") ?? null;
704
876
  const test = pick(nodeDeps, "vitest") ?? pick(nodeDeps, "jest") ?? pick(nodeDeps, "@playwright/test") ?? pick(nodeDeps, "cypress") ?? null;
877
+ const worker = pick(
878
+ nodeDeps,
879
+ "agenda",
880
+ "bullmq",
881
+ "bull",
882
+ "bee-queue",
883
+ "bree",
884
+ "node-cron",
885
+ "cron",
886
+ "amqplib",
887
+ "amqp-connection-manager",
888
+ "kafkajs",
889
+ "sqs-consumer",
890
+ "rhea"
891
+ );
705
892
  return {
706
893
  language: hasTs ? "ts" : pkg ? "js" : "unknown",
707
894
  framework,
@@ -709,6 +896,7 @@ function detectStack(cwd, pkg, pyproject, cargo) {
709
896
  forms,
710
897
  state,
711
898
  test,
899
+ worker,
712
900
  deps: Array.from(nodeDeps)
713
901
  };
714
902
  }
@@ -758,6 +946,7 @@ function pickPresetCandidate(stack, monorepo) {
758
946
  if (fw === "fastify") return "fastify";
759
947
  if (fw === "hono") return "hono";
760
948
  if (fw === "elysia") return "elysia";
949
+ if (stack.worker && (fw === "express" || fw === null)) return "background-worker";
761
950
  if (fw === "express") return "express-mongoose";
762
951
  return "custom";
763
952
  }
@@ -772,29 +961,24 @@ function guessQualityGate(pkg, pm, stack) {
772
961
  const runner = pm ?? "npm";
773
962
  const scripts = pkg.scripts ?? {};
774
963
  const has = (name) => typeof scripts[name] === "string";
775
- if (has("validate")) {
776
- return { fast: `${runner} run typecheck`, full: `${runner} run validate` };
777
- }
778
- if (has("check:all")) {
779
- return { fast: `${runner} run typecheck`, full: `${runner} run check:all` };
780
- }
964
+ const run = (name) => `${runner} run ${name}`;
965
+ const typecheck = has("typecheck") ? run("typecheck") : has("type-check") ? run("type-check") : has("check") ? run("check") : has("compile") ? run("compile") : null;
966
+ if (has("validate")) return { fast: typecheck ?? run("validate"), full: run("validate") };
967
+ if (has("check:all")) return { fast: typecheck ?? run("check:all"), full: run("check:all") };
781
968
  const fastParts = [];
782
- if (has("typecheck")) fastParts.push(`${runner} run typecheck`);
783
- else if (has("type-check")) fastParts.push(`${runner} run type-check`);
784
- else if (has("check")) fastParts.push(`${runner} run check`);
785
- else if (has("compile")) fastParts.push(`${runner} run compile`);
969
+ if (typecheck) fastParts.push(typecheck);
786
970
  const fullParts = [...fastParts];
787
- if (has("lint")) fullParts.push(`${runner} run lint`);
788
- if (has("test:unit")) fullParts.push(`${runner} run test:unit`);
789
- else if (has("test")) fullParts.push(`${runner} run test`);
790
- if (fastParts.length === 0 && fullParts.length === 0) return null;
791
- const fast = fastParts.join(" && ") || `${runner} run lint`;
792
- const full = fullParts.join(" && ") || fast;
971
+ if (has("lint")) fullParts.push(run("lint"));
972
+ if (has("test:unit")) fullParts.push(run("test:unit"));
973
+ else if (has("test")) fullParts.push(run("test"));
974
+ if (fullParts.length === 0) return null;
975
+ const fast = fastParts.length > 0 ? fastParts.join(" && ") : fullParts[0];
976
+ const full = fullParts.join(" && ");
793
977
  return { fast, full };
794
978
  }
795
979
 
796
980
  // src/lib/plugins.ts
797
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
981
+ import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
798
982
  import { resolve as resolve3, sep } from "path";
799
983
  import { z as z5 } from "zod";
800
984
  var AGENT_ROLES = [
@@ -914,12 +1098,12 @@ function loadPlugin(pluginId) {
914
1098
  }
915
1099
  const packageRoot = getPluginPath(pluginId);
916
1100
  const manifestPath = bundledPluginManifestPath(pluginId);
917
- if (!existsSync5(manifestPath)) {
1101
+ if (!existsSync6(manifestPath)) {
918
1102
  throw new PluginManifestError(`plugin.json not found at ${manifestPath}`);
919
1103
  }
920
1104
  let parsed;
921
1105
  try {
922
- parsed = JSON.parse(readFileSync5(manifestPath, "utf-8"));
1106
+ parsed = JSON.parse(readFileSync6(manifestPath, "utf-8"));
923
1107
  } catch (err) {
924
1108
  throw new PluginManifestError(`Invalid JSON in ${manifestPath}: ${err.message}`);
925
1109
  }
@@ -976,8 +1160,8 @@ function loadEnabledPlugins(pluginsConfig) {
976
1160
  }
977
1161
 
978
1162
  // src/lib/migrate.ts
979
- import { mkdirSync, existsSync as existsSync6, copyFileSync, readdirSync as readdirSync3, statSync as statSync3, rmSync as rmSync2 } from "fs";
980
- import { join as join5, resolve as resolve4, dirname as dirname3 } from "path";
1163
+ import { mkdirSync, existsSync as existsSync7, copyFileSync, readdirSync as readdirSync3, statSync as statSync3, rmSync as rmSync2 } from "fs";
1164
+ import { join as join6, resolve as resolve4, dirname as dirname3 } from "path";
981
1165
 
982
1166
  // src/lib/home.ts
983
1167
  import { homedir } from "os";
@@ -994,7 +1178,7 @@ function safeHomedir() {
994
1178
 
995
1179
  // src/lib/migrate.ts
996
1180
  function migrationsRootLazy() {
997
- return join5(safeHomedir(), ".navori", "migrations");
1181
+ return join6(safeHomedir(), ".navori", "migrations");
998
1182
  }
999
1183
  function timestamp() {
1000
1184
  const d = /* @__PURE__ */ new Date();
@@ -1018,7 +1202,7 @@ function copyRecursive(src, dest) {
1018
1202
  if (stat.isDirectory()) {
1019
1203
  mkdirSync(dest, { recursive: true });
1020
1204
  for (const entry of readdirSync3(src)) {
1021
- copyRecursive(join5(src, entry), join5(dest, entry));
1205
+ copyRecursive(join6(src, entry), join6(dest, entry));
1022
1206
  }
1023
1207
  } else if (stat.isFile()) {
1024
1208
  mkdirSync(dirname3(dest), { recursive: true });
@@ -1026,7 +1210,7 @@ function copyRecursive(src, dest) {
1026
1210
  }
1027
1211
  }
1028
1212
  function createMigrationBackup(repoRoot, repoName) {
1029
- const dir = join5(migrationsRootLazy(), timestamp(), repoName);
1213
+ const dir = join6(migrationsRootLazy(), timestamp(), repoName);
1030
1214
  mkdirSync(dir, { recursive: true });
1031
1215
  const candidates = [
1032
1216
  ".claude",
@@ -1040,8 +1224,8 @@ function createMigrationBackup(repoRoot, repoName) {
1040
1224
  const moved = [];
1041
1225
  for (const rel of candidates) {
1042
1226
  const src = resolve4(repoRoot, rel);
1043
- if (!existsSync6(src)) continue;
1044
- const dest = join5(dir, rel);
1227
+ if (!existsSync7(src)) continue;
1228
+ const dest = join6(dir, rel);
1045
1229
  copyRecursive(src, dest);
1046
1230
  moved.push(rel);
1047
1231
  }
@@ -1050,7 +1234,7 @@ function createMigrationBackup(repoRoot, repoName) {
1050
1234
  function removeOriginals(repoRoot, paths) {
1051
1235
  for (const rel of paths) {
1052
1236
  const target = resolve4(repoRoot, rel);
1053
- if (!existsSync6(target)) continue;
1237
+ if (!existsSync7(target)) continue;
1054
1238
  rmSync2(target, { recursive: true, force: true });
1055
1239
  }
1056
1240
  }
@@ -1059,11 +1243,11 @@ function migrationsRoot() {
1059
1243
  }
1060
1244
 
1061
1245
  // src/lib/workspace.ts
1062
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync4, mkdirSync as mkdirSync2, statSync as statSync4, copyFileSync as copyFileSync2, rmSync as rmSync3 } from "fs";
1063
- import { join as join6 } from "path";
1246
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync4, mkdirSync as mkdirSync2, statSync as statSync4, copyFileSync as copyFileSync2, rmSync as rmSync3 } from "fs";
1247
+ import { join as join7 } from "path";
1064
1248
  import { z as z6 } from "zod";
1065
1249
  function workspacesRootLazy() {
1066
- return join6(safeHomedir(), ".navori", "workspaces");
1250
+ return join7(safeHomedir(), ".navori", "workspaces");
1067
1251
  }
1068
1252
  var MANIFEST_NAME = "workspace.json";
1069
1253
  var RepoEntrySchema = z6.object({
@@ -1075,6 +1259,7 @@ var RepoEntrySchema = z6.object({
1075
1259
  });
1076
1260
  var WorkspaceDefaultsSchema = z6.object({
1077
1261
  branchBase: z6.string().optional(),
1262
+ prTarget: z6.string().optional(),
1078
1263
  commits: z6.enum(["conventional", "conventional-es", "free"]).optional(),
1079
1264
  language: z6.enum(["es", "en"]).optional(),
1080
1265
  engines: z6.array(z6.string()).optional(),
@@ -1105,13 +1290,13 @@ var WorkspaceError = class extends NavoriError {
1105
1290
  }
1106
1291
  };
1107
1292
  function workspaceDirectory(name) {
1108
- return join6(workspacesRootLazy(), name);
1293
+ return join7(workspacesRootLazy(), name);
1109
1294
  }
1110
1295
  function workspacePath(name) {
1111
- return join6(workspaceDirectory(name), MANIFEST_NAME);
1296
+ return join7(workspaceDirectory(name), MANIFEST_NAME);
1112
1297
  }
1113
1298
  function legacyWorkspacePath(name) {
1114
- return join6(workspacesRootLazy(), `${name}.json`);
1299
+ return join7(workspacesRootLazy(), `${name}.json`);
1115
1300
  }
1116
1301
  function ensureWorkspacesRoot() {
1117
1302
  mkdirSync2(workspacesRootLazy(), { recursive: true });
@@ -1119,7 +1304,7 @@ function ensureWorkspacesRoot() {
1119
1304
  function migrateLegacyLayoutIfNeeded(name) {
1120
1305
  const legacy = legacyWorkspacePath(name);
1121
1306
  const current = workspacePath(name);
1122
- if (!existsSync7(legacy) || existsSync7(current)) return;
1307
+ if (!existsSync8(legacy) || existsSync8(current)) return;
1123
1308
  const dir = workspaceDirectory(name);
1124
1309
  mkdirSync2(dir, { recursive: true });
1125
1310
  copyFileSync2(legacy, current);
@@ -1129,7 +1314,7 @@ function migrateLegacyLayoutIfNeeded(name) {
1129
1314
  }
1130
1315
  }
1131
1316
  function listWorkspaces() {
1132
- if (!existsSync7(workspacesRootLazy())) return [];
1317
+ if (!existsSync8(workspacesRootLazy())) return [];
1133
1318
  for (const entry of readdirSync4(workspacesRootLazy())) {
1134
1319
  if (entry.endsWith(".json")) {
1135
1320
  const name = entry.replace(/\.json$/, "");
@@ -1138,23 +1323,23 @@ function listWorkspaces() {
1138
1323
  }
1139
1324
  const names = [];
1140
1325
  for (const entry of readdirSync4(workspacesRootLazy())) {
1141
- const full = join6(workspacesRootLazy(), entry);
1326
+ const full = join7(workspacesRootLazy(), entry);
1142
1327
  try {
1143
1328
  if (!statSync4(full).isDirectory()) continue;
1144
1329
  } catch {
1145
1330
  continue;
1146
1331
  }
1147
- if (existsSync7(join6(full, MANIFEST_NAME))) names.push(entry);
1332
+ if (existsSync8(join7(full, MANIFEST_NAME))) names.push(entry);
1148
1333
  }
1149
1334
  return names.sort();
1150
1335
  }
1151
1336
  function loadWorkspace(name) {
1152
1337
  migrateLegacyLayoutIfNeeded(name);
1153
1338
  const path = workspacePath(name);
1154
- if (!existsSync7(path)) return null;
1339
+ if (!existsSync8(path)) return null;
1155
1340
  let raw;
1156
1341
  try {
1157
- raw = readFileSync6(path, "utf-8").replace(/^/, "");
1342
+ raw = readFileSync7(path, "utf-8").replace(/^/, "");
1158
1343
  } catch (err) {
1159
1344
  throw new WorkspaceError(`Cannot read workspace '${name}': ${err.message}`);
1160
1345
  }
@@ -1174,7 +1359,7 @@ function writeWorkspace(workspace) {
1174
1359
  ensureWorkspacesRoot();
1175
1360
  const dir = workspaceDirectory(workspace.name);
1176
1361
  mkdirSync2(dir, { recursive: true });
1177
- mkdirSync2(join6(dir, workspace.ticketsDir), { recursive: true });
1362
+ mkdirSync2(join7(dir, workspace.ticketsDir), { recursive: true });
1178
1363
  const path = workspacePath(workspace.name);
1179
1364
  const validated = WorkspaceConfigSchema.parse({
1180
1365
  $schema: "https://navori.dev/schema/navori.workspace.v1.json",
@@ -1187,18 +1372,18 @@ function writeWorkspace(workspace) {
1187
1372
  // src/commands/render.ts
1188
1373
  import { defineCommand } from "citty";
1189
1374
  import * as p from "@clack/prompts";
1190
- import { existsSync as existsSync11 } from "fs";
1375
+ import { existsSync as existsSync12 } from "fs";
1191
1376
  import { resolve as resolve9 } from "path";
1192
1377
 
1193
1378
  // src/engines/claude/index.ts
1194
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync10, chmodSync } from "fs";
1195
- import { basename as basename3, dirname as dirname5, join as join8, relative as relative2, resolve as resolve8 } from "path";
1379
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync11, chmodSync } from "fs";
1380
+ import { basename as basename3, dirname as dirname5, join as join9, relative as relative2, resolve as resolve8 } from "path";
1196
1381
 
1197
1382
  // src/lib/backup.ts
1198
- import { mkdirSync as mkdirSync3, copyFileSync as copyFileSync3, existsSync as existsSync8, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync5 } from "fs";
1199
- import { dirname as dirname4, join as join7, relative, resolve as resolve5 } from "path";
1383
+ import { mkdirSync as mkdirSync3, copyFileSync as copyFileSync3, existsSync as existsSync9, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync5 } from "fs";
1384
+ import { dirname as dirname4, join as join8, relative, resolve as resolve5 } from "path";
1200
1385
  function backupRootLazy() {
1201
- return join7(safeHomedir(), ".navori", "backups");
1386
+ return join8(safeHomedir(), ".navori", "backups");
1202
1387
  }
1203
1388
  var DEFAULT_RETENTION_DAYS = 30;
1204
1389
  function timestamp2() {
@@ -1219,13 +1404,13 @@ function timestamp2() {
1219
1404
  ].join("");
1220
1405
  }
1221
1406
  function createBackup(repoRoot, paths, options = {}) {
1222
- const dir = join7(backupRootLazy(), timestamp2());
1407
+ const dir = join8(backupRootLazy(), timestamp2());
1223
1408
  mkdirSync3(dir, { recursive: true });
1224
1409
  const exclude = (options.exclude ?? []).map((e) => e.replace(/\/+$/, ""));
1225
1410
  const copied = [];
1226
1411
  for (const p16 of paths) {
1227
1412
  const abs = resolve5(repoRoot, p16);
1228
- if (!existsSync8(abs)) continue;
1413
+ if (!existsSync9(abs)) continue;
1229
1414
  copyRecursive2(abs, dir, repoRoot, exclude, copied);
1230
1415
  }
1231
1416
  return { path: dir, files: copied };
@@ -1236,12 +1421,12 @@ function copyRecursive2(abs, backupDir, repoRoot, exclude, copied) {
1236
1421
  const stat = statSync5(abs);
1237
1422
  if (stat.isDirectory()) {
1238
1423
  for (const entry of readdirSync5(abs)) {
1239
- copyRecursive2(join7(abs, entry), backupDir, repoRoot, exclude, copied);
1424
+ copyRecursive2(join8(abs, entry), backupDir, repoRoot, exclude, copied);
1240
1425
  }
1241
1426
  return;
1242
1427
  }
1243
1428
  if (!stat.isFile()) return;
1244
- const dest = join7(backupDir, rel);
1429
+ const dest = join8(backupDir, rel);
1245
1430
  mkdirSync3(dirname4(dest), { recursive: true });
1246
1431
  copyFileSync3(abs, dest);
1247
1432
  copied.push(rel);
@@ -1255,11 +1440,11 @@ function isExcluded(rel, exclude) {
1255
1440
  }
1256
1441
  function purgeOldBackups(retentionDays = DEFAULT_RETENTION_DAYS) {
1257
1442
  const root = backupRootLazy();
1258
- if (!existsSync8(root)) return [];
1443
+ if (!existsSync9(root)) return [];
1259
1444
  const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
1260
1445
  const pruned = [];
1261
1446
  for (const entry of readdirSync5(root)) {
1262
- const full = join7(root, entry);
1447
+ const full = join8(root, entry);
1263
1448
  const stat = statSync5(full);
1264
1449
  if (!stat.isDirectory()) continue;
1265
1450
  if (stat.mtimeMs < cutoff) {
@@ -1274,7 +1459,7 @@ function backupRoot() {
1274
1459
  }
1275
1460
 
1276
1461
  // src/lib/render-plan.ts
1277
- import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
1462
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
1278
1463
  import { resolve as resolve6 } from "path";
1279
1464
 
1280
1465
  // src/lib/marker.ts
@@ -1492,7 +1677,7 @@ var CORE_SOURCE_ID = "@navori/core";
1492
1677
  var CORE_MANAGED_ASSETS = [
1493
1678
  { id: "idioma-rol", relPath: "core-assets/managed/idioma-rol.md", availableLanguages: ["es"] },
1494
1679
  { id: "formato-respuesta", relPath: "core-assets/managed/formato-respuesta.md", availableLanguages: ["es"] },
1495
- { id: "tipado-fuerte", relPath: "core-assets/managed/tipado-fuerte.md", availableLanguages: ["es"] },
1680
+ { id: "tipado-fuerte", relPath: "core-assets/managed/tipado-fuerte.md", availableLanguages: ["es"], condition: "project.typedLanguage" },
1496
1681
  { id: "operaciones-seguras", relPath: "core-assets/managed/operaciones-seguras.md", availableLanguages: ["es"] },
1497
1682
  { id: "arranque-sesion", relPath: "core-assets/managed/arranque-sesion.md", availableLanguages: ["es"] },
1498
1683
  { id: "cierre-sesion", relPath: "core-assets/managed/cierre-sesion.md", availableLanguages: ["es"] }
@@ -1505,7 +1690,7 @@ function resolveAssetPath(asset, language = "es") {
1505
1690
  }
1506
1691
  const langPath = asset.relPath.replace(/^core-assets\/managed\//, `core-assets/managed/${language}/`);
1507
1692
  const abs = resolve6(root, langPath);
1508
- if (existsSync9(abs)) return { path: abs, fallback: false };
1693
+ if (existsSync10(abs)) return { path: abs, fallback: false };
1509
1694
  return { path: resolve6(root, asset.relPath), fallback: true };
1510
1695
  }
1511
1696
  function interpolateTemplate(content, config) {
@@ -1529,7 +1714,8 @@ function interpolateTemplate(content, config) {
1529
1714
  return match;
1530
1715
  });
1531
1716
  }
1532
- function computeRenderPlan(existing, config, repoRoot, options = {}) {
1717
+ function computeRenderPlan(existing, inputConfig, repoRoot, options = {}) {
1718
+ const config = effectiveConfig(inputConfig);
1533
1719
  const skipIds = options.skipIds ?? /* @__PURE__ */ new Set();
1534
1720
  const forceIds = options.forceIds ?? /* @__PURE__ */ new Set();
1535
1721
  let working = existing;
@@ -1558,7 +1744,7 @@ function computeRenderPlan(existing, config, repoRoot, options = {}) {
1558
1744
  }
1559
1745
  const resolved = resolveAssetPath(asset, language);
1560
1746
  if (resolved.fallback) languageFallbacks.push(asset.id);
1561
- const rawContent = readFileSync7(resolved.path, "utf-8");
1747
+ const rawContent = readFileSync8(resolved.path, "utf-8");
1562
1748
  const content = interpolateTemplate(rawContent, config);
1563
1749
  const result = injectManagedSection(working, asset.id, content, {
1564
1750
  source: CORE_SOURCE_ID,
@@ -1603,7 +1789,7 @@ function computeRenderPlan(existing, config, repoRoot, options = {}) {
1603
1789
  for (const extra of loaded.def.extras.managed) {
1604
1790
  if (skipIds.has(extra.id)) continue;
1605
1791
  const absPath = resolve6(loaded.assetRoot, extra.relPath);
1606
- const rawContent = readFileSync7(absPath, "utf-8");
1792
+ const rawContent = readFileSync8(absPath, "utf-8");
1607
1793
  const content = interpolateTemplate(rawContent, config);
1608
1794
  const result = injectManagedSection(working, extra.id, content, {
1609
1795
  source: CORE_SOURCE_ID,
@@ -1649,7 +1835,7 @@ function computeRenderPlan(existing, config, repoRoot, options = {}) {
1649
1835
  const pluginSource = `@navori/plugin-${plugin.manifest.id}`;
1650
1836
  for (const entry of plugin.managedAssets) {
1651
1837
  if (skipIds.has(entry.id)) continue;
1652
- const rawContent = readFileSync7(entry.absPath, "utf-8");
1838
+ const rawContent = readFileSync8(entry.absPath, "utf-8");
1653
1839
  const content = interpolateTemplate(rawContent, config);
1654
1840
  const result = injectManagedSection(working, entry.id, content, {
1655
1841
  source: pluginSource,
@@ -1707,7 +1893,7 @@ function isNavoriOwnedSettings(parsed) {
1707
1893
  }
1708
1894
 
1709
1895
  // src/engines/claude/build-settings.ts
1710
- import { readFileSync as readFileSync8 } from "fs";
1896
+ import { readFileSync as readFileSync9 } from "fs";
1711
1897
  import { resolve as resolve7 } from "path";
1712
1898
 
1713
1899
  // src/engines/claude/interpolate.ts
@@ -1794,7 +1980,7 @@ var GUARD_HOOK_DEST = ".claude/hooks/guard-destructive.sh";
1794
1980
  var SETTINGS_BASE_REL = "core-assets/settings/settings-base.json";
1795
1981
  function buildClaudeSettings(config, plugins) {
1796
1982
  const basePath = resolve7(getCoreRoot(), SETTINGS_BASE_REL);
1797
- const baseRaw = readFileSync8(basePath, "utf-8");
1983
+ const baseRaw = readFileSync9(basePath, "utf-8");
1798
1984
  const baseInterp = interpolate(baseRaw, config, {
1799
1985
  extraVars: { coreVersion: readBundledCoreVersion() }
1800
1986
  });
@@ -1846,7 +2032,38 @@ function buildClaudeSettings(config, plugins) {
1846
2032
  });
1847
2033
  }
1848
2034
  }
1849
- return settings;
2035
+ return coalesceHookMatchers(settings);
2036
+ }
2037
+ function coalesceHookMatchers(settings) {
2038
+ const hooks = settings.hooks;
2039
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) return settings;
2040
+ const coalesced = {};
2041
+ for (const [event, entries] of Object.entries(hooks)) {
2042
+ if (!Array.isArray(entries)) {
2043
+ coalesced[event] = entries;
2044
+ continue;
2045
+ }
2046
+ const buckets = [];
2047
+ const passthrough = [];
2048
+ for (const entry of entries) {
2049
+ if (typeof entry !== "object" || entry === null || !Array.isArray(entry.hooks)) {
2050
+ passthrough.push(entry);
2051
+ continue;
2052
+ }
2053
+ const e = entry;
2054
+ const existing = buckets.find((b) => b.matcher === e.matcher);
2055
+ if (existing) {
2056
+ const seen = new Set(existing.hooks.map((h) => JSON.stringify(h)));
2057
+ for (const h of e.hooks) {
2058
+ if (!seen.has(JSON.stringify(h))) existing.hooks.push(h);
2059
+ }
2060
+ } else {
2061
+ buckets.push({ ...e.matcher !== void 0 ? { matcher: e.matcher } : {}, hooks: [...e.hooks] });
2062
+ }
2063
+ }
2064
+ coalesced[event] = [...buckets, ...passthrough];
2065
+ }
2066
+ return { ...settings, hooks: coalesced };
1850
2067
  }
1851
2068
  function pluginHooksToClaudeShape(entries) {
1852
2069
  const grouped = {};
@@ -1866,7 +2083,7 @@ function pluginHooksToClaudeShape(entries) {
1866
2083
  }
1867
2084
 
1868
2085
  // src/engines/claude/render-managed-file.ts
1869
- import { readFileSync as readFileSync9 } from "fs";
2086
+ import { readFileSync as readFileSync10 } from "fs";
1870
2087
 
1871
2088
  // src/lib/frontmatter.ts
1872
2089
  var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
@@ -1943,7 +2160,7 @@ function serialize(merged, assetFm) {
1943
2160
  // src/engines/claude/render-managed-file.ts
1944
2161
  function renderManagedFile(input) {
1945
2162
  const commentStyle = input.commentStyle ?? inferCommentStyle(input.assetPath);
1946
- const raw = readFileSync9(input.assetPath, "utf-8");
2163
+ const raw = readFileSync10(input.assetPath, "utf-8");
1947
2164
  const asset = parseAsset(raw, commentStyle);
1948
2165
  const interpolatedFmObj = interpolateFrontmatter(asset.frontmatter, input.config, input.extraVars);
1949
2166
  const interpolatedBody = interpolate(asset.managedBody, input.config, {
@@ -2077,27 +2294,44 @@ var CORE_AGENTS = [
2077
2294
  var CORE_SKILLS = ["verify-before-done", "loop-back-debug", "review-diff"];
2078
2295
  var CORE_META = { source: "@navori/core", version: readBundledCoreVersion() };
2079
2296
  var SKILLS_INDEX_ID = "skills-index";
2297
+ function extraConditionMet(extra, config) {
2298
+ if (!extra.condition) return true;
2299
+ return resolveCondition(config, extra.condition);
2300
+ }
2080
2301
  function buildSkillsIndexBody(config, localSkills, repoRoot) {
2081
2302
  const rows = [];
2082
- for (const id of CORE_SKILLS) rows.push(`- \`${id}\` \u2014 navori`);
2303
+ const listed = /* @__PURE__ */ new Set();
2304
+ for (const id of CORE_SKILLS) {
2305
+ rows.push(`- \`${id}\` \u2014 navori`);
2306
+ listed.add(id);
2307
+ }
2083
2308
  if (config.preset && config.preset !== "custom") {
2084
2309
  try {
2085
2310
  const loaded = loadPreset(config.preset, repoRoot);
2086
2311
  for (const e of loaded?.def.extras.skills ?? []) {
2312
+ if (!extraConditionMet(e, config)) continue;
2087
2313
  const name = basename3(e.destRelPath).replace(/\.md$/, "");
2088
2314
  rows.push(`- \`${name}\` \u2014 preset (\`${config.preset}\`)`);
2315
+ listed.add(name);
2089
2316
  }
2090
2317
  } catch {
2091
2318
  }
2092
2319
  }
2320
+ for (const id of config.project?.libraries ?? []) {
2321
+ if (listed.has(id) || !librarySkillById(id)) continue;
2322
+ rows.push(`- \`${id}\` \u2014 library (detected)`);
2323
+ listed.add(id);
2324
+ }
2093
2325
  for (const name of localSkills) {
2094
2326
  rows.push(`- \`${name}\` \u2014 project-local (\`.claude/skills/${name}.md\`)`);
2095
2327
  }
2328
+ if (rows.length === 0) return null;
2329
+ const localNote = localSkills.length > 0 ? ["Los `project-local` son tuyos \u2014 navori los indexa pero no toca su contenido."] : [];
2096
2330
  return [
2097
2331
  "## Skills disponibles",
2098
2332
  "",
2099
2333
  "Skills que los agentes pueden aplicar; cada uno vive en `.claude/skills/<id>.md`.",
2100
- "Los `project-local` son tuyos \u2014 navori los indexa pero no toca su contenido.",
2334
+ ...localNote,
2101
2335
  "",
2102
2336
  ...rows,
2103
2337
  ""
@@ -2147,7 +2381,8 @@ function buildContextoProyectoBody(config) {
2147
2381
  ""
2148
2382
  ].join("\n");
2149
2383
  }
2150
- function renderClaudeEngine(cwd, config, options = {}) {
2384
+ function renderClaudeEngine(cwd, inputConfig, options = {}) {
2385
+ const config = effectiveConfig(inputConfig);
2151
2386
  const dryRun = options.dryRun === true;
2152
2387
  const force = options.force === true;
2153
2388
  const repoRoot = options.repoRoot ?? cwd;
@@ -2156,8 +2391,8 @@ function renderClaudeEngine(cwd, config, options = {}) {
2156
2391
  const warnings = [];
2157
2392
  const pending = [];
2158
2393
  let inspected = 0;
2159
- const claudeMdPath = join8(cwd, "CLAUDE.md");
2160
- const claudeMdExisting = existsSync10(claudeMdPath) ? readFileSync10(claudeMdPath, "utf-8") : "";
2394
+ const claudeMdPath = join9(cwd, "CLAUDE.md");
2395
+ const claudeMdExisting = existsSync11(claudeMdPath) ? readFileSync11(claudeMdPath, "utf-8") : "";
2161
2396
  const claudeMdPlan = computeRenderPlan(claudeMdExisting, config, repoRoot, {
2162
2397
  skipIds: options.skipIds,
2163
2398
  forceIds: options.forceIds
@@ -2165,11 +2400,12 @@ function renderClaudeEngine(cwd, config, options = {}) {
2165
2400
  inspected += 1;
2166
2401
  const localSkills = config.project?.localSkills ?? [];
2167
2402
  let claudeMdContent = claudeMdPlan.next;
2168
- if (localSkills.length > 0) {
2403
+ const skillsIndexBody = buildSkillsIndexBody(config, localSkills, repoRoot);
2404
+ if (skillsIndexBody !== null) {
2169
2405
  const result = injectManagedSection(
2170
2406
  claudeMdContent,
2171
2407
  SKILLS_INDEX_ID,
2172
- buildSkillsIndexBody(config, localSkills, repoRoot),
2408
+ skillsIndexBody,
2173
2409
  CORE_META,
2174
2410
  "html",
2175
2411
  options.forceIds?.has(SKILLS_INDEX_ID) ?? false
@@ -2313,6 +2549,9 @@ function renderClaudeEngine(cwd, config, options = {}) {
2313
2549
  } else {
2314
2550
  warnings.push("quality-gate hook skipped: config.qualityGate.fast no est\xE1 set");
2315
2551
  }
2552
+ const renderedSkillDests = new Set(
2553
+ CORE_SKILLS.map((id) => `.claude/skills/${id}.md`)
2554
+ );
2316
2555
  if (config.preset && config.preset !== "custom") {
2317
2556
  let loaded = null;
2318
2557
  try {
@@ -2336,6 +2575,8 @@ function renderClaudeEngine(cwd, config, options = {}) {
2336
2575
  ...loaded.def.extras.hooks.map((e) => ({ extra: e, exec: true }))
2337
2576
  ];
2338
2577
  for (const { extra, exec } of allFileExtras) {
2578
+ if (!extraConditionMet(extra, config)) continue;
2579
+ renderedSkillDests.add(extra.destRelPath);
2339
2580
  inspected += 1;
2340
2581
  applyManagedFilePlan(
2341
2582
  planManagedFile({
@@ -2354,6 +2595,32 @@ function renderClaudeEngine(cwd, config, options = {}) {
2354
2595
  }
2355
2596
  }
2356
2597
  }
2598
+ for (const id of config.project?.libraries ?? []) {
2599
+ const skill = librarySkillById(id);
2600
+ if (!skill) continue;
2601
+ const destRelPath = `.claude/skills/${id}.md`;
2602
+ if (renderedSkillDests.has(destRelPath)) continue;
2603
+ renderedSkillDests.add(destRelPath);
2604
+ inspected += 1;
2605
+ applyManagedFilePlan(
2606
+ planManagedFile({
2607
+ cwd,
2608
+ assetRoot: coreAssets,
2609
+ assetRelPath: `lib-skills/${id}.md`,
2610
+ destRelPath,
2611
+ // The managed-block id is the bare skill id — the SAME id the
2612
+ // express-mongoose preset used to write for mongoose/zod/joi before they
2613
+ // moved to this layer. On upgrade the marker is recognized and updated
2614
+ // in place; a distinct id (e.g. `${id}-lib`) would leave the preset-era
2615
+ // block untouched and append a duplicate block in the same file.
2616
+ managedId: id,
2617
+ config
2618
+ }),
2619
+ cwd,
2620
+ pending,
2621
+ skipped
2622
+ );
2623
+ }
2357
2624
  for (const plugin of enabledPlugins) {
2358
2625
  for (const script of plugin.scriptAssets) {
2359
2626
  inspected += 1;
@@ -2399,7 +2666,7 @@ function renderClaudeEngine(cwd, config, options = {}) {
2399
2666
  };
2400
2667
  }
2401
2668
  if (!dryRun) {
2402
- const hasExistingTarget = pending.some((p16) => existsSync10(p16.path));
2669
+ const hasExistingTarget = pending.some((p16) => existsSync11(p16.path));
2403
2670
  if (hasExistingTarget) {
2404
2671
  const handle = createBackup(cwd, ["CLAUDE.md", ".claude"], {
2405
2672
  exclude: [".claude/settings.local.json", ".claude/progress"]
@@ -2444,15 +2711,15 @@ function isAgentEnabled(config, key) {
2444
2711
  return h[key] !== false;
2445
2712
  }
2446
2713
  function planSettings(cwd, config, plugins, force = false) {
2447
- const path = join8(cwd, ".claude/settings.json");
2714
+ const path = join9(cwd, ".claude/settings.json");
2448
2715
  const newSettings = buildClaudeSettings(config, plugins);
2449
2716
  const newJson = JSON.stringify(newSettings, null, 2) + "\n";
2450
- if (!existsSync10(path)) {
2717
+ if (!existsSync11(path)) {
2451
2718
  return { kind: "write", path, content: newJson, status: "created" };
2452
2719
  }
2453
2720
  let parsed;
2454
2721
  try {
2455
- parsed = JSON.parse(readFileSync10(path, "utf-8"));
2722
+ parsed = JSON.parse(readFileSync11(path, "utf-8"));
2456
2723
  } catch (err) {
2457
2724
  if (force) {
2458
2725
  return { kind: "write", path, content: newJson, status: "updated" };
@@ -2473,14 +2740,14 @@ function planSettings(cwd, config, plugins, force = false) {
2473
2740
  reason: "settings.json existe sin `$navori.managed = true`. Corr\xE9 'navori init' en modo replace o 'navori render --force --apply' para adoptar."
2474
2741
  };
2475
2742
  }
2476
- const current = readFileSync10(path, "utf-8");
2743
+ const current = readFileSync11(path, "utf-8");
2477
2744
  if (current === newJson) return { kind: "noop" };
2478
2745
  return { kind: "write", path, content: newJson, status: "updated" };
2479
2746
  }
2480
2747
  function planManagedFile(input) {
2481
2748
  const assetPath = resolve8(input.assetRoot, input.assetRelPath);
2482
- const destPath = join8(input.cwd, input.destRelPath);
2483
- const existing = existsSync10(destPath) ? readFileSync10(destPath, "utf-8") : null;
2749
+ const destPath = join9(input.cwd, input.destRelPath);
2750
+ const existing = existsSync11(destPath) ? readFileSync11(destPath, "utf-8") : null;
2484
2751
  const result = renderManagedFile({
2485
2752
  assetPath,
2486
2753
  existingContent: existing,
@@ -2507,10 +2774,10 @@ function applyManagedFilePlan(plan, cwd, pending, skipped, chmodExec = false) {
2507
2774
  pending.push({ path: plan.path, content: plan.content, status: plan.status, chmodExec });
2508
2775
  }
2509
2776
  function planBootstrapFile(input) {
2510
- const destPath = join8(input.cwd, input.destRelPath);
2511
- if (existsSync10(destPath)) return { kind: "noop" };
2777
+ const destPath = join9(input.cwd, input.destRelPath);
2778
+ if (existsSync11(destPath)) return { kind: "noop" };
2512
2779
  const assetPath = resolve8(getCoreRoot(), "core-assets", input.assetRelPath);
2513
- const raw = readFileSync10(assetPath, "utf-8");
2780
+ const raw = readFileSync11(assetPath, "utf-8");
2514
2781
  return { kind: "write", path: destPath, content: interpolate(raw, input.config) };
2515
2782
  }
2516
2783
  function applyBootstrapPlan(plan, _cwd, pending) {
@@ -2518,20 +2785,20 @@ function applyBootstrapPlan(plan, _cwd, pending) {
2518
2785
  pending.push({ path: plan.path, content: plan.content, status: "created" });
2519
2786
  }
2520
2787
  function applySubBlockInject(input) {
2521
- const targetAbs = join8(input.cwd, input.skill.injectInto);
2788
+ const targetAbs = join9(input.cwd, input.skill.injectInto);
2522
2789
  let currentContent;
2523
2790
  const pendingEntry = input.pending.find((p16) => p16.path === targetAbs);
2524
2791
  if (pendingEntry) {
2525
2792
  currentContent = pendingEntry.content;
2526
- } else if (existsSync10(targetAbs)) {
2527
- currentContent = readFileSync10(targetAbs, "utf-8");
2793
+ } else if (existsSync11(targetAbs)) {
2794
+ currentContent = readFileSync11(targetAbs, "utf-8");
2528
2795
  } else {
2529
2796
  input.warnings.push(
2530
2797
  `skill '${input.skill.id}' (de @navori/plugin-${input.plugin.manifest.id}) no inyectado: target ${input.skill.injectInto} ausente (\xBFagente disabled en config.harness?)`
2531
2798
  );
2532
2799
  return;
2533
2800
  }
2534
- const rawSkill = readFileSync10(input.skill.absPath, "utf-8");
2801
+ const rawSkill = readFileSync11(input.skill.absPath, "utf-8");
2535
2802
  const skillBody = stripFrontmatter(rawSkill);
2536
2803
  const interpolated = interpolate(skillBody, input.config);
2537
2804
  const result = injectManagedSection(
@@ -2563,10 +2830,10 @@ function applySubBlockInject(input) {
2563
2830
  });
2564
2831
  }
2565
2832
  function planPluginScript(cwd, script, config) {
2566
- const destPath = join8(cwd, ".claude/scripts", script.dest);
2567
- const raw = readFileSync10(script.src, "utf-8");
2833
+ const destPath = join9(cwd, ".claude/scripts", script.dest);
2834
+ const raw = readFileSync11(script.src, "utf-8");
2568
2835
  const interpolated = interpolate(raw, config);
2569
- const existing = existsSync10(destPath) ? readFileSync10(destPath, "utf-8") : null;
2836
+ const existing = existsSync11(destPath) ? readFileSync11(destPath, "utf-8") : null;
2570
2837
  if (existing === interpolated) return { kind: "noop" };
2571
2838
  return {
2572
2839
  kind: "write",
@@ -2661,7 +2928,7 @@ function runRender(cwd, dryRunOrOptions = false, force = false) {
2661
2928
  const workspaceFilter = opts.workspaceFilter ?? null;
2662
2929
  const configPath = `${cwd}/navori.config.json`;
2663
2930
  const claudeMdPath = `${cwd}/CLAUDE.md`;
2664
- if (!existsSync11(configPath)) {
2931
+ if (!existsSync12(configPath)) {
2665
2932
  return {
2666
2933
  ok: false,
2667
2934
  reason: `No navori.config.json at ${configPath}`,
@@ -2799,7 +3066,7 @@ var renderCommand = defineCommand({
2799
3066
  benchStart();
2800
3067
  const cwd = resolve9(args.cwd ?? process.cwd());
2801
3068
  p.intro(brand("render"));
2802
- if (!existsSync11(cwd)) {
3069
+ if (!existsSync12(cwd)) {
2803
3070
  p.cancel(`Directory not found: ${cwd}`);
2804
3071
  process.exit(1);
2805
3072
  }
@@ -3005,6 +3272,7 @@ var ES = {
3005
3272
  wroteConfig: (path) => `Escrib\xED ${path}`,
3006
3273
  recPluginsEnabled: (list) => `Plugins recomendados activados: ${list}`,
3007
3274
  presetGapNotice: (stack) => `Detect\xE9 un proyecto '${stack}', pero todav\xEDa no hay un preset oficial para ese stack. Se instala el harness completo (agentes, gates, protocolo, SDD) y funciona desde ya; lo \xFAnico que falta son skills espec\xEDficas de '${stack}'. Quedas con el baseline (preset: custom). Para cubrir el hueco: crea tu preset local con 'navori preset init ${stack}', o agrega skills sueltas en project.localSkills.`,
3275
+ placeholderNameNotice: (name) => `El name '${name}' parece un placeholder de scaffold (heredado del package.json sin renombrar). Ren\xF3mbralo en package.json o edita "name" en navori.config.json si no es el nombre real del repo.`,
3008
3276
  workspaceDefaultsTitle: (name) => `Defaults del workspace \xB7 ${name}`,
3009
3277
  detectedTitle: "Detectado en este repo",
3010
3278
  filesFoundTitle: "Archivos encontrados",
@@ -3111,6 +3379,7 @@ var EN = {
3111
3379
  wroteConfig: (path) => `Wrote ${path}`,
3112
3380
  recPluginsEnabled: (list) => `Recommended plugins enabled: ${list}`,
3113
3381
  presetGapNotice: (stack) => `Detected a '${stack}' project, but there's no official preset for that stack yet. The full harness installs (agents, gates, protocol, SDD) and works right away; the only thing missing are '${stack}'-specific skills. You stay on the baseline (preset: custom). To cover the gap: scaffold your local preset with 'navori preset init ${stack}', or add individual skills via project.localSkills.`,
3382
+ placeholderNameNotice: (name) => `The name '${name}' looks like a scaffold placeholder (carried over from an un-renamed package.json). Rename it in package.json or edit "name" in navori.config.json if it isn't the repo's real name.`,
3114
3383
  workspaceDefaultsTitle: (name) => `Workspace defaults \xB7 ${name}`,
3115
3384
  detectedTitle: "Detected from this repo",
3116
3385
  filesFoundTitle: "Files found",
@@ -3209,7 +3478,7 @@ function formatWorkspaceSummary(ws, lang = "es") {
3209
3478
  }
3210
3479
 
3211
3480
  // src/engines/claude/prompts-loader.ts
3212
- import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
3481
+ import { readFileSync as readFileSync12, existsSync as existsSync13 } from "fs";
3213
3482
  import { resolve as resolve10 } from "path";
3214
3483
  import { z as z7 } from "zod";
3215
3484
  var SelectOptionSchema = z7.object({
@@ -3249,10 +3518,10 @@ function loadPrompts(enabledPlugins) {
3249
3518
  }
3250
3519
  function loadCorePrompts(warnings) {
3251
3520
  const path = resolve10(getCoreRoot(), CORE_PROMPTS_REL);
3252
- if (!existsSync12(path)) return [];
3521
+ if (!existsSync13(path)) return [];
3253
3522
  let parsed;
3254
3523
  try {
3255
- parsed = JSON.parse(readFileSync11(path, "utf-8"));
3524
+ parsed = JSON.parse(readFileSync12(path, "utf-8"));
3256
3525
  } catch (err) {
3257
3526
  warnings.push(`core prompts.json no parsea: ${err.message}`);
3258
3527
  return [];
@@ -3266,8 +3535,8 @@ function loadCorePrompts(warnings) {
3266
3535
  }
3267
3536
 
3268
3537
  // src/lib/scan.ts
3269
- import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync6 } from "fs";
3270
- import { join as join9 } from "path";
3538
+ import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
3539
+ import { join as join10 } from "path";
3271
3540
  function scanMonorepoWorkspaces(cwd) {
3272
3541
  const patterns = collectWorkspacePatterns(cwd);
3273
3542
  if (patterns.length === 0) return [];
@@ -3283,86 +3552,6 @@ function scanMonorepoWorkspaces(cwd) {
3283
3552
  }
3284
3553
  return results.sort((a, b) => a.path.localeCompare(b.path));
3285
3554
  }
3286
- function collectWorkspacePatterns(cwd) {
3287
- const pnpmYaml = join9(cwd, "pnpm-workspace.yaml");
3288
- if (existsSync13(pnpmYaml)) {
3289
- try {
3290
- return parsePnpmWorkspaceYaml(readFileSync12(pnpmYaml, "utf-8"));
3291
- } catch {
3292
- return [];
3293
- }
3294
- }
3295
- const pkgPath = join9(cwd, "package.json");
3296
- if (existsSync13(pkgPath)) {
3297
- try {
3298
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8").replace(/^/, ""));
3299
- return readPackageJsonWorkspaces(pkg.workspaces);
3300
- } catch {
3301
- return [];
3302
- }
3303
- }
3304
- return [];
3305
- }
3306
- function parsePnpmWorkspaceYaml(content) {
3307
- const lines = content.split(/\r?\n/);
3308
- let i = 0;
3309
- while (i < lines.length) {
3310
- const line = lines[i];
3311
- const m = line.match(/^packages\s*:\s*(.*)$/);
3312
- if (m) {
3313
- const inline = m[1].trim();
3314
- if (inline.startsWith("[")) {
3315
- return parseInlineYamlArray(inline);
3316
- }
3317
- const items = [];
3318
- i++;
3319
- while (i < lines.length) {
3320
- const cur = lines[i];
3321
- if (/^\s*#/.test(cur) || /^\s*$/.test(cur)) {
3322
- i++;
3323
- continue;
3324
- }
3325
- const itemMatch = cur.match(/^\s+-\s+(.+?)\s*(?:#.*)?$/);
3326
- if (!itemMatch) break;
3327
- const raw = itemMatch[1].trim();
3328
- const unquoted = stripYamlQuotes(raw);
3329
- if (unquoted && !unquoted.startsWith("!")) items.push(unquoted);
3330
- i++;
3331
- }
3332
- return items;
3333
- }
3334
- i++;
3335
- }
3336
- return [];
3337
- }
3338
- function parseInlineYamlArray(inline) {
3339
- const closed = inline.lastIndexOf("]");
3340
- if (closed < 0) return [];
3341
- const body = inline.slice(1, closed);
3342
- return body.split(",").map((s) => stripYamlQuotes(s.trim())).filter((s) => s && !s.startsWith("!"));
3343
- }
3344
- function stripYamlQuotes(raw) {
3345
- if (raw.length >= 2) {
3346
- const first = raw[0];
3347
- const last = raw[raw.length - 1];
3348
- if (first === '"' && last === '"' || first === "'" && last === "'") {
3349
- return raw.slice(1, -1);
3350
- }
3351
- }
3352
- return raw;
3353
- }
3354
- function readPackageJsonWorkspaces(raw) {
3355
- if (Array.isArray(raw)) {
3356
- return raw.filter((s) => typeof s === "string" && !s.startsWith("!"));
3357
- }
3358
- if (raw && typeof raw === "object" && "packages" in raw) {
3359
- const pkgs = raw.packages;
3360
- if (Array.isArray(pkgs)) {
3361
- return pkgs.filter((s) => typeof s === "string" && !s.startsWith("!"));
3362
- }
3363
- }
3364
- return [];
3365
- }
3366
3555
  function expandPattern(cwd, pattern) {
3367
3556
  const segments = pattern.split("/").filter(Boolean);
3368
3557
  if (segments.length === 0) return [];
@@ -3374,9 +3563,9 @@ function walk(cwd, accum, remaining) {
3374
3563
  }
3375
3564
  const [head, ...tail] = remaining;
3376
3565
  const currentRel = accum.join("/");
3377
- const currentAbs = currentRel ? join9(cwd, currentRel) : cwd;
3566
+ const currentAbs = currentRel ? join10(cwd, currentRel) : cwd;
3378
3567
  if (head === "*") {
3379
- if (!existsSync13(currentAbs)) return [];
3568
+ if (!existsSync14(currentAbs)) return [];
3380
3569
  let entries;
3381
3570
  try {
3382
3571
  entries = readdirSync6(currentAbs, { withFileTypes: true });
@@ -3385,8 +3574,8 @@ function walk(cwd, accum, remaining) {
3385
3574
  }
3386
3575
  return entries.filter((d) => d.isDirectory() && !d.name.startsWith(".")).flatMap((d) => walk(cwd, [...accum, d.name], tail));
3387
3576
  }
3388
- const next = join9(currentAbs, head);
3389
- if (!existsSync13(next)) return [];
3577
+ const next = join10(currentAbs, head);
3578
+ if (!existsSync14(next)) return [];
3390
3579
  return walk(cwd, [...accum, head], tail);
3391
3580
  }
3392
3581
  function diffWorkspaces(detected, configured) {
@@ -3398,8 +3587,8 @@ function diffWorkspaces(detected, configured) {
3398
3587
  return { added, existing, orphan };
3399
3588
  }
3400
3589
  function describeWorkspace(cwd, relPath) {
3401
- const abs = join9(cwd, relPath);
3402
- if (!existsSync13(join9(abs, "package.json"))) return null;
3590
+ const abs = join10(cwd, relPath);
3591
+ if (!existsSync14(join10(abs, "package.json"))) return null;
3403
3592
  const project = detectProject(abs);
3404
3593
  return {
3405
3594
  name: project.name ?? relPath.split("/").pop(),
@@ -3480,11 +3669,11 @@ var initCommand = defineCommand2({
3480
3669
  p2.intro(brand("init"));
3481
3670
  const bootstrapLang = normalizeLang(args.lang) ?? "es";
3482
3671
  const bootstrapT = t(bootstrapLang);
3483
- if (!existsSync14(cwd)) {
3672
+ if (!existsSync15(cwd)) {
3484
3673
  p2.cancel(bootstrapT.dirNotFound(cwd));
3485
3674
  process.exit(1);
3486
3675
  }
3487
- if (existsSync14(configPath)) {
3676
+ if (existsSync15(configPath)) {
3488
3677
  p2.cancel(bootstrapT.configExists(configPath));
3489
3678
  process.exit(1);
3490
3679
  }
@@ -3539,9 +3728,13 @@ var initCommand = defineCommand2({
3539
3728
  if (detected.suggestedPresetGap) {
3540
3729
  p2.log.warn(tr.presetGapNotice(detected.suggestedPresetGap));
3541
3730
  }
3731
+ if (detected.name && isPlaceholderName(detected.name)) {
3732
+ p2.log.warn(tr.placeholderNameNotice(detected.name));
3733
+ }
3542
3734
  const wsDefaults = workspaceConfig?.defaults;
3543
3735
  const defaultEngines = wsDefaults?.engines ?? (detected.existingEngines.length > 0 ? detected.existingEngines : ["claude"]);
3544
3736
  const defaultBranchBase = wsDefaults?.branchBase ?? detected.branchBase ?? "main";
3737
+ const defaultPrTarget = wsDefaults?.prTarget;
3545
3738
  const defaultLanguage = wsDefaults?.language ?? lang;
3546
3739
  const defaultCommits = wsDefaults?.commits;
3547
3740
  if (autoYes) {
@@ -3562,7 +3755,11 @@ var initCommand = defineCommand2({
3562
3755
  rootPreset: detected.suggestedPreset
3563
3756
  });
3564
3757
  const fallbackQg = args.recommended ? detected.qualityGate ?? buildRecommendedQualityGate(detected) : detected.qualityGate;
3565
- const projectBlock = args.recommended ? buildRecommendedProject(detected) : {};
3758
+ const projectBlock = {
3759
+ ...args.recommended ? buildRecommendedProject(detected) : {},
3760
+ libraries: detected.libraries,
3761
+ codeLanguage: detected.stack.language
3762
+ };
3566
3763
  writeConfig(configPath, {
3567
3764
  name: detected.name,
3568
3765
  ...args.workspace ? { workspace: args.workspace } : {},
@@ -3570,6 +3767,7 @@ var initCommand = defineCommand2({
3570
3767
  preset: detected.suggestedPreset,
3571
3768
  language: defaultLanguage,
3572
3769
  branchBase: defaultBranchBase,
3770
+ ...defaultPrTarget ? { prTarget: defaultPrTarget } : {},
3573
3771
  ...defaultCommits ? { commits: defaultCommits } : {},
3574
3772
  ...fallbackQg ? { qualityGate: fallbackQg } : {},
3575
3773
  ...Object.keys(mergedPlugins2).length > 0 ? { plugins: mergedPlugins2 } : {},
@@ -3877,13 +4075,19 @@ var initCommand = defineCommand2({
3877
4075
  preset,
3878
4076
  language,
3879
4077
  branchBase,
4078
+ ...defaultPrTarget ? { prTarget: defaultPrTarget } : {},
3880
4079
  ...defaultCommits ? { commits: defaultCommits } : {},
3881
4080
  ...qualityGate ? { qualityGate } : {},
3882
4081
  ...Object.keys(mergedPlugins).length > 0 ? { plugins: mergedPlugins } : {},
3883
4082
  ...Object.keys(agentAssignments).length > 0 ? { agentAssignments } : {},
3884
4083
  // Always write `project` so the schema fills empty arrays and render emits
3885
4084
  // no `<not configured: project.*>` placeholders (see autoYes path above).
3886
- project: project ?? {},
4085
+ // Library skills and codeLanguage are auto-derived from the stack.
4086
+ project: {
4087
+ ...project ?? {},
4088
+ libraries: detected.libraries,
4089
+ codeLanguage: detected.stack.language
4090
+ },
3887
4091
  ...monorepoBlock ? { monorepo: monorepoBlock } : {}
3888
4092
  });
3889
4093
  p2.log.success(tr.wroteConfig(configPath));
@@ -4083,7 +4287,7 @@ ${lines}${more}`);
4083
4287
  }
4084
4288
  }
4085
4289
  async function offerPreCommitHook(cwd, opts) {
4086
- if (!existsSync14(join10(cwd, ".git"))) return;
4290
+ if (!existsSync15(join11(cwd, ".git"))) return;
4087
4291
  let wanted = opts.force;
4088
4292
  if (!wanted && !opts.autoYes) {
4089
4293
  const answer = await p2.confirm({
@@ -4102,10 +4306,10 @@ async function offerPreCommitHook(cwd, opts) {
4102
4306
  }
4103
4307
  }
4104
4308
  function writePreCommitHook(cwd) {
4105
- const huskyDir = join10(cwd, ".husky");
4106
- const hookPath = existsSync14(huskyDir) ? join10(huskyDir, "pre-commit") : join10(cwd, ".git", "hooks", "pre-commit");
4309
+ const huskyDir = join11(cwd, ".husky");
4310
+ const hookPath = existsSync15(huskyDir) ? join11(huskyDir, "pre-commit") : join11(cwd, ".git", "hooks", "pre-commit");
4107
4311
  const relPath = relative3(cwd, hookPath);
4108
- if (existsSync14(hookPath)) return { ok: false, path: relPath };
4312
+ if (existsSync15(hookPath)) return { ok: false, path: relPath };
4109
4313
  const body = [
4110
4314
  "#!/usr/bin/env sh",
4111
4315
  "# navori pre-commit drift gate \u2014 scaffolded by 'navori init' (opt-in).",
@@ -4342,14 +4546,14 @@ function formatProjectValue(v) {
4342
4546
  // src/commands/doctor.ts
4343
4547
  import { defineCommand as defineCommand3 } from "citty";
4344
4548
  import * as p3 from "@clack/prompts";
4345
- import { existsSync as existsSync16, readFileSync as readFileSync14, readdirSync as readdirSync8 } from "fs";
4346
- import { join as join12, resolve as resolve12, relative as relative4 } from "path";
4549
+ import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync8 } from "fs";
4550
+ import { join as join13, resolve as resolve12, relative as relative4 } from "path";
4347
4551
 
4348
4552
  // src/lib/health.ts
4349
- import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
4350
- import { join as join11 } from "path";
4553
+ import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
4554
+ import { join as join12 } from "path";
4351
4555
  function listMarkers(filePath) {
4352
- if (!existsSync15(filePath)) return [];
4556
+ if (!existsSync16(filePath)) return [];
4353
4557
  const content = readFileSync13(filePath, "utf-8");
4354
4558
  const re = /<!-- navori:managed [^>]*-->/g;
4355
4559
  const result = [];
@@ -4395,10 +4599,10 @@ function scanManagedDrift(cwd, config) {
4395
4599
  }
4396
4600
  }
4397
4601
  const files = [];
4398
- if (existsSync15(join11(cwd, "CLAUDE.md"))) files.push("CLAUDE.md");
4602
+ if (existsSync16(join12(cwd, "CLAUDE.md"))) files.push("CLAUDE.md");
4399
4603
  for (const dir of [".claude/agents", ".claude/skills"]) {
4400
- const absDir = join11(cwd, dir);
4401
- if (!existsSync15(absDir)) continue;
4604
+ const absDir = join12(cwd, dir);
4605
+ if (!existsSync16(absDir)) continue;
4402
4606
  try {
4403
4607
  for (const file of readdirSync7(absDir)) {
4404
4608
  if (file.endsWith(".md")) files.push(`${dir}/${file}`);
@@ -4408,7 +4612,7 @@ function scanManagedDrift(cwd, config) {
4408
4612
  }
4409
4613
  }
4410
4614
  for (const rel of files) {
4411
- const abs = join11(cwd, rel);
4615
+ const abs = join12(cwd, rel);
4412
4616
  const fileContent = (() => {
4413
4617
  try {
4414
4618
  return readFileSync13(abs, "utf-8");
@@ -4493,7 +4697,7 @@ var doctorCommand = defineCommand3({
4493
4697
  const configPath = `${cwd}/navori.config.json`;
4494
4698
  const claudeMdPath = `${cwd}/CLAUDE.md`;
4495
4699
  if (!args.json) p3.intro(brand("doctor"));
4496
- if (!existsSync16(cwd)) {
4700
+ if (!existsSync17(cwd)) {
4497
4701
  if (args.json) {
4498
4702
  console.log(JSON.stringify({ ok: false, error: "directory-missing", cwd }));
4499
4703
  } else {
@@ -4501,7 +4705,7 @@ var doctorCommand = defineCommand3({
4501
4705
  }
4502
4706
  process.exit(1);
4503
4707
  }
4504
- if (!existsSync16(configPath)) {
4708
+ if (!existsSync17(configPath)) {
4505
4709
  if (args.json) {
4506
4710
  console.log(JSON.stringify({ ok: false, error: "config-missing", configPath }));
4507
4711
  } else {
@@ -4537,6 +4741,7 @@ var doctorCommand = defineCommand3({
4537
4741
  const missingPreset = config.preset !== "custom" && resolvedPreset === null ? config.preset : null;
4538
4742
  const presetOverride = resolvedPreset?.source === "local" && presetExists(config.preset) ? config.preset : null;
4539
4743
  const missingPresetFiles = scanMissingPresetFiles(cwd, config);
4744
+ const placeholderName = isPlaceholderName(config.name) ? config.name : null;
4540
4745
  const report = {
4541
4746
  // Drift is informational ("update available"), not an error — don't
4542
4747
  // flip `ok` for it. Missing plugins, corrupted settings.json, missing
@@ -4546,10 +4751,10 @@ var doctorCommand = defineCommand3({
4546
4751
  configPath,
4547
4752
  config,
4548
4753
  checks: {
4549
- claudeMdExists: existsSync16(claudeMdPath),
4550
- agentsMdExists: existsSync16(`${cwd}/AGENTS.md`),
4551
- claudeDirExists: existsSync16(`${cwd}/.claude`),
4552
- progressDirExists: existsSync16(`${cwd}/${config.progress?.dir ?? "progress"}`)
4754
+ claudeMdExists: existsSync17(claudeMdPath),
4755
+ agentsMdExists: existsSync17(`${cwd}/AGENTS.md`),
4756
+ claudeDirExists: existsSync17(`${cwd}/.claude`),
4757
+ progressDirExists: existsSync17(`${cwd}/${config.progress?.dir ?? "progress"}`)
4553
4758
  },
4554
4759
  managedBlocks: markers,
4555
4760
  missingPlugins,
@@ -4558,7 +4763,8 @@ var doctorCommand = defineCommand3({
4558
4763
  missingInvariants,
4559
4764
  missingPreset,
4560
4765
  presetOverride,
4561
- missingPresetFiles
4766
+ missingPresetFiles,
4767
+ placeholderName
4562
4768
  };
4563
4769
  if (args.json) {
4564
4770
  console.log(JSON.stringify(report, null, 2));
@@ -4621,6 +4827,11 @@ ${lines.join("\n")}`);
4621
4827
  `Preset local '${presetOverride}' (.navori/presets/${presetOverride}/) sombrea el preset oficial del mismo nombre \u2014 se usa el local. Ren\xF3mbralo si el override no es intencional.`
4622
4828
  );
4623
4829
  }
4830
+ if (placeholderName) {
4831
+ p3.log.warn(
4832
+ `El name '${placeholderName}' parece un placeholder de scaffold (probablemente heredado del package.json sin renombrar). Edita "name" en navori.config.json si no es el nombre real del repo.`
4833
+ );
4834
+ }
4624
4835
  if (missingPresetFiles.length > 0) {
4625
4836
  const lines = missingPresetFiles.map(
4626
4837
  (f) => ` ${color.red(sym.fail)} ${accent(f.id)} ${dim(`\u2014 falta ${f.path}`)}`
@@ -4631,7 +4842,7 @@ ${lines.join("\n")}`
4631
4842
  );
4632
4843
  }
4633
4844
  const missingLocalSkills = (config.project?.localSkills ?? []).filter(
4634
- (name) => !existsSync16(join12(cwd, ".claude/skills", `${name}.md`))
4845
+ (name) => !existsSync17(join13(cwd, ".claude/skills", `${name}.md`))
4635
4846
  );
4636
4847
  if (missingLocalSkills.length > 0) {
4637
4848
  const lines = missingLocalSkills.map(
@@ -4690,8 +4901,8 @@ ${lines.join("\n")}`
4690
4901
  }
4691
4902
  });
4692
4903
  function scanCorruptedSettings(cwd) {
4693
- const path = join12(cwd, ".claude/settings.json");
4694
- if (!existsSync16(path)) return [];
4904
+ const path = join13(cwd, ".claude/settings.json");
4905
+ if (!existsSync17(path)) return [];
4695
4906
  try {
4696
4907
  JSON.parse(readFileSync14(path, "utf-8"));
4697
4908
  return [];
@@ -4713,7 +4924,7 @@ function scanMissingPresetFiles(cwd, config) {
4713
4924
  const missing = [];
4714
4925
  for (const e of [...managed, ...agents, ...skills, ...hooks]) {
4715
4926
  const abs = resolve12(loaded.assetRoot, e.relPath);
4716
- if (!existsSync16(abs)) missing.push({ id: e.id, path: relative4(cwd, abs) });
4927
+ if (!existsSync17(abs)) missing.push({ id: e.id, path: relative4(cwd, abs) });
4717
4928
  }
4718
4929
  return missing;
4719
4930
  }
@@ -4749,15 +4960,15 @@ function scanMissingInvariants(cwd, config) {
4749
4960
  }
4750
4961
  function readRenderedText(cwd) {
4751
4962
  const parts = [];
4752
- const claudeMd = join12(cwd, "CLAUDE.md");
4753
- if (existsSync16(claudeMd)) {
4963
+ const claudeMd = join13(cwd, "CLAUDE.md");
4964
+ if (existsSync17(claudeMd)) {
4754
4965
  try {
4755
4966
  parts.push(readFileSync14(claudeMd, "utf-8"));
4756
4967
  } catch {
4757
4968
  }
4758
4969
  }
4759
- const claudeDir = join12(cwd, ".claude");
4760
- if (existsSync16(claudeDir)) collectText(claudeDir, parts);
4970
+ const claudeDir = join13(cwd, ".claude");
4971
+ if (existsSync17(claudeDir)) collectText(claudeDir, parts);
4761
4972
  return parts.join("\n");
4762
4973
  }
4763
4974
  function collectText(dir, parts) {
@@ -4768,7 +4979,7 @@ function collectText(dir, parts) {
4768
4979
  return;
4769
4980
  }
4770
4981
  for (const entry of entries) {
4771
- const abs = join12(dir, entry.name);
4982
+ const abs = join13(dir, entry.name);
4772
4983
  if (entry.isDirectory()) {
4773
4984
  collectText(abs, parts);
4774
4985
  } else if (entry.isFile() && TEXT_EXTENSIONS.some((e) => entry.name.endsWith(e))) {
@@ -4805,8 +5016,8 @@ function collectAssignments(config) {
4805
5016
  // src/commands/sync.ts
4806
5017
  import { defineCommand as defineCommand4 } from "citty";
4807
5018
  import * as p4 from "@clack/prompts";
4808
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4809
- import { resolve as resolve13, join as join13 } from "path";
5019
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
5020
+ import { resolve as resolve13, join as join14 } from "path";
4810
5021
 
4811
5022
  // src/lib/diff.ts
4812
5023
  function formatLineDiff(current, proposed, _options = {}) {
@@ -4851,11 +5062,11 @@ var syncCommand = defineCommand4({
4851
5062
  const cwd = resolve13(args.cwd ?? process.cwd());
4852
5063
  const configPath = `${cwd}/navori.config.json`;
4853
5064
  p4.intro(brand("sync"));
4854
- if (!existsSync17(cwd)) {
5065
+ if (!existsSync18(cwd)) {
4855
5066
  p4.cancel(`Directory not found: ${cwd}`);
4856
5067
  process.exit(1);
4857
5068
  }
4858
- if (!existsSync17(configPath)) {
5069
+ if (!existsSync18(configPath)) {
4859
5070
  p4.cancel(`No navori.config.json at ${configPath}. Run 'navori init' first.`);
4860
5071
  process.exit(1);
4861
5072
  }
@@ -5010,8 +5221,8 @@ async function resolveConflictsInteractively(plans) {
5010
5221
  (e) => e.status === "user-modified-skipped"
5011
5222
  );
5012
5223
  if (cmConflicts.length === 0) continue;
5013
- const claudeMdPath = join13(tp.target.cwd, "CLAUDE.md");
5014
- const existing = existsSync17(claudeMdPath) ? readFileSync15(claudeMdPath, "utf-8") : "";
5224
+ const claudeMdPath = join14(tp.target.cwd, "CLAUDE.md");
5225
+ const existing = existsSync18(claudeMdPath) ? readFileSync15(claudeMdPath, "utf-8") : "";
5015
5226
  const skipIds = /* @__PURE__ */ new Set();
5016
5227
  const forceIds = /* @__PURE__ */ new Set();
5017
5228
  for (const e of cmConflicts) {
@@ -5103,13 +5314,13 @@ function summarize2(writtenCount, conflictCount) {
5103
5314
  // src/commands/add.ts
5104
5315
  import { defineCommand as defineCommand5 } from "citty";
5105
5316
  import * as p5 from "@clack/prompts";
5106
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
5317
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
5107
5318
  import { resolve as resolve14 } from "path";
5108
5319
  import { spawnSync as spawnSync3 } from "child_process";
5109
5320
 
5110
5321
  // src/lib/which.ts
5111
- import { existsSync as existsSync18, statSync as statSync6 } from "fs";
5112
- import { join as join14 } from "path";
5322
+ import { existsSync as existsSync19, statSync as statSync6 } from "fs";
5323
+ import { join as join15 } from "path";
5113
5324
  function hasBinary(name) {
5114
5325
  const pathEnv = process.env.PATH ?? "";
5115
5326
  const sep2 = process.platform === "win32" ? ";" : ":";
@@ -5117,8 +5328,8 @@ function hasBinary(name) {
5117
5328
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
5118
5329
  for (const dir of dirs) {
5119
5330
  for (const ext of exts) {
5120
- const candidate = join14(dir, name + ext);
5121
- if (existsSync18(candidate)) {
5331
+ const candidate = join15(dir, name + ext);
5332
+ if (existsSync19(candidate)) {
5122
5333
  try {
5123
5334
  if (statSync6(candidate).isFile()) return true;
5124
5335
  } catch {
@@ -5194,11 +5405,11 @@ var addCommand = defineCommand5({
5194
5405
  } else {
5195
5406
  p5.intro(brand(`add ${accent(args.plugin)}`));
5196
5407
  }
5197
- if (!existsSync19(cwd)) {
5408
+ if (!existsSync20(cwd)) {
5198
5409
  p5.cancel(`Directory not found: ${cwd}`);
5199
5410
  process.exit(1);
5200
5411
  }
5201
- if (!existsSync19(configPath)) {
5412
+ if (!existsSync20(configPath)) {
5202
5413
  p5.cancel(`No navori.config.json at ${configPath}. Run 'navori init' first.`);
5203
5414
  process.exit(1);
5204
5415
  }
@@ -5317,7 +5528,51 @@ function printSuggestions(cwd, configPath) {
5317
5528
  // src/commands/workspace.ts
5318
5529
  import { defineCommand as defineCommand6 } from "citty";
5319
5530
  import * as p6 from "@clack/prompts";
5320
- import { existsSync as existsSync20 } from "fs";
5531
+ import { existsSync as existsSync21 } from "fs";
5532
+ import { join as join16 } from "path";
5533
+
5534
+ // src/lib/workspace-defaults.ts
5535
+ var VALID_DEFAULT_KEYS = "branchBase, prTarget, commits, language, engines, plugins.<id>.enabled";
5536
+ function parseBool(value) {
5537
+ if (value === "true") return true;
5538
+ if (value === "false") return false;
5539
+ return null;
5540
+ }
5541
+ function applyDefault(current, key, rawValue) {
5542
+ const next = { ...current };
5543
+ const pluginMatch = key.match(/^plugins\.([a-z0-9][a-z0-9-]*)\.enabled$/);
5544
+ if (pluginMatch) {
5545
+ const id = pluginMatch[1];
5546
+ const enabled2 = parseBool(rawValue);
5547
+ if (enabled2 === null) {
5548
+ return { ok: false, error: `Value for '${key}' must be 'true' or 'false', got '${rawValue}'.` };
5549
+ }
5550
+ next.plugins = { ...current.plugins ?? {}, [id]: { enabled: enabled2 } };
5551
+ } else if (key === "engines") {
5552
+ next.engines = rawValue.split(",").map((s) => s.trim()).filter(Boolean);
5553
+ } else if (key === "branchBase" || key === "prTarget" || key === "commits" || key === "language") {
5554
+ next[key] = rawValue;
5555
+ } else {
5556
+ return { ok: false, error: `Unknown default key '${key}'. Valid keys: ${VALID_DEFAULT_KEYS}.` };
5557
+ }
5558
+ const parsed = WorkspaceDefaultsSchema.safeParse(next);
5559
+ if (!parsed.success) {
5560
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
5561
+ return { ok: false, error: `Invalid value for '${key}': ${detail}` };
5562
+ }
5563
+ return { ok: true, defaults: parsed.data };
5564
+ }
5565
+
5566
+ // src/commands/workspace.ts
5567
+ function readRepoConfigName(repoPath) {
5568
+ const cfgPath = join16(repoPath, "navori.config.json");
5569
+ if (!existsSync21(cfgPath)) return null;
5570
+ try {
5571
+ return readConfig(cfgPath).name;
5572
+ } catch {
5573
+ return null;
5574
+ }
5575
+ }
5321
5576
  var initSubCommand = defineCommand6({
5322
5577
  meta: {
5323
5578
  name: "init",
@@ -5335,7 +5590,7 @@ var initSubCommand = defineCommand6({
5335
5590
  process.exit(1);
5336
5591
  }
5337
5592
  const path = workspacePath(name);
5338
- if (existsSync20(path)) {
5593
+ if (existsSync21(path)) {
5339
5594
  console.error(`Workspace '${name}' already exists at ${path}`);
5340
5595
  process.exit(1);
5341
5596
  }
@@ -5449,13 +5704,42 @@ Or list known workspaces: navori workspace ls
5449
5704
  rows.push(["repos", String(workspace.repos.length)]);
5450
5705
  p6.log.message(kv(rows));
5451
5706
  if (workspace.repos.length > 0) {
5707
+ const configNames = /* @__PURE__ */ new Map();
5708
+ for (const repo of workspace.repos) {
5709
+ configNames.set(repo.name, readRepoConfigName(repo.path));
5710
+ }
5452
5711
  const repoLines = workspace.repos.map((repo) => {
5453
5712
  const stack = repo.stack ? dim(` [${repo.stack}]`) : "";
5454
5713
  const desc = repo.description ? dim(` \u2014 ${repo.description}`) : "";
5455
- return ` ${color.cyan(sym.bullet)} ${accent(repo.name)}${stack} ${dim(repo.path)}${desc}`;
5714
+ const cfgName = configNames.get(repo.name);
5715
+ const cfgLabel = cfgName && cfgName !== repo.name ? dim(` (name: ${cfgName})`) : "";
5716
+ return ` ${color.cyan(sym.bullet)} ${accent(repo.name)}${stack}${cfgLabel} ${dim(repo.path)}${desc}`;
5456
5717
  });
5457
5718
  p6.log.message(`Repos:
5458
5719
  ${repoLines.join("\n")}`);
5720
+ const byConfigName = /* @__PURE__ */ new Map();
5721
+ const placeholders = [];
5722
+ for (const repo of workspace.repos) {
5723
+ const cfgName = configNames.get(repo.name);
5724
+ if (!cfgName) continue;
5725
+ byConfigName.set(cfgName, [...byConfigName.get(cfgName) ?? [], repo.name]);
5726
+ if (isPlaceholderName(cfgName)) placeholders.push({ repo: repo.name, name: cfgName });
5727
+ }
5728
+ const collisions = [...byConfigName.entries()].filter(([, repos]) => repos.length > 1);
5729
+ if (collisions.length > 0) {
5730
+ const lines = collisions.map(
5731
+ ([name2, repos]) => ` ${color.red(sym.fail)} ${accent(name2)} ${dim("\u2190")} ${repos.join(", ")}`
5732
+ );
5733
+ p6.log.warn(
5734
+ `Colisi\xF3n de name entre repos (mismo config.name) \u2014 ren\xF3mbralos en su package.json / navori.config.json para que cada repo tenga identidad \xFAnica:
5735
+ ${lines.join("\n")}`
5736
+ );
5737
+ }
5738
+ if (placeholders.length > 0) {
5739
+ const lines = placeholders.map((pl) => ` ${color.yellow("!")} ${accent(pl.repo)} ${dim("\u2192")} '${pl.name}'`);
5740
+ p6.log.warn(`Names placeholder (scaffold sin renombrar):
5741
+ ${lines.join("\n")}`);
5742
+ }
5459
5743
  }
5460
5744
  p6.outro(dim("Done"));
5461
5745
  }
@@ -5547,14 +5831,14 @@ var deleteSubCommand = defineCommand6({
5547
5831
  return;
5548
5832
  }
5549
5833
  }
5550
- const { renameSync: renameSync3, existsSync: existsSync30, mkdirSync: mkdirSync10 } = await import("fs");
5834
+ const { renameSync: renameSync3, existsSync: existsSync31, mkdirSync: mkdirSync10 } = await import("fs");
5551
5835
  const { join: joinPath } = await import("path");
5552
5836
  const { homedir: homedir2 } = await import("os");
5553
5837
  const trashRoot = joinPath(homedir2(), ".navori", ".trash");
5554
5838
  mkdirSync10(trashRoot, { recursive: true });
5555
5839
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5556
5840
  const dest = joinPath(trashRoot, `${name}-${ts}`);
5557
- if (existsSync30(dir)) renameSync3(dir, dest);
5841
+ if (existsSync31(dir)) renameSync3(dir, dest);
5558
5842
  p6.outro(`Moved to ${dest}. Restore manually if needed.`);
5559
5843
  }
5560
5844
  });
@@ -5592,6 +5876,130 @@ var addRepoSubCommand = defineCommand6({
5592
5876
  p6.outro(dim("Done"));
5593
5877
  }
5594
5878
  });
5879
+ var setDefaultSubCommand = defineCommand6({
5880
+ meta: {
5881
+ name: "set-default",
5882
+ description: "Set a default applied to every repo in a workspace"
5883
+ },
5884
+ args: {
5885
+ workspace: { type: "positional", description: "Workspace name", required: true },
5886
+ key: {
5887
+ type: "positional",
5888
+ description: `Default key (${VALID_DEFAULT_KEYS})`,
5889
+ required: true
5890
+ },
5891
+ value: {
5892
+ type: "positional",
5893
+ description: "Value (engines: comma-separated; plugins enabled: true|false)",
5894
+ required: true
5895
+ }
5896
+ },
5897
+ run({ args }) {
5898
+ const ws = loadWorkspace(args.workspace);
5899
+ if (!ws) {
5900
+ console.error(`Workspace '${args.workspace}' not found`);
5901
+ process.exit(1);
5902
+ }
5903
+ const result = applyDefault(ws.defaults, args.key, args.value);
5904
+ if (!result.ok || !result.defaults) {
5905
+ console.error(result.error ?? "Could not apply default");
5906
+ process.exit(1);
5907
+ }
5908
+ ws.defaults = result.defaults;
5909
+ const written = writeWorkspace(ws);
5910
+ p6.intro(brand(`workspace set-default ${accent(ws.name)}`));
5911
+ p6.log.success(`Set ${accent(args.key)} ${dim(`(${written})`)}`);
5912
+ p6.log.message(kv([["defaults", JSON.stringify(ws.defaults)]]));
5913
+ p6.outro(dim("Done"));
5914
+ }
5915
+ });
5916
+ var renderSubCommand = defineCommand6({
5917
+ meta: {
5918
+ name: "render",
5919
+ description: "Render every repo registered in a workspace"
5920
+ },
5921
+ args: {
5922
+ name: { type: "positional", description: "Workspace name", required: true },
5923
+ apply: {
5924
+ type: "boolean",
5925
+ description: "Write changes to disk. Without it, every repo is previewed (no files touched)."
5926
+ },
5927
+ force: {
5928
+ type: "boolean",
5929
+ description: "Regenerate settings.json even if corrupted or missing the $navori marker."
5930
+ }
5931
+ },
5932
+ run({ args }) {
5933
+ const ws = loadWorkspace(args.name);
5934
+ if (!ws) {
5935
+ console.error(`Workspace '${args.name}' not found`);
5936
+ process.exit(1);
5937
+ }
5938
+ const apply = Boolean(args.apply);
5939
+ const preview = !apply;
5940
+ const force = Boolean(args.force);
5941
+ p6.intro(brand(`workspace render ${accent(ws.name)}`));
5942
+ if (ws.repos.length === 0) {
5943
+ p6.log.info("No repos registered. Add one with 'navori workspace add-repo'.");
5944
+ p6.outro(dim("Done"));
5945
+ return;
5946
+ }
5947
+ const rows = [];
5948
+ for (const repo of ws.repos) {
5949
+ if (!existsSync21(repo.path)) {
5950
+ rows.push({ name: repo.name, status: "missing", detail: repo.path });
5951
+ continue;
5952
+ }
5953
+ try {
5954
+ const result = runRender(repo.path, { dryRun: preview, force });
5955
+ if (!result.ok) {
5956
+ rows.push({ name: repo.name, status: "error", detail: result.reason ?? "render failed" });
5957
+ continue;
5958
+ }
5959
+ const allEntries = result.entries.concat(...result.workspaces.map((w) => w.entries));
5960
+ const anyPending = result.written || result.workspaces.some((w) => w.written);
5961
+ const status = anyPending ? preview ? "would-write" : "written" : "up-to-date";
5962
+ rows.push({ name: repo.name, status, detail: summarizeEntries(allEntries) });
5963
+ } catch (err) {
5964
+ rows.push({ name: repo.name, status: "error", detail: err.message });
5965
+ }
5966
+ }
5967
+ const marker = {
5968
+ written: color.green(sym.ok),
5969
+ "would-write": color.yellow(sym.bullet),
5970
+ "up-to-date": dim(sym.bullet),
5971
+ missing: color.red(sym.fail),
5972
+ error: color.red(sym.fail)
5973
+ };
5974
+ const lines = rows.map((r) => {
5975
+ const detail = r.detail ? dim(` ${r.detail}`) : "";
5976
+ return ` ${marker[r.status]} ${accent(r.name)} ${dim(r.status)}${detail}`;
5977
+ });
5978
+ p6.log.message(lines.join("\n"));
5979
+ const failed = rows.filter((r) => r.status === "error" || r.status === "missing").length;
5980
+ const pending = rows.filter((r) => r.status === "written" || r.status === "would-write").length;
5981
+ const ok = rows.length - failed;
5982
+ const summary = `${ok}/${rows.length} ok \xB7 ${pending} ${preview ? "would change" : "changed"} \xB7 ${failed} failed`;
5983
+ if (failed > 0) {
5984
+ p6.outro(`${color.yellow("Done with errors")} ${dim(summary)}`);
5985
+ process.exit(1);
5986
+ }
5987
+ p6.outro(`${preview ? color.yellow("Preview") : color.green("Done")} ${dim(summary)}`);
5988
+ }
5989
+ });
5990
+ function summarizeEntries(entries) {
5991
+ const counts = entries.reduce((acc, e) => {
5992
+ acc[e.status] = (acc[e.status] ?? 0) + 1;
5993
+ return acc;
5994
+ }, {});
5995
+ const parts = [];
5996
+ if (counts.created) parts.push(`${counts.created} created`);
5997
+ if (counts.updated) parts.push(`${counts.updated} updated`);
5998
+ if (counts["user-modified-skipped"]) parts.push(`${counts["user-modified-skipped"]} conflict`);
5999
+ if (counts["removed-condition-false"]) parts.push(`${counts["removed-condition-false"]} removed`);
6000
+ if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
6001
+ return parts.join(", ");
6002
+ }
5595
6003
  var workspaceCommand = defineCommand6({
5596
6004
  meta: {
5597
6005
  name: "workspace",
@@ -5602,6 +6010,8 @@ var workspaceCommand = defineCommand6({
5602
6010
  ls: lsSubCommand,
5603
6011
  show: showSubCommand,
5604
6012
  "add-repo": addRepoSubCommand,
6013
+ "set-default": setDefaultSubCommand,
6014
+ render: renderSubCommand,
5605
6015
  rename: renameSubCommand,
5606
6016
  delete: deleteSubCommand
5607
6017
  }
@@ -5613,8 +6023,8 @@ import * as p7 from "@clack/prompts";
5613
6023
  import { readFileSync as readFileSync18 } from "fs";
5614
6024
 
5615
6025
  // src/lib/tickets.ts
5616
- import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync9, statSync as statSync7, mkdirSync as mkdirSync6, renameSync as renameSync2, rmSync as rmSync5 } from "fs";
5617
- import { join as join15, resolve as resolve15 } from "path";
6026
+ import { existsSync as existsSync22, readFileSync as readFileSync17, readdirSync as readdirSync9, statSync as statSync7, mkdirSync as mkdirSync6, renameSync as renameSync2, rmSync as rmSync5 } from "fs";
6027
+ import { join as join17, resolve as resolve15 } from "path";
5618
6028
  var TicketError = class extends NavoriError {
5619
6029
  constructor(message) {
5620
6030
  super("ticket-error", message);
@@ -5623,7 +6033,7 @@ var TicketError = class extends NavoriError {
5623
6033
  function ticketsDir(workspaceName) {
5624
6034
  const ws = loadWorkspace(workspaceName);
5625
6035
  if (!ws) throw new TicketError(`Workspace '${workspaceName}' not found`);
5626
- return join15(workspaceDirectory(workspaceName), ws.ticketsDir);
6036
+ return join17(workspaceDirectory(workspaceName), ws.ticketsDir);
5627
6037
  }
5628
6038
  function readTitle(path) {
5629
6039
  try {
@@ -5640,13 +6050,13 @@ function readTitle(path) {
5640
6050
  }
5641
6051
  function listTickets(workspaceName) {
5642
6052
  const dir = ticketsDir(workspaceName);
5643
- if (!existsSync21(dir)) return [];
6053
+ if (!existsSync22(dir)) return [];
5644
6054
  const out = [];
5645
6055
  const collect = (folder, state) => {
5646
- if (!existsSync21(folder)) return;
6056
+ if (!existsSync22(folder)) return;
5647
6057
  for (const entry of readdirSync9(folder)) {
5648
6058
  if (!entry.endsWith(".md")) continue;
5649
- const full = join15(folder, entry);
6059
+ const full = join17(folder, entry);
5650
6060
  try {
5651
6061
  if (!statSync7(full).isFile()) continue;
5652
6062
  } catch {
@@ -5661,7 +6071,7 @@ function listTickets(workspaceName) {
5661
6071
  }
5662
6072
  };
5663
6073
  collect(dir, "active");
5664
- collect(join15(dir, "_archive"), "archive");
6074
+ collect(join17(dir, "_archive"), "archive");
5665
6075
  return out;
5666
6076
  }
5667
6077
  function findTicket(workspaceName, id) {
@@ -5696,9 +6106,9 @@ function archiveTicket(workspaceName, id) {
5696
6106
  if (!summary) throw new TicketError(`Ticket '${id}' not found in workspace '${workspaceName}'`);
5697
6107
  if (summary.state === "archive") return summary;
5698
6108
  const dir = ticketsDir(workspaceName);
5699
- const archiveDir = join15(dir, "_archive");
6109
+ const archiveDir = join17(dir, "_archive");
5700
6110
  mkdirSync6(archiveDir, { recursive: true });
5701
- const dest = join15(archiveDir, `${id}.md`);
6111
+ const dest = join17(archiveDir, `${id}.md`);
5702
6112
  renameSync2(summary.path, dest);
5703
6113
  return { id, path: dest, title: summary.title, state: "archive" };
5704
6114
  }
@@ -5712,9 +6122,9 @@ function createTicket(workspaceName, id, title) {
5712
6122
  throw new TicketError(`Invalid ticket id '${id}'. Use letters, digits, hyphens, underscores.`);
5713
6123
  }
5714
6124
  const dir = ticketsDir(workspaceName);
5715
- if (!existsSync21(dir)) throw new TicketError(`Tickets directory does not exist: ${dir}`);
5716
- const path = join15(dir, `${id}.md`);
5717
- if (existsSync21(path)) throw new TicketError(`Ticket '${id}' already exists at ${path}`);
6125
+ if (!existsSync22(dir)) throw new TicketError(`Tickets directory does not exist: ${dir}`);
6126
+ const path = join17(dir, `${id}.md`);
6127
+ if (existsSync22(path)) throw new TicketError(`Ticket '${id}' already exists at ${path}`);
5718
6128
  const finalTitle = title?.trim() || id;
5719
6129
  writeFileAtomic(path, defaultTemplate(id, finalTitle));
5720
6130
  return {
@@ -5729,8 +6139,8 @@ function findReferencingRepos(repoPaths, ticketId) {
5729
6139
  const idPattern = new RegExp(`\\b${ticketId}\\b`);
5730
6140
  for (const repoPath of repoPaths) {
5731
6141
  const abs = resolve15(repoPath);
5732
- const current = join15(abs, "progress", "current.md");
5733
- if (!existsSync21(current)) continue;
6142
+ const current = join17(abs, "progress", "current.md");
6143
+ if (!existsSync22(current)) continue;
5734
6144
  try {
5735
6145
  const content = readFileSync17(current, "utf-8");
5736
6146
  const matches = [];
@@ -5965,7 +6375,7 @@ var ticketCommand = defineCommand7({
5965
6375
  // src/commands/configure.ts
5966
6376
  import { defineCommand as defineCommand8 } from "citty";
5967
6377
  import * as p8 from "@clack/prompts";
5968
- import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
6378
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
5969
6379
  import { resolve as resolve16 } from "path";
5970
6380
  var ENGINE_OPTIONS2 = [
5971
6381
  { value: "claude", label: "Claude Code (.claude/)" },
@@ -5979,9 +6389,9 @@ function fail(msg) {
5979
6389
  process.exit(1);
5980
6390
  }
5981
6391
  function loadOrExit(cwd) {
5982
- if (!existsSync22(cwd)) fail(`Directory not found: ${cwd}`);
6392
+ if (!existsSync23(cwd)) fail(`Directory not found: ${cwd}`);
5983
6393
  const configPath = resolve16(cwd, "navori.config.json");
5984
- if (!existsSync22(configPath)) fail(`No navori.config.json at ${configPath}. Run 'navori init' first.`);
6394
+ if (!existsSync23(configPath)) fail(`No navori.config.json at ${configPath}. Run 'navori init' first.`);
5985
6395
  const config = readConfig(configPath);
5986
6396
  const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
5987
6397
  return { config, path: configPath, raw };
@@ -6167,6 +6577,46 @@ var branchBaseSubCommand = defineCommand8({
6167
6577
  p8.outro("Run 'navori render --apply' to update the gate scripts.");
6168
6578
  }
6169
6579
  });
6580
+ var prTargetSubCommand = defineCommand8({
6581
+ meta: {
6582
+ name: "pr-target",
6583
+ description: "Set the branch PRs target (gh pr create --base); defaults to branchBase"
6584
+ },
6585
+ args: {
6586
+ cwd: { type: "string", description: "Directory (default: cwd)" },
6587
+ value: { type: "positional", description: "Branch name (e.g. develop)", required: false }
6588
+ },
6589
+ async run({ args }) {
6590
+ const cwd = resolve16(args.cwd ?? process.cwd());
6591
+ const { config, path, raw } = loadOrExit(cwd);
6592
+ p8.intro(brand("configure pr-target"));
6593
+ let value = args.value?.trim();
6594
+ if (!value) {
6595
+ const fallback = config.prTarget ?? config.branchBase;
6596
+ const input = await p8.text({
6597
+ message: "Branch PRs open against (gh pr create --base)",
6598
+ placeholder: fallback,
6599
+ defaultValue: fallback
6600
+ });
6601
+ if (p8.isCancel(input)) {
6602
+ p8.cancel("Cancelled");
6603
+ return;
6604
+ }
6605
+ value = input.trim();
6606
+ }
6607
+ if (!value) {
6608
+ p8.cancel("Branch name cannot be empty");
6609
+ return;
6610
+ }
6611
+ raw.prTarget = value;
6612
+ persist(path, raw);
6613
+ p8.log.success(`prTarget \u2192 ${value}`);
6614
+ if (value === config.branchBase) {
6615
+ p8.log.message(dim(`Same as branchBase \u2014 PRs target ${value} as before.`));
6616
+ }
6617
+ p8.outro("Run 'navori render --apply' to update the PR skills.");
6618
+ }
6619
+ });
6170
6620
  var enginesSubCommand = defineCommand8({
6171
6621
  meta: {
6172
6622
  name: "engines",
@@ -6247,6 +6697,7 @@ var configureCommand = defineCommand8({
6247
6697
  plugins: pluginsSubCommand,
6248
6698
  "quality-gate": qualityGateSubCommand,
6249
6699
  "branch-base": branchBaseSubCommand,
6700
+ "pr-target": prTargetSubCommand,
6250
6701
  language: languageSubCommand,
6251
6702
  engines: enginesSubCommand,
6252
6703
  workspace: workspaceSubCommand
@@ -6256,7 +6707,7 @@ var configureCommand = defineCommand8({
6256
6707
  // src/commands/update.ts
6257
6708
  import { defineCommand as defineCommand9 } from "citty";
6258
6709
  import * as p9 from "@clack/prompts";
6259
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
6710
+ import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
6260
6711
  import { resolve as resolve17 } from "path";
6261
6712
  function diffConfig(current, detected) {
6262
6713
  const out = [];
@@ -6317,18 +6768,18 @@ var updateCommand = defineCommand9({
6317
6768
  const cwd = resolve17(args.cwd ?? process.cwd());
6318
6769
  const configPath = `${cwd}/navori.config.json`;
6319
6770
  p9.intro(brand("update"));
6320
- if (!existsSync23(cwd)) {
6771
+ if (!existsSync24(cwd)) {
6321
6772
  p9.cancel(`Directory not found: ${cwd}`);
6322
6773
  process.exit(1);
6323
6774
  }
6324
- if (!existsSync23(configPath)) {
6775
+ if (!existsSync24(configPath)) {
6325
6776
  p9.cancel(`No navori.config.json at ${configPath}. Run 'navori init' first.`);
6326
6777
  process.exit(1);
6327
6778
  }
6328
6779
  const config = readConfig(configPath);
6329
6780
  const detected = detectProject(cwd);
6330
6781
  const diffs = diffConfig(config, detected);
6331
- const claudeMd = existsSync23(`${cwd}/CLAUDE.md`) ? readFileSync20(`${cwd}/CLAUDE.md`, "utf-8") : "";
6782
+ const claudeMd = existsSync24(`${cwd}/CLAUDE.md`) ? readFileSync20(`${cwd}/CLAUDE.md`, "utf-8") : "";
6332
6783
  const plan = computeRenderPlan(claudeMd, config, cwd);
6333
6784
  if (diffs.length === 0 && !plan.changed && plan.updatesAvailable.length === 0) {
6334
6785
  p9.outro("Up to date \u2014 nothing to update");
@@ -6381,7 +6832,7 @@ ${lines.join("\n")}`);
6381
6832
  }
6382
6833
  if (plan.changed || plan.updatesAvailable.length > 0 || diffs.length > 0) {
6383
6834
  const freshConfig = readConfig(configPath);
6384
- const claudeMdNow = existsSync23(`${cwd}/CLAUDE.md`) ? readFileSync20(`${cwd}/CLAUDE.md`, "utf-8") : "";
6835
+ const claudeMdNow = existsSync24(`${cwd}/CLAUDE.md`) ? readFileSync20(`${cwd}/CLAUDE.md`, "utf-8") : "";
6385
6836
  const freshPlan = computeRenderPlan(claudeMdNow, freshConfig, cwd);
6386
6837
  const fresheConflicts = freshPlan.entries.filter((e) => e.status === "user-modified-skipped");
6387
6838
  if (fresheConflicts.length > 0 && !args.yes) {
@@ -6392,7 +6843,7 @@ ${lines.join("\n")}`);
6392
6843
  return;
6393
6844
  }
6394
6845
  if (freshPlan.changed) {
6395
- if (existsSync23(`${cwd}/CLAUDE.md`)) {
6846
+ if (existsSync24(`${cwd}/CLAUDE.md`)) {
6396
6847
  const handle = createBackup(cwd, ["CLAUDE.md"]);
6397
6848
  purgeOldBackups();
6398
6849
  p9.log.message(`Backup: ${handle.path}`);
@@ -6410,14 +6861,14 @@ ${lines.join("\n")}`);
6410
6861
  // src/commands/backup.ts
6411
6862
  import { defineCommand as defineCommand10 } from "citty";
6412
6863
  import * as p10 from "@clack/prompts";
6413
- import { existsSync as existsSync24, readdirSync as readdirSync10, statSync as statSync8, copyFileSync as copyFileSync4, mkdirSync as mkdirSync7 } from "fs";
6414
- import { join as join16, relative as relative5, resolve as resolve18, dirname as dirname7 } from "path";
6864
+ import { existsSync as existsSync25, readdirSync as readdirSync10, statSync as statSync8, copyFileSync as copyFileSync4, mkdirSync as mkdirSync7 } from "fs";
6865
+ import { join as join18, relative as relative5, resolve as resolve18, dirname as dirname7 } from "path";
6415
6866
  function listBackups() {
6416
6867
  const root = backupRoot();
6417
- if (!existsSync24(root)) return [];
6868
+ if (!existsSync25(root)) return [];
6418
6869
  const entries = [];
6419
6870
  for (const name of readdirSync10(root)) {
6420
- const full = join16(root, name);
6871
+ const full = join18(root, name);
6421
6872
  try {
6422
6873
  const stat = statSync8(full);
6423
6874
  if (!stat.isDirectory()) continue;
@@ -6432,7 +6883,7 @@ function listBackups() {
6432
6883
  function collectFiles(root, dir) {
6433
6884
  const out = [];
6434
6885
  for (const entry of readdirSync10(dir)) {
6435
- const full = join16(dir, entry);
6886
+ const full = join18(dir, entry);
6436
6887
  try {
6437
6888
  const stat = statSync8(full);
6438
6889
  if (stat.isDirectory()) {
@@ -6498,9 +6949,9 @@ var restoreSubCommand = defineCommand10({
6498
6949
  async run({ args }) {
6499
6950
  const ts = args.timestamp;
6500
6951
  const cwd = resolve18(args.cwd ?? process.cwd());
6501
- const backupDir = join16(backupRoot(), ts);
6952
+ const backupDir = join18(backupRoot(), ts);
6502
6953
  p10.intro(brand(`backup restore ${accent(ts)}`));
6503
- if (!existsSync24(backupDir)) {
6954
+ if (!existsSync25(backupDir)) {
6504
6955
  p10.cancel(`Backup not found: ${backupDir}`);
6505
6956
  process.exit(1);
6506
6957
  }
@@ -6522,8 +6973,8 @@ var restoreSubCommand = defineCommand10({
6522
6973
  }
6523
6974
  }
6524
6975
  for (const rel of files) {
6525
- const src = join16(backupDir, rel);
6526
- const dest = join16(cwd, rel);
6976
+ const src = join18(backupDir, rel);
6977
+ const dest = join18(cwd, rel);
6527
6978
  mkdirSync7(dirname7(dest), { recursive: true });
6528
6979
  copyFileSync4(src, dest);
6529
6980
  }
@@ -6554,19 +7005,19 @@ var backupCommand = defineCommand10({
6554
7005
  // src/commands/migrations.ts
6555
7006
  import { defineCommand as defineCommand11 } from "citty";
6556
7007
  import * as p11 from "@clack/prompts";
6557
- import { existsSync as existsSync25, readdirSync as readdirSync11, statSync as statSync9, copyFileSync as copyFileSync5, mkdirSync as mkdirSync8 } from "fs";
6558
- import { join as join17, relative as relative6, resolve as resolve19, dirname as dirname8 } from "path";
7008
+ import { existsSync as existsSync26, readdirSync as readdirSync11, statSync as statSync9, copyFileSync as copyFileSync5, mkdirSync as mkdirSync8 } from "fs";
7009
+ import { join as join19, relative as relative6, resolve as resolve19, dirname as dirname8 } from "path";
6559
7010
  function listMigrations() {
6560
7011
  const root = migrationsRoot();
6561
- if (!existsSync25(root)) return [];
7012
+ if (!existsSync26(root)) return [];
6562
7013
  const entries = [];
6563
7014
  for (const ts of readdirSync11(root)) {
6564
- const tsDir = join17(root, ts);
7015
+ const tsDir = join19(root, ts);
6565
7016
  try {
6566
7017
  const stat = statSync9(tsDir);
6567
7018
  if (!stat.isDirectory()) continue;
6568
7019
  for (const repoName of readdirSync11(tsDir)) {
6569
- const repoDir = join17(tsDir, repoName);
7020
+ const repoDir = join19(tsDir, repoName);
6570
7021
  try {
6571
7022
  const repoStat = statSync9(repoDir);
6572
7023
  if (!repoStat.isDirectory()) continue;
@@ -6584,7 +7035,7 @@ function listMigrations() {
6584
7035
  function collectFiles2(root, dir) {
6585
7036
  const out = [];
6586
7037
  for (const entry of readdirSync11(dir)) {
6587
- const full = join17(dir, entry);
7038
+ const full = join19(dir, entry);
6588
7039
  try {
6589
7040
  const stat = statSync9(full);
6590
7041
  if (stat.isDirectory()) {
@@ -6653,9 +7104,9 @@ var restoreSubCommand2 = defineCommand11({
6653
7104
  const ts = args.timestamp;
6654
7105
  const repoName = args.repo;
6655
7106
  const cwd = resolve19(args.cwd ?? process.cwd());
6656
- const migrationDir = join17(migrationsRoot(), ts, repoName);
7107
+ const migrationDir = join19(migrationsRoot(), ts, repoName);
6657
7108
  p11.intro(brand(`migrations restore ${accent(`${ts}/${repoName}`)}`));
6658
- if (!existsSync25(migrationDir)) {
7109
+ if (!existsSync26(migrationDir)) {
6659
7110
  p11.cancel(`Migration not found: ${migrationDir}`);
6660
7111
  process.exit(1);
6661
7112
  }
@@ -6678,8 +7129,8 @@ var restoreSubCommand2 = defineCommand11({
6678
7129
  }
6679
7130
  }
6680
7131
  for (const rel of files) {
6681
- const src = join17(migrationDir, rel);
6682
- const dest = join17(cwd, rel);
7132
+ const src = join19(migrationDir, rel);
7133
+ const dest = join19(cwd, rel);
6683
7134
  mkdirSync8(dirname8(dest), { recursive: true });
6684
7135
  copyFileSync5(src, dest);
6685
7136
  }
@@ -6711,8 +7162,8 @@ var migrationsCommand = defineCommand11({
6711
7162
  // src/commands/preset.ts
6712
7163
  import { defineCommand as defineCommand12 } from "citty";
6713
7164
  import * as p12 from "@clack/prompts";
6714
- import { existsSync as existsSync26, mkdirSync as mkdirSync9 } from "fs";
6715
- import { join as join18, resolve as resolve20 } from "path";
7165
+ import { existsSync as existsSync27, mkdirSync as mkdirSync9 } from "fs";
7166
+ import { join as join20, resolve as resolve20 } from "path";
6716
7167
  var PRESET_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
6717
7168
  function stackTemplate(id) {
6718
7169
  return [
@@ -6780,7 +7231,7 @@ var initSubCommand2 = defineCommand12({
6780
7231
  process.exit(1);
6781
7232
  }
6782
7233
  const presetDir = resolve20(cwd, ".navori/presets", id);
6783
- if (existsSync26(presetDir)) {
7234
+ if (existsSync27(presetDir)) {
6784
7235
  p12.cancel(
6785
7236
  `Ya existe .navori/presets/${id}/ \u2014 b\xF3rralo o usa otro id si quieres regenerarlo.`
6786
7237
  );
@@ -6806,11 +7257,11 @@ var initSubCommand2 = defineCommand12({
6806
7257
  },
6807
7258
  invariants: []
6808
7259
  };
6809
- mkdirSync9(join18(presetDir, "managed"), { recursive: true });
6810
- mkdirSync9(join18(presetDir, "skills"), { recursive: true });
6811
- writeFileAtomic(join18(presetDir, `${id}.json`), JSON.stringify(manifest, null, 2) + "\n");
6812
- writeFileAtomic(join18(presetDir, "managed", "stack.md"), stackTemplate(id));
6813
- writeFileAtomic(join18(presetDir, "skills", `${skillId}.md`), skillTemplate(skillId));
7260
+ mkdirSync9(join20(presetDir, "managed"), { recursive: true });
7261
+ mkdirSync9(join20(presetDir, "skills"), { recursive: true });
7262
+ writeFileAtomic(join20(presetDir, `${id}.json`), JSON.stringify(manifest, null, 2) + "\n");
7263
+ writeFileAtomic(join20(presetDir, "managed", "stack.md"), stackTemplate(id));
7264
+ writeFileAtomic(join20(presetDir, "skills", `${skillId}.md`), skillTemplate(skillId));
6814
7265
  p12.log.success(`Creado .navori/presets/${id}/`);
6815
7266
  p12.log.message(
6816
7267
  [
@@ -6819,8 +7270,8 @@ var initSubCommand2 = defineCommand12({
6819
7270
  ` ${dim("\xB7")} skills/${skillId}.md`
6820
7271
  ].join("\n")
6821
7272
  );
6822
- const configPath = join18(cwd, "navori.config.json");
6823
- if (existsSync26(configPath)) {
7273
+ const configPath = join20(cwd, "navori.config.json");
7274
+ if (existsSync27(configPath)) {
6824
7275
  const config = readConfig(configPath);
6825
7276
  writeConfig(configPath, { ...config, preset: id });
6826
7277
  p12.log.success(`navori.config.json \u2192 preset: ${accent(id)}`);
@@ -6847,10 +7298,10 @@ var presetCommand = defineCommand12({
6847
7298
  import { defineCommand as defineCommand13 } from "citty";
6848
7299
  import * as p13 from "@clack/prompts";
6849
7300
  import { resolve as resolve21 } from "path";
6850
- import { existsSync as existsSync27 } from "fs";
7301
+ import { existsSync as existsSync28 } from "fs";
6851
7302
  function runScan(opts) {
6852
7303
  const configPath = resolve21(opts.cwd, "navori.config.json");
6853
- if (!existsSync27(configPath)) {
7304
+ if (!existsSync28(configPath)) {
6854
7305
  return { kind: "no-config", configPath };
6855
7306
  }
6856
7307
  const config = readConfig(configPath);
@@ -7021,7 +7472,7 @@ async function collectPresetOverrides(added) {
7021
7472
  // src/commands/status.ts
7022
7473
  import { defineCommand as defineCommand14 } from "citty";
7023
7474
  import * as p14 from "@clack/prompts";
7024
- import { existsSync as existsSync28 } from "fs";
7475
+ import { existsSync as existsSync29 } from "fs";
7025
7476
  import { resolve as resolve22 } from "path";
7026
7477
  var statusCommand = defineCommand14({
7027
7478
  meta: {
@@ -7035,7 +7486,7 @@ var statusCommand = defineCommand14({
7035
7486
  async run({ args }) {
7036
7487
  const cwd = resolve22(args.cwd ?? process.cwd());
7037
7488
  const configPath = `${cwd}/navori.config.json`;
7038
- if (!existsSync28(configPath)) {
7489
+ if (!existsSync29(configPath)) {
7039
7490
  if (args.json) {
7040
7491
  console.log(JSON.stringify({ ok: false, error: "config-missing", configPath }));
7041
7492
  } else {
@@ -7059,7 +7510,7 @@ var statusCommand = defineCommand14({
7059
7510
  }
7060
7511
  throw err;
7061
7512
  }
7062
- const claudeMdExists = existsSync28(`${cwd}/CLAUDE.md`);
7513
+ const claudeMdExists = existsSync29(`${cwd}/CLAUDE.md`);
7063
7514
  const missingPlugins = collectMissingPlugins(config);
7064
7515
  const drifts = scanManagedDrift(cwd, config);
7065
7516
  const enabledPlugins = Object.entries(config.plugins ?? {}).filter(([, v]) => v.enabled === true).map(([k]) => k);
@@ -7107,7 +7558,7 @@ var statusCommand = defineCommand14({
7107
7558
  import { defineCommand as defineCommand15 } from "citty";
7108
7559
  import * as p15 from "@clack/prompts";
7109
7560
  import { performance as performance2 } from "perf_hooks";
7110
- import { existsSync as existsSync29 } from "fs";
7561
+ import { existsSync as existsSync30 } from "fs";
7111
7562
  import { resolve as resolve23 } from "path";
7112
7563
  var benchCommand = defineCommand15({
7113
7564
  meta: {
@@ -7121,7 +7572,7 @@ var benchCommand = defineCommand15({
7121
7572
  async run({ args }) {
7122
7573
  const cwd = resolve23(args.cwd ?? process.cwd());
7123
7574
  p15.intro(brand("bench"));
7124
- if (!existsSync29(`${cwd}/navori.config.json`)) {
7575
+ if (!existsSync30(`${cwd}/navori.config.json`)) {
7125
7576
  p15.cancel(`No navori.config.json at ${cwd}. Run 'navori init' first.`);
7126
7577
  process.exit(1);
7127
7578
  }