agentic-workflow-manager 3.13.4 → 3.13.6

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 (90) hide show
  1. package/dist/src/commands/add.js +5 -0
  2. package/dist/src/commands/doctor.js +1 -0
  3. package/dist/src/commands/hooks/claude.js +15 -2
  4. package/dist/src/commands/hooks/shared.js +6 -1
  5. package/dist/src/commands/init.js +1 -0
  6. package/dist/src/commands/pin.js +24 -4
  7. package/dist/src/commands/preflight/checks.js +3 -2
  8. package/dist/src/commands/registry/add.js +1 -1
  9. package/dist/src/commands/sensors/index.js +7 -0
  10. package/dist/src/commands/sensors/init.js +48 -7
  11. package/dist/src/commands/sensors/install.js +20 -11
  12. package/dist/src/commands/sensors/run.js +31 -29
  13. package/dist/src/commands/sync.js +29 -0
  14. package/dist/src/commands/watch/index.js +11 -1
  15. package/dist/src/core/artifact-name.js +80 -0
  16. package/dist/src/core/bundle-install.js +55 -12
  17. package/dist/src/core/bundles.js +72 -13
  18. package/dist/src/core/context/regenerate.js +21 -4
  19. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  20. package/dist/src/core/diagnostics/checks.js +26 -2
  21. package/dist/src/core/diagnostics/context.js +91 -25
  22. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  23. package/dist/src/core/discovery.js +15 -23
  24. package/dist/src/core/export/transform.js +30 -25
  25. package/dist/src/core/frontmatter.js +223 -0
  26. package/dist/src/core/init/mutation-targets.js +11 -0
  27. package/dist/src/core/init/steps.js +70 -19
  28. package/dist/src/core/install-planner.js +62 -17
  29. package/dist/src/core/install-transaction.js +13 -1
  30. package/dist/src/core/journal/adapter.js +10 -1
  31. package/dist/src/core/paths.js +61 -8
  32. package/dist/src/core/profile.js +12 -0
  33. package/dist/src/core/provider-artifacts.js +38 -13
  34. package/dist/src/core/provider-version.js +12 -6
  35. package/dist/src/core/registries.js +40 -19
  36. package/dist/src/core/renderers/canonical-agent.js +33 -2
  37. package/dist/src/core/renderers/registry.js +37 -0
  38. package/dist/src/core/renderers/skill-source.js +11 -19
  39. package/dist/src/core/skill-integrity.js +71 -10
  40. package/dist/src/core/update-check.js +14 -2
  41. package/dist/src/core/versioning.js +17 -2
  42. package/dist/src/index.js +31 -23
  43. package/dist/src/providers/index.js +7 -2
  44. package/dist/src/ui/text.js +10 -2
  45. package/dist/src/utils/config.js +7 -8
  46. package/dist/tests/commands/doctor.test.js +6 -0
  47. package/dist/tests/commands/pin.test.js +29 -2
  48. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  49. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  50. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  51. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  52. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  53. package/dist/tests/commands/sensors/run.test.js +24 -51
  54. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  55. package/dist/tests/commands/sensors/status.test.js +27 -12
  56. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  57. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  58. package/dist/tests/core/context/regenerate.test.js +39 -8
  59. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  60. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  61. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  62. package/dist/tests/core/discovery.test.js +10 -1
  63. package/dist/tests/core/export/transform.test.js +110 -21
  64. package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
  65. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  66. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  67. package/dist/tests/core/init/orchestrator.test.js +1 -1
  68. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  69. package/dist/tests/core/init/steps.test.js +11 -5
  70. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  71. package/dist/tests/core/paths.test.js +7 -34
  72. package/dist/tests/core/project-skill-links.test.js +159 -0
  73. package/dist/tests/core/provider-artifacts.test.js +66 -14
  74. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  75. package/dist/tests/core/reconciliation.test.js +15 -15
  76. package/dist/tests/core/registry-manifest.test.js +1 -1
  77. package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
  78. package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
  79. package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
  80. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  81. package/dist/tests/core/skill-integrity.test.js +10 -10
  82. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  83. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  84. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  85. package/dist/tests/providers/index.test.js +1 -1
  86. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  87. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  88. package/dist/tests/ui/text.test.js +9 -0
  89. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  90. package/package.json +6 -1
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Regresion: `compareSemver` devolvia NaN ante una entrada malformada, y como
4
+ // `NaN > 0` y `NaN < 0` son AMBOS false, el gate de version fallaba ABIERTO:
5
+ // `verifyMinCliVersions` dejaba pasar en silencio un CLI por debajo del minimo.
6
+ // Para un gate, la direccion correcta de fallo es la contraria.
7
+ const versioning_1 = require("../../src/core/versioning");
8
+ describe('compareSemver', () => {
9
+ it.each([
10
+ ['1.2.3', '1.2.3', 0],
11
+ ['1.2.4', '1.2.3', 1],
12
+ ['1.2.3', '1.3.0', -1],
13
+ ['2.0.0', '1.99.99', 1],
14
+ ['v1.2.3', '1.2.3', 0],
15
+ ])('compara %s con %s', (a, b, sign) => {
16
+ expect(Math.sign((0, versioning_1.compareSemver)(a, b))).toBe(sign);
17
+ });
18
+ it.each([
19
+ ['incompleta', '1.2'],
20
+ ['no numerica', 'x.y.z'],
21
+ ['vacia', ''],
22
+ ['basura', 'latest'],
23
+ ])('lanza ante una version %s en vez de devolver NaN', (_n, bad) => {
24
+ expect(() => (0, versioning_1.compareSemver)(bad, '1.0.0')).toThrow(/not a semver/i);
25
+ expect(() => (0, versioning_1.compareSemver)('1.0.0', bad)).toThrow(/not a semver/i);
26
+ });
27
+ it('nunca devuelve NaN — el valor que hacia fallar el gate abierto', () => {
28
+ expect(Number.isNaN((0, versioning_1.compareSemver)('1.0.0', '2.0.0'))).toBe(false);
29
+ });
30
+ });
@@ -16,7 +16,7 @@ describe('skill-integrity multi-root', () => {
16
16
  afterEach(() => {
17
17
  fs_1.default.rmSync(tmp, { recursive: true, force: true });
18
18
  });
19
- it('repairGlobalSkills re-links a dangling symlink to the FIRST root that has the skill', () => {
19
+ it('repairSkillLinks re-links a dangling symlink to the FIRST root that has the skill', () => {
20
20
  const skillsDir = path_1.default.join(tmp, 'installed');
21
21
  const rootA = path_1.default.join(tmp, 'base');
22
22
  const rootB = path_1.default.join(tmp, 'personal');
@@ -25,16 +25,16 @@ describe('skill-integrity multi-root', () => {
25
25
  fs_1.default.writeFileSync(path_1.default.join(rootB, 'skills', 'mine', 'SKILL.md'), '---\nname: mine\n---\n');
26
26
  // dangling symlink: points to a deleted target
27
27
  fs_1.default.symlinkSync(path_1.default.join(tmp, 'gone', 'mine'), path_1.default.join(skillsDir, 'mine'), 'dir');
28
- const { repairGlobalSkills: repair } = require('../../src/core/skill-integrity');
28
+ const { repairSkillLinks: repair } = require('../../src/core/skill-integrity');
29
29
  const r = repair(skillsDir, [rootA, rootB]);
30
30
  expect(r.relinked).toEqual(['mine']);
31
31
  expect(fs_1.default.realpathSync(path_1.default.join(skillsDir, 'mine'))).toBe(fs_1.default.realpathSync(path_1.default.join(rootB, 'skills', 'mine')));
32
32
  });
33
- it('classifyGlobalSkills marks dead when NO root has the skill', () => {
33
+ it('classifySkillLinks marks dead when NO root has the skill', () => {
34
34
  const skillsDir = path_1.default.join(tmp, 'installed');
35
35
  fs_1.default.mkdirSync(skillsDir, { recursive: true });
36
36
  fs_1.default.symlinkSync(path_1.default.join(tmp, 'gone', 'nope'), path_1.default.join(skillsDir, 'nope'), 'dir');
37
- const { classifyGlobalSkills: classify } = require('../../src/core/skill-integrity');
37
+ const { classifySkillLinks: classify } = require('../../src/core/skill-integrity');
38
38
  const c = classify(skillsDir, [path_1.default.join(tmp, 'base')]);
39
39
  expect(c.dead).toEqual(['nope']);
40
40
  });
@@ -53,7 +53,7 @@ function makeRegistrySkill(registryContentDir, name) {
53
53
  fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `# ${name}\n`);
54
54
  return dir;
55
55
  }
56
- describe('classifyGlobalSkills', () => {
56
+ describe('classifySkillLinks', () => {
57
57
  it('classifies valid / repairable / dead', () => {
58
58
  const { skillsDir, registryContentDir } = setup();
59
59
  // valid: symlink a una skill que existe en el registry
@@ -64,13 +64,13 @@ describe('classifyGlobalSkills', () => {
64
64
  fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/beta'), path_1.default.join(skillsDir, 'beta'), 'dir');
65
65
  // dead: symlink colgante y la skill NO existe en el registry
66
66
  fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/gamma'), path_1.default.join(skillsDir, 'gamma'), 'dir');
67
- const result = (0, skill_integrity_1.classifyGlobalSkills)(skillsDir, [registryContentDir]);
67
+ const result = (0, skill_integrity_1.classifySkillLinks)(skillsDir, [registryContentDir]);
68
68
  expect(result.valid).toEqual(['alpha']);
69
69
  expect(result.repairable).toEqual(['beta']);
70
70
  expect(result.dead).toEqual(['gamma']);
71
71
  });
72
72
  it('returns empty arrays when the skills dir does not exist', () => {
73
- const result = (0, skill_integrity_1.classifyGlobalSkills)('/nonexistent/dir', ['/also/nonexistent']);
73
+ const result = (0, skill_integrity_1.classifySkillLinks)('/nonexistent/dir', ['/also/nonexistent']);
74
74
  expect(result).toEqual({ valid: [], repairable: [], dead: [] });
75
75
  });
76
76
  });
@@ -117,7 +117,7 @@ describe('reconcileAllSkillLinks (#4 — awm update, all providers)', () => {
117
117
  }
118
118
  });
119
119
  });
120
- describe('repairGlobalSkills', () => {
120
+ describe('repairSkillLinks', () => {
121
121
  it('re-links repairable to registry content dir and prunes dead; valid untouched; idempotent', () => {
122
122
  const { skillsDir, registryContentDir } = setup();
123
123
  const okTarget = makeRegistrySkill(registryContentDir, 'alpha');
@@ -125,7 +125,7 @@ describe('repairGlobalSkills', () => {
125
125
  makeRegistrySkill(registryContentDir, 'beta');
126
126
  fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/beta'), path_1.default.join(skillsDir, 'beta'), 'dir');
127
127
  fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/gamma'), path_1.default.join(skillsDir, 'gamma'), 'dir');
128
- const r1 = (0, skill_integrity_1.repairGlobalSkills)(skillsDir, [registryContentDir]);
128
+ const r1 = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registryContentDir]);
129
129
  expect(r1.relinked).toEqual(['beta']);
130
130
  expect(r1.pruned).toEqual(['gamma']);
131
131
  // beta ahora apunta al registry content dir y resuelve
@@ -137,7 +137,7 @@ describe('repairGlobalSkills', () => {
137
137
  // alpha intacto
138
138
  expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'alpha'))).toBe(true);
139
139
  // idempotente: segunda corrida no cambia nada
140
- const r2 = (0, skill_integrity_1.repairGlobalSkills)(skillsDir, [registryContentDir]);
140
+ const r2 = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registryContentDir]);
141
141
  expect(r2.relinked).toEqual([]);
142
142
  expect(r2.pruned).toEqual([]);
143
143
  });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // Regresion: `repairSkillLinks` hacia `rmSync` del link colgante y RECIEN
7
+ // DESPUES intentaba recrearlo con `symlinkSync(..., 'dir')`. En Windows un
8
+ // symlink de directorio exige SeCreateSymbolicLinkPrivilege, denegado por
9
+ // defecto en cuentas sin privilegios — asi que el link se borraba y la
10
+ // recreacion tiraba EPERM: el usuario quedaba PEOR que antes, y con la entrada
11
+ // ya ausente el siguiente `awm init` ni siquiera podia verla como reparable.
12
+ //
13
+ // `executor.ts` documenta en extenso que en win32 hay que usar `'junction'`
14
+ // (que cualquier cuenta puede crear, y que libuv reporta igual que un symlink);
15
+ // este sitio nunca recibio ese fix.
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const skill_integrity_1 = require("../../src/core/skill-integrity");
20
+ describe('repairSkillLinks: un fallo nunca deja al usuario peor que antes', () => {
21
+ let skillsDir;
22
+ let registry;
23
+ beforeEach(() => {
24
+ const base = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-repair-'));
25
+ skillsDir = path_1.default.join(base, 'skills-installed');
26
+ registry = path_1.default.join(base, 'registry');
27
+ fs_1.default.mkdirSync(skillsDir, { recursive: true });
28
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'skills', 'alpha'), { recursive: true });
29
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: d\n---\nb');
30
+ });
31
+ /** Un symlink colgante: apunta a algo que no existe, pero el registry SI
32
+ * tiene la skill — o sea, reparable. */
33
+ function danglingLink(name) {
34
+ fs_1.default.symlinkSync(path_1.default.join(skillsDir, '__no_existe__'), path_1.default.join(skillsDir, name), 'dir');
35
+ }
36
+ it('repara un link colgante apuntandolo al registry', () => {
37
+ danglingLink('alpha');
38
+ const result = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
39
+ expect(result.relinked).toContain('alpha');
40
+ expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'alpha'))).toBe(true);
41
+ expect(fs_1.default.realpathSync(path_1.default.join(skillsDir, 'alpha')))
42
+ .toBe(fs_1.default.realpathSync(path_1.default.join(registry, 'skills', 'alpha')));
43
+ });
44
+ it('si la creacion del link falla, el estado original NO se destruye', () => {
45
+ danglingLink('alpha');
46
+ const before = fs_1.default.readlinkSync(path_1.default.join(skillsDir, 'alpha'));
47
+ // Simula la denegacion de privilegio de Windows.
48
+ const spy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => {
49
+ const err = new Error('operation not permitted');
50
+ err.code = 'EPERM';
51
+ throw err;
52
+ });
53
+ try {
54
+ const result = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
55
+ expect(result.failed).toContain('alpha');
56
+ expect(result.relinked).not.toContain('alpha');
57
+ // Lo que importa: la entrada sigue existiendo, con su target
58
+ // original. Antes quedaba borrada — invisible incluso para el
59
+ // proximo intento de reparacion.
60
+ expect(fs_1.default.lstatSync(path_1.default.join(skillsDir, 'alpha')).isSymbolicLink()).toBe(true);
61
+ expect(fs_1.default.readlinkSync(path_1.default.join(skillsDir, 'alpha'))).toBe(before);
62
+ }
63
+ finally {
64
+ spy.mockRestore();
65
+ }
66
+ });
67
+ it('no deja archivos de staging tirados tras una reparacion exitosa', () => {
68
+ danglingLink('alpha');
69
+ (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
70
+ expect(fs_1.default.readdirSync(skillsDir).filter((e) => e.includes('.relink'))).toEqual([]);
71
+ });
72
+ it('no deja archivos de staging tirados tras un fallo', () => {
73
+ danglingLink('alpha');
74
+ const spy = jest.spyOn(fs_1.default, 'renameSync').mockImplementation(() => { throw new Error('boom'); });
75
+ try {
76
+ (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
77
+ }
78
+ finally {
79
+ spy.mockRestore();
80
+ }
81
+ // El staging puede sobrevivir a un crash duro, pero se limpia en el
82
+ // siguiente intento — se verifica que una segunda pasada converge.
83
+ (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
84
+ expect(fs_1.default.readdirSync(skillsDir).filter((e) => e.includes('.relink'))).toEqual([]);
85
+ });
86
+ it('poda un link muerto (sin contraparte en el registry)', () => {
87
+ danglingLink('fantasma');
88
+ const result = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
89
+ expect(result.pruned).toContain('fantasma');
90
+ expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'fantasma'))).toBe(false);
91
+ });
92
+ });
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // `awm sync` over a profile with N extensions ran N independent transactions —
7
+ // one `planInstall` + `applyInstallPlan` per extension, in a loop. Each is atomic on
8
+ // its own, none is atomic with the others, so a failure on extension 3 left 1 and 2
9
+ // installed and the tree in a state that matched neither before nor after:
10
+ //
11
+ // extensions: [a, b, c]
12
+ // a → tx-1 committed
13
+ // b → tx-2 committed
14
+ // c → throws ← a and b stay installed, sync exits non-zero
15
+ //
16
+ // And the transaction IDs — the only handle `awm backup restore` accepts — were
17
+ // collected into `transactionIds` and then never printed by any caller, so even the
18
+ // committed halves could not be named to undo them by hand.
19
+ //
20
+ // One plan for the whole sync. `planInstall` already dedups by physical target across
21
+ // artifacts, so combining the extensions loses nothing; and `applyInstallPlan` already
22
+ // rolls its own plan back on any failure, so combining is what makes the sync atomic.
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const os_1 = __importDefault(require("os"));
25
+ const path_1 = __importDefault(require("path"));
26
+ const bundle_install_1 = require("../../src/core/bundle-install");
27
+ function bundle(name, skills, contentRoot) {
28
+ return {
29
+ name, description: '', version: '1.0.0', scope: 'project', visibility: 'public',
30
+ dependsOn: [], skills: skills.map(n => ({ name: n })), workflows: [], agents: [],
31
+ source: `bundles/${name}`, contentRoot,
32
+ };
33
+ }
34
+ describe('awm sync is one transaction, not one per extension', () => {
35
+ let projectRoot;
36
+ let contentDir;
37
+ let home;
38
+ let saved;
39
+ let bundles;
40
+ beforeEach(() => {
41
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-home-'));
42
+ saved = { HOME: process.env.HOME, AWM_HOME: process.env.AWM_HOME };
43
+ process.env.HOME = home;
44
+ process.env.AWM_HOME = path_1.default.join(home, '.awm');
45
+ fs_1.default.mkdirSync(path_1.default.join(home, '.awm'), { recursive: true });
46
+ fs_1.default.writeFileSync(path_1.default.join(home, '.awm', 'preferences.json'), JSON.stringify({
47
+ enabledAgents: ['claude-code'], defaultAgent: 'claude-code',
48
+ installMethod: 'symlink', defaultScope: 'global',
49
+ }));
50
+ contentDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-content-'));
51
+ for (const s of ['skill-a', 'skill-b', 'skill-c']) {
52
+ fs_1.default.mkdirSync(path_1.default.join(contentDir, 'skills', s), { recursive: true });
53
+ fs_1.default.writeFileSync(path_1.default.join(contentDir, 'skills', s, 'SKILL.md'), `---\nname: ${s}\ndescription: d\n---\n`);
54
+ }
55
+ bundles = [
56
+ bundle('ext-a', ['skill-a'], contentDir),
57
+ bundle('ext-b', ['skill-b'], contentDir),
58
+ bundle('ext-c', ['skill-c'], contentDir),
59
+ ];
60
+ projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-proj-'));
61
+ fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
62
+ fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['ext-a', 'ext-b', 'ext-c'] }));
63
+ });
64
+ afterEach(() => {
65
+ if (saved.HOME === undefined)
66
+ delete process.env.HOME;
67
+ else
68
+ process.env.HOME = saved.HOME;
69
+ if (saved.AWM_HOME === undefined)
70
+ delete process.env.AWM_HOME;
71
+ else
72
+ process.env.AWM_HOME = saved.AWM_HOME;
73
+ for (const d of [home, contentDir, projectRoot])
74
+ fs_1.default.rmSync(d, { recursive: true, force: true });
75
+ });
76
+ const run = (applyPlan) => (0, bundle_install_1.syncProfile)({ projectRoot, bundles, agents: ['claude-code'], method: 'symlink', contentDir, applyPlan });
77
+ it('applies a single plan covering every extension', () => {
78
+ const plans = [];
79
+ const result = run((plan) => {
80
+ plans.push(plan);
81
+ return { installed: plan.operations.map(o => o.targetPath), skipped: [], transactionId: 'tx-1', modifiedFiles: [] };
82
+ });
83
+ expect(plans).toHaveLength(1);
84
+ const names = plans[0].operations.map(o => path_1.default.basename(o.targetPath)).sort();
85
+ expect(names).toEqual(['skill-a', 'skill-b', 'skill-c']);
86
+ expect(result.transactionIds).toEqual(['tx-1']);
87
+ });
88
+ it('installs nothing at all when the apply fails — no half-synced tree', () => {
89
+ // The failure is on the LAST extension on purpose: with one transaction per
90
+ // extension, ext-a and ext-b were already committed by the time ext-c blew up.
91
+ const committed = [];
92
+ expect(() => run((plan) => {
93
+ const names = plan.operations.map(o => path_1.default.basename(o.targetPath));
94
+ if (names.includes('skill-c'))
95
+ throw new Error('verification failed');
96
+ committed.push(...names);
97
+ return { installed: names, skipped: [], transactionId: `tx-${committed.length}`, modifiedFiles: [] };
98
+ })).toThrow(/verification failed/);
99
+ expect(committed).toEqual([]);
100
+ });
101
+ it('skips unknown extensions without splitting the transaction', () => {
102
+ fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['ext-a', 'ghost', 'ext-c'] }));
103
+ const plans = [];
104
+ const result = run((plan) => {
105
+ plans.push(plan);
106
+ return { installed: [], skipped: [], transactionId: 'tx-1', modifiedFiles: [] };
107
+ });
108
+ expect(plans).toHaveLength(1);
109
+ expect(plans[0].operations.map(o => path_1.default.basename(o.targetPath)).sort()).toEqual(['skill-a', 'skill-c']);
110
+ expect(result.skipped.some(s => s.includes('ghost'))).toBe(true);
111
+ });
112
+ it('does not apply anything when every extension is unknown', () => {
113
+ fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['ghost'] }));
114
+ let applyCalls = 0;
115
+ const result = run(() => {
116
+ applyCalls++;
117
+ return { installed: [], skipped: [], transactionId: 'tx-1', modifiedFiles: [] };
118
+ });
119
+ expect(applyCalls).toBe(0);
120
+ expect(result.transactionIds).toEqual([]);
121
+ });
122
+ });
@@ -75,9 +75,9 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
75
75
  fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
76
76
  fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/run-hook.cmd'), '#!/bin/sh\nexec sh "$1"\n', { mode: 0o755 });
77
77
  fs_1.default.mkdirSync(path_1.default.join(root, 'skills/using-awm'), { recursive: true });
78
- fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.');
78
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\ndescription: Entry point for AWM skills.\n---\nMUST invoke skills.');
79
79
  fs_1.default.mkdirSync(path_1.default.join(root, 'skills/development-process'), { recursive: true });
80
- fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\n---\nOrchestrates the dev lifecycle.');
80
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\ndescription: Orchestrates the development lifecycle.\n---\nOrchestrates the dev lifecycle.');
81
81
  fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
82
82
  version: 1,
83
83
  bundles: [{ name: 'dev-core', source: 'bundles/dev-core', version: '1.0.0', scope: 'baseline', visibility: 'public' }],
@@ -56,7 +56,7 @@ describe('Providers Routing', () => {
56
56
  expect((0, providers_1.providerFor)('codex')).toEqual({
57
57
  label: 'Codex',
58
58
  minimumVersion: '0.145.0',
59
- versionCommand: { command: 'codex', args: ['--version'] },
59
+ versionCommand: { command: 'codex', args: ['--version'], versionPattern: /^codex-cli (\d+\.\d+\.\d+)$/ },
60
60
  skill: {
61
61
  global: path_1.default.join(tmpHome, '.agents/skills'),
62
62
  local: '.agents/skills',
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // Structural guard, in the vein of exec-invocation-explicit-stdio.test.ts: it does not
7
+ // test behaviour, it forbids the SHAPE that produced a whole family of bugs.
8
+ //
9
+ // "What filename does renderer R produce" existed in four places. Three agreed; the
10
+ // fourth — the one deciding whether a rendered artifact counted as installed — had
11
+ // never heard of `.mdc` or `.instructions.md`, so `awm add` re-installed the same
12
+ // Cursor/Copilot artifacts on every run and `awm doctor` called a healthy install
13
+ // absent. Each instance was individually reasonable; the class is what cost a release.
14
+ //
15
+ // `core/renderers/registry.ts` is now the single table. This test fails the moment a
16
+ // second one appears, whether by a new literal map or by a fresh `switch` over renderer
17
+ // ids somewhere that should have asked the table.
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const SRC = path_1.default.join(__dirname, '..', '..', 'src');
21
+ const REGISTRY = path_1.default.join(SRC, 'core', 'renderers', 'registry.ts');
22
+ /** Every .ts file under src/, recursively. */
23
+ function sourceFiles(dir) {
24
+ const out = [];
25
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
26
+ const full = path_1.default.join(dir, entry.name);
27
+ if (entry.isDirectory())
28
+ out.push(...sourceFiles(full));
29
+ else if (entry.name.endsWith('.ts'))
30
+ out.push(full);
31
+ }
32
+ return out;
33
+ }
34
+ /** The file's lines with `//` comments and blank lines dropped — a renderer id named in
35
+ * prose (a doc comment explaining the bug) is not a second implementation of the table. */
36
+ function codeLines(file) {
37
+ const raw = fs_1.default.readFileSync(file, 'utf-8').split('\n');
38
+ const out = [];
39
+ let inBlockComment = false;
40
+ raw.forEach((line, i) => {
41
+ let l = line;
42
+ if (inBlockComment) {
43
+ const end = l.indexOf('*/');
44
+ if (end === -1)
45
+ return;
46
+ inBlockComment = false;
47
+ l = l.slice(end + 2);
48
+ }
49
+ const start = l.indexOf('/*');
50
+ if (start !== -1) {
51
+ inBlockComment = l.indexOf('*/', start) === -1;
52
+ l = l.slice(0, start) + (inBlockComment ? '' : l.slice(l.indexOf('*/', start) + 2));
53
+ }
54
+ const lineComment = l.indexOf('//');
55
+ if (lineComment !== -1)
56
+ l = l.slice(0, lineComment);
57
+ if (l.trim())
58
+ out.push({ line: l, n: i + 1 });
59
+ });
60
+ return out;
61
+ }
62
+ /** The extensions the renderer table owns. A literal here, outside the table, IS the bug. */
63
+ const RENDERED_EXTENSIONS = ['.toml', '.mdc', '.instructions.md'];
64
+ describe('the renderer table is the only renderer table', () => {
65
+ const files = sourceFiles(SRC).filter((f) => f !== REGISTRY);
66
+ it('no other source file maps a renderer id to a file extension', () => {
67
+ const offenders = [];
68
+ for (const file of files) {
69
+ for (const { line, n } of codeLines(file)) {
70
+ // A renderer-to-extension mapping looks like an id and an extension on
71
+ // the same line of code. That is exactly the shape of every copy that
72
+ // drifted, and nothing legitimate needs to write both at once now that
73
+ // `rendererExtension`/`renderedFilename` exist.
74
+ const namesRenderer = /'(codex-agent-toml|cursor-mdc|copilot-instructions)'/.test(line);
75
+ const namesExtension = RENDERED_EXTENSIONS.some((e) => line.includes(`'${e}'`) || line.includes(`"${e}"`));
76
+ if (namesRenderer && namesExtension) {
77
+ offenders.push(`${path_1.default.relative(SRC, file)}:${n}: ${line.trim()}`);
78
+ }
79
+ }
80
+ }
81
+ expect(offenders).toEqual([]);
82
+ });
83
+ it('the table itself still covers every declared renderer', () => {
84
+ // The guard above is only worth anything if the one surviving table is complete:
85
+ // a renderer added to `RendererId` but not to the table would make the guard
86
+ // push every caller toward a lookup that has no answer.
87
+ const { RENDERER_IDS } = require('../../src/providers');
88
+ const { rendererExtension } = require('../../src/core/renderers/registry');
89
+ for (const id of RENDERER_IDS) {
90
+ // `null` (the `link` renderer, which keeps the name as-is) is an ANSWER;
91
+ // `undefined` is the table not knowing about the renderer at all.
92
+ expect(rendererExtension(id)).not.toBeUndefined();
93
+ }
94
+ });
95
+ });
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // Structural guard, same family as exec-invocation-explicit-stdio.test.ts.
7
+ //
8
+ // `fs.symlinkSync(target, dest)` with no third argument creates a DIRECTORY symlink on
9
+ // Windows when the target is a directory, and that needs SeCreateSymbolicLinkPrivilege
10
+ // — denied by default on unprivileged accounts, including GitHub Actions'
11
+ // windows-latest runner. The same omission has been fixed one site at a time, more
12
+ // than once, each fix landing where the bug was reported and nowhere else:
13
+ // `executor.ts` learned `'junction'`, and `skill-integrity.ts` did not, for a whole
14
+ // release — its comment still says "este sitio nunca recibio el fix".
15
+ //
16
+ // So the rule is about the CALL, not about any one call site: every `fs.symlinkSync`
17
+ // either passes an explicit type argument, or is wrapped in a `try` whose `catch`
18
+ // falls back to a copy. Those are the only two ways this survives Windows, and one of
19
+ // them has to be visible at the call.
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const SRC = path_1.default.join(__dirname, '..', '..', 'src');
23
+ function sourceFiles(dir) {
24
+ const out = [];
25
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
26
+ const full = path_1.default.join(dir, entry.name);
27
+ if (entry.isDirectory())
28
+ out.push(...sourceFiles(full));
29
+ else if (entry.name.endsWith('.ts'))
30
+ out.push(full);
31
+ }
32
+ return out;
33
+ }
34
+ /** True when `lines[i]` sits inside a try block whose catch copies instead. Deliberately
35
+ * shallow — it looks a few lines ahead for the catch, which is what every real fallback
36
+ * in this codebase looks like. A cleverer arrangement that defeats it should be written
37
+ * with an explicit type argument instead. */
38
+ function hasCopyFallback(lines, i) {
39
+ const window = lines.slice(Math.max(0, i - 3), i + 8).join('\n');
40
+ return /\btry\b/.test(window) && /catch\b/.test(window) && /copyFileSync|cpSync/.test(window);
41
+ }
42
+ describe('every symlink creation survives Windows on purpose', () => {
43
+ it('passes an explicit type argument or falls back to a copy', () => {
44
+ const offenders = [];
45
+ for (const file of sourceFiles(SRC)) {
46
+ const lines = fs_1.default.readFileSync(file, 'utf-8').split('\n');
47
+ lines.forEach((line, i) => {
48
+ if (!/\bfs\.symlinkSync\s*\(/.test(line))
49
+ return;
50
+ // Three arguments — the third is the type ('junction' | 'dir' | 'file').
51
+ const explicitType = /symlinkSync\s*\([^)]*,[^)]*,\s*[^)]+\)/.test(line) ||
52
+ /'junction'|'dir'|'file'/.test(lines.slice(i, i + 3).join('\n'));
53
+ if (explicitType || hasCopyFallback(lines, i))
54
+ return;
55
+ offenders.push(`${path_1.default.relative(SRC, file)}:${i + 1}: ${line.trim()}`);
56
+ });
57
+ }
58
+ expect(offenders).toEqual([]);
59
+ });
60
+ it('the guard actually rejects a bare two-argument call', () => {
61
+ // Without this, a guard whose regex silently stopped matching would read as a
62
+ // clean bill of health forever — the failure mode of every structural test.
63
+ const bare = ['fs.symlinkSync(source, dest);'];
64
+ const explicit = ["fs.symlinkSync(source, dest, 'junction');"];
65
+ const wrapped = ['try {', 'fs.symlinkSync(source, dest);', '} catch {', 'fs.copyFileSync(source, dest);', '}'];
66
+ const rejects = (lines, i) => !/symlinkSync\s*\([^)]*,[^)]*,\s*[^)]+\)/.test(lines[i]) &&
67
+ !/'junction'|'dir'|'file'/.test(lines.slice(i, i + 3).join('\n')) &&
68
+ !hasCopyFallback(lines, i);
69
+ expect(rejects(bare, 0)).toBe(true);
70
+ expect(rejects(explicit, 0)).toBe(false);
71
+ expect(rejects(wrapped, 1)).toBe(false);
72
+ });
73
+ });
@@ -44,6 +44,15 @@ describe('truncate', () => {
44
44
  it('returns empty for non-positive width', () => {
45
45
  expect((0, text_1.truncate)('abc', 0)).toBe('');
46
46
  });
47
+ it('colapsa saltos de linea a un espacio — la celda es de UNA linea', () => {
48
+ // Regresion: `displayWidth` cuenta `\n` como ancho 1, asi que una string
49
+ // multilinea corta pasaba el chequeo y se emitia cruda, partiendo la fila
50
+ // de `awm list` y desalineando todo lo siguiente. Alcanzable desde que el
51
+ // lector de frontmatter resuelve block scalars literales (`|`).
52
+ expect((0, text_1.truncate)('linea uno\nlinea dos', 80)).toBe('linea uno linea dos');
53
+ expect((0, text_1.truncate)('a\r\n b', 80)).toBe('a b');
54
+ expect((0, text_1.truncate)('linea uno\nlinea dos', 12)).toBe('linea uno l…');
55
+ });
47
56
  });
48
57
  describe('wrap', () => {
49
58
  it('breaks text at word boundaries within width', () => {
@@ -25,9 +25,9 @@ describe('registry-view override markers', () => {
25
25
  fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
26
26
  });
27
27
  it('packageDetailLines marks an overridden skill with its registry name', () => {
28
- const { REGISTRIES_DIR } = require('../../src/core/registries');
28
+ const { registriesDir } = require('../../src/core/registries');
29
29
  const { buildPackageView, packageDetailLines } = require('../../src/utils/registry-view');
30
- const teamSkillPath = path_1.default.join(REGISTRIES_DIR, 'team-acme', 'skills', 'brainstorming');
30
+ const teamSkillPath = path_1.default.join(registriesDir(), 'team-acme', 'skills', 'brainstorming');
31
31
  const view = buildPackageView([{ name: 'brainstorming', path: teamSkillPath, description: 'd', overrode: '/old/path' }], [], [], []);
32
32
  const lines = packageDetailLines(view[0]).join('\n');
33
33
  expect(lines).toContain('brainstorming');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.13.4",
3
+ "version": "3.13.6",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -34,6 +34,9 @@
34
34
  ],
35
35
  "author": "Kodria",
36
36
  "license": "ISC",
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
37
40
  "description": "CLI to manage Agentic Workflows - install skills, workflows, and processes for AI agents",
38
41
  "dependencies": {
39
42
  "@clack/prompts": "^1.0.1",
@@ -43,10 +46,12 @@
43
46
  },
44
47
  "devDependencies": {
45
48
  "@types/jest": "^30.0.0",
49
+ "@types/js-yaml": "4.0.9",
46
50
  "@types/node": "^25.3.0",
47
51
  "dependency-cruiser": "^17.4.3",
48
52
  "eslint": "^10.4.1",
49
53
  "jest": "^30.2.0",
54
+ "js-yaml": "4.1.0",
50
55
  "ts-jest": "^29.4.6",
51
56
  "typescript": "^5.9.3"
52
57
  }