agentic-workflow-manager 5.0.1 → 6.0.0
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/src/commands/hooks/codex.js +13 -7
- package/dist/src/commands/hooks/index.js +26 -6
- package/dist/src/core/paths.js +20 -0
- package/dist/src/providers/index.js +39 -11
- package/dist/tests/commands/hooks/codex.test.js +21 -0
- package/dist/tests/core/context/orchestrator.test.js +5 -5
- package/dist/tests/core/context/strategies/codex-agents.test.js +3 -3
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/providers/index.test.js +5 -0
- package/dist/tests/structural/provider-paths-honor-config-home.test.js +102 -0
- package/package.json +1 -1
|
@@ -129,18 +129,24 @@ function computeCodexHookStatus(agent) {
|
|
|
129
129
|
const settingsEntry = checkCodexSettingsEntry(config.settingsPath, config.scriptsDir, config.matcher, config.eventName);
|
|
130
130
|
const allOk = sessionStartScript.ok && settingsEntry.ok;
|
|
131
131
|
const settingsOnlyMissing = !settingsEntry.ok && sessionStartScript.ok;
|
|
132
|
-
let overall;
|
|
133
|
-
if (allOk)
|
|
134
|
-
overall = 'HEALTHY';
|
|
135
|
-
else if (settingsOnlyMissing)
|
|
136
|
-
overall = 'NOT_INSTALLED';
|
|
137
|
-
else
|
|
138
|
-
overall = 'DEGRADED';
|
|
139
132
|
// Trust only makes sense once the AWM entry is actually present in
|
|
140
133
|
// hooks.json — otherwise there's nothing installed to trust.
|
|
141
134
|
const trust = settingsEntry.ok
|
|
142
135
|
? computeCodexTrust(sessionStartScript, scriptPath, heartbeatPath)
|
|
143
136
|
: undefined;
|
|
137
|
+
// `overall` IGNORABA `trust` por completo, asi que `awm hooks status` imprimia
|
|
138
|
+
// `Status: HEALTHY` dos lineas debajo de `Trust: … pending-trust`. Una corrida real
|
|
139
|
+
// del playbook leyo esa salida y concluyo que el hook andaba. `doctor --json` decia
|
|
140
|
+
// la verdad; la vista humana, no — y es la que mira una persona. Ver D-010.
|
|
141
|
+
let overall;
|
|
142
|
+
if (!allOk)
|
|
143
|
+
overall = settingsOnlyMissing ? 'NOT_INSTALLED' : 'DEGRADED';
|
|
144
|
+
else if (trust === 'stale')
|
|
145
|
+
overall = 'DEGRADED';
|
|
146
|
+
else if (trust === 'pending-trust')
|
|
147
|
+
overall = 'PENDING_TRUST';
|
|
148
|
+
else
|
|
149
|
+
overall = 'HEALTHY';
|
|
144
150
|
return {
|
|
145
151
|
overall,
|
|
146
152
|
trust,
|
|
@@ -13,6 +13,16 @@ const status_1 = require("./status");
|
|
|
13
13
|
const providers_1 = require("../../providers");
|
|
14
14
|
const registries_1 = require("../../core/registries");
|
|
15
15
|
const HOOK_TARGETS = providers_1.AGENT_TARGETS.filter((a) => (0, providers_1.getHookConfig)(a));
|
|
16
|
+
/**
|
|
17
|
+
* `hooks` nacio con `-t, --target` y el resto del CLI (`add`, `remove`, `sync`,
|
|
18
|
+
* `update`, `doctor`) usa `-a, --agent`. La asimetria se cobro una corrida real del
|
|
19
|
+
* playbook: `awm hooks status --agent codex` fallaba con `unknown option`. Se acepta
|
|
20
|
+
* `--agent` como alias en vez de renombrar, para no romper a quien ya scriptea
|
|
21
|
+
* `--target`. Ver D-010.
|
|
22
|
+
*/
|
|
23
|
+
function resolveHookTarget(options) {
|
|
24
|
+
return (options.agent ?? options.target ?? 'claude-code');
|
|
25
|
+
}
|
|
16
26
|
function targetOptionDescription() {
|
|
17
27
|
return `Target harness (${HOOK_TARGETS.join('|')})`;
|
|
18
28
|
}
|
|
@@ -21,9 +31,10 @@ function registerHooksCommand(program) {
|
|
|
21
31
|
hooks.command('install')
|
|
22
32
|
.description('Install the AWM bootstrap hook into the target harness')
|
|
23
33
|
.option('-t, --target <target>', targetOptionDescription(), 'claude-code')
|
|
34
|
+
.option('-a, --agent <agent>', 'Alias de --target, por simetria con el resto del CLI')
|
|
24
35
|
.option('-y, --yes', 'Skip interactive confirmations', false)
|
|
25
36
|
.action(async (options) => {
|
|
26
|
-
const agent = (options
|
|
37
|
+
const agent = resolveHookTarget(options);
|
|
27
38
|
const prefs = (0, config_1.getPreferences)();
|
|
28
39
|
const hooksRoot = (0, registries_1.capabilityRoot)('hooks');
|
|
29
40
|
if (!hooksRoot) {
|
|
@@ -69,9 +80,10 @@ function registerHooksCommand(program) {
|
|
|
69
80
|
hooks.command('uninstall')
|
|
70
81
|
.description('Remove the AWM bootstrap hook')
|
|
71
82
|
.option('-t, --target <target>', targetOptionDescription(), 'claude-code')
|
|
83
|
+
.option('-a, --agent <agent>', 'Alias de --target, por simetria con el resto del CLI')
|
|
72
84
|
.option('-y, --yes', 'Skip interactive confirmations', false)
|
|
73
85
|
.action(async (options) => {
|
|
74
|
-
const agent = (options
|
|
86
|
+
const agent = resolveHookTarget(options);
|
|
75
87
|
if (!options.yes && process.stdin.isTTY) {
|
|
76
88
|
const ok = await (0, prompts_1.confirm)({ message: 'Remove AWM bootstrap hook from settings.json?' });
|
|
77
89
|
if ((0, prompts_1.isCancel)(ok) || ok !== true) {
|
|
@@ -98,8 +110,9 @@ function registerHooksCommand(program) {
|
|
|
98
110
|
hooks.command('status')
|
|
99
111
|
.description('Check the bootstrap hook installation status')
|
|
100
112
|
.option('-t, --target <target>', targetOptionDescription(), 'claude-code')
|
|
113
|
+
.option('-a, --agent <agent>', 'Alias de --target, por simetria con el resto del CLI')
|
|
101
114
|
.action((options) => {
|
|
102
|
-
const agent = (options
|
|
115
|
+
const agent = resolveHookTarget(options);
|
|
103
116
|
try {
|
|
104
117
|
const result = (0, status_1.computeHookStatus)(agent);
|
|
105
118
|
const symbol = (ok) => ok ? picocolors_1.default.green('✓') : picocolors_1.default.red('✗');
|
|
@@ -118,10 +131,17 @@ function registerHooksCommand(program) {
|
|
|
118
131
|
}
|
|
119
132
|
console.log('');
|
|
120
133
|
const overall = result.overall === 'HEALTHY' ? picocolors_1.default.green(result.overall) :
|
|
121
|
-
result.overall === 'NOT_INSTALLED'
|
|
122
|
-
picocolors_1.default.
|
|
134
|
+
result.overall === 'NOT_INSTALLED' || result.overall === 'PENDING_TRUST'
|
|
135
|
+
? picocolors_1.default.yellow(result.overall)
|
|
136
|
+
: picocolors_1.default.red(result.overall);
|
|
123
137
|
console.log(` Status: ${overall}`);
|
|
124
|
-
if (result.overall
|
|
138
|
+
if (result.overall === 'PENDING_TRUST') {
|
|
139
|
+
console.log(picocolors_1.default.dim(' El hook esta instalado y bien formado, pero nunca se lo vio correr.'));
|
|
140
|
+
console.log(picocolors_1.default.dim(' Abri una sesion del agente; si sigue igual, no se esta disparando.'));
|
|
141
|
+
}
|
|
142
|
+
// PENDING_TRUST no sale 1: no hay nada roto que arreglar, y salir 1 en
|
|
143
|
+
// toda instalacion recien hecha convertiria el comando en ruido.
|
|
144
|
+
if (result.overall !== 'HEALTHY' && result.overall !== 'PENDING_TRUST') {
|
|
125
145
|
process.exit(1);
|
|
126
146
|
}
|
|
127
147
|
}
|
package/dist/src/core/paths.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.WINDOWS_KNOWN_GAP = void 0;
|
|
7
7
|
exports.homeDir = homeDir;
|
|
8
8
|
exports.awmHome = awmHome;
|
|
9
|
+
exports.providerConfigHome = providerConfigHome;
|
|
9
10
|
exports.platform = platform;
|
|
10
11
|
exports.isWindowsNative = isWindowsNative;
|
|
11
12
|
exports.platformLabel = platformLabel;
|
|
@@ -27,6 +28,25 @@ function homeDir() {
|
|
|
27
28
|
function awmHome() {
|
|
28
29
|
return process.env.AWM_HOME || path_1.default.join(homeDir(), '.awm');
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Raiz de configuracion de un agente, honrando SU propia variable de entorno.
|
|
33
|
+
*
|
|
34
|
+
* AWM honra `AWM_HOME` para si mismo (ver `awmHome` arriba) y durante mucho tiempo le
|
|
35
|
+
* nego esa misma cortesia a todos los agentes que administra: cada ruta de proveedor se
|
|
36
|
+
* armaba desde `homeDir()` y punto. En una maquina con `CODEX_HOME` apuntando a otro
|
|
37
|
+
* lado, AWM escribia el hook en `~/.codex/hooks.json` y Codex lo leia desde
|
|
38
|
+
* `$CODEX_HOME/hooks.json` — instalacion correcta, en el archivo que nadie mira. El hook
|
|
39
|
+
* quedaba instalado y mudo, y `doctor` lo reportaba presente porque miraba el mismo
|
|
40
|
+
* lugar equivocado donde lo habia escrito.
|
|
41
|
+
*
|
|
42
|
+
* `envVar` en `null` significa "este proveedor no declara override conocido": se usa el
|
|
43
|
+
* default y listo. No se inventan variables — una que no exista de verdad seria peor que
|
|
44
|
+
* ninguna, porque prometeria una configurabilidad que no funciona.
|
|
45
|
+
*/
|
|
46
|
+
function providerConfigHome(envVar, defaultDir) {
|
|
47
|
+
const override = envVar ? process.env[envVar]?.trim() : undefined;
|
|
48
|
+
return override || path_1.default.join(homeDir(), defaultDir);
|
|
49
|
+
}
|
|
30
50
|
/** Raw platform string (wrapper over process.platform for testability). */
|
|
31
51
|
function platform() {
|
|
32
52
|
return process.platform;
|
|
@@ -45,19 +45,42 @@ function unsupportedScopeError(artifactType, scope, providerLabel, reason) {
|
|
|
45
45
|
return new Error(`${artifactType} ${scope} scope is not supported by ${providerLabel}` +
|
|
46
46
|
(reason ? `: ${reason}` : '.'));
|
|
47
47
|
}
|
|
48
|
+
/** El override declarado por cada agente. Una sola tabla: agregar un proveedor o
|
|
49
|
+
* descubrir la variable de otro es una edicion de UNA linea, y el guard estructural
|
|
50
|
+
* `provider-paths-honor-config-home` verifica que TODAS sus rutas la respeten. */
|
|
51
|
+
const CONFIG_HOME = {
|
|
52
|
+
// Confirmada contra el binario real: Codex 0.146.0 lee `$CODEX_HOME/hooks.json`.
|
|
53
|
+
// Una corrida del playbook en una maquina con CODEX_HOME seteado encontro el hook
|
|
54
|
+
// instalado donde Codex no mira. Ver docs/decisions.md D-011.
|
|
55
|
+
codex: { envVar: 'CODEX_HOME', dir: '.codex' },
|
|
56
|
+
// Los demas usan su directorio por defecto. `null` es una afirmacion honesta —
|
|
57
|
+
// "no le conocemos override"— y no un "no tiene". Cuando se confirme uno contra su
|
|
58
|
+
// binario, se cambia aca y el guard obliga a que todas sus rutas lo sigan.
|
|
59
|
+
'claude-code': { envVar: null, dir: '.claude' },
|
|
60
|
+
opencode: { envVar: null, dir: '.config/opencode' },
|
|
61
|
+
cursor: { envVar: null, dir: '.cursor' },
|
|
62
|
+
copilot: { envVar: null, dir: '.github' },
|
|
63
|
+
antigravity: { envVar: null, dir: '.gemini/antigravity' },
|
|
64
|
+
};
|
|
65
|
+
function configHomeFor(agent) {
|
|
66
|
+
const declared = CONFIG_HOME[agent];
|
|
67
|
+
return { ...declared, resolved: (0, paths_1.providerConfigHome)(declared.envVar, declared.dir) };
|
|
68
|
+
}
|
|
48
69
|
function providers() {
|
|
49
70
|
const home = (0, paths_1.homeDir)();
|
|
50
71
|
const awm = (0, paths_1.awmHome)();
|
|
72
|
+
const root = (agent) => configHomeFor(agent).resolved;
|
|
51
73
|
return {
|
|
52
74
|
antigravity: {
|
|
53
75
|
label: 'Antigravity',
|
|
76
|
+
configHome: configHomeFor('antigravity'),
|
|
54
77
|
skill: {
|
|
55
|
-
global: path_1.default.join(
|
|
78
|
+
global: path_1.default.join(root('antigravity'), 'skills'),
|
|
56
79
|
local: '.agent/skills',
|
|
57
80
|
renderer: 'link',
|
|
58
81
|
},
|
|
59
82
|
workflow: {
|
|
60
|
-
global: path_1.default.join(
|
|
83
|
+
global: path_1.default.join(root('antigravity'), 'global_workflows'),
|
|
61
84
|
local: '.agent/workflows',
|
|
62
85
|
renderer: 'link',
|
|
63
86
|
},
|
|
@@ -65,6 +88,7 @@ function providers() {
|
|
|
65
88
|
},
|
|
66
89
|
opencode: {
|
|
67
90
|
label: 'OpenCode',
|
|
91
|
+
configHome: configHomeFor('opencode'),
|
|
68
92
|
skill: {
|
|
69
93
|
global: path_1.default.join(home, '.agents/skills'),
|
|
70
94
|
local: '.agents/skills',
|
|
@@ -72,32 +96,33 @@ function providers() {
|
|
|
72
96
|
},
|
|
73
97
|
workflow: null,
|
|
74
98
|
agent: {
|
|
75
|
-
global: path_1.default.join(
|
|
99
|
+
global: path_1.default.join(root('opencode'), 'agents'),
|
|
76
100
|
local: '.agents/profiles',
|
|
77
101
|
renderer: 'link',
|
|
78
102
|
},
|
|
79
103
|
injection: {
|
|
80
104
|
type: 'config-instructions',
|
|
81
|
-
configPath: path_1.default.join(
|
|
105
|
+
configPath: path_1.default.join(root('opencode'), 'opencode.json'),
|
|
82
106
|
field: 'instructions',
|
|
83
107
|
},
|
|
84
108
|
},
|
|
85
109
|
'claude-code': {
|
|
86
110
|
label: 'Claude Code',
|
|
111
|
+
configHome: configHomeFor('claude-code'),
|
|
87
112
|
skill: {
|
|
88
|
-
global: path_1.default.join(
|
|
113
|
+
global: path_1.default.join(root('claude-code'), 'skills'),
|
|
89
114
|
local: '.claude/skills',
|
|
90
115
|
renderer: 'link',
|
|
91
116
|
},
|
|
92
117
|
workflow: null,
|
|
93
118
|
agent: {
|
|
94
|
-
global: path_1.default.join(
|
|
119
|
+
global: path_1.default.join(root('claude-code'), 'agents'),
|
|
95
120
|
local: '.claude/agents',
|
|
96
121
|
renderer: 'link',
|
|
97
122
|
},
|
|
98
123
|
hooks: {
|
|
99
124
|
type: 'cc-settings-merge',
|
|
100
|
-
settingsPath: path_1.default.join(
|
|
125
|
+
settingsPath: path_1.default.join(root('claude-code'), 'settings.json'),
|
|
101
126
|
scriptsDir: path_1.default.join(awm, 'hooks'),
|
|
102
127
|
matcher: 'startup|clear|compact',
|
|
103
128
|
eventName: 'SessionStart',
|
|
@@ -106,6 +131,7 @@ function providers() {
|
|
|
106
131
|
},
|
|
107
132
|
codex: {
|
|
108
133
|
label: 'Codex',
|
|
134
|
+
configHome: configHomeFor('codex'),
|
|
109
135
|
minimumVersion: '0.145.0',
|
|
110
136
|
versionCommand: { command: 'codex', args: ['--version'], versionPattern: /^codex-cli (\d+\.\d+\.\d+)$/ },
|
|
111
137
|
skill: {
|
|
@@ -115,27 +141,28 @@ function providers() {
|
|
|
115
141
|
},
|
|
116
142
|
workflow: null,
|
|
117
143
|
agent: {
|
|
118
|
-
global: path_1.default.join(
|
|
144
|
+
global: path_1.default.join(root('codex'), 'agents'),
|
|
119
145
|
local: '.codex/agents',
|
|
120
146
|
renderer: 'codex-agent-toml',
|
|
121
147
|
},
|
|
122
148
|
hooks: {
|
|
123
149
|
type: 'codex-hooks-json',
|
|
124
|
-
settingsPath: path_1.default.join(
|
|
150
|
+
settingsPath: path_1.default.join(root('codex'), 'hooks.json'),
|
|
125
151
|
scriptsDir: path_1.default.join(awm, 'hooks/codex'),
|
|
126
152
|
matcher: 'startup|resume|clear|compact',
|
|
127
153
|
eventName: 'SessionStart',
|
|
128
154
|
},
|
|
129
155
|
injection: {
|
|
130
156
|
type: 'managed-agents-md',
|
|
131
|
-
globalPath: path_1.default.join(
|
|
157
|
+
globalPath: path_1.default.join(root('codex'), 'AGENTS.md'),
|
|
132
158
|
localFile: 'AGENTS.md',
|
|
133
159
|
},
|
|
134
160
|
},
|
|
135
161
|
cursor: {
|
|
136
162
|
label: 'Cursor',
|
|
163
|
+
configHome: configHomeFor('cursor'),
|
|
137
164
|
skill: {
|
|
138
|
-
global: path_1.default.join(
|
|
165
|
+
global: path_1.default.join(root('cursor'), 'rules'),
|
|
139
166
|
local: '.cursor/rules',
|
|
140
167
|
renderer: 'cursor-mdc',
|
|
141
168
|
},
|
|
@@ -153,6 +180,7 @@ function providers() {
|
|
|
153
180
|
},
|
|
154
181
|
copilot: {
|
|
155
182
|
label: 'Copilot',
|
|
183
|
+
configHome: configHomeFor('copilot'),
|
|
156
184
|
skill: {
|
|
157
185
|
global: null,
|
|
158
186
|
globalUnsupportedReason: 'GitHub Copilot has no user-level skill discovery mechanism — skills must be installed per-project.',
|
|
@@ -167,6 +167,27 @@ describe('installHook / computeHookStatus / uninstallHook — Codex adapter', ()
|
|
|
167
167
|
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
168
168
|
expect(computeHookStatus('codex').trust).toBe(expected); // verifies R18
|
|
169
169
|
});
|
|
170
|
+
// `overall` NUNCA se assertaba — solo `.trust` — y por eso el bug vivio: se calculaba
|
|
171
|
+
// ignorando trust por completo, asi que `awm hooks status` imprimia `Status: HEALTHY`
|
|
172
|
+
// dos lineas debajo de `Trust: … pending-trust`. Una corrida real del playbook leyo esa
|
|
173
|
+
// salida y concluyo que el hook andaba. Ver D-010.
|
|
174
|
+
it.each([
|
|
175
|
+
[false, 'PENDING_TRUST'],
|
|
176
|
+
[true, 'HEALTHY'],
|
|
177
|
+
])('overall refleja el trust, no solo la presencia de archivos (heartbeat=%s)', (heartbeat, expected) => {
|
|
178
|
+
installCodexFixture({ heartbeat: heartbeat });
|
|
179
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
180
|
+
expect(computeHookStatus('codex').overall).toBe(expected);
|
|
181
|
+
});
|
|
182
|
+
it('un heartbeat obsoleto degrada: el script cambio desde la ultima corrida confirmada', () => {
|
|
183
|
+
installCodexFixture({ heartbeat: true });
|
|
184
|
+
fs_1.default.writeFileSync(path_1.default.join(codexScriptsDir, 'session-start'), '#!/usr/bin/env bash\necho "changed"', { mode: 0o755 });
|
|
185
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
186
|
+
const status = computeHookStatus('codex');
|
|
187
|
+
expect(status.trust).toBe('stale');
|
|
188
|
+
// Esto era `HEALTHY`: la rotura mas clara de las tres, reportada en verde.
|
|
189
|
+
expect(status.overall).toBe('DEGRADED');
|
|
190
|
+
});
|
|
170
191
|
it('reports stale trust when the heartbeat hash no longer matches the installed script', () => {
|
|
171
192
|
installCodexFixture({ heartbeat: true });
|
|
172
193
|
fs_1.default.writeFileSync(path_1.default.join(codexScriptsDir, 'session-start'), '#!/usr/bin/env bash\necho "changed"', { mode: 0o755 });
|
|
@@ -26,7 +26,7 @@ function tmpRegistry() {
|
|
|
26
26
|
}
|
|
27
27
|
describe('InjectionOrchestrator (claude-code dispatch via HookMergeStrategy)', () => {
|
|
28
28
|
const ccOverride = {
|
|
29
|
-
label: 'Claude Code', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
29
|
+
label: 'Claude Code', configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
30
30
|
injection: { type: 'cc-settings-merge' },
|
|
31
31
|
hooks: { type: 'cc-settings-merge', settingsPath: '', scriptsDir: '', matcher: '', eventName: '' },
|
|
32
32
|
};
|
|
@@ -64,7 +64,7 @@ describe('InjectionOrchestrator (claude-code dispatch via HookMergeStrategy)', (
|
|
|
64
64
|
it('throws when providerOverride has no injection (does not fall through to real agent config)', () => {
|
|
65
65
|
const noInjection = {
|
|
66
66
|
label: 'Test',
|
|
67
|
-
skill: { global: '', local: '', renderer: 'link' },
|
|
67
|
+
configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '', renderer: 'link' },
|
|
68
68
|
workflow: null,
|
|
69
69
|
agent: null,
|
|
70
70
|
};
|
|
@@ -85,7 +85,7 @@ describe('InjectionOrchestrator (opencode, real strategy)', () => {
|
|
|
85
85
|
registryRoot = tmpRegistry();
|
|
86
86
|
orch = new orchestrator_1.InjectionOrchestrator({
|
|
87
87
|
providerOverride: {
|
|
88
|
-
label: 'OpenCode', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
88
|
+
label: 'OpenCode', configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
89
89
|
injection: { type: 'config-instructions', configPath, field: 'instructions' },
|
|
90
90
|
},
|
|
91
91
|
contextPathOverride: absPath,
|
|
@@ -150,7 +150,7 @@ describe('InjectionOrchestrator (codex, managed AGENTS.md strategy)', () => {
|
|
|
150
150
|
fs_1.default.writeFileSync(agentsPath, '# User rules\n');
|
|
151
151
|
const orch = new orchestrator_1.InjectionOrchestrator({
|
|
152
152
|
providerOverride: {
|
|
153
|
-
label: 'Codex', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
153
|
+
label: 'Codex', configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
154
154
|
injection: { type: 'managed-agents-md', globalPath: agentsPath, localFile: 'AGENTS.md' },
|
|
155
155
|
},
|
|
156
156
|
contextPathOverride: contextPath,
|
|
@@ -202,7 +202,7 @@ describe('InjectionOrchestrator (local scope materializes under the project, not
|
|
|
202
202
|
it('materializes to projectContextPath(projectRoot) for scope local, never to globalContextPath()', () => {
|
|
203
203
|
const orch = new orchestrator_1.InjectionOrchestrator({
|
|
204
204
|
providerOverride: {
|
|
205
|
-
label: 'Copilot', skill: { global: null, local: '.github/instructions', renderer: 'link' }, workflow: null, agent: null,
|
|
205
|
+
label: 'Copilot', configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: null, local: '.github/instructions', renderer: 'link' }, workflow: null, agent: null,
|
|
206
206
|
injection: { type: 'managed-agents-md', globalPath: null, localFile: 'AGENTS.md' },
|
|
207
207
|
},
|
|
208
208
|
});
|
|
@@ -287,7 +287,7 @@ describe('CodexAgentsStrategy', () => {
|
|
|
287
287
|
function codexProvider(globalPath) {
|
|
288
288
|
return {
|
|
289
289
|
label: 'Codex',
|
|
290
|
-
skill: { global: '', local: '', renderer: 'link' },
|
|
290
|
+
configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '', renderer: 'link' },
|
|
291
291
|
workflow: null,
|
|
292
292
|
agent: null,
|
|
293
293
|
injection: { type: 'managed-agents-md', globalPath, localFile: 'AGENTS.md' },
|
|
@@ -296,7 +296,7 @@ function codexProvider(globalPath) {
|
|
|
296
296
|
function copilotProvider() {
|
|
297
297
|
return {
|
|
298
298
|
label: 'Copilot',
|
|
299
|
-
skill: { global: null, local: '.github/instructions', renderer: 'link' },
|
|
299
|
+
configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: null, local: '.github/instructions', renderer: 'link' },
|
|
300
300
|
workflow: null,
|
|
301
301
|
agent: null,
|
|
302
302
|
injection: { type: 'managed-agents-md', globalPath: null, localFile: 'AGENTS.md' },
|
|
@@ -305,7 +305,7 @@ function copilotProvider() {
|
|
|
305
305
|
function cursorProvider() {
|
|
306
306
|
return {
|
|
307
307
|
label: 'Cursor',
|
|
308
|
-
skill: { global: '', local: '.cursor/rules', renderer: 'link' },
|
|
308
|
+
configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '.cursor/rules', renderer: 'link' },
|
|
309
309
|
workflow: null,
|
|
310
310
|
agent: null,
|
|
311
311
|
injection: { type: 'managed-agents-md', globalPath: null, localFile: 'AGENTS.md' },
|
|
@@ -18,7 +18,7 @@ function setup(opencodeJson) {
|
|
|
18
18
|
fs_1.default.mkdirSync(path_1.default.dirname(absPath), { recursive: true });
|
|
19
19
|
fs_1.default.writeFileSync(absPath, 'CTX');
|
|
20
20
|
const provider = {
|
|
21
|
-
label: 'OpenCode', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
21
|
+
label: 'OpenCode', configHome: { envVar: null, dir: '.test', resolved: '/tmp/test-config-home' }, skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
22
22
|
injection: { type: 'config-instructions', configPath, field: 'instructions' },
|
|
23
23
|
};
|
|
24
24
|
const input = {
|
|
@@ -55,6 +55,8 @@ describe('Providers Routing', () => {
|
|
|
55
55
|
it('declares the complete Codex capability metadata', () => {
|
|
56
56
|
expect((0, providers_1.providerFor)('codex')).toEqual({
|
|
57
57
|
label: 'Codex',
|
|
58
|
+
// El unico proveedor con override confirmado contra su binario real (D-011).
|
|
59
|
+
configHome: { envVar: 'CODEX_HOME', dir: '.codex', resolved: path_1.default.join(tmpHome, '.codex') },
|
|
58
60
|
minimumVersion: '0.145.0',
|
|
59
61
|
versionCommand: { command: 'codex', args: ['--version'], versionPattern: /^codex-cli (\d+\.\d+\.\d+)$/ },
|
|
60
62
|
skill: {
|
|
@@ -86,6 +88,7 @@ describe('Providers Routing', () => {
|
|
|
86
88
|
const graph = (0, providers_1.providers)();
|
|
87
89
|
expect(graph.antigravity).toEqual({
|
|
88
90
|
label: 'Antigravity',
|
|
91
|
+
configHome: { envVar: null, dir: '.gemini/antigravity', resolved: path_1.default.join(tmpHome, '.gemini/antigravity') },
|
|
89
92
|
skill: {
|
|
90
93
|
global: path_1.default.join(tmpHome, '.gemini/antigravity/skills'),
|
|
91
94
|
local: '.agent/skills',
|
|
@@ -100,6 +103,7 @@ describe('Providers Routing', () => {
|
|
|
100
103
|
});
|
|
101
104
|
expect(graph.opencode).toEqual({
|
|
102
105
|
label: 'OpenCode',
|
|
106
|
+
configHome: { envVar: null, dir: '.config/opencode', resolved: path_1.default.join(tmpHome, '.config/opencode') },
|
|
103
107
|
skill: {
|
|
104
108
|
global: path_1.default.join(tmpHome, '.agents/skills'),
|
|
105
109
|
local: '.agents/skills',
|
|
@@ -119,6 +123,7 @@ describe('Providers Routing', () => {
|
|
|
119
123
|
});
|
|
120
124
|
expect(graph['claude-code']).toEqual({
|
|
121
125
|
label: 'Claude Code',
|
|
126
|
+
configHome: { envVar: null, dir: '.claude', resolved: path_1.default.join(tmpHome, '.claude') },
|
|
122
127
|
skill: {
|
|
123
128
|
global: path_1.default.join(tmpHome, '.claude/skills'),
|
|
124
129
|
local: '.claude/skills',
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
// Un proveedor que declara override de home tiene que respetarlo en TODAS sus rutas.
|
|
7
|
+
//
|
|
8
|
+
// El caso que lo motivo: en una maquina con `CODEX_HOME` apuntando fuera del home, AWM
|
|
9
|
+
// instalaba el hook en `~/.codex/hooks.json` mientras Codex leia
|
|
10
|
+
// `$CODEX_HOME/hooks.json`. Instalacion correcta, en el archivo que nadie mira — el hook
|
|
11
|
+
// quedaba mudo, y `doctor` lo reportaba presente porque miraba el mismo lugar equivocado
|
|
12
|
+
// donde lo habia escrito. Tres rutas afectadas (`hooks.json`, `agents/`, `AGENTS.md`), y
|
|
13
|
+
// arreglar una o dos habria dejado el bug vivo en la tercera.
|
|
14
|
+
//
|
|
15
|
+
// Por eso el guard es una PROPIEDAD sobre la tabla entera y no una lista de rutas: no se
|
|
16
|
+
// puede satisfacer arreglando un sitio, y un proveedor nuevo lo hereda sin que nadie se
|
|
17
|
+
// acuerde de agregarlo. Ver docs/decisions.md D-011.
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const providers_1 = require("../../src/providers");
|
|
20
|
+
/** Toda ruta absoluta que la config de un proveedor declara, con su nombre de campo. */
|
|
21
|
+
function absolutePaths(config) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const push = (field, value) => {
|
|
24
|
+
if (typeof value === 'string' && path_1.default.isAbsolute(value))
|
|
25
|
+
out.push({ field, value });
|
|
26
|
+
};
|
|
27
|
+
push('skill.global', config.skill.global);
|
|
28
|
+
push('workflow.global', config.workflow?.global);
|
|
29
|
+
push('agent.global', config.agent?.global);
|
|
30
|
+
push('hooks.settingsPath', config.hooks?.settingsPath);
|
|
31
|
+
if (config.injection?.type === 'config-instructions')
|
|
32
|
+
push('injection.configPath', config.injection.configPath);
|
|
33
|
+
if (config.injection?.type === 'managed-agents-md')
|
|
34
|
+
push('injection.globalPath', config.injection.globalPath);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** Vuelve a pedir la tabla con `env` aplicado. `providers()` la reconstruye en cada
|
|
38
|
+
* llamada leyendo el entorno, asi que no hace falta resetear modulos. */
|
|
39
|
+
function providersWith(env) {
|
|
40
|
+
const previous = {};
|
|
41
|
+
for (const [k, v] of Object.entries(env)) {
|
|
42
|
+
previous[k] = process.env[k];
|
|
43
|
+
if (v === undefined)
|
|
44
|
+
delete process.env[k];
|
|
45
|
+
else
|
|
46
|
+
process.env[k] = v;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return (0, providers_1.providers)();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
for (const [k, v] of Object.entries(previous)) {
|
|
53
|
+
if (v === undefined)
|
|
54
|
+
delete process.env[k];
|
|
55
|
+
else
|
|
56
|
+
process.env[k] = v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const HOME = path_1.default.join(path_1.default.sep, 'tmp', 'awm-guard-home');
|
|
61
|
+
const ELSEWHERE = path_1.default.join(path_1.default.sep, 'tmp', 'awm-guard-elsewhere');
|
|
62
|
+
describe('a provider that declares a config-home override honors it everywhere', () => {
|
|
63
|
+
for (const agent of providers_1.AGENT_TARGETS) {
|
|
64
|
+
const declared = providersWith({ HOME })[agent].configHome;
|
|
65
|
+
if (!declared.envVar)
|
|
66
|
+
continue;
|
|
67
|
+
it(`${agent}: setting ${declared.envVar} moves every path that lives under its own dir`, () => {
|
|
68
|
+
const base = providersWith({ HOME, [declared.envVar]: undefined })[agent];
|
|
69
|
+
const moved = providersWith({ HOME, [declared.envVar]: ELSEWHERE })[agent];
|
|
70
|
+
const defaultRoot = path_1.default.join(HOME, declared.dir);
|
|
71
|
+
const under = absolutePaths(base).filter((p) => p.value.startsWith(defaultRoot));
|
|
72
|
+
// Si esto diera 0, el test pasaria sin verificar nada — y ese es exactamente
|
|
73
|
+
// el modo de falla que hace inutil a un guard.
|
|
74
|
+
expect(under.length).toBeGreaterThan(0);
|
|
75
|
+
const movedByField = new Map(absolutePaths(moved).map((p) => [p.field, p.value]));
|
|
76
|
+
const stragglers = under.filter((p) => !movedByField.get(p.field)?.startsWith(ELSEWHERE));
|
|
77
|
+
expect(stragglers.map((p) => `${p.field} = ${movedByField.get(p.field)}`)).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
it(`${agent}: an empty or blank ${declared.envVar} falls back to the default, it does not resolve to nothing`, () => {
|
|
80
|
+
// Una variable exportada vacia es comun en scripts (`export CODEX_HOME=`) y
|
|
81
|
+
// tomarla al pie de la letra dejaria las rutas colgando de la raiz.
|
|
82
|
+
for (const blank of ['', ' ']) {
|
|
83
|
+
const cfg = providersWith({ HOME, [declared.envVar]: blank })[agent];
|
|
84
|
+
expect(cfg.configHome.resolved).toBe(path_1.default.join(HOME, declared.dir));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
it('shared, cross-agent conventions are NOT moved by one agent’s override', () => {
|
|
89
|
+
// `~/.agents/skills` lo comparten Codex y OpenCode: es del ecosistema, no de un
|
|
90
|
+
// agente. Moverlo con CODEX_HOME desconectaria a OpenCode de sus propias skills.
|
|
91
|
+
const moved = providersWith({ HOME, CODEX_HOME: ELSEWHERE });
|
|
92
|
+
expect(moved.codex.skill.global).toBe(path_1.default.join(HOME, '.agents/skills'));
|
|
93
|
+
expect(moved.opencode.skill.global).toBe(path_1.default.join(HOME, '.agents/skills'));
|
|
94
|
+
});
|
|
95
|
+
it('every agent declares a config home, so a new provider cannot skip the question', () => {
|
|
96
|
+
const table = providersWith({ HOME });
|
|
97
|
+
for (const agent of providers_1.AGENT_TARGETS) {
|
|
98
|
+
expect(typeof table[agent].configHome.dir).toBe('string');
|
|
99
|
+
expect(table[agent].configHome.dir.length).toBeGreaterThan(0);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|