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.
- package/dist/scripts/support-matrix.js +138 -0
- package/dist/src/commands/add.js +5 -0
- package/dist/src/commands/doctor.js +1 -0
- package/dist/src/commands/hooks/claude.js +15 -2
- package/dist/src/commands/hooks/shared.js +6 -1
- package/dist/src/commands/init.js +1 -0
- package/dist/src/commands/pin.js +24 -4
- package/dist/src/commands/preflight/checks.js +3 -2
- package/dist/src/commands/registry/add.js +1 -1
- package/dist/src/commands/sensors/index.js +7 -0
- package/dist/src/commands/sensors/init.js +48 -7
- package/dist/src/commands/sensors/install.js +20 -11
- package/dist/src/commands/sensors/run.js +31 -29
- package/dist/src/commands/sync.js +29 -0
- package/dist/src/commands/watch/index.js +11 -1
- package/dist/src/core/artifact-name.js +80 -0
- package/dist/src/core/bundle-install.js +55 -12
- package/dist/src/core/bundles.js +72 -13
- package/dist/src/core/context/regenerate.js +21 -4
- package/dist/src/core/context/strategies/codex-agents.js +18 -2
- package/dist/src/core/diagnostics/checks.js +26 -2
- package/dist/src/core/diagnostics/context.js +91 -25
- package/dist/src/core/diagnostics/provider-checks.js +62 -14
- package/dist/src/core/discovery.js +2 -2
- package/dist/src/core/init/mutation-targets.js +34 -1
- package/dist/src/core/init/steps.js +70 -19
- package/dist/src/core/install-planner.js +62 -17
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/adapter.js +10 -1
- package/dist/src/core/paths.js +61 -8
- package/dist/src/core/profile.js +12 -0
- package/dist/src/core/provider-artifacts.js +38 -13
- package/dist/src/core/provider-version.js +12 -6
- package/dist/src/core/registries.js +40 -19
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/skill-integrity.js +71 -10
- package/dist/src/core/update-check.js +14 -2
- package/dist/src/core/versioning.js +17 -2
- package/dist/src/index.js +31 -23
- package/dist/src/providers/index.js +7 -2
- package/dist/src/utils/config.js +7 -8
- package/dist/tests/commands/doctor.test.js +6 -0
- package/dist/tests/commands/pin.test.js +29 -2
- package/dist/tests/commands/preflight/preflight.test.js +31 -17
- package/dist/tests/commands/registry/install-bundles.test.js +6 -6
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
- package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +24 -51
- package/dist/tests/commands/sensors/status-windows.test.js +39 -22
- package/dist/tests/commands/sensors/status.test.js +27 -12
- package/dist/tests/commands/watch/runner.test.js +10 -0
- package/dist/tests/core/artifact-name-containment.test.js +86 -0
- package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
- package/dist/tests/core/context/regenerate.test.js +39 -8
- package/dist/tests/core/diagnostics/checks.test.js +14 -6
- package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
- package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
- package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
- package/dist/tests/core/init/mutation-targets-canonical.test.js +81 -0
- package/dist/tests/core/init/mutation-targets.test.js +4 -4
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +11 -5
- package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
- package/dist/tests/core/paths.test.js +7 -34
- package/dist/tests/core/project-skill-links.test.js +159 -0
- package/dist/tests/core/provider-artifacts.test.js +66 -14
- package/dist/tests/core/provider-capability-guards.test.js +92 -0
- package/dist/tests/core/reconciliation.test.js +15 -15
- package/dist/tests/core/registry-manifest.test.js +1 -1
- package/dist/tests/core/semver-fails-closed.test.js +30 -0
- package/dist/tests/core/skill-integrity.test.js +10 -10
- package/dist/tests/core/skill-repair-safety.test.js +92 -0
- package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
- package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
- package/dist/tests/providers/index.test.js +1 -1
- package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +100 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/support/tmp.js +31 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- 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
|
|
198
|
-
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust'
|
|
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
|
-
// `
|
|
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
|
-
//
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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:
|
|
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' }));
|