@3lineas/grooveskills 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,8 @@
1
+ // src/main.ts
2
+ import { basename as basename3 } from "path";
3
+ import { userInfo as userInfo2 } from "os";
4
+ import picocolors5 from "picocolors";
5
+
1
6
  // src/domains/detector/project-detector.ts
2
7
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
3
8
  import { join as join2 } from "path";
@@ -37,11 +42,11 @@ function detectDependencies(targetPath) {
37
42
  try {
38
43
  const raw = readFileSync(packageJsonPath, "utf-8");
39
44
  const parsed = JSON.parse(raw);
40
- for (const dep of Object.keys(parsed.dependencies ?? {})) {
41
- detected.add(dep);
45
+ for (const dependencyName of Object.keys(parsed.dependencies ?? {})) {
46
+ detected.add(dependencyName);
42
47
  }
43
- for (const dep of Object.keys(parsed.devDependencies ?? {})) {
44
- detected.add(dep);
48
+ for (const dependencyName of Object.keys(parsed.devDependencies ?? {})) {
49
+ detected.add(dependencyName);
45
50
  }
46
51
  } catch {
47
52
  }
@@ -265,7 +270,9 @@ function detectProjectContext(targetPath) {
265
270
  if (skill.dependencies.length === 0) {
266
271
  continue;
267
272
  }
268
- const hasMatch = skill.dependencies.some((dep) => dependencies.includes(dep));
273
+ const hasMatch = skill.dependencies.some(
274
+ (dependencyName) => dependencies.includes(dependencyName)
275
+ );
269
276
  if (hasMatch && !suggestedSkillIds.includes(skill.id)) {
270
277
  suggestedSkillIds.push(skill.id);
271
278
  }
@@ -284,21 +291,21 @@ function detectProjectContext(targetPath) {
284
291
  }
285
292
 
286
293
  // src/domains/ui/ui-orchestrator.ts
287
- import * as p5 from "@clack/prompts";
288
- import pc3 from "picocolors";
294
+ import * as prompts6 from "@clack/prompts";
295
+ import picocolors4 from "picocolors";
289
296
 
290
297
  // src/domains/ui/banners.ts
291
- import pc from "picocolors";
298
+ import picocolors from "picocolors";
292
299
  function renderBanner() {
293
- const title = pc.bold(pc.white(" GROOVE SKILLS "));
294
- const badge = pc.bgMagenta(pc.black(title));
295
- const subtitle = pc.dim("Multi-Agent AI Skills & Architectural Standards");
300
+ const title = picocolors.bold(picocolors.white(" GROOVE SKILLS "));
301
+ const badge = picocolors.bgMagenta(picocolors.black(title));
302
+ const subtitle = picocolors.dim("Multi-Agent AI Skills & Architectural Standards");
296
303
  return `${badge} ${subtitle}
297
304
  `;
298
305
  }
299
306
 
300
307
  // src/domains/ui/prompt-skills.ts
301
- import * as p from "@clack/prompts";
308
+ import * as prompts from "@clack/prompts";
302
309
  async function promptSkills(suggestedSkillIds) {
303
310
  const selectableSkills = SKILL_CATALOG.filter((skill) => !skill.isCoreHarness);
304
311
  const options = selectableSkills.map((skill) => {
@@ -309,7 +316,7 @@ async function promptSkills(suggestedSkillIds) {
309
316
  hint: isSuggested ? "Suggested based on your project stack" : skill.description
310
317
  };
311
318
  });
312
- const selected = await p.multiselect({
319
+ const selected = await prompts.multiselect({
313
320
  message: "Which skills would you like to install in this project?",
314
321
  options,
315
322
  initialValues: suggestedSkillIds,
@@ -319,7 +326,7 @@ async function promptSkills(suggestedSkillIds) {
319
326
  }
320
327
 
321
328
  // src/domains/ui/prompt-agents.ts
322
- import * as p2 from "@clack/prompts";
329
+ import * as prompts2 from "@clack/prompts";
323
330
  async function promptAgents(detectedAgents) {
324
331
  const options = [
325
332
  { value: "antigravity", label: "Antigravity", hint: ".agents/skills (Google Agentic IDE)" },
@@ -328,7 +335,7 @@ async function promptAgents(detectedAgents) {
328
335
  { value: "gemini", label: "Gemini Code Assist", hint: ".gemini/skills (Google Gemini)" }
329
336
  ];
330
337
  const initialValues = detectedAgents.length > 0 ? detectedAgents : ["antigravity"];
331
- const selected = await p2.multiselect({
338
+ const selected = await prompts2.multiselect({
332
339
  message: "Which AI agents should be configured?",
333
340
  options,
334
341
  initialValues,
@@ -338,16 +345,16 @@ async function promptAgents(detectedAgents) {
338
345
  }
339
346
 
340
347
  // src/domains/ui/prompt-language.ts
341
- import * as p3 from "@clack/prompts";
348
+ import * as prompts3 from "@clack/prompts";
342
349
  async function promptLanguage(defaultLanguage = "es") {
343
350
  const options = [
344
- { value: "es", label: "Espa\xF1ol (Spanish)", hint: "Recomendado para proyectos en espa\xF1ol" },
351
+ { value: "es", label: "Espa\xF1ol (Spanish)", hint: "Recommended for Spanish-speaking projects" },
345
352
  { value: "en", label: "English", hint: "Standard international English" },
346
- { value: "pt", label: "Portugu\xEAs (Portuguese)", hint: "Portugu\xEAs para o Brasil e Portugal" },
347
- { value: "it", label: "Italiano (Italian)", hint: "Comunicazione in lingua italiana" },
348
- { value: "fr", label: "Fran\xE7ais (French)", hint: "Communication en langue fran\xE7aise" }
353
+ { value: "pt", label: "Portugu\xEAs (Portuguese)", hint: "Portuguese for Brazil and Portugal" },
354
+ { value: "it", label: "Italiano (Italian)", hint: "Italian language communication" },
355
+ { value: "fr", label: "Fran\xE7ais (French)", hint: "French language communication" }
349
356
  ];
350
- const selected = await p3.select({
357
+ const selected = await prompts3.select({
351
358
  message: "Which language should AI agents use for user-facing copy and communication?",
352
359
  options,
353
360
  initialValue: defaultLanguage
@@ -356,9 +363,9 @@ async function promptLanguage(defaultLanguage = "es") {
356
363
  }
357
364
 
358
365
  // src/domains/ui/prompt-confirm-overwrite.ts
359
- import * as p4 from "@clack/prompts";
366
+ import * as prompts4 from "@clack/prompts";
360
367
  async function promptConfirmOverwrite() {
361
- return await p4.confirm({
368
+ return await prompts4.confirm({
362
369
  message: "Some skills already exist in this project. Would you like to overwrite them with the latest version?",
363
370
  initialValue: true
364
371
  });
@@ -368,10 +375,10 @@ async function promptConfirmOverwrite() {
368
375
  import { lstatSync } from "fs";
369
376
  import { join as join3 } from "path";
370
377
  var AGENT_DESTINATION_MAP = {
371
- antigravity: (dir) => join3(".agents", "skills", dir),
372
- claude: (dir) => join3(".claude", "skills", dir),
373
- cursor: (dir) => join3(".cursor", "rules", `${dir}.mdc`),
374
- gemini: (dir) => join3(".gemini", "skills", dir)
378
+ antigravity: (directoryName) => join3(".agents", "skills", directoryName),
379
+ claude: (directoryName) => join3(".claude", "skills", directoryName),
380
+ cursor: (directoryName) => join3(".cursor", "rules", `${directoryName}.mdc`),
381
+ gemini: (directoryName) => join3(".gemini", "skills", directoryName)
375
382
  };
376
383
  function verifySkillExists(targetPath, skillDirectoryName, agents) {
377
384
  for (const agent of agents) {
@@ -387,10 +394,10 @@ function verifySkillExists(targetPath, skillDirectoryName, agents) {
387
394
  }
388
395
 
389
396
  // src/domains/installer/install-orchestrator.ts
390
- import { join as join11 } from "path";
397
+ import { join as join12 } from "path";
391
398
 
392
399
  // src/domains/adapters/skill-reference-builder.ts
393
- import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync3, rmSync, unlinkSync } from "fs";
400
+ import { existsSync as existsSync5, lstatSync as lstatSync3, mkdirSync, readFileSync as readFileSync3, rmSync as rmSync2, unlinkSync } from "fs";
394
401
  import { join as join5 } from "path";
395
402
 
396
403
  // src/domains/project-store/project-paths.ts
@@ -412,48 +419,194 @@ function getProjectConfigPath(targetPath) {
412
419
  }
413
420
 
414
421
  // src/domains/fs/safe-file-writer.ts
415
- import { chmodSync, existsSync as existsSync3, writeFileSync } from "fs";
422
+ import { chmodSync, existsSync as existsSync4, renameSync, rmSync, writeFileSync } from "fs";
423
+
424
+ // src/domains/fs/inspect-file-access.ts
425
+ import { execFileSync } from "child_process";
426
+ import { accessSync, constants, existsSync as existsSync3, lstatSync as lstatSync2 } from "fs";
427
+ import { dirname } from "path";
428
+ var UF_IMMUTABLE = 2;
429
+ var SF_IMMUTABLE = 131072;
430
+ function resolveNearestExistingDir(startDir) {
431
+ let current = startDir;
432
+ while (!existsSync3(current)) {
433
+ const parent = dirname(current);
434
+ if (parent === current) {
435
+ break;
436
+ }
437
+ current = parent;
438
+ }
439
+ return current;
440
+ }
441
+ function detectMacOsImmutableFlag(filePath, lstat) {
442
+ const rawFlags = lstat.flags;
443
+ if (typeof rawFlags === "number") {
444
+ return Boolean(rawFlags & (UF_IMMUTABLE | SF_IMMUTABLE));
445
+ }
446
+ if (process.platform !== "darwin") {
447
+ return false;
448
+ }
449
+ try {
450
+ const out = execFileSync("stat", ["-f", "%f", filePath], {
451
+ encoding: "utf-8",
452
+ stdio: ["ignore", "pipe", "ignore"]
453
+ }).trim();
454
+ const parsedFlags = Number.parseInt(out, 10);
455
+ if (Number.isNaN(parsedFlags)) {
456
+ return false;
457
+ }
458
+ return Boolean(parsedFlags & (UF_IMMUTABLE | SF_IMMUTABLE));
459
+ } catch {
460
+ return false;
461
+ }
462
+ }
463
+ function inspectFileAccess(filePath) {
464
+ const currentUid = typeof process.getuid === "function" ? process.getuid() : null;
465
+ const parentDir = dirname(filePath);
466
+ const checkDir = resolveNearestExistingDir(parentDir);
467
+ let dirWritable = false;
468
+ try {
469
+ accessSync(checkDir, constants.W_OK);
470
+ dirWritable = true;
471
+ } catch {
472
+ dirWritable = false;
473
+ }
474
+ let lstat = null;
475
+ try {
476
+ lstat = lstatSync2(filePath);
477
+ } catch {
478
+ lstat = null;
479
+ }
480
+ if (!lstat) {
481
+ return {
482
+ filePath,
483
+ exists: false,
484
+ isSymlink: false,
485
+ ownedByCurrentUser: true,
486
+ ownerUid: null,
487
+ currentUid,
488
+ mode: null,
489
+ immutableFlag: false,
490
+ dirWritable,
491
+ parentDir
492
+ };
493
+ }
494
+ const isSymlink = lstat.isSymbolicLink();
495
+ const ownerUid = lstat.uid;
496
+ const ownedByCurrentUser = currentUid === null || ownerUid === currentUid;
497
+ const mode = lstat.mode & 511;
498
+ const immutableFlag = detectMacOsImmutableFlag(filePath, lstat);
499
+ return {
500
+ filePath,
501
+ exists: true,
502
+ isSymlink,
503
+ ownedByCurrentUser,
504
+ ownerUid,
505
+ currentUid,
506
+ mode,
507
+ immutableFlag,
508
+ dirWritable,
509
+ parentDir
510
+ };
511
+ }
512
+ function formatOwnerName(uid) {
513
+ if (uid === 0) {
514
+ return "root";
515
+ }
516
+ if (uid === null) {
517
+ return "another user";
518
+ }
519
+ return `uid ${uid}`;
520
+ }
416
521
 
417
522
  // src/errors/file-permission-error.ts
523
+ function buildDiagnosisReason(diagnosis) {
524
+ if (diagnosis.immutableFlag) {
525
+ return "immutable flag (uchg) is set";
526
+ }
527
+ if (diagnosis.exists && !diagnosis.ownedByCurrentUser) {
528
+ return `owned by ${formatOwnerName(diagnosis.ownerUid)}`;
529
+ }
530
+ if (!diagnosis.dirWritable) {
531
+ return `parent directory '${diagnosis.parentDir}' is not writable`;
532
+ }
533
+ return "permission denied";
534
+ }
535
+ function buildSuggestedFix(diagnosis) {
536
+ if (diagnosis.immutableFlag) {
537
+ return `chflags nouchg "${diagnosis.filePath}"`;
538
+ }
539
+ if (diagnosis.exists && !diagnosis.ownedByCurrentUser) {
540
+ return `sudo chown "$(id -un)" "${diagnosis.filePath}"`;
541
+ }
542
+ if (!diagnosis.dirWritable) {
543
+ return `chmod u+w "${diagnosis.parentDir}"`;
544
+ }
545
+ return `chmod u+w "${diagnosis.filePath}"`;
546
+ }
418
547
  var FilePermissionError = class extends Error {
419
548
  code = "EACCES";
420
549
  filePath;
550
+ diagnosis;
421
551
  suggestedFixCommand;
422
- constructor(filePath) {
423
- super(`No tienes permisos de escritura para modificar '${filePath}'.`);
552
+ constructor(filePath, diagnosis) {
553
+ const resolved = diagnosis ?? inspectFileAccess(filePath);
554
+ const reason = buildDiagnosisReason(resolved);
555
+ super(`Cannot write to ${filePath} (${reason})`);
424
556
  this.name = "FilePermissionError";
425
557
  this.filePath = filePath;
426
- this.suggestedFixCommand = `chmod u+w "${filePath}"`;
558
+ this.diagnosis = resolved;
559
+ this.suggestedFixCommand = buildSuggestedFix(resolved);
427
560
  }
428
561
  };
429
562
 
430
563
  // src/domains/fs/safe-file-writer.ts
431
- function safeWriteFileSync(filePath, content) {
564
+ function safeWriteFileSync(filePath, content, options) {
565
+ const chmodFunction = options?.chmodImpl ?? chmodSync;
432
566
  try {
433
567
  writeFileSync(filePath, content, "utf-8");
568
+ return { strategy: "direct", previousOwnerUid: null };
434
569
  } catch (error) {
435
- const err = error;
436
- if (err.code === "EACCES" || err.code === "EPERM") {
437
- if (existsSync3(filePath)) {
570
+ const nodeError = error;
571
+ if (nodeError.code !== "EACCES" && nodeError.code !== "EPERM") {
572
+ throw error;
573
+ }
574
+ }
575
+ const diagnosis = inspectFileAccess(filePath);
576
+ if (diagnosis.exists && diagnosis.ownedByCurrentUser && !diagnosis.immutableFlag) {
577
+ try {
578
+ const targetMode = (diagnosis.mode ?? 420) | 128;
579
+ chmodFunction(filePath, targetMode);
580
+ writeFileSync(filePath, content, "utf-8");
581
+ return { strategy: "chmod", previousOwnerUid: diagnosis.ownerUid };
582
+ } catch {
583
+ }
584
+ }
585
+ if (diagnosis.exists && diagnosis.dirWritable && !diagnosis.immutableFlag) {
586
+ const tempPath = `${filePath}.groove-tmp-${process.pid}-${Date.now()}`;
587
+ try {
588
+ const preservedMode = (diagnosis.mode ?? 420) | 128;
589
+ writeFileSync(tempPath, content, { encoding: "utf-8", mode: preservedMode });
590
+ renameSync(tempPath, filePath);
591
+ return { strategy: "replaced", previousOwnerUid: diagnosis.ownerUid };
592
+ } catch {
593
+ if (existsSync4(tempPath)) {
438
594
  try {
439
- chmodSync(filePath, 420);
440
- writeFileSync(filePath, content, "utf-8");
441
- return;
595
+ rmSync(tempPath, { force: true });
442
596
  } catch {
443
597
  }
444
598
  }
445
- throw new FilePermissionError(filePath);
446
599
  }
447
- throw error;
448
600
  }
601
+ throw new FilePermissionError(filePath, diagnosis);
449
602
  }
450
603
 
451
604
  // src/domains/adapters/skill-reference-builder.ts
452
605
  function removeExistingPathSafely(path) {
453
- const fileExists = existsSync4(path);
606
+ const fileExists = existsSync5(path);
454
607
  if (!fileExists) {
455
608
  try {
456
- const lstat = lstatSync2(path);
609
+ const lstat = lstatSync3(path);
457
610
  if (lstat.isSymbolicLink()) {
458
611
  unlinkSync(path);
459
612
  }
@@ -462,13 +615,13 @@ function removeExistingPathSafely(path) {
462
615
  return;
463
616
  }
464
617
  try {
465
- const lstat = lstatSync2(path);
618
+ const lstat = lstatSync3(path);
466
619
  if (lstat.isSymbolicLink()) {
467
620
  unlinkSync(path);
468
621
  return;
469
622
  }
470
623
  if (lstat.isDirectory()) {
471
- rmSync(path, { recursive: true, force: true });
624
+ rmSync2(path, { recursive: true, force: true });
472
625
  return;
473
626
  }
474
627
  unlinkSync(path);
@@ -476,7 +629,7 @@ function removeExistingPathSafely(path) {
476
629
  }
477
630
  }
478
631
  function extractSkillDescription(canonicalSkillMdPath, fallback) {
479
- if (!existsSync4(canonicalSkillMdPath)) {
632
+ if (!existsSync5(canonicalSkillMdPath)) {
480
633
  return fallback;
481
634
  }
482
635
  try {
@@ -526,7 +679,7 @@ function writeCursorRuleReference(targetPath, cursorRelativeRulesDir, skill, tra
526
679
  transaction.recordCreatedDir(rulesDir);
527
680
  transaction.recordBeforeWrite(targetFile);
528
681
  }
529
- if (!existsSync4(rulesDir)) {
682
+ if (!existsSync5(rulesDir)) {
530
683
  mkdirSync(rulesDir, { recursive: true });
531
684
  }
532
685
  removeExistingPathSafely(targetFile);
@@ -596,31 +749,31 @@ var GeminiAdapter = class {
596
749
  };
597
750
 
598
751
  // src/domains/project-store/sync-project-store.ts
599
- import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, rmSync as rmSync2 } from "fs";
752
+ import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
600
753
  import { join as join7 } from "path";
601
754
 
602
755
  // src/domains/installer/resolve-skills-source.ts
603
756
  import { fileURLToPath } from "url";
604
- import { dirname, resolve, join as join6 } from "path";
605
- import { existsSync as existsSync5 } from "fs";
757
+ import { dirname as dirname2, resolve, join as join6 } from "path";
758
+ import { existsSync as existsSync6 } from "fs";
606
759
  function resolveSkillsSourceRoot() {
607
760
  const currentFilePath = fileURLToPath(import.meta.url);
608
- const currentDir = dirname(currentFilePath);
761
+ const currentDir = dirname2(currentFilePath);
609
762
  const candidateFromDist = resolve(currentDir, "../skills");
610
- if (existsSync5(candidateFromDist)) {
763
+ if (existsSync6(candidateFromDist)) {
611
764
  return candidateFromDist;
612
765
  }
613
766
  const candidateFromSrc = resolve(currentDir, "../../../skills");
614
- if (existsSync5(candidateFromSrc)) {
767
+ if (existsSync6(candidateFromSrc)) {
615
768
  return candidateFromSrc;
616
769
  }
617
770
  let searchDir = currentDir;
618
- for (let i = 0; i < 5; i++) {
771
+ for (let depth = 0; depth < 5; depth++) {
619
772
  const candidate = join6(searchDir, "skills");
620
- if (existsSync5(candidate)) {
773
+ if (existsSync6(candidate)) {
621
774
  return candidate;
622
775
  }
623
- const parent = dirname(searchDir);
776
+ const parent = dirname2(searchDir);
624
777
  if (parent === searchDir) {
625
778
  break;
626
779
  }
@@ -638,13 +791,13 @@ function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents, transac
638
791
  if (transaction) {
639
792
  transaction.recordCreatedDir(projectSkillsDir);
640
793
  }
641
- if (!existsSync6(projectSkillsDir)) {
794
+ if (!existsSync7(projectSkillsDir)) {
642
795
  mkdirSync2(projectSkillsDir, { recursive: true });
643
796
  }
644
797
  const legacyCursorRules = join7(targetPath, ".grooveskills", "cursor-rules");
645
- if (existsSync6(legacyCursorRules)) {
798
+ if (existsSync7(legacyCursorRules)) {
646
799
  try {
647
- rmSync2(legacyCursorRules, { recursive: true, force: true });
800
+ rmSync3(legacyCursorRules, { recursive: true, force: true });
648
801
  } catch {
649
802
  }
650
803
  }
@@ -658,14 +811,14 @@ function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents, transac
658
811
  if (transaction) {
659
812
  transaction.recordCreatedDir(destSkillDir);
660
813
  }
661
- if (existsSync6(sourceSkillDir)) {
814
+ if (existsSync7(sourceSkillDir)) {
662
815
  cpSync(sourceSkillDir, destSkillDir, { recursive: true });
663
816
  }
664
817
  }
665
818
  }
666
819
 
667
820
  // src/domains/harness/install-harness.ts
668
- import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "fs";
821
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "fs";
669
822
  import { join as join10 } from "path";
670
823
 
671
824
  // src/domains/harness/harness-languages.ts
@@ -714,14 +867,14 @@ var SUPPORTED_LANGUAGES = {
714
867
 
715
868
  // src/domains/harness/harness-templates.ts
716
869
  function buildContextTemplate(detectedDependencies, language = "es", profile) {
717
- const depsFormatted = detectedDependencies.length > 0 ? detectedDependencies.map((d) => `- \`${d}\``).join("\n") : "- (No external dependencies detected)";
718
- const langConfig = SUPPORTED_LANGUAGES[language];
870
+ const dependenciesFormatted = detectedDependencies.length > 0 ? detectedDependencies.map((dependencyName) => `- \`${dependencyName}\``).join("\n") : "- (No external dependencies detected)";
871
+ const languageConfig = SUPPORTED_LANGUAGES[language];
719
872
  const projectName = profile?.projectName ?? "Project";
720
- const pkgManager = profile?.packageManager ?? "npm";
873
+ const packageManager = profile?.packageManager ?? "npm";
721
874
  const commandLines = [];
722
875
  if (profile?.commands) {
723
- for (const [key, cmd] of Object.entries(profile.commands)) {
724
- commandLines.push(`- **${key}**: \`${cmd}\``);
876
+ for (const [commandKey, commandValue] of Object.entries(profile.commands)) {
877
+ commandLines.push(`- **${commandKey}**: \`${commandValue}\``);
725
878
  }
726
879
  }
727
880
  const commandsSection = commandLines.length > 0 ? commandLines.join("\n") : `- Build: Refer to \`package.json\` scripts.
@@ -729,62 +882,62 @@ function buildContextTemplate(detectedDependencies, language = "es", profile) {
729
882
  - Tests: Refer to test runner configuration.`;
730
883
  const aliasLines = [];
731
884
  if (profile?.pathAliases) {
732
- for (const [alias, target] of Object.entries(profile.pathAliases)) {
733
- aliasLines.push(`- \`${alias}\` -> \`${target}\``);
885
+ for (const [aliasPattern, aliasTarget] of Object.entries(profile.pathAliases)) {
886
+ aliasLines.push(`- \`${aliasPattern}\` -> \`${aliasTarget}\``);
734
887
  }
735
888
  }
736
889
  const aliasesSection = aliasLines.length > 0 ? `
737
890
  ### Path Aliases
738
891
  ${aliasLines.join("\n")}
739
892
  ` : "";
740
- const dirLines = [];
893
+ const directoryLines = [];
741
894
  if (profile?.sourceDirectories && profile.sourceDirectories.length > 0) {
742
- for (const dir of profile.sourceDirectories) {
743
- dirLines.push(`- \`${dir}/\``);
895
+ for (const directoryName of profile.sourceDirectories) {
896
+ directoryLines.push(`- \`${directoryName}/\``);
744
897
  }
745
898
  }
746
- const dirsSection = dirLines.length > 0 ? `
899
+ const directoriesSection = directoryLines.length > 0 ? `
747
900
  ### Source Modules
748
- ${dirLines.join("\n")}
901
+ ${directoryLines.join("\n")}
749
902
  ` : "";
750
903
  return `# Project Context & Architecture: ${projectName}
751
904
 
752
905
  This file provides AI agents with instant architectural orientation for this project.
753
906
 
754
907
  ## 1. Stack & Dependencies
755
- - **Package Manager**: \`${pkgManager}\`
756
- ${depsFormatted}
908
+ - **Package Manager**: \`${packageManager}\`
909
+ ${dependenciesFormatted}
757
910
 
758
911
  ## 2. Directory Structure Conventions
759
912
  - Follow single-responsibility per file (<200 lines).
760
913
  - Maintain direct imports with path aliases; avoid barrel files.
761
914
  - Keep domain boundaries explicit and clean.
762
- ${aliasesSection}${dirsSection}
915
+ ${aliasesSection}${directoriesSection}
763
916
  ## 3. Communication & User Copy Language
764
- - **Configured Language**: ${langConfig.nativeName} (\`${language}\`).
765
- - **Policy**: ${langConfig.instruction}
917
+ - **Configured Language**: ${languageConfig.nativeName} (\`${language}\`).
918
+ - **Policy**: ${languageConfig.instruction}
766
919
 
767
920
  ## 4. Essential Commands
768
921
  ${commandsSection}
769
922
  `;
770
923
  }
771
924
  function buildDecisionsTemplate(language = "es") {
772
- const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
773
- const langConfig = SUPPORTED_LANGUAGES[language];
925
+ const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
926
+ const languageConfig = SUPPORTED_LANGUAGES[language];
774
927
  return `# Architecture Decision Records (ADR)
775
928
 
776
929
  An append-only log of significant technical and architectural choices.
777
930
 
778
931
  | Date | Title | Decision | Reason & Consequences |
779
932
  | :--- | :--- | :--- | :--- |
780
- | ${now} | Adopt GrooveSkills Harness | Initialized \`.grooveskills/\` control plane with ODD | Clean, encapsulated agent memory, 3-lane ODD flow, and ${langConfig.nativeName} copy standard |
933
+ | ${currentDate} | Adopt GrooveSkills Harness | Initialized \`.grooveskills/\` control plane with ODD | Clean, encapsulated agent memory, 3-lane ODD flow, and ${languageConfig.nativeName} copy standard |
781
934
  `;
782
935
  }
783
936
  function buildOnboardingTaskTemplate(projectName = "Project") {
784
- const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
937
+ const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
785
938
  return `---
786
939
  status: pending
787
- createdAt: ${now}
940
+ createdAt: ${currentDate}
788
941
  ---
789
942
 
790
943
  # 00 \u2014 Initial Project Onboarding & Architecture Exploration: ${projectName}
@@ -840,7 +993,7 @@ By [Output / Technical Change], [Outcome / Verifiable Behavior] will occur, prov
840
993
  `;
841
994
  }
842
995
  function buildAgentHarnessBlock(language = "es") {
843
- const langConfig = SUPPORTED_LANGUAGES[language];
996
+ const languageConfig = SUPPORTED_LANGUAGES[language];
844
997
  return `<!-- grooveskills:harness:start -->
845
998
  # GrooveSkills Harness \u2014 Agent Rules & Memory
846
999
 
@@ -853,9 +1006,9 @@ This project uses the **GrooveSkills Harness** located in \`.grooveskills/\`.
853
1006
  - **Lane 2 (Task)**: Direct implementation. Declare Outcome Hypothesis and checklist in \`.grooveskills/memory/tasks/<slug>.md\`. Only check off tasks with observed evidence under \`Evidence\`.
854
1007
  - **Lane 3 (Spec)**: Ambiguous or high-impact architecture. Clarify and validate the spec in \`.grooveskills/memory/tasks/<slug>.md\` before writing code.
855
1008
 
856
- ## 2. User Communication Language (${langConfig.nativeName})
857
- - **Language**: \`${language}\` (${langConfig.nativeName}).
858
- - **Requirement**: ${langConfig.instruction}
1009
+ ## 2. User Communication Language (${languageConfig.nativeName})
1010
+ - **Language**: \`${language}\` (${languageConfig.nativeName}).
1011
+ - **Requirement**: ${languageConfig.instruction}
859
1012
 
860
1013
  ## 3. Memory Locations
861
1014
  - Architecture context: \`.grooveskills/memory/context.md\`
@@ -865,11 +1018,11 @@ This project uses the **GrooveSkills Harness** located in \`.grooveskills/\`.
865
1018
  }
866
1019
 
867
1020
  // src/domains/harness/scan-project-onboarding.ts
868
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
1021
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
869
1022
  import { basename, join as join8 } from "path";
870
1023
  function detectProjectName(targetPath) {
871
1024
  const packageJsonPath = join8(targetPath, "package.json");
872
- if (existsSync7(packageJsonPath)) {
1025
+ if (existsSync8(packageJsonPath)) {
873
1026
  try {
874
1027
  const raw = readFileSync4(packageJsonPath, "utf-8");
875
1028
  const parsed = JSON.parse(raw);
@@ -880,7 +1033,7 @@ function detectProjectName(targetPath) {
880
1033
  }
881
1034
  }
882
1035
  const cargoPath = join8(targetPath, "Cargo.toml");
883
- if (existsSync7(cargoPath)) {
1036
+ if (existsSync8(cargoPath)) {
884
1037
  try {
885
1038
  const cargoRaw = readFileSync4(cargoPath, "utf-8");
886
1039
  const nameMatch = cargoRaw.match(/name\s*=\s*"([^"]+)"/);
@@ -891,12 +1044,12 @@ function detectProjectName(targetPath) {
891
1044
  }
892
1045
  }
893
1046
  const pyprojectPath = join8(targetPath, "pyproject.toml");
894
- if (existsSync7(pyprojectPath)) {
1047
+ if (existsSync8(pyprojectPath)) {
895
1048
  try {
896
- const pyRaw = readFileSync4(pyprojectPath, "utf-8");
897
- const pyMatch = pyRaw.match(/name\s*=\s*"([^"]+)"/);
898
- if (pyMatch?.[1]) {
899
- return pyMatch[1];
1049
+ const pyprojectRaw = readFileSync4(pyprojectPath, "utf-8");
1050
+ const nameMatch = pyprojectRaw.match(/name\s*=\s*"([^"]+)"/);
1051
+ if (nameMatch?.[1]) {
1052
+ return nameMatch[1];
900
1053
  }
901
1054
  } catch {
902
1055
  }
@@ -904,32 +1057,32 @@ function detectProjectName(targetPath) {
904
1057
  return basename(targetPath) || "project";
905
1058
  }
906
1059
  function detectPackageManager(targetPath) {
907
- if (existsSync7(join8(targetPath, "pnpm-lock.yaml"))) {
1060
+ if (existsSync8(join8(targetPath, "pnpm-lock.yaml"))) {
908
1061
  return "pnpm";
909
1062
  }
910
- if (existsSync7(join8(targetPath, "bun.lockb")) || existsSync7(join8(targetPath, "bun.lock"))) {
1063
+ if (existsSync8(join8(targetPath, "bun.lockb")) || existsSync8(join8(targetPath, "bun.lock"))) {
911
1064
  return "bun";
912
1065
  }
913
- if (existsSync7(join8(targetPath, "yarn.lock"))) {
1066
+ if (existsSync8(join8(targetPath, "yarn.lock"))) {
914
1067
  return "yarn";
915
1068
  }
916
- if (existsSync7(join8(targetPath, "package-lock.json"))) {
1069
+ if (existsSync8(join8(targetPath, "package-lock.json"))) {
917
1070
  return "npm";
918
1071
  }
919
- if (existsSync7(join8(targetPath, "Cargo.lock")) || existsSync7(join8(targetPath, "Cargo.toml"))) {
1072
+ if (existsSync8(join8(targetPath, "Cargo.lock")) || existsSync8(join8(targetPath, "Cargo.toml"))) {
920
1073
  return "cargo";
921
1074
  }
922
- if (existsSync7(join8(targetPath, "uv.lock"))) {
1075
+ if (existsSync8(join8(targetPath, "uv.lock"))) {
923
1076
  return "uv";
924
1077
  }
925
- if (existsSync7(join8(targetPath, "poetry.lock"))) {
1078
+ if (existsSync8(join8(targetPath, "poetry.lock"))) {
926
1079
  return "poetry";
927
1080
  }
928
1081
  return "npm";
929
1082
  }
930
- function detectCommands(targetPath, pkgManager) {
1083
+ function detectCommands(targetPath, packageManager) {
931
1084
  const packageJsonPath = join8(targetPath, "package.json");
932
- if (!existsSync7(packageJsonPath)) {
1085
+ if (!existsSync8(packageJsonPath)) {
933
1086
  return {};
934
1087
  }
935
1088
  try {
@@ -937,7 +1090,7 @@ function detectCommands(targetPath, pkgManager) {
937
1090
  const parsed = JSON.parse(raw);
938
1091
  const scripts = parsed.scripts ?? {};
939
1092
  const commands = {};
940
- const runner = pkgManager === "npm" ? "npm run" : pkgManager;
1093
+ const runner = packageManager === "npm" ? "npm run" : packageManager;
941
1094
  if (scripts.build) {
942
1095
  commands.build = `${runner} build`;
943
1096
  }
@@ -964,7 +1117,7 @@ function detectCommands(targetPath, pkgManager) {
964
1117
  }
965
1118
  function detectPathAliases(targetPath) {
966
1119
  const tsconfigPath = join8(targetPath, "tsconfig.json");
967
- if (!existsSync7(tsconfigPath)) {
1120
+ if (!existsSync8(tsconfigPath)) {
968
1121
  return {};
969
1122
  }
970
1123
  try {
@@ -972,9 +1125,9 @@ function detectPathAliases(targetPath) {
972
1125
  const parsed = JSON.parse(raw);
973
1126
  const paths = parsed.compilerOptions?.paths ?? {};
974
1127
  const aliases = {};
975
- for (const [key, targets] of Object.entries(paths)) {
976
- if (Array.isArray(targets) && typeof targets[0] === "string") {
977
- aliases[key] = targets[0];
1128
+ for (const [aliasKey, aliasTargets] of Object.entries(paths)) {
1129
+ if (Array.isArray(aliasTargets) && typeof aliasTargets[0] === "string") {
1130
+ aliases[aliasKey] = aliasTargets[0];
978
1131
  }
979
1132
  }
980
1133
  return aliases;
@@ -984,7 +1137,7 @@ function detectPathAliases(targetPath) {
984
1137
  }
985
1138
  function detectSourceDirectories(targetPath) {
986
1139
  const srcPath = join8(targetPath, "src");
987
- const scanTarget = existsSync7(srcPath) ? srcPath : targetPath;
1140
+ const scanTarget = existsSync8(srcPath) ? srcPath : targetPath;
988
1141
  try {
989
1142
  const entries = readdirSync2(scanTarget, { withFileTypes: true });
990
1143
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist");
@@ -1008,30 +1161,36 @@ function scanProjectOnboarding(targetPath) {
1008
1161
  }
1009
1162
 
1010
1163
  // src/domains/harness/upsert-agent-instructions.ts
1011
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1164
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
1012
1165
  import { join as join9 } from "path";
1013
1166
  var START_MARKER = "<!-- grooveskills:harness:start -->";
1014
1167
  var END_MARKER = "<!-- grooveskills:harness:end -->";
1168
+ function writeWithTransaction(filePath, content, transaction) {
1169
+ const outcome = safeWriteFileSync(filePath, content);
1170
+ if (outcome.strategy === "replaced" && transaction) {
1171
+ transaction.recordReplacedFile(filePath, outcome.previousOwnerUid);
1172
+ }
1173
+ }
1015
1174
  function applyHarnessBlock(filePath, language = "es", transaction) {
1016
1175
  if (transaction) {
1017
1176
  transaction.recordBeforeWrite(filePath);
1018
1177
  }
1019
1178
  const block = buildAgentHarnessBlock(language);
1020
- if (!existsSync8(filePath)) {
1021
- safeWriteFileSync(filePath, `${block}
1022
- `);
1179
+ if (!existsSync9(filePath)) {
1180
+ writeWithTransaction(filePath, `${block}
1181
+ `, transaction);
1023
1182
  return;
1024
1183
  }
1025
1184
  const existingContent = readFileSync5(filePath, "utf-8");
1026
1185
  if (existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER)) {
1027
1186
  const regex = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}`, "g");
1028
1187
  const updated = existingContent.replace(regex, block);
1029
- safeWriteFileSync(filePath, updated);
1188
+ writeWithTransaction(filePath, updated, transaction);
1030
1189
  return;
1031
1190
  }
1032
1191
  const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
1033
- safeWriteFileSync(filePath, `${existingContent}${separator}${block}
1034
- `);
1192
+ writeWithTransaction(filePath, `${existingContent}${separator}${block}
1193
+ `, transaction);
1035
1194
  }
1036
1195
  function upsertAgentInstructions(targetPath, selectedAgents, language = "es", transaction) {
1037
1196
  const updatedFiles = [];
@@ -1047,65 +1206,63 @@ function upsertAgentInstructions(targetPath, selectedAgents, language = "es", tr
1047
1206
  }
1048
1207
 
1049
1208
  // src/domains/harness/install-harness.ts
1209
+ function writeTrackedFile(filePath, content, transaction) {
1210
+ if (transaction) {
1211
+ transaction.recordBeforeWrite(filePath);
1212
+ }
1213
+ const outcome = safeWriteFileSync(filePath, content);
1214
+ if (outcome.strategy === "replaced" && transaction) {
1215
+ transaction.recordReplacedFile(filePath, outcome.previousOwnerUid);
1216
+ }
1217
+ }
1050
1218
  function installHarness(options) {
1051
1219
  const memoryDir = getProjectMemoryDir(options.targetPath);
1052
1220
  const tasksDir = getProjectTasksDir(options.targetPath);
1053
1221
  const language = options.language ?? "es";
1054
1222
  const forceOnboarding = Boolean(options.forceOnboarding);
1055
- const tx = options.transaction;
1056
- if (tx) {
1057
- tx.recordCreatedDir(memoryDir);
1058
- tx.recordCreatedDir(tasksDir);
1223
+ const transaction = options.transaction;
1224
+ if (transaction) {
1225
+ transaction.recordCreatedDir(memoryDir);
1226
+ transaction.recordCreatedDir(tasksDir);
1059
1227
  }
1060
- if (!existsSync9(tasksDir)) {
1228
+ if (!existsSync10(tasksDir)) {
1061
1229
  mkdirSync3(tasksDir, { recursive: true });
1062
1230
  }
1063
1231
  const createdFiles = [];
1064
1232
  const contextPath = join10(memoryDir, "context.md");
1065
- const shouldWriteContext = !existsSync9(contextPath) || forceOnboarding;
1233
+ const shouldWriteContext = !existsSync10(contextPath) || forceOnboarding;
1066
1234
  if (shouldWriteContext) {
1067
- if (tx) {
1068
- tx.recordBeforeWrite(contextPath);
1069
- }
1070
1235
  const profile = scanProjectOnboarding(options.targetPath);
1071
1236
  const contextContent = buildContextTemplate(
1072
1237
  options.detectedDependencies,
1073
1238
  language,
1074
1239
  profile
1075
1240
  );
1076
- safeWriteFileSync(contextPath, contextContent);
1241
+ writeTrackedFile(contextPath, contextContent, transaction);
1077
1242
  createdFiles.push(contextPath);
1078
1243
  const onboardingTaskPath = join10(tasksDir, "00-onboarding.md");
1079
- if (tx) {
1080
- tx.recordBeforeWrite(onboardingTaskPath);
1081
- }
1082
- safeWriteFileSync(
1244
+ writeTrackedFile(
1083
1245
  onboardingTaskPath,
1084
- buildOnboardingTaskTemplate(profile.projectName)
1246
+ buildOnboardingTaskTemplate(profile.projectName),
1247
+ transaction
1085
1248
  );
1086
1249
  createdFiles.push(onboardingTaskPath);
1087
1250
  }
1088
1251
  const decisionsPath = join10(memoryDir, "decisions.md");
1089
- if (!existsSync9(decisionsPath)) {
1090
- if (tx) {
1091
- tx.recordBeforeWrite(decisionsPath);
1092
- }
1093
- safeWriteFileSync(decisionsPath, buildDecisionsTemplate(language));
1252
+ if (!existsSync10(decisionsPath)) {
1253
+ writeTrackedFile(decisionsPath, buildDecisionsTemplate(language), transaction);
1094
1254
  createdFiles.push(decisionsPath);
1095
1255
  }
1096
1256
  const templateTaskPath = join10(tasksDir, "_template.md");
1097
- if (!existsSync9(templateTaskPath)) {
1098
- if (tx) {
1099
- tx.recordBeforeWrite(templateTaskPath);
1100
- }
1101
- safeWriteFileSync(templateTaskPath, buildTaskTemplate());
1257
+ if (!existsSync10(templateTaskPath)) {
1258
+ writeTrackedFile(templateTaskPath, buildTaskTemplate(), transaction);
1102
1259
  createdFiles.push(templateTaskPath);
1103
1260
  }
1104
1261
  const instructions = upsertAgentInstructions(
1105
1262
  options.targetPath,
1106
1263
  options.selectedAgents,
1107
1264
  language,
1108
- tx
1265
+ transaction
1109
1266
  );
1110
1267
  createdFiles.push(...instructions);
1111
1268
  return {
@@ -1115,14 +1272,14 @@ function installHarness(options) {
1115
1272
  }
1116
1273
 
1117
1274
  // src/domains/harness/write-project-config.ts
1118
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
1275
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
1119
1276
  function readExistingConfig(configPath) {
1120
1277
  const fallback = {
1121
1278
  skills: [],
1122
1279
  agents: [],
1123
1280
  harness: false
1124
1281
  };
1125
- if (!existsSync10(configPath)) {
1282
+ if (!existsSync11(configPath)) {
1126
1283
  return fallback;
1127
1284
  }
1128
1285
  try {
@@ -1156,21 +1313,25 @@ function writeProjectConfig(targetPath, newSkills, newAgents, harnessEnabled, la
1156
1313
  skills: mergedSkills,
1157
1314
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1158
1315
  };
1159
- safeWriteFileSync(configPath, `${JSON.stringify(config, null, 2)}
1316
+ const outcome = safeWriteFileSync(configPath, `${JSON.stringify(config, null, 2)}
1160
1317
  `);
1318
+ if (outcome.strategy === "replaced" && transaction) {
1319
+ transaction.recordReplacedFile(configPath, outcome.previousOwnerUid);
1320
+ }
1161
1321
  return configPath;
1162
1322
  }
1163
1323
 
1164
1324
  // src/domains/fs/install-transaction.ts
1165
- import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
1325
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
1166
1326
  var InstallTransaction = class {
1167
1327
  originalFiles = /* @__PURE__ */ new Map();
1168
1328
  createdDirs = [];
1329
+ replacedFiles = [];
1169
1330
  recordBeforeWrite(filePath) {
1170
1331
  if (this.originalFiles.has(filePath)) {
1171
1332
  return;
1172
1333
  }
1173
- if (existsSync11(filePath)) {
1334
+ if (existsSync12(filePath)) {
1174
1335
  try {
1175
1336
  this.originalFiles.set(filePath, readFileSync7(filePath, "utf-8"));
1176
1337
  } catch {
@@ -1181,42 +1342,118 @@ var InstallTransaction = class {
1181
1342
  }
1182
1343
  }
1183
1344
  recordCreatedDir(dirPath) {
1184
- if (!existsSync11(dirPath) && !this.createdDirs.includes(dirPath)) {
1345
+ if (!existsSync12(dirPath) && !this.createdDirs.includes(dirPath)) {
1185
1346
  this.createdDirs.push(dirPath);
1186
1347
  }
1187
1348
  }
1349
+ recordReplacedFile(filePath, previousOwnerUid) {
1350
+ if (!this.replacedFiles.some((item) => item.filePath === filePath)) {
1351
+ this.replacedFiles.push({ filePath, previousOwnerUid });
1352
+ }
1353
+ }
1354
+ getReplacedFiles() {
1355
+ return [...this.replacedFiles];
1356
+ }
1188
1357
  rollback() {
1189
1358
  const restoredFiles = [];
1190
1359
  const removedFiles = [];
1191
1360
  const removedDirs = [];
1361
+ const failures = [];
1192
1362
  for (const [filePath, content] of this.originalFiles.entries()) {
1193
1363
  try {
1194
1364
  if (content === null) {
1195
- if (existsSync11(filePath)) {
1196
- rmSync3(filePath, { force: true });
1365
+ if (existsSync12(filePath)) {
1366
+ rmSync4(filePath, { force: true });
1197
1367
  removedFiles.push(filePath);
1198
1368
  }
1199
- } else {
1200
- safeWriteFileSync(filePath, content);
1201
- restoredFiles.push(filePath);
1369
+ continue;
1202
1370
  }
1203
- } catch {
1371
+ if (existsSync12(filePath)) {
1372
+ try {
1373
+ const currentContent = readFileSync7(filePath, "utf-8");
1374
+ if (currentContent === content) {
1375
+ continue;
1376
+ }
1377
+ } catch {
1378
+ }
1379
+ }
1380
+ safeWriteFileSync(filePath, content);
1381
+ restoredFiles.push(filePath);
1382
+ } catch (error) {
1383
+ const reason = error instanceof Error ? error.message : String(error);
1384
+ failures.push({ path: filePath, reason });
1204
1385
  }
1205
1386
  }
1206
- const sortedDirs = [...this.createdDirs].sort((a, b) => b.length - a.length);
1207
- for (const dir of sortedDirs) {
1387
+ const sortedDirs = [...this.createdDirs].sort(
1388
+ (firstDir, secondDir) => secondDir.length - firstDir.length
1389
+ );
1390
+ for (const directoryPath of sortedDirs) {
1208
1391
  try {
1209
- if (existsSync11(dir) && readdirSync3(dir).length === 0) {
1210
- rmSync3(dir, { recursive: true, force: true });
1211
- removedDirs.push(dir);
1392
+ if (existsSync12(directoryPath) && readdirSync3(directoryPath).length === 0) {
1393
+ rmSync4(directoryPath, { recursive: true, force: true });
1394
+ removedDirs.push(directoryPath);
1212
1395
  }
1213
- } catch {
1396
+ } catch (error) {
1397
+ const reason = error instanceof Error ? error.message : String(error);
1398
+ failures.push({ path: directoryPath, reason });
1214
1399
  }
1215
1400
  }
1216
- return { restoredFiles, removedFiles, removedDirs };
1401
+ return {
1402
+ restoredFiles,
1403
+ removedFiles,
1404
+ removedDirs,
1405
+ failures,
1406
+ isClean: failures.length === 0
1407
+ };
1217
1408
  }
1218
1409
  };
1219
1410
 
1411
+ // src/domains/fs/preflight-write-check.ts
1412
+ import { join as join11 } from "path";
1413
+ function canRecoverOrWriteTarget(filePath) {
1414
+ const diagnosis = inspectFileAccess(filePath);
1415
+ if (diagnosis.immutableFlag) {
1416
+ throw new FilePermissionError(filePath, diagnosis);
1417
+ }
1418
+ if (!diagnosis.exists) {
1419
+ if (!diagnosis.dirWritable) {
1420
+ throw new FilePermissionError(filePath, diagnosis);
1421
+ }
1422
+ return;
1423
+ }
1424
+ const ownerWritable = diagnosis.ownedByCurrentUser && ((diagnosis.mode ?? 0) & 128) !== 0;
1425
+ if (ownerWritable) {
1426
+ return;
1427
+ }
1428
+ if (diagnosis.ownedByCurrentUser) {
1429
+ return;
1430
+ }
1431
+ if (diagnosis.dirWritable) {
1432
+ return;
1433
+ }
1434
+ throw new FilePermissionError(filePath, diagnosis);
1435
+ }
1436
+ function runPreflightWriteCheck(options) {
1437
+ const memoryDir = getProjectMemoryDir(options.targetPath);
1438
+ const tasksDir = getProjectTasksDir(options.targetPath);
1439
+ const candidatePaths = [
1440
+ join11(options.targetPath, "AGENTS.md")
1441
+ ];
1442
+ if (options.selectedAgents.includes("claude")) {
1443
+ candidatePaths.push(join11(options.targetPath, "CLAUDE.md"));
1444
+ }
1445
+ candidatePaths.push(
1446
+ getProjectConfigPath(options.targetPath),
1447
+ join11(memoryDir, "context.md"),
1448
+ join11(memoryDir, "decisions.md"),
1449
+ join11(tasksDir, "00-onboarding.md"),
1450
+ join11(tasksDir, "_template.md")
1451
+ );
1452
+ for (const filePath of candidatePaths) {
1453
+ canRecoverOrWriteTarget(filePath);
1454
+ }
1455
+ }
1456
+
1220
1457
  // src/errors/install-transaction-error.ts
1221
1458
  var InstallTransactionError = class extends Error {
1222
1459
  stage;
@@ -1225,7 +1462,7 @@ var InstallTransactionError = class extends Error {
1225
1462
  suggestedFix;
1226
1463
  constructor(options) {
1227
1464
  const rawMessage = options.cause instanceof Error ? options.cause.message : String(options.cause);
1228
- super(`Error durante la etapa '${options.stage}': ${rawMessage}`);
1465
+ super(`Error during stage '${options.stage}': ${rawMessage}`);
1229
1466
  this.name = "InstallTransactionError";
1230
1467
  this.stage = options.stage;
1231
1468
  this.causeError = options.cause;
@@ -1245,10 +1482,10 @@ function formatSuggestedFix(cause) {
1245
1482
  if (cause instanceof FilePermissionError) {
1246
1483
  return cause.suggestedFixCommand;
1247
1484
  }
1248
- const err = cause;
1249
- if (err?.code === "EACCES" || err?.code === "EPERM") {
1250
- if (err.path) {
1251
- return `chmod u+w "${err.path}"`;
1485
+ const nodeError = cause;
1486
+ if (nodeError?.code === "EACCES" || nodeError?.code === "EPERM") {
1487
+ if (nodeError.path) {
1488
+ return `chmod u+w "${nodeError.path}"`;
1252
1489
  }
1253
1490
  }
1254
1491
  return void 0;
@@ -1262,18 +1499,23 @@ async function installSkills(options) {
1262
1499
  if (!skillsToInstall.includes("odd-ai")) {
1263
1500
  skillsToInstall.push("odd-ai");
1264
1501
  }
1265
- let currentStage = "Sincronizaci\xF3n del almac\xE9n de skills (.grooveskills/skills)";
1502
+ let currentStage = "Preflight check";
1266
1503
  try {
1504
+ runPreflightWriteCheck({
1505
+ targetPath: options.targetPath,
1506
+ selectedAgents: options.selectedAgents
1507
+ });
1508
+ currentStage = "Syncing skills store (.grooveskills/skills)";
1267
1509
  syncProjectStore(options.targetPath, skillsToInstall, options.selectedAgents, transaction);
1268
1510
  const projectSkillsDir = getProjectSkillsDir(options.targetPath);
1269
1511
  const createdFiles = [];
1270
- currentStage = "Configuraci\xF3n de referencias de agentes (.agents, .claude, etc.)";
1512
+ currentStage = "Configuring agent references (.agents, .claude, etc.)";
1271
1513
  for (const skillId of skillsToInstall) {
1272
- const skill = SKILL_CATALOG.find((item) => item.id === skillId);
1514
+ const skill = SKILL_CATALOG.find((catalogItem) => catalogItem.id === skillId);
1273
1515
  if (!skill) {
1274
1516
  continue;
1275
1517
  }
1276
- const skillSourceDir = join11(projectSkillsDir, skill.directoryName);
1518
+ const skillSourceDir = join12(projectSkillsDir, skill.directoryName);
1277
1519
  for (const agentId of options.selectedAgents) {
1278
1520
  const adapter = ADAPTER_REGISTRY[agentId];
1279
1521
  if (!adapter) {
@@ -1288,7 +1530,7 @@ async function installSkills(options) {
1288
1530
  createdFiles.push(...files);
1289
1531
  }
1290
1532
  }
1291
- currentStage = "Instalaci\xF3n del Groove Harness (contexto, decisiones, tareas e instrucciones)";
1533
+ currentStage = "Installing Groove Harness (context, decisions, tasks, and instructions)";
1292
1534
  const harnessResult = installHarness({
1293
1535
  targetPath: options.targetPath,
1294
1536
  selectedAgents: options.selectedAgents,
@@ -1299,7 +1541,7 @@ async function installSkills(options) {
1299
1541
  transaction
1300
1542
  });
1301
1543
  createdFiles.push(...harnessResult.createdFiles);
1302
- currentStage = "Persistencia de la configuraci\xF3n del proyecto (.grooveskills/config.json)";
1544
+ currentStage = "Persisting project configuration (.grooveskills/config.json)";
1303
1545
  const configPath = writeProjectConfig(
1304
1546
  options.targetPath,
1305
1547
  skillsToInstall,
@@ -1314,7 +1556,8 @@ async function installSkills(options) {
1314
1556
  configuredAgents: options.selectedAgents,
1315
1557
  createdFiles,
1316
1558
  harnessInitialized: harnessResult.onboardingInitialized,
1317
- language: options.language
1559
+ language: options.language,
1560
+ replacedFiles: transaction.getReplacedFiles()
1318
1561
  };
1319
1562
  } catch (cause) {
1320
1563
  const rollbackReport = transaction.rollback();
@@ -1329,17 +1572,17 @@ async function installSkills(options) {
1329
1572
  }
1330
1573
 
1331
1574
  // src/domains/ui/render-onboarding-prompt.ts
1332
- import pc2 from "picocolors";
1575
+ import picocolors2 from "picocolors";
1333
1576
  function formatOnboardingPrompt(language = "es") {
1334
- const langConfig = SUPPORTED_LANGUAGES[language] ?? SUPPORTED_LANGUAGES.es;
1335
- const title = langConfig.onboardingPromptTitle;
1336
- const prompt = langConfig.onboardingPrompt;
1337
- const separator = pc2.cyan("\u2500".repeat(60));
1577
+ const languageConfig = SUPPORTED_LANGUAGES[language] ?? SUPPORTED_LANGUAGES.es;
1578
+ const title = languageConfig.onboardingPromptTitle;
1579
+ const prompt = languageConfig.onboardingPrompt;
1580
+ const separator = picocolors2.cyan("\u2500".repeat(60));
1338
1581
  const formattedOutput = [
1339
1582
  separator,
1340
- pc2.bold(pc2.yellow(title)),
1583
+ picocolors2.bold(picocolors2.yellow(title)),
1341
1584
  "",
1342
- pc2.green(prompt),
1585
+ picocolors2.green(prompt),
1343
1586
  separator
1344
1587
  ].join("\n");
1345
1588
  return {
@@ -1349,75 +1592,130 @@ function formatOnboardingPrompt(language = "es") {
1349
1592
  };
1350
1593
  }
1351
1594
 
1595
+ // src/domains/ui/render-install-report.ts
1596
+ import { basename as basename2 } from "path";
1597
+ import { userInfo } from "os";
1598
+ import * as prompts5 from "@clack/prompts";
1599
+ import picocolors3 from "picocolors";
1600
+ function getCurrentUsername() {
1601
+ try {
1602
+ return userInfo().username;
1603
+ } catch {
1604
+ return process.env.USER || process.env.LOGNAME || "current user";
1605
+ }
1606
+ }
1607
+ function formatReplacedFileNotes(replacedFiles = []) {
1608
+ if (replacedFiles.length === 0) {
1609
+ return [];
1610
+ }
1611
+ const currentUser = getCurrentUsername();
1612
+ return replacedFiles.map((replacedItem) => {
1613
+ const fileName = basename2(replacedItem.filePath);
1614
+ const previousOwner = formatOwnerName(replacedItem.previousOwnerUid);
1615
+ return picocolors3.yellow(
1616
+ `Note: ${fileName} was owned by ${previousOwner}. It was replaced atomically and is now owned by ${currentUser}.`
1617
+ );
1618
+ });
1619
+ }
1620
+ function renderInstallFailure(error) {
1621
+ const { stage, causeError, rollbackReport, suggestedFix } = error;
1622
+ const causeMessage = causeError instanceof Error ? causeError.message : String(causeError);
1623
+ const nothingTouched = rollbackReport.restoredFiles.length === 0 && rollbackReport.removedFiles.length === 0 && rollbackReport.removedDirs.length === 0 && rollbackReport.failures.length === 0;
1624
+ const lines = [
1625
+ `${picocolors3.bold("Failed stage:")} ${picocolors3.yellow(stage)}`,
1626
+ `${picocolors3.bold("Cause:")} ${picocolors3.red(causeMessage)}`
1627
+ ];
1628
+ if (nothingTouched) {
1629
+ lines.push(`${picocolors3.bold("Rollback:")} Nothing to roll back, no files were modified.`);
1630
+ } else {
1631
+ lines.push(
1632
+ "",
1633
+ picocolors3.bold("Rollback summary:"),
1634
+ ` \u2022 Restored files: ${picocolors3.cyan(String(rollbackReport.restoredFiles.length))}`,
1635
+ ` \u2022 Removed temporary files: ${picocolors3.cyan(String(rollbackReport.removedFiles.length))}`,
1636
+ ` \u2022 Removed empty directories: ${picocolors3.cyan(String(rollbackReport.removedDirs.length))}`
1637
+ );
1638
+ if (!rollbackReport.isClean) {
1639
+ lines.push("", picocolors3.red("Rollback was incomplete. These files may need manual review:"));
1640
+ for (const failure of rollbackReport.failures) {
1641
+ lines.push(` \u2022 ${failure.path} (${failure.reason})`);
1642
+ }
1643
+ }
1644
+ }
1645
+ if (suggestedFix) {
1646
+ lines.push(`${picocolors3.bold("Suggested fix:")} ${picocolors3.green(suggestedFix)}`);
1647
+ }
1648
+ prompts5.note(lines.join("\n"), "Installation Aborted");
1649
+ if (!rollbackReport.isClean) {
1650
+ prompts5.outro(
1651
+ picocolors3.red(
1652
+ "Rollback was incomplete. Please review the files listed above and apply the suggested fix."
1653
+ )
1654
+ );
1655
+ return;
1656
+ }
1657
+ prompts5.outro(
1658
+ picocolors3.yellow(
1659
+ nothingTouched ? "No files were modified. Apply the suggested fix and try again." : "Your project was restored to its previous state. Apply the suggested fix and try again."
1660
+ )
1661
+ );
1662
+ }
1663
+
1352
1664
  // src/domains/ui/ui-orchestrator.ts
1353
1665
  async function resolveLanguageSelection(harnessExists, configuredLanguage, forceOnboarding) {
1354
1666
  if (harnessExists && !forceOnboarding) {
1355
1667
  return configuredLanguage ?? "es";
1356
1668
  }
1357
- const langChoice = await promptLanguage(configuredLanguage ?? "es");
1358
- if (p5.isCancel(langChoice)) {
1359
- p5.cancel("Instalaci\xF3n cancelada. \xA1Que tengas un excelente d\xEDa!");
1669
+ const languageSelection = await promptLanguage(configuredLanguage ?? "es");
1670
+ if (prompts6.isCancel(languageSelection)) {
1671
+ prompts6.cancel("Installation cancelled. Have a wonderful day!");
1360
1672
  process.exit(0);
1361
1673
  }
1362
- return langChoice;
1674
+ return languageSelection;
1363
1675
  }
1364
1676
  async function resolveOverwriteConfirmation(targetPath, selectedSkills, selectedAgents) {
1365
1677
  const anyExists = selectedSkills.some((skillId) => {
1366
- const meta = SKILL_CATALOG.find((s) => s.id === skillId);
1367
- return meta ? verifySkillExists(targetPath, meta.directoryName, selectedAgents) : false;
1678
+ const skillMetadata = SKILL_CATALOG.find((skill) => skill.id === skillId);
1679
+ return skillMetadata ? verifySkillExists(targetPath, skillMetadata.directoryName, selectedAgents) : false;
1368
1680
  });
1369
1681
  if (!anyExists) {
1370
1682
  return true;
1371
1683
  }
1372
- const confirm2 = await promptConfirmOverwrite();
1373
- if (p5.isCancel(confirm2) || !confirm2) {
1374
- p5.cancel("Instalaci\xF3n detenida para proteger las skills existentes.");
1684
+ const overwriteConfirmed = await promptConfirmOverwrite();
1685
+ if (prompts6.isCancel(overwriteConfirmed) || !overwriteConfirmed) {
1686
+ prompts6.cancel("Installation stopped to protect existing skills.");
1375
1687
  process.exit(0);
1376
1688
  }
1377
- return Boolean(confirm2);
1378
- }
1379
- function renderInstallFailure(error) {
1380
- const { stage, causeError, rollbackReport, suggestedFix } = error;
1381
- const rawMsg = causeError instanceof Error ? causeError.message : String(causeError);
1382
- const lines = [
1383
- `${pc3.bold("Fase afectada:")} ${pc3.yellow(stage)}`,
1384
- `${pc3.bold("Causa:")} ${pc3.red(rawMsg)}`,
1385
- "",
1386
- pc3.bold("Reporte de Rollback autom\xE1tico:"),
1387
- ` \u2022 Archivos restaurados a su estado previo: ${pc3.cyan(String(rollbackReport.restoredFiles.length))}`,
1388
- ` \u2022 Archivos temporales eliminados: ${pc3.cyan(String(rollbackReport.removedFiles.length))}`,
1389
- ` \u2022 Directorios vac\xEDos eliminados: ${pc3.cyan(String(rollbackReport.removedDirs.length))}`
1390
- ];
1391
- if (suggestedFix) {
1392
- lines.push(
1393
- "",
1394
- pc3.bold("Soluci\xF3n recomendada:"),
1395
- ` Ejecuta en tu terminal: ${pc3.green(suggestedFix)}`
1396
- );
1397
- }
1398
- p5.note(lines.join("\n"), "Detalles de la interrupci\xF3n y reversi\xF3n");
1399
- p5.outro(pc3.yellow("Tu proyecto no sufri\xF3 alteraciones y se preserv\xF3 intacto. Corrige la causa y vuelve a intentarlo cuando gustes."));
1689
+ return Boolean(overwriteConfirmed);
1400
1690
  }
1401
1691
  async function runInteractiveUi(context, forceOnboarding = false) {
1402
1692
  console.log(renderBanner());
1403
- p5.intro(pc3.magenta("\xA1Bienvenido a GrooveSkills! Configuremos tus agentes de IA con est\xE1ndares limpios de arquitectura."));
1693
+ prompts6.intro(
1694
+ picocolors4.magenta(
1695
+ "Welcome to GrooveSkills! Let's tailor your AI agents with clean architectural standards."
1696
+ )
1697
+ );
1404
1698
  if (context.detectedDependencies.length > 0) {
1405
- const harnessInfo = context.harnessExists && !forceOnboarding ? pc3.green(`Activo (.grooveskills/memory, idioma: ${context.configuredLanguage ?? "es"})`) : pc3.cyan("Ser\xE1 inicializado");
1406
- p5.note(
1407
- `Dependencias detectadas: ${context.detectedDependencies.map((d) => pc3.bold(d)).join(", ")}
1408
- Carpetas de IA activas: ${context.detectedAgents.map((a) => pc3.green(a)).join(", ") || "Ninguna (se crear\xE1n)"}
1699
+ const harnessInfo = context.harnessExists && !forceOnboarding ? picocolors4.green(
1700
+ `Active (.grooveskills/memory, lang: ${context.configuredLanguage ?? "es"})`
1701
+ ) : picocolors4.cyan("Will be initialized");
1702
+ const formattedDependencies = context.detectedDependencies.map((dependencyName) => picocolors4.bold(dependencyName)).join(", ");
1703
+ const formattedAgents = context.detectedAgents.map((agentName) => picocolors4.green(agentName)).join(", ") || "None (will be created)";
1704
+ prompts6.note(
1705
+ `Detected dependencies: ${formattedDependencies}
1706
+ Active AI folders: ${formattedAgents}
1409
1707
  GrooveSkills Harness: ${harnessInfo}`,
1410
- "Entorno del Proyecto"
1708
+ "Project Environment"
1411
1709
  );
1412
1710
  }
1413
1711
  const selectedSkills = await promptSkills(context.suggestedSkillIds);
1414
- if (p5.isCancel(selectedSkills)) {
1415
- p5.cancel("Instalaci\xF3n cancelada. \xA1Que tengas un excelente d\xEDa!");
1712
+ if (prompts6.isCancel(selectedSkills)) {
1713
+ prompts6.cancel("Installation cancelled. Have a wonderful day!");
1416
1714
  process.exit(0);
1417
1715
  }
1418
1716
  const selectedAgents = await promptAgents(context.detectedAgents);
1419
- if (p5.isCancel(selectedAgents)) {
1420
- p5.cancel("Instalaci\xF3n cancelada. \xA1Que tengas un excelente d\xEDa!");
1717
+ if (prompts6.isCancel(selectedAgents)) {
1718
+ prompts6.cancel("Installation cancelled. Have a wonderful day!");
1421
1719
  process.exit(0);
1422
1720
  }
1423
1721
  const selectedLanguage = await resolveLanguageSelection(
@@ -1430,10 +1728,10 @@ GrooveSkills Harness: ${harnessInfo}`,
1430
1728
  selectedSkills,
1431
1729
  selectedAgents
1432
1730
  );
1433
- const s = p5.spinner();
1434
- s.start("Instalando y adaptando skills para tus agentes de IA configurados...");
1731
+ const installSpinner = prompts6.spinner();
1732
+ installSpinner.start("Installing and adapting skills across your configured AI agents...");
1435
1733
  try {
1436
- const result = await installSkills({
1734
+ const installResult = await installSkills({
1437
1735
  targetPath: context.targetPath,
1438
1736
  selectedSkillIds: selectedSkills,
1439
1737
  selectedAgents,
@@ -1443,24 +1741,30 @@ GrooveSkills Harness: ${harnessInfo}`,
1443
1741
  language: selectedLanguage,
1444
1742
  forceOnboarding
1445
1743
  });
1446
- s.stop(pc3.green("\xA1Todas las skills y el harness fueron configurados exitosamente!"));
1447
- const harnessStatus = result.harnessInitialized ? pc3.green(`Inicializado en .grooveskills/ (Idioma: ${selectedLanguage})`) : pc3.cyan(`Preservado en .grooveskills/ (Idioma: ${selectedLanguage})`);
1448
- p5.note(
1449
- `Skills configuradas: ${result.installedSkills.map((sk) => pc3.bold(sk)).join(", ")}
1450
- Agentes objetivo: ${result.configuredAgents.map((a) => pc3.cyan(a)).join(", ")}
1451
- GrooveSkills Harness: ${harnessStatus}`,
1452
- "Resumen de Instalaci\xF3n"
1744
+ installSpinner.stop(
1745
+ picocolors4.green("All skills and harness have been successfully configured!")
1453
1746
  );
1454
- if (result.harnessInitialized) {
1747
+ const harnessStatus = installResult.harnessInitialized ? picocolors4.green(`Initialized in .grooveskills/ (Language: ${selectedLanguage})`) : picocolors4.cyan(`Preserved in .grooveskills/ (Language: ${selectedLanguage})`);
1748
+ const formattedInstalledSkills = installResult.installedSkills.map((skillId) => picocolors4.bold(skillId)).join(", ");
1749
+ const formattedConfiguredAgents = installResult.configuredAgents.map((agentId) => picocolors4.cyan(agentId)).join(", ");
1750
+ const summaryLines = [
1751
+ `Configured skills: ${formattedInstalledSkills}`,
1752
+ `Target agents: ${formattedConfiguredAgents}`,
1753
+ `GrooveSkills Harness: ${harnessStatus}`,
1754
+ ...formatReplacedFileNotes(installResult.replacedFiles)
1755
+ ];
1756
+ prompts6.note(summaryLines.join("\n"), "Installation Summary");
1757
+ if (installResult.harnessInitialized) {
1455
1758
  const promptRender = formatOnboardingPrompt(selectedLanguage);
1456
- p5.note(
1457
- pc3.green(promptRender.prompt),
1458
- promptRender.title
1459
- );
1759
+ prompts6.note(picocolors4.green(promptRender.prompt), promptRender.title);
1460
1760
  }
1461
- p5.outro(pc3.green("\xA1Tus agentes de IA ahora est\xE1n potenciados con los h\xE1bitos arquitect\xF3nicos de tu equipo! \u{1F680}"));
1761
+ prompts6.outro(
1762
+ picocolors4.green(
1763
+ "Your AI programming pair is now empowered with your team's architectural habits! \u{1F680}"
1764
+ )
1765
+ );
1462
1766
  } catch (error) {
1463
- s.stop(pc3.yellow("La instalaci\xF3n no pudo completarse."));
1767
+ installSpinner.stop(picocolors4.yellow("Installation could not be completed."));
1464
1768
  if (error instanceof InstallTransactionError) {
1465
1769
  renderInstallFailure(error);
1466
1770
  return;
@@ -1470,6 +1774,13 @@ GrooveSkills Harness: ${harnessStatus}`,
1470
1774
  }
1471
1775
 
1472
1776
  // src/main.ts
1777
+ function resolveUsername() {
1778
+ try {
1779
+ return userInfo2().username;
1780
+ } catch {
1781
+ return process.env.USER || process.env.LOGNAME || "current user";
1782
+ }
1783
+ }
1473
1784
  async function runGrooveSkills(options = {}) {
1474
1785
  const targetPath = process.cwd();
1475
1786
  const context = detectProjectContext(targetPath);
@@ -1489,6 +1800,18 @@ async function runGrooveSkills(options = {}) {
1489
1800
  language,
1490
1801
  forceOnboarding: options.forceOnboarding
1491
1802
  });
1803
+ if (result.replacedFiles && result.replacedFiles.length > 0) {
1804
+ const currentUser = resolveUsername();
1805
+ for (const replacedItem of result.replacedFiles) {
1806
+ const fileName = basename3(replacedItem.filePath);
1807
+ const previousOwner = formatOwnerName(replacedItem.previousOwnerUid);
1808
+ console.log(
1809
+ picocolors5.yellow(
1810
+ `Note: ${fileName} was owned by ${previousOwner}. It was replaced atomically and is now owned by ${currentUser}.`
1811
+ )
1812
+ );
1813
+ }
1814
+ }
1492
1815
  if (result.harnessInitialized) {
1493
1816
  const promptRender = formatOnboardingPrompt(language);
1494
1817
  console.log(`
@@ -1504,4 +1827,4 @@ export {
1504
1827
  InstallTransactionError,
1505
1828
  runGrooveSkills
1506
1829
  };
1507
- //# sourceMappingURL=chunk-E63GTCOE.js.map
1830
+ //# sourceMappingURL=chunk-SP3ESDM5.js.map