agentic-workflow-manager 3.13.5 → 3.13.7

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 (84) hide show
  1. package/dist/scripts/support-matrix.js +138 -0
  2. package/dist/src/commands/add.js +5 -0
  3. package/dist/src/commands/doctor.js +1 -0
  4. package/dist/src/commands/hooks/claude.js +15 -2
  5. package/dist/src/commands/hooks/shared.js +6 -1
  6. package/dist/src/commands/init.js +1 -0
  7. package/dist/src/commands/pin.js +24 -4
  8. package/dist/src/commands/preflight/checks.js +3 -2
  9. package/dist/src/commands/registry/add.js +1 -1
  10. package/dist/src/commands/sensors/index.js +7 -0
  11. package/dist/src/commands/sensors/init.js +48 -7
  12. package/dist/src/commands/sensors/install.js +20 -11
  13. package/dist/src/commands/sensors/run.js +31 -29
  14. package/dist/src/commands/sync.js +29 -0
  15. package/dist/src/commands/watch/index.js +11 -1
  16. package/dist/src/core/artifact-name.js +80 -0
  17. package/dist/src/core/bundle-install.js +55 -12
  18. package/dist/src/core/bundles.js +72 -13
  19. package/dist/src/core/context/regenerate.js +21 -4
  20. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  21. package/dist/src/core/diagnostics/checks.js +26 -2
  22. package/dist/src/core/diagnostics/context.js +91 -25
  23. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  24. package/dist/src/core/discovery.js +2 -2
  25. package/dist/src/core/init/mutation-targets.js +34 -1
  26. package/dist/src/core/init/steps.js +70 -19
  27. package/dist/src/core/install-planner.js +62 -17
  28. package/dist/src/core/install-transaction.js +13 -1
  29. package/dist/src/core/journal/adapter.js +10 -1
  30. package/dist/src/core/paths.js +61 -8
  31. package/dist/src/core/profile.js +12 -0
  32. package/dist/src/core/provider-artifacts.js +38 -13
  33. package/dist/src/core/provider-version.js +12 -6
  34. package/dist/src/core/registries.js +40 -19
  35. package/dist/src/core/renderers/registry.js +37 -0
  36. package/dist/src/core/skill-integrity.js +71 -10
  37. package/dist/src/core/update-check.js +14 -2
  38. package/dist/src/core/versioning.js +17 -2
  39. package/dist/src/index.js +31 -23
  40. package/dist/src/providers/index.js +7 -2
  41. package/dist/src/utils/config.js +7 -8
  42. package/dist/tests/commands/doctor.test.js +6 -0
  43. package/dist/tests/commands/pin.test.js +29 -2
  44. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  45. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  46. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  47. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  48. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  49. package/dist/tests/commands/sensors/run.test.js +24 -51
  50. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  51. package/dist/tests/commands/sensors/status.test.js +27 -12
  52. package/dist/tests/commands/watch/runner.test.js +10 -0
  53. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  54. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  55. package/dist/tests/core/context/regenerate.test.js +39 -8
  56. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  57. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  58. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  59. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  60. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  61. package/dist/tests/core/init/mutation-targets-canonical.test.js +81 -0
  62. package/dist/tests/core/init/mutation-targets.test.js +4 -4
  63. package/dist/tests/core/init/orchestrator.test.js +1 -1
  64. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  65. package/dist/tests/core/init/steps.test.js +11 -5
  66. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  67. package/dist/tests/core/paths.test.js +7 -34
  68. package/dist/tests/core/project-skill-links.test.js +159 -0
  69. package/dist/tests/core/provider-artifacts.test.js +66 -14
  70. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  71. package/dist/tests/core/reconciliation.test.js +15 -15
  72. package/dist/tests/core/registry-manifest.test.js +1 -1
  73. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  74. package/dist/tests/core/skill-integrity.test.js +10 -10
  75. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  76. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  77. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  78. package/dist/tests/providers/index.test.js +1 -1
  79. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  80. package/dist/tests/structural/support-matrix-is-current.test.js +100 -0
  81. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  82. package/dist/tests/support/tmp.js +31 -0
  83. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  84. package/package.json +6 -2
@@ -4,7 +4,7 @@ const checks_1 = require("../../../src/core/diagnostics/checks");
4
4
  function healthyMachine() {
5
5
  return {
6
6
  registryCache: { present: true, gitState: 'clean' },
7
- hook: { present: true, degraded: false },
7
+ hook: { present: true, degraded: false, applicable: true },
8
8
  devCore: { present: true, brokenLinks: [] },
9
9
  ambient: { wanted: [], installed: [] },
10
10
  contextInjection: [],
@@ -16,6 +16,7 @@ function healthyProject() {
16
16
  root: '/repo/belanz',
17
17
  profile: { present: true, extensions: ['frontend'] },
18
18
  activeBundles: { expected: ['frontend-craft'], linked: ['frontend-craft'], broken: [] },
19
+ orphanLinks: { repairable: [], dead: [] },
19
20
  sensors: { present: true },
20
21
  constitution: { present: true },
21
22
  context: { present: true, file: 'CLAUDE.md' },
@@ -80,12 +81,12 @@ describe('runChecks — machine.cli', () => {
80
81
  describe('runChecks — machine.hook / devCore', () => {
81
82
  it('hook degraded → warn', () => {
82
83
  const m = healthyMachine();
83
- m.hook = { present: true, degraded: true };
84
+ m.hook = { present: true, degraded: true, applicable: true };
84
85
  expect(byId({ machine: m, project: null }, 'machine.hook').status).toBe('warn');
85
86
  });
86
87
  it('hook absent → missing + awm init', () => {
87
88
  const m = healthyMachine();
88
- m.hook = { present: false };
89
+ m.hook = { present: false, applicable: true };
89
90
  const c = byId({ machine: m, project: null }, 'machine.hook');
90
91
  expect(c.status).toBe('missing');
91
92
  expect(c.remedy).toEqual({ kind: 'command', value: 'awm init' });
@@ -162,7 +163,7 @@ describe('machineChecks — global skill integrity', () => {
162
163
  return {
163
164
  machine: {
164
165
  registryCache: { present: true, gitState: 'clean' },
165
- hook: { present: true, degraded: false },
166
+ hook: { present: true, degraded: false, applicable: true },
166
167
  devCore: { present: true, brokenLinks: [] },
167
168
  ambient: { wanted: [], installed: [] },
168
169
  contextInjection: [],
@@ -194,8 +195,15 @@ describe('computeProviderOverall (Task 9)', () => {
194
195
  checks: states.map((state, i) => ({ id: CHECK_IDS[i % CHECK_IDS.length], state })),
195
196
  };
196
197
  }
197
- it('healthy when every check is in an ok/pending/warn state', () => {
198
- expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust', 'stale'])])).toBe('healthy');
198
+ it('healthy when every check is in an ok/pending state', () => {
199
+ expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust'])])).toBe('healthy');
200
+ });
201
+ it('degraded cuando algun check esta stale — un contexto viejo tiene que verse en el exit code', () => {
202
+ // Antes `stale` quedaba fuera de los estados degradantes, asi que doctor
203
+ // imprimia ⚠ junto a `status: healthy` y exit 0. Un AGENTS.md
204
+ // desactualizado (o pisado por otro provider) era invisible para
205
+ // cualquier gate de CI que mirara el codigo de salida.
206
+ expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'stale'])])).toBe('degraded');
199
207
  });
200
208
  it('degrades on a single broken/missing/absent/unsupported/conflict check', () => {
201
209
  expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'absent'])])).toBe('degraded');
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  // `contextStatus` about 'global' for these providers always resolved to
16
16
  // 'absent' regardless of whether the local injection actually succeeded.
17
17
  // 3. `skillsGlobalCheck`'s renderer-awareness fix (deferred Task 4.3 finding):
18
- // `classifyGlobalSkills` only ever sees symlinks, so a rendered format
18
+ // `classifySkillLinks` only ever sees symlinks, so a rendered format
19
19
  // (cursor-mdc, copilot-instructions) always scanned as "0 broken" and
20
20
  // reported a false-green 'healthy' regardless of what was actually on
21
21
  // disk. Non-'link' renderers now report presence-only, honestly.
@@ -174,9 +174,9 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
174
174
  const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
175
175
  fs_1.default.mkdirSync(rulesDir, { recursive: true });
176
176
  fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'development-process.mdc'), '---\ndescription: dev process\nalwaysApply: true\n---\n\nBody.');
177
- // classifyGlobalSkills only ever sees symlinks (`if (!lst.isSymbolicLink()) continue;`)
177
+ // classifySkillLinks only ever sees symlinks (`if (!lst.isSymbolicLink()) continue;`)
178
178
  // — a real scan over rulesDir would find nothing here either. Stubbed explicitly so the
179
- // test proves the FIX (renderer-gating), not an accident of what classifyGlobalSkills does.
179
+ // test proves the FIX (renderer-gating), not an accident of what classifySkillLinks does.
180
180
  const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
181
181
  const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
182
182
  const facts = gatherProviderChecks(['cursor'], scanSkills);
@@ -0,0 +1,69 @@
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: el ESCRITOR estampa la extension del renderer sobre el nombre
7
+ // (`using-awm.mdc`, `using-awm.instructions.md`, `development-process.toml`)
8
+ // pero los LECTORES de diagnostico buscaban el nombre pelado. Consecuencia para
9
+ // Cursor y Copilot: nada figuraba jamas como instalado, asi que
10
+ // - `awm init` NUNCA era idempotente — reinstalaba el baseline entero en cada
11
+ // corrida, dejando 2 directorios de backup sin podar por corrida;
12
+ // - `awm doctor` mostraba `✖ dev-core` / `✖ active bundles` de forma
13
+ // permanente, con un remedio (`awm init` / `awm sync`) que no podia
14
+ // satisfacerlo nunca.
15
+ // Y lo mismo para los artefactos `agent` de claude-code — el provider POR
16
+ // DEFECTO — porque su installName es `<n>.md` y el lector mapeaba solo el caso
17
+ // `.toml` de codex.
18
+ //
19
+ // Estos tests recorren la misma funcion que usa el escritor
20
+ // (`renderedFilename`), asi que lector y escritor no pueden volver a discrepar.
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const os_1 = __importDefault(require("os"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const registry_1 = require("../../../src/core/renderers/registry");
25
+ const install_planner_1 = require("../../../src/core/install-planner");
26
+ describe('renderedFilename: una sola fuente de verdad para el nombre en disco', () => {
27
+ it.each([
28
+ ['link deja el nombre tal cual', 'using-awm', 'link', 'using-awm'],
29
+ ['link conserva el .md del installName', 'development-process.md', 'link', 'development-process.md'],
30
+ ['cursor-mdc', 'using-awm', 'cursor-mdc', 'using-awm.mdc'],
31
+ ['cursor-mdc recorta el .md antes de estampar', 'using-awm.md', 'cursor-mdc', 'using-awm.mdc'],
32
+ ['copilot-instructions', 'using-awm', 'copilot-instructions', 'using-awm.instructions.md'],
33
+ ['codex-agent-toml', 'development-process.md', 'codex-agent-toml', 'development-process.toml'],
34
+ ])('%s', (_n, installName, renderer, expected) => {
35
+ expect((0, registry_1.renderedFilename)(installName, renderer)).toBe(expected);
36
+ });
37
+ it('no usa path.parse().name — un nombre con puntos internos no se trunca', () => {
38
+ // `path.parse('v1.2-migration').name` === 'v1', lo que perderia
39
+ // `2-migration` y podria colisionar con otro skill llamado `v1`.
40
+ expect((0, registry_1.renderedFilename)('v1.2-migration', 'cursor-mdc')).toBe('v1.2-migration.mdc');
41
+ });
42
+ });
43
+ describe('el lector de diagnostico ve lo que el escritor dejo en disco', () => {
44
+ let projectRoot;
45
+ beforeEach(() => {
46
+ projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-rendered-'));
47
+ });
48
+ afterEach(() => {
49
+ fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
50
+ });
51
+ // El invariante que importa: el nombre que el escritor produce y el que el
52
+ // lector busca salen de la MISMA funcion. Se verifica sobre el destino real
53
+ // que planifica `physicalTarget`, no sobre una reconstruccion a mano.
54
+ it.each([
55
+ ['cursor'], ['copilot'], ['claude-code'], ['codex'], ['opencode'], ['antigravity'],
56
+ ])('%s: el basename del destino planificado coincide con renderedFilename', (agent) => {
57
+ const { targetPath, renderer } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, agent, 'local', projectRoot);
58
+ expect(path_1.default.basename(targetPath)).toBe((0, registry_1.renderedFilename)('using-awm', renderer));
59
+ });
60
+ it('cursor: el destino termina en .mdc (el lector que buscaba el nombre pelado nunca lo encontraba)', () => {
61
+ const { targetPath } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'cursor', 'local', projectRoot);
62
+ expect(path_1.default.basename(targetPath)).toBe('using-awm.mdc');
63
+ expect(fs_1.default.existsSync(path_1.default.join(path_1.default.dirname(targetPath), 'using-awm'))).toBe(false);
64
+ });
65
+ it('copilot: el destino termina en .instructions.md', () => {
66
+ const { targetPath } = (0, install_planner_1.physicalTarget)({ name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/src' }, 'copilot', 'local', projectRoot);
67
+ expect(path_1.default.basename(targetPath)).toBe('using-awm.instructions.md');
68
+ });
69
+ });
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // `awm init` and `awm update` disagreed about what "the baseline" is.
4
+ //
5
+ // init (steps.ts) bundles.find(b => b.scope === 'baseline') ← the FIRST one
6
+ // update (reconciliation.ts) bundles.filter(b => b.scope === 'baseline' …) ← all of them
7
+ //
8
+ // A registry shipping two baseline bundles therefore installed one on `awm init` and
9
+ // two on the next `awm update` — and the diagnostic that decides whether the baseline
10
+ // is satisfied used `find` too, so it agreed with init and disagreed with update. The
11
+ // second bundle appeared out of nowhere on an unrelated command, and nothing before
12
+ // that had reported it missing.
13
+ //
14
+ // `filter` is the correct reading: `scope: 'baseline'` is a property a bundle HAS, not
15
+ // a slot only one bundle can occupy.
16
+ const steps_1 = require("../../../src/core/init/steps");
17
+ function bundle(name, scope) {
18
+ return {
19
+ name, description: '', version: '1.0.0', scope, visibility: 'public',
20
+ dependsOn: [], skills: [], workflows: [], agents: [], source: `bundles/${name}`,
21
+ };
22
+ }
23
+ function deps(bundles) {
24
+ const installs = [];
25
+ const d = {
26
+ cwd: '/repo',
27
+ agent: 'claude-code',
28
+ enabledAgents: ['claude-code'],
29
+ bundles,
30
+ installMethod: 'symlink',
31
+ registryRoot: '',
32
+ contentDir: '',
33
+ sensorPacksRoot: '',
34
+ ctx: {
35
+ machine: { devCore: { present: false, brokenLinks: [] } },
36
+ project: null,
37
+ },
38
+ actions: { installBundle: (o) => { installs.push(o.bundleName); } },
39
+ };
40
+ return { d, installs };
41
+ }
42
+ describe('awm init installs every baseline bundle, like awm update reconciles every one', () => {
43
+ it('installs all of them, not just the first', () => {
44
+ const { d, installs } = deps([
45
+ bundle('dev-core', 'baseline'),
46
+ bundle('team-baseline', 'baseline'),
47
+ bundle('frontend', 'project'),
48
+ ]);
49
+ const result = (0, steps_1.stepDevCore)(d);
50
+ expect(installs).toEqual(['dev-core', 'team-baseline']);
51
+ expect(result.action).toBe('applied');
52
+ });
53
+ it('installs nothing from a non-baseline scope', () => {
54
+ const { d, installs } = deps([bundle('dev-core', 'baseline'), bundle('extras', 'ambient')]);
55
+ (0, steps_1.stepDevCore)(d);
56
+ expect(installs).toEqual(['dev-core']);
57
+ });
58
+ it('still skips entirely when the baseline is already satisfied', () => {
59
+ const { d, installs } = deps([bundle('dev-core', 'baseline'), bundle('team-baseline', 'baseline')]);
60
+ d.ctx.machine.devCore.present = true;
61
+ expect((0, steps_1.stepDevCore)(d).action).toBe('skipped');
62
+ expect(installs).toEqual([]);
63
+ });
64
+ });
@@ -0,0 +1,117 @@
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
+ // `stepContextInjection` is a MACHINE-level step — it runs even when `ctx.project`
7
+ // is null. For providers whose context delivery is project-scope (managed-agents-md
8
+ // with `globalPath: null` — Cursor, Copilot) it then fell back to writing at `d.cwd`:
9
+ //
10
+ // $ cd ~/scratch && awm init -a copilot # not a project: no .git, no package.json
11
+ // ~/scratch/AGENTS.md ← written
12
+ // ~/scratch/.awm/context/awm-context.md ← written
13
+ //
14
+ // `planInitMutationTargets` enumerates both of those paths only inside
15
+ // `if (projectRoot)`, so with no project root neither entered the backup session. A
16
+ // later failed step rolled back everything that WAS enumerated and left these two
17
+ // behind — while the outcome reported `rolledBack: true` and listed `restoredFiles`,
18
+ // i.e. claimed a clean restore it had not performed.
19
+ //
20
+ // `--machine-only` reached the same place from the other side: it nulls `ctx.project`
21
+ // deliberately, so the step took the `?? d.cwd` branch even inside a real project.
22
+ //
23
+ // Two different situations were being read as one. Running in a directory the user
24
+ // means as their project but which has no marker yet is legitimate — Copilot has no
25
+ // other channel, so removing that write would leave it with no context at all. What
26
+ // was wrong is that the path was never enumerated, so it survived rollback; that is
27
+ // fixed in mutation-targets, which now covers `cwd` for these providers regardless of
28
+ // whether a project root was found.
29
+ //
30
+ // `--machine-only` is the case that must not write at all: it nulls `ctx.project` on
31
+ // purpose, and this step read that as "no project found" and wrote into cwd anyway —
32
+ // under the one flag whose entire promise is that it won't touch the project.
33
+ const fs_1 = __importDefault(require("fs"));
34
+ const path_1 = __importDefault(require("path"));
35
+ const steps_1 = require("../../../src/core/init/steps");
36
+ const mutation_targets_1 = require("../../../src/core/init/mutation-targets");
37
+ const materializer_1 = require("../../../src/core/context/materializer");
38
+ const tmp_1 = require("../../support/tmp");
39
+ function deps(agent, cwd, projectRoot, machineOnly = false) {
40
+ const installed = [];
41
+ const d = {
42
+ cwd,
43
+ agent,
44
+ enabledAgents: [agent],
45
+ bundles: [],
46
+ installMethod: 'symlink',
47
+ registryRoot: '',
48
+ contentDir: '',
49
+ sensorPacksRoot: '',
50
+ machineOnly,
51
+ ctx: {
52
+ machine: {},
53
+ project: projectRoot ? { root: projectRoot } : null,
54
+ },
55
+ actions: {
56
+ contextStatus: () => 'absent',
57
+ installContext: (op) => { installed.push(op); },
58
+ },
59
+ };
60
+ return { d, installed };
61
+ }
62
+ describe('project-scope context injection without a project', () => {
63
+ let cwd;
64
+ beforeEach(() => { cwd = (0, tmp_1.mkCanonicalTmpDir)('awm-noproj-'); });
65
+ afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
66
+ it.each(['copilot', 'cursor'])('%s: writes nothing at all under --machine-only', (agent) => {
67
+ const { d, installed } = deps(agent, cwd, null, true);
68
+ const result = (0, steps_1.stepContextInjection)(d);
69
+ expect(installed).toEqual([]);
70
+ expect(result.action).toBe('skipped');
71
+ expect(result.detail).toMatch(/machine-only/i);
72
+ });
73
+ it.each(['copilot', 'cursor'])('%s: still delivers into an unmarked cwd — its only channel — now that the path is backed up', (agent) => {
74
+ const { d, installed } = deps(agent, cwd, null);
75
+ const result = (0, steps_1.stepContextInjection)(d);
76
+ expect(result.action).toBe('applied');
77
+ expect(installed[0].projectRoot).toBe(cwd);
78
+ // the write is only safe because mutation-targets enumerates it — asserted below
79
+ expect((0, mutation_targets_1.planInitMutationTargets)({ cwd, agent, bundles: [] }))
80
+ .toContain(path_1.default.join(cwd, 'AGENTS.md'));
81
+ });
82
+ it('leaves a GLOBAL-scope provider untouched by --machine-only', () => {
83
+ // --machine-only excludes the project, not the machine. Codex's context is
84
+ // machine-global, so it must still be installed.
85
+ const { d, installed } = deps('codex', cwd, null, true);
86
+ expect((0, steps_1.stepContextInjection)(d).action).toBe('applied');
87
+ expect(installed).toHaveLength(1);
88
+ });
89
+ it('still injects for a local-scope provider when a project root IS known', () => {
90
+ const { d, installed } = deps('copilot', cwd, cwd);
91
+ const result = (0, steps_1.stepContextInjection)(d);
92
+ expect(result.action).toBe('applied');
93
+ expect(installed).toHaveLength(1);
94
+ expect(installed[0].projectRoot).toBe(cwd);
95
+ });
96
+ it('leaves global-scope injection (codex) alone — it needs no project', () => {
97
+ const { d, installed } = deps('codex', cwd, null);
98
+ const result = (0, steps_1.stepContextInjection)(d);
99
+ expect(result.action).toBe('applied');
100
+ expect(installed).toHaveLength(1);
101
+ expect(installed[0].scope).toBe('global');
102
+ });
103
+ });
104
+ describe('mutation-targets covers the local-scope context paths independently', () => {
105
+ let cwd;
106
+ beforeEach(() => { cwd = (0, tmp_1.mkCanonicalTmpDir)('awm-noproj-mt-'); });
107
+ afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
108
+ it.each(['copilot', 'cursor'])('%s: enumerates AGENTS.md and .awm/context even with no project root', (agent) => {
109
+ const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd, agent, bundles: [] });
110
+ expect(targets).toContain(path_1.default.join(cwd, 'AGENTS.md'));
111
+ expect(targets).toContain((0, materializer_1.projectContextPath)(cwd));
112
+ });
113
+ it('does not invent those targets for a provider that never writes them', () => {
114
+ const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd, agent: 'claude-code', bundles: [] });
115
+ expect(targets).not.toContain((0, materializer_1.projectContextPath)(cwd));
116
+ });
117
+ });
@@ -0,0 +1,81 @@
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
+ // `planInitMutationTargets` mezclaba dos formas de la misma ruta.
7
+ //
8
+ // Deriva algunos destinos de `cwd` tal como lo recibe, y otros de
9
+ // `findProjectRoot(cwd)` — que hace `realpathSync`. Mientras las dos coincidan, el `Set`
10
+ // deduplica y nadie lo nota. En macOS NO coinciden: `/var/folders/…` es un symlink a
11
+ // `/private/var/folders/…`, asi que la CI de macOS mostro `AGENTS.md` y
12
+ // `.awm/context/awm-context.md` DOS VECES en la lista de destinos, una por cada forma:
13
+ //
14
+ // /var/folders/…/AGENTS.md ← derivado de cwd
15
+ // /private/var/folders/…/AGENTS.md ← derivado de findProjectRoot(cwd)
16
+ //
17
+ // Esa lista es lo que `beginBackupSession` respalda y lo que el rollback restaura. Un
18
+ // mismo archivo entrando dos veces significa dos entradas de backup para un solo
19
+ // archivo, y un rollback que lo restaura dos veces — sobre un mecanismo cuyo unico
20
+ // trabajo es dejar el disco exactamente como estaba.
21
+ //
22
+ // El bug no necesita macOS: cualquier `cwd` que pase por un symlink lo reproduce. Este
23
+ // test arma esa situacion a mano, asi que corre igual en los tres sistemas.
24
+ const fs_1 = __importDefault(require("fs"));
25
+ const os_1 = __importDefault(require("os"));
26
+ const path_1 = __importDefault(require("path"));
27
+ const mutation_targets_1 = require("../../../src/core/init/mutation-targets");
28
+ describe('los destinos de mutacion son canonicos: un archivo, una entrada', () => {
29
+ let real;
30
+ let linkDir;
31
+ beforeEach(() => {
32
+ // `real` es el directorio de verdad; `linkDir` es un symlink que apunta ahi.
33
+ // Pasar `linkDir` como cwd es exactamente lo que hace macOS con /var → /private/var.
34
+ const base = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-canon-')));
35
+ real = path_1.default.join(base, 'real');
36
+ linkDir = path_1.default.join(base, 'link');
37
+ fs_1.default.mkdirSync(path_1.default.join(real, '.git'), { recursive: true }); // marcador de proyecto
38
+ fs_1.default.symlinkSync(real, linkDir, 'dir');
39
+ });
40
+ afterEach(() => {
41
+ fs_1.default.rmSync(path_1.default.dirname(real), { recursive: true, force: true });
42
+ });
43
+ /** Todo destino resuelto a su forma canonica: dos entradas que resuelven al mismo
44
+ * archivo son la misma entrada, sin importar como se escribieron. */
45
+ const canonical = (p) => {
46
+ try {
47
+ return fs_1.default.realpathSync(p);
48
+ }
49
+ catch { /* aun no existe */ }
50
+ const parent = path_1.default.dirname(p);
51
+ try {
52
+ return path_1.default.join(fs_1.default.realpathSync(parent), path_1.default.basename(p));
53
+ }
54
+ catch {
55
+ return p;
56
+ }
57
+ };
58
+ it.each(['copilot', 'cursor'])('%s: no enumera el mismo archivo dos veces cuando cwd pasa por un symlink', (agent) => {
59
+ const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd: linkDir, agent, bundles: [] });
60
+ const seen = new Map();
61
+ for (const t of targets) {
62
+ const key = canonical(t);
63
+ seen.set(key, [...(seen.get(key) ?? []), t]);
64
+ }
65
+ const duplicados = [...seen.entries()].filter(([, forms]) => forms.length > 1);
66
+ expect(duplicados).toEqual([]);
67
+ });
68
+ it('sigue enumerando AGENTS.md y el contexto materializado — deduplicar no es perder', () => {
69
+ // La correccion es colapsar formas equivalentes, NO dejar de cubrir el archivo:
70
+ // sub-enumerar es el unico modo de falla que derrota el rollback en silencio.
71
+ const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd: linkDir, agent: 'copilot', bundles: [] })
72
+ .map(canonical);
73
+ expect(targets).toContain(path_1.default.join(real, 'AGENTS.md'));
74
+ expect(targets).toContain(path_1.default.join(real, '.awm', 'context', 'awm-context.md'));
75
+ });
76
+ it('no depende del symlink: con un cwd ya canonico da el mismo resultado', () => {
77
+ const viaLink = (0, mutation_targets_1.planInitMutationTargets)({ cwd: linkDir, agent: 'copilot', bundles: [] }).map(canonical).sort();
78
+ const viaReal = (0, mutation_targets_1.planInitMutationTargets)({ cwd: real, agent: 'copilot', bundles: [] }).map(canonical).sort();
79
+ expect(viaLink).toEqual(viaReal);
80
+ });
81
+ });
@@ -17,9 +17,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  // tests/core/skill-integrity.test.ts. Per CLAUDE.md, no test may touch the
18
18
  // real ~/.awm.
19
19
  const fs_1 = __importDefault(require("fs"));
20
- const os_1 = __importDefault(require("os"));
21
20
  const path_1 = __importDefault(require("path"));
22
21
  const materializer_1 = require("../../../src/core/context/materializer");
22
+ const tmp_1 = require("../../support/tmp");
23
23
  function bundle(name, scope, skills) {
24
24
  return {
25
25
  name, description: '', version: '1.0.0', scope, visibility: 'public',
@@ -32,7 +32,7 @@ let originalHome;
32
32
  let originalAwmHome;
33
33
  let extraDirs;
34
34
  beforeEach(() => {
35
- tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-'));
35
+ tmpHome = (0, tmp_1.mkCanonicalTmpDir)('awm-mutation-targets-');
36
36
  originalHome = process.env.HOME;
37
37
  originalAwmHome = process.env.AWM_HOME;
38
38
  process.env.HOME = tmpHome;
@@ -67,7 +67,7 @@ function load() {
67
67
  }
68
68
  /** A cwd guaranteed to have no project-root marker (.git/package.json/.awm) in its ancestry. */
69
69
  function bareCwd() {
70
- const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-cwd-'));
70
+ const dir = (0, tmp_1.mkCanonicalTmpDir)('awm-mutation-targets-cwd-');
71
71
  extraDirs.push(dir);
72
72
  return dir;
73
73
  }
@@ -158,7 +158,7 @@ describe('planInitMutationTargets', () => {
158
158
  });
159
159
  describe('project-level targets', () => {
160
160
  function makeProjectRoot() {
161
- const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-project-'));
161
+ const root = (0, tmp_1.mkCanonicalTmpDir)('awm-mutation-targets-project-');
162
162
  fs_1.default.mkdirSync(path_1.default.join(root, '.git')); // project-root marker for findProjectRoot
163
163
  extraDirs.push(root);
164
164
  return root;
@@ -58,7 +58,7 @@ describe('runInitSteps — orchestrator', () => {
58
58
  function buildDeps(cwd) {
59
59
  const { gatherContext } = require('../../../src/core/diagnostics/context');
60
60
  const { discoverAllBundles } = require('../../../src/core/bundles');
61
- const { contentRoots, REGISTRIES_DIR } = require('../../../src/core/registries');
61
+ const { contentRoots, registriesDir } = require('../../../src/core/registries');
62
62
  const { defaultActions } = require('../../../src/core/init/steps');
63
63
  // Seed content at ~/.awm/registries/baseline (content root IS the registry root).
64
64
  const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
@@ -57,7 +57,7 @@ describe('stepCache — registry sync error results', () => {
57
57
  const ctx = {
58
58
  machine: {
59
59
  registryCache: { present: false },
60
- hook: { present: true, degraded: false },
60
+ hook: { present: true, degraded: false, applicable: true },
61
61
  devCore: { present: true, brokenLinks: [] },
62
62
  ambient: { wanted: [], installed: [] },
63
63
  contextInjection: [],
@@ -19,7 +19,7 @@ function bundle(name, scope, skills) {
19
19
  function machine() {
20
20
  return {
21
21
  registryCache: { present: true, gitState: 'clean' },
22
- hook: { present: true, degraded: false },
22
+ hook: { present: true, degraded: false, applicable: true },
23
23
  devCore: { present: true, brokenLinks: [] },
24
24
  ambient: { wanted: [], installed: [] },
25
25
  contextInjection: [],
@@ -31,6 +31,7 @@ function project(over = {}) {
31
31
  root: '/repo',
32
32
  profile: { present: true, extensions: [] },
33
33
  activeBundles: { expected: [], linked: [], broken: [] },
34
+ orphanLinks: { repairable: [], dead: [] },
34
35
  sensors: { present: true },
35
36
  constitution: { present: true },
36
37
  context: { present: true, file: 'CLAUDE.md' },
@@ -101,14 +102,14 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
101
102
  it('hook installs when absent', () => {
102
103
  const a = spies();
103
104
  const m = machine();
104
- m.hook = { present: false };
105
+ m.hook = { present: false, applicable: true };
105
106
  expect((0, steps_1.stepHook)(deps({ machine: m, project: null }, a)).action).toBe('applied');
106
107
  expect(a.installHook).toHaveBeenCalled();
107
108
  });
108
109
  it('hook reinstalls when present but degraded', () => {
109
110
  const a = spies();
110
111
  const m = machine();
111
- m.hook = { present: true, degraded: true };
112
+ m.hook = { present: true, degraded: true, applicable: true };
112
113
  expect((0, steps_1.stepHook)(deps({ machine: m, project: null }, a)).action).toBe('applied');
113
114
  expect(a.installHook).toHaveBeenCalled();
114
115
  });
@@ -123,7 +124,7 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
123
124
  it('hook skips (not calls installHook) for an agent with no hook mechanism', () => {
124
125
  const a = spies();
125
126
  const m = machine();
126
- m.hook = { present: false };
127
+ m.hook = { present: false, applicable: true };
127
128
  const r = (0, steps_1.stepHook)(deps({ machine: m, project: null }, a, { agent: 'opencode', enabledAgents: ['opencode'] }));
128
129
  expect(r.action).toBe('skipped');
129
130
  expect(a.installHook).not.toHaveBeenCalled();
@@ -533,7 +534,7 @@ describe('stepContextInjection', () => {
533
534
  it('installs Copilot context at local scope with projectRoot (no global AGENTS.md-equivalent)', () => {
534
535
  const a = spies();
535
536
  a.contextStatus.mockReturnValue('absent');
536
- const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'copilot', cwd: '/repo' }));
537
+ const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: project({ root: '/repo' }) }, a, { agent: 'copilot', cwd: '/repo' }));
537
538
  expect(r.action).toBe('applied');
538
539
  expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'copilot', scope: 'local', projectRoot: '/repo' }));
539
540
  });
@@ -550,6 +551,11 @@ describe('stepContextInjection', () => {
550
551
  expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'copilot', scope: 'local', projectRoot: '/repo' }));
551
552
  });
552
553
  it('falls back to raw cwd when no project was discovered', () => {
554
+ // Legitimate: Copilot has no channel other than a project AGENTS.md, and a
555
+ // directory without .git/package.json is still where the user is working. What
556
+ // made this a rollback hole was mutation-targets.ts enumerating that path only
557
+ // inside `if (projectRoot)` — fixed there; see
558
+ // tests/core/init/context-injection-no-project.test.ts.
553
559
  const a = spies();
554
560
  a.contextStatus.mockReturnValue('absent');
555
561
  const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'copilot', cwd: '/nowhere' }));