agentic-workflow-manager 3.0.1 → 3.2.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.
Files changed (103) hide show
  1. package/dist/src/commands/add.js +84 -0
  2. package/dist/src/commands/agent.js +95 -0
  3. package/dist/src/commands/backup.js +46 -0
  4. package/dist/src/commands/doctor.js +80 -4
  5. package/dist/src/commands/export.js +48 -0
  6. package/dist/src/commands/hooks/claude.js +192 -0
  7. package/dist/src/commands/hooks/codex.js +191 -0
  8. package/dist/src/commands/hooks/index.js +23 -8
  9. package/dist/src/commands/hooks/install.js +11 -107
  10. package/dist/src/commands/hooks/resync.js +28 -18
  11. package/dist/src/commands/hooks/shared.js +94 -0
  12. package/dist/src/commands/hooks/status.js +13 -64
  13. package/dist/src/commands/hooks/uninstall.js +11 -47
  14. package/dist/src/commands/init.js +90 -28
  15. package/dist/src/commands/registry/index.js +1 -1
  16. package/dist/src/commands/sync.js +113 -0
  17. package/dist/src/commands/update.js +107 -0
  18. package/dist/src/core/agent-targets.js +50 -0
  19. package/dist/src/core/artifact-state.js +65 -0
  20. package/dist/src/core/atomic-file.js +72 -0
  21. package/dist/src/core/bundle-install.js +60 -38
  22. package/dist/src/core/bundles.js +21 -0
  23. package/dist/src/core/context/managed-block.js +190 -0
  24. package/dist/src/core/context/orchestrator.js +3 -1
  25. package/dist/src/core/context/project-constitution-inject.js +1 -1
  26. package/dist/src/core/context/regenerate.js +3 -3
  27. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  28. package/dist/src/core/diagnostics/checks.js +16 -0
  29. package/dist/src/core/diagnostics/context.js +37 -8
  30. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  31. package/dist/src/core/discovery.js +9 -3
  32. package/dist/src/core/executor.js +30 -9
  33. package/dist/src/core/export/index.js +51 -0
  34. package/dist/src/core/export/pack.js +58 -0
  35. package/dist/src/core/export/resolve.js +77 -0
  36. package/dist/src/core/export/transform.js +57 -0
  37. package/dist/src/core/export/types.js +2 -0
  38. package/dist/src/core/init/mutation-targets.js +139 -0
  39. package/dist/src/core/init/provider-facts.js +164 -0
  40. package/dist/src/core/init/steps.js +65 -9
  41. package/dist/src/core/install-planner.js +206 -0
  42. package/dist/src/core/install-transaction.js +405 -0
  43. package/dist/src/core/profile.js +1 -1
  44. package/dist/src/core/provider-artifacts.js +54 -0
  45. package/dist/src/core/provider-version.js +36 -0
  46. package/dist/src/core/reconciliation.js +61 -0
  47. package/dist/src/core/registries.js +10 -0
  48. package/dist/src/core/renderers/canonical-agent.js +27 -0
  49. package/dist/src/core/renderers/codex-agent.js +60 -0
  50. package/dist/src/core/skill-integrity.js +4 -3
  51. package/dist/src/index.js +99 -289
  52. package/dist/src/providers/index.js +143 -39
  53. package/dist/src/ui/provider-preflight.js +26 -0
  54. package/dist/src/utils/config.js +101 -9
  55. package/dist/tests/commands/agent.test.js +103 -0
  56. package/dist/tests/commands/backup.test.js +109 -0
  57. package/dist/tests/commands/doctor.test.js +65 -4
  58. package/dist/tests/commands/export.test.js +64 -0
  59. package/dist/tests/commands/hooks/codex.test.js +230 -0
  60. package/dist/tests/commands/hooks/install.test.js +20 -1
  61. package/dist/tests/commands/hooks/resync.test.js +53 -6
  62. package/dist/tests/commands/hooks/status.test.js +10 -0
  63. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  64. package/dist/tests/commands/init.test.js +195 -8
  65. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  66. package/dist/tests/core/agent-targets.test.js +45 -0
  67. package/dist/tests/core/artifact-state.test.js +144 -0
  68. package/dist/tests/core/atomic-file.test.js +96 -0
  69. package/dist/tests/core/bundle-install.test.js +208 -17
  70. package/dist/tests/core/bundles.test.js +12 -0
  71. package/dist/tests/core/context/managed-block.test.js +90 -0
  72. package/dist/tests/core/context/orchestrator.test.js +48 -3
  73. package/dist/tests/core/context/regenerate.test.js +1 -1
  74. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  75. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  76. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  77. package/dist/tests/core/diagnostics/context.test.js +74 -0
  78. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  79. package/dist/tests/core/executor.test.js +20 -0
  80. package/dist/tests/core/export/engine.test.js +104 -0
  81. package/dist/tests/core/export/pack.test.js +122 -0
  82. package/dist/tests/core/export/resolve.test.js +78 -0
  83. package/dist/tests/core/export/transform.test.js +74 -0
  84. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  85. package/dist/tests/core/init/orchestrator.test.js +1 -1
  86. package/dist/tests/core/init/provider-facts.test.js +129 -0
  87. package/dist/tests/core/init/steps.test.js +106 -2
  88. package/dist/tests/core/install-planner.test.js +224 -0
  89. package/dist/tests/core/install-transaction.test.js +257 -0
  90. package/dist/tests/core/provider-artifacts.test.js +64 -0
  91. package/dist/tests/core/provider-version.test.js +53 -0
  92. package/dist/tests/core/reconciliation.test.js +165 -0
  93. package/dist/tests/core/registries-sync.test.js +4 -4
  94. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  95. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  96. package/dist/tests/core/versioning.test.js +1 -1
  97. package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
  98. package/dist/tests/providers/hooks-config.test.js +43 -25
  99. package/dist/tests/providers/index.test.js +144 -39
  100. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  101. package/dist/tests/ui/provider-preflight.test.js +25 -0
  102. package/dist/tests/utils/config.test.js +178 -18
  103. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const codex_agent_1 = require("../../../src/core/renderers/codex-agent");
4
+ const canonical = `---
5
+ name: development-process
6
+ description: Orchestrates the development lifecycle
7
+ mode: primary
8
+ ---
9
+
10
+ # Development Process
11
+
12
+ Invoke the \`development-process\` skill before implementation.
13
+ `;
14
+ it('renders deterministic native Codex TOML', () => {
15
+ expect((0, codex_agent_1.renderCodexAgent)(canonical)).toBe(`name = "development-process"
16
+ description = "Orchestrates the development lifecycle"
17
+ developer_instructions = """
18
+ # Development Process
19
+
20
+ Invoke the \`development-process\` skill before implementation.
21
+ """
22
+ `); // verifies R8, R9
23
+ });
24
+ it.each([
25
+ ['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
26
+ ['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
27
+ ['---\nname: ok\ndescription: x\n---\n', 'non-empty instruction body'],
28
+ ['name: ok', 'frontmatter'],
29
+ ])('rejects invalid canonical agents before rendering', (source, message) => {
30
+ expect(() => (0, codex_agent_1.renderCodexAgent)(source)).toThrow(message); // verifies R17
31
+ });
32
+ it('escapes an embedded backslash and runs of double quotes of varying length in the instructions body', () => {
33
+ const instructionsBody = [
34
+ 'Backslash: \\',
35
+ 'One quote: "',
36
+ 'Three quotes: """',
37
+ 'Four quotes: """"',
38
+ 'Five quotes: """""',
39
+ 'Seven quotes: """""""',
40
+ 'Eight quotes: """"""""',
41
+ ].join('\n');
42
+ const source = `---
43
+ name: quote-agent
44
+ description: Handles many quotes
45
+ ---
46
+
47
+ ${instructionsBody}
48
+ `;
49
+ expect((0, codex_agent_1.renderCodexAgent)(source)).toBe(`name = "quote-agent"
50
+ description = "Handles many quotes"
51
+ developer_instructions = """
52
+ Backslash: \\\\
53
+ One quote: \\"
54
+ Three quotes: \\"\\"\\"
55
+ Four quotes: \\"\\"\\"\\"
56
+ Five quotes: \\"\\"\\"\\"\\"
57
+ Seven quotes: \\"\\"\\"\\"\\"\\"\\"
58
+ Eight quotes: \\"\\"\\"\\"\\"\\"\\"\\"
59
+ """
60
+ `); // verifies R8, R9 (guards against greedy triple-quote-run escaping producing invalid TOML)
61
+ });
62
+ it('escapes U+007F (DEL) in single-line TOML string fields', () => {
63
+ const source = `---
64
+ name: ok
65
+ description: hasdel
66
+ ---
67
+
68
+ Body text.
69
+ `;
70
+ expect((0, codex_agent_1.renderCodexAgent)(source)).toBe(`name = "ok"
71
+ description = "has\\u007fdel"
72
+ developer_instructions = """
73
+ Body text.
74
+ """
75
+ `); // verifies R8, R9
76
+ });
77
+ it('escapes U+007F (DEL) embedded in the multiline instructions body', () => {
78
+ const del = String.fromCharCode(0x7f);
79
+ const source = `---
80
+ name: ok
81
+ description: fine
82
+ ---
83
+
84
+ Body${del}text.
85
+ `;
86
+ expect((0, codex_agent_1.renderCodexAgent)(source)).toBe(`name = "ok"
87
+ description = "fine"
88
+ developer_instructions = """
89
+ Body\\u007ftext.
90
+ """
91
+ `); // verifies R8, R9
92
+ });
93
+ it('escapes other C0 control characters (e.g. NUL, U+000B) embedded in the multiline instructions body while leaving tab and newline raw', () => {
94
+ const nul = String.fromCharCode(0x00);
95
+ const vt = String.fromCharCode(0x0b);
96
+ const unitSep = String.fromCharCode(0x1f);
97
+ const source = `---
98
+ name: ok
99
+ description: fine
100
+ ---
101
+
102
+ Body${nul}with${vt}control${unitSep}chars\tand\na newline.
103
+ `;
104
+ expect((0, codex_agent_1.renderCodexAgent)(source)).toBe(`name = "ok"
105
+ description = "fine"
106
+ developer_instructions = """
107
+ Body\\u0000with\\u000bcontrol\\u001fchars\tand
108
+ a newline.
109
+ """
110
+ `); // verifies R8, R9
111
+ });
112
+ it('escapes other C0 control characters (e.g. NUL, U+000B) in single-line TOML string fields', () => {
113
+ const nul = String.fromCharCode(0x00);
114
+ const vt = String.fromCharCode(0x0b);
115
+ const source = `---
116
+ name: ok
117
+ description: has${nul}nul${vt}vt
118
+ ---
119
+
120
+ Body text.
121
+ `;
122
+ expect((0, codex_agent_1.renderCodexAgent)(source)).toBe(`name = "ok"
123
+ description = "has\\u0000nul\\u000bvt"
124
+ developer_instructions = """
125
+ Body text.
126
+ """
127
+ `); // verifies R8, R9
128
+ });
@@ -152,7 +152,7 @@ describe('versioning core', () => {
152
152
  it('lee channel dev y pin por nombre desde preferences', () => {
153
153
  const awmDir = path_1.default.join(tmpHome, '.awm');
154
154
  fs_1.default.mkdirSync(awmDir, { recursive: true });
155
- fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', channel: 'dev', pins: { base: '1.2.0', equipo: '0.3.0' } }));
155
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', channel: 'dev', pins: { base: '1.2.0', equipo: '0.3.0' } }));
156
156
  const { machineVersionOpts } = require('../../src/core/versioning');
157
157
  expect(machineVersionOpts('base')).toEqual({ pin: '1.2.0', channel: 'dev' });
158
158
  expect(machineVersionOpts('equipo')).toEqual({ pin: '0.3.0', channel: 'dev' });
@@ -0,0 +1,206 @@
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
+ // cli/tests/integration/codex-provider-isolated.test.ts
7
+ //
8
+ // Task 9, Step 5 — a genuinely-real, isolated-home E2E: unlike the rest of the
9
+ // suite (which stubs installBundle/installHook/etc. to observe call shape),
10
+ // this test lets `awm init --agent codex` run its REAL pipeline end-to-end
11
+ // (real symlinks, real Codex TOML rendering, real preferences writes) against
12
+ // a hand-seeded registry fixture, then asserts on the actual resulting
13
+ // filesystem — never against the real `~/.awm` (CLAUDE.md's "never touch
14
+ // ~/.awm" rule; HOME/AWM_HOME are isolated tmpdirs for the whole test, same
15
+ // pattern as tests/commands/hooks/install.test.ts).
16
+ //
17
+ // Matches the plan's illustrative Task 9 Step 5 snippet: OpenCode is enabled
18
+ // (and really initialized) alongside Claude Code BEFORE the real
19
+ // `awm init --agent codex` run, exactly the "OpenCode already enabled, then
20
+ // Codex joins" scenario this whole plan exists to support. This used to trip
21
+ // install-planner.ts's `assertCompleteSharedGroup` (R14, Task 5) — OpenCode
22
+ // and Codex share the exact same physical skills directory (~/.agents/skills,
23
+ // providers/index.ts) — because `core/init/steps.ts`'s `stepDevCore`/
24
+ // `stepAmbient` passed a `[agent]` singleton as `selectedAgents`, which R14
25
+ // refuses whenever a co-owner of the shared target is independently enabled.
26
+ // That was a confirmed BLOCKER (found in post-implementation QA): it made
27
+ // `awm init --agent codex` structurally fail whenever OpenCode was already
28
+ // enabled, and vice versa. Fixed by having `stepDevCore`/`stepAmbient`
29
+ // compute the complete shared-skill-target group among currently-enabled
30
+ // agents (`install-planner.ts`'s `agentsSharingSkillTarget`, used via
31
+ // `steps.ts`'s `sharedInstallAgents`) instead of a singleton — see
32
+ // tests/core/init/steps.test.ts for the unit-level coverage of that
33
+ // computation. This test proves the real end-to-end flow now succeeds.
34
+ //
35
+ // While building this fixture, this test also caught and fixed a real,
36
+ // previously-untested bug: `runInit` called `assertClaudeBaselinePreserved`
37
+ // unconditionally, even when the run's OWN target was claude-code — meaning
38
+ // any genuinely successful `awm init` (no --agent, the CLI's default/most
39
+ // common path) with real registry content would falsely throw "Claude Code
40
+ // baseline changed during a non-Claude init" and exit 2. See
41
+ // src/commands/init.ts's `beforeClaudeFacts` guard.
42
+ const fs_1 = __importDefault(require("fs"));
43
+ const os_1 = __importDefault(require("os"));
44
+ const path_1 = __importDefault(require("path"));
45
+ describe('codex provider — isolated home E2E (Task 9)', () => {
46
+ let tmpHome;
47
+ let tmpWork;
48
+ let originalHome;
49
+ let originalAwmHome;
50
+ let writeSpy;
51
+ function stdout() {
52
+ return writeSpy.mock.calls.map((c) => c[0]).join('');
53
+ }
54
+ beforeEach(() => {
55
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-home-'));
56
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-work-'));
57
+ originalHome = process.env.HOME;
58
+ originalAwmHome = process.env.AWM_HOME;
59
+ process.env.HOME = tmpHome;
60
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
61
+ jest.resetModules();
62
+ // Every runInit/runDoctor call in this suite writes a full text report to
63
+ // stdout — mock it so the real test suite's own output stays readable, and
64
+ // recover it via `stdout()` where a test needs to inspect the JSON payload.
65
+ writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
66
+ });
67
+ afterEach(() => {
68
+ writeSpy.mockRestore();
69
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
70
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
71
+ if (originalHome === undefined)
72
+ delete process.env.HOME;
73
+ else
74
+ process.env.HOME = originalHome;
75
+ if (originalAwmHome === undefined)
76
+ delete process.env.AWM_HOME;
77
+ else
78
+ process.env.AWM_HOME = originalAwmHome;
79
+ });
80
+ /** Hand-seeds a registry content root with everything a real `awm init` needs:
81
+ * hooks (Claude + Codex), the bootstrap skill, one baseline bundle providing
82
+ * both a skill and a Codex-native agent artifact. No git repo involved — the
83
+ * registry cache is content-root-shaped, exactly what a real clone produces. */
84
+ function seedPublicRegistryFixture(root) {
85
+ fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
86
+ fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
87
+ fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/run-hook.cmd'), '#!/bin/sh\nexec sh "$1"\n', { mode: 0o755 });
88
+ fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/codex-session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
89
+ fs_1.default.mkdirSync(path_1.default.join(root, 'skills/using-awm'), { recursive: true });
90
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.');
91
+ fs_1.default.mkdirSync(path_1.default.join(root, 'skills/development-process'), { recursive: true });
92
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\n---\nOrchestrates the dev lifecycle.');
93
+ fs_1.default.mkdirSync(path_1.default.join(root, 'agents'), { recursive: true });
94
+ fs_1.default.writeFileSync(path_1.default.join(root, 'agents/development-process.md'), '---\nname: development-process\ndescription: Orchestrates the dev lifecycle.\n---\nFollow the AWM development process end to end.');
95
+ fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
96
+ version: 1,
97
+ bundles: [{ name: 'dev-core', source: 'bundles/dev-core', version: '1.0.0', scope: 'baseline', visibility: 'public' }],
98
+ }));
99
+ fs_1.default.mkdirSync(path_1.default.join(root, 'bundles/dev-core'), { recursive: true });
100
+ fs_1.default.writeFileSync(path_1.default.join(root, 'bundles/dev-core/bundle.json'), JSON.stringify({
101
+ name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
102
+ dependsOn: [], skills: ['development-process'], workflows: [], agents: ['development-process'],
103
+ }));
104
+ fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
105
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
106
+ }
107
+ function writePrefs(prefs) {
108
+ const { savePreferences } = require('../../src/utils/config');
109
+ savePreferences(prefs);
110
+ }
111
+ function prefsWith(enabledAgents) {
112
+ return { defaultAgent: enabledAgents[0], enabledAgents, installMethod: 'symlink', defaultScope: 'local' };
113
+ }
114
+ function readPrefs() {
115
+ return JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/preferences.json'), 'utf8'));
116
+ }
117
+ /** Cheap recursive content snapshot for "did this subtree change at all" assertions
118
+ * (same shape as tests/commands/init.test.ts's snapshotTree). */
119
+ function snapshotTree(dir) {
120
+ if (!fs_1.default.existsSync(dir))
121
+ return null;
122
+ const walk = (p) => {
123
+ const st = fs_1.default.lstatSync(p);
124
+ if (st.isSymbolicLink())
125
+ return { type: 'symlink', target: fs_1.default.readlinkSync(p) };
126
+ if (st.isDirectory()) {
127
+ const entries = fs_1.default.readdirSync(p).sort();
128
+ return { type: 'dir', entries: Object.fromEntries(entries.map((e) => [e, walk(path_1.default.join(p, e))])) };
129
+ }
130
+ return { type: 'file', content: fs_1.default.readFileSync(p, 'utf8') };
131
+ };
132
+ return walk(dir);
133
+ }
134
+ it('initializes Codex beside a real Claude Code + OpenCode install without touching Claude files', async () => {
135
+ expect(process.env.HOME).toBe(tmpHome);
136
+ expect(process.env.AWM_HOME).toBe(path_1.default.join(tmpHome, '.awm'));
137
+ seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
138
+ const { runInit } = require('../../src/commands/init');
139
+ // Real, full, unstubbed init for claude-code — installs the real hook,
140
+ // real global skill symlink and real global agent symlink.
141
+ const claudeCode = await runInit({ cwd: tmpWork, yes: true });
142
+ expect(claudeCode).toBeLessThanOrEqual(1);
143
+ expect(fs_1.default.lstatSync(path_1.default.join(tmpHome, '.claude/skills/development-process')).isSymbolicLink()).toBe(true);
144
+ expect(readPrefs().enabledAgents).toEqual(['claude-code']);
145
+ // Real, full, unstubbed init for opencode — enables it alongside Claude
146
+ // Code and materializes its real ~/.agents/skills symlink, the exact
147
+ // physical directory Codex shares (providers/index.ts). This is the
148
+ // scenario the plan's own Task 9 Step 5 snippet illustrates ("OpenCode
149
+ // already enabled, then Codex joins") and the one the BLOCKER fixed in
150
+ // core/init/steps.ts (sharedInstallAgents) makes work end-to-end.
151
+ const opencode = await runInit({ cwd: tmpWork, yes: true, agent: 'opencode' });
152
+ expect(opencode).toBeLessThanOrEqual(1);
153
+ expect(readPrefs().enabledAgents).toEqual(['claude-code', 'opencode']);
154
+ const opencodeSkillLinkBefore = fs_1.default.realpathSync(path_1.default.join(tmpHome, '.agents/skills/development-process'));
155
+ const claudeBefore = snapshotTree(path_1.default.join(tmpHome, '.claude'));
156
+ // Real, full, unstubbed init for codex — real Codex hook, real shared
157
+ // skill symlink (co-owned with OpenCode), real Codex-native TOML agent
158
+ // render. Before the fix, this call threw: "Shared skill target cannot
159
+ // diverge; select the complete shared target group: opencode,codex".
160
+ const codex = await runInit({
161
+ cwd: tmpWork,
162
+ yes: true,
163
+ agent: 'codex',
164
+ // Real `codex --version` execution is unreliable inside this sandboxed
165
+ // test runner (subprocess exec of freshly-written fixture binaries is
166
+ // blocked even with PATH correctly set — verified directly; see task
167
+ // notes). Every other real-pipeline test in the suite (init.test.ts's
168
+ // `codexInitOptions`) uses this exact seam for the same reason — R2's
169
+ // gate itself is unit-tested separately in provider-version.test.ts.
170
+ assertProviderSupported: () => ({ provider: 'codex', version: '0.150.0' }),
171
+ });
172
+ expect(codex).toBeLessThanOrEqual(1);
173
+ expect(readPrefs().enabledAgents).toEqual(['claude-code', 'opencode', 'codex']);
174
+ // R1/R7: Codex's global skill dir (~/.agents/skills, shared by design with
175
+ // OpenCode — providers/index.ts) got a real symlink into the registry —
176
+ // the SAME physical link OpenCode's own init already produced, now
177
+ // co-owned by both agents (R15/R15.1) rather than fought over.
178
+ const skillLink = path_1.default.join(tmpHome, '.agents/skills/development-process');
179
+ expect(fs_1.default.lstatSync(skillLink).isSymbolicLink()).toBe(true);
180
+ expect(fs_1.default.realpathSync(skillLink)).toContain(path_1.default.join('.awm/registries/baseline/skills/development-process'));
181
+ // OpenCode's own skill link is untouched — same physical target, same source.
182
+ expect(fs_1.default.realpathSync(skillLink)).toBe(opencodeSkillLinkBefore);
183
+ // R8: the canonical agent got rendered into Codex's native .toml shape.
184
+ const tomlPath = path_1.default.join(tmpHome, '.codex/agents/development-process.toml');
185
+ const toml = fs_1.default.readFileSync(tomlPath, 'utf8');
186
+ expect(toml).toContain('developer_instructions = """');
187
+ expect(toml).toContain('name = "development-process"');
188
+ // R18: the Codex hook got installed for real.
189
+ const hooksJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.codex/hooks.json'), 'utf8'));
190
+ expect(hooksJson.hooks.SessionStart).toHaveLength(1);
191
+ // R19/R19.1/R23: nothing under Claude's baseline moved during the Codex run.
192
+ expect(snapshotTree(path_1.default.join(tmpHome, '.claude'))).toEqual(claudeBefore);
193
+ });
194
+ it('doctor reports the isolated Codex install as supported/healthy, not against the real ~/.awm', () => {
195
+ seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
196
+ writePrefs(prefsWith(['claude-code', 'codex']));
197
+ const { runDoctor } = require('../../src/commands/doctor');
198
+ // R20: doctor resolves a single explicit provider without needing every
199
+ // enabled agent to be independently initialized first.
200
+ runDoctor({ cwd: tmpWork, json: true, agent: 'codex' });
201
+ const report = JSON.parse(stdout());
202
+ expect(report.providers.map((p) => p.id)).toEqual(['codex']);
203
+ const codexReport = report.providers[0];
204
+ expect(codexReport.checks.find((c) => c.id === 'binary.version').state).toBe('missing');
205
+ });
206
+ });
@@ -1,25 +1,50 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
3
9
  const providers_1 = require("../../src/providers");
4
10
  describe('Hook configuration in providers', () => {
11
+ const originalHome = process.env.HOME;
12
+ const originalAwmHome = process.env.AWM_HOME;
13
+ let tmpHome;
14
+ let tmpWork;
15
+ beforeEach(() => {
16
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-hook-home-'));
17
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-hook-work-'));
18
+ process.env.HOME = tmpHome;
19
+ process.env.AWM_HOME = path_1.default.join(tmpWork, 'awm');
20
+ });
21
+ afterEach(() => {
22
+ if (originalHome === undefined)
23
+ delete process.env.HOME;
24
+ else
25
+ process.env.HOME = originalHome;
26
+ if (originalAwmHome === undefined)
27
+ delete process.env.AWM_HOME;
28
+ else
29
+ process.env.AWM_HOME = originalAwmHome;
30
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
31
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
32
+ });
5
33
  it('claude-code provider defines a HookConfig', () => {
6
- const cc = providers_1.PROVIDERS['claude-code'];
34
+ const cc = (0, providers_1.providerFor)('claude-code');
7
35
  expect(cc.hooks).toBeDefined();
8
36
  expect(cc.hooks?.type).toBe('cc-settings-merge');
9
37
  expect(cc.hooks?.eventName).toBe('SessionStart');
10
38
  expect(cc.hooks?.matcher).toBe('startup|clear|compact');
11
39
  });
12
- it('claude-code settingsPath resolves to ~/.claude/settings.json', () => {
13
- const cc = providers_1.PROVIDERS['claude-code'];
14
- expect(cc.hooks?.settingsPath).toMatch(/\.claude\/settings\.json$/);
40
+ it('preserves the legacy Claude Code AWM_HOME hooks directory', () => {
41
+ const cc = (0, providers_1.providerFor)('claude-code');
42
+ expect(cc.hooks?.settingsPath).toBe(path_1.default.join(tmpHome, '.claude/settings.json'));
43
+ expect(cc.hooks?.scriptsDir).toBe(path_1.default.join(process.env.AWM_HOME, 'hooks'));
15
44
  });
16
- it('claude-code scriptsDir resolves to ~/.awm/hooks/', () => {
17
- const cc = providers_1.PROVIDERS['claude-code'];
18
- expect(cc.hooks?.scriptsDir).toMatch(/\.awm\/hooks$/);
19
- });
20
- it('antigravity and opencode have no hooks (single-harness scope)', () => {
21
- expect(providers_1.PROVIDERS['antigravity'].hooks).toBeUndefined();
22
- expect(providers_1.PROVIDERS['opencode'].hooks).toBeUndefined();
45
+ it('antigravity and opencode have no hooks', () => {
46
+ expect((0, providers_1.providerFor)('antigravity').hooks).toBeUndefined();
47
+ expect((0, providers_1.providerFor)('opencode').hooks).toBeUndefined();
23
48
  });
24
49
  it('getHookConfig returns config for supported target', () => {
25
50
  const config = (0, providers_1.getHookConfig)('claude-code');
@@ -27,19 +52,12 @@ describe('Hook configuration in providers', () => {
27
52
  expect(config?.type).toBe('cc-settings-merge');
28
53
  });
29
54
  it('getHookConfig returns undefined for unsupported target', () => {
30
- const config = (0, providers_1.getHookConfig)('antigravity');
31
- expect(config).toBeUndefined();
32
- });
33
- it('respects AWM_HOME env var override for scriptsDir', () => {
34
- const originalEnv = process.env.AWM_HOME;
35
- process.env.AWM_HOME = '/tmp/awm-test';
36
- // Re-import to pick up env change
37
- jest.resetModules();
38
- const { PROVIDERS: P } = require('../../src/providers');
39
- expect(P['claude-code'].hooks.scriptsDir).toBe('/tmp/awm-test/hooks');
40
- if (originalEnv === undefined)
41
- delete process.env.AWM_HOME;
42
- else
43
- process.env.AWM_HOME = originalEnv;
55
+ expect((0, providers_1.getHookConfig)('antigravity')).toBeUndefined();
56
+ });
57
+ it('respects AWM_HOME changes at call time', () => {
58
+ const nextAwmHome = path_1.default.join(tmpWork, 'next-awm');
59
+ process.env.AWM_HOME = nextAwmHome;
60
+ expect((0, providers_1.getHookConfig)('claude-code')?.scriptsDir)
61
+ .toBe(path_1.default.join(nextAwmHome, 'hooks'));
44
62
  });
45
63
  });
@@ -3,56 +3,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- // tests/providers/index.test.ts
7
- const providers_1 = require("../../src/providers");
6
+ const fs_1 = __importDefault(require("fs"));
8
7
  const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const providers_1 = require("../../src/providers");
9
10
  describe('Providers Routing', () => {
10
- // ── Existing Antigravity tests (preserved) ──
11
- it('routes antigravity global skills correctly', () => {
12
- const result = (0, providers_1.getTargetPath)('skill', 'antigravity', 'global');
13
- expect(result).toBe(`${os_1.default.homedir()}/.gemini/antigravity/skills`);
11
+ const originalHome = process.env.HOME;
12
+ const originalAwmHome = process.env.AWM_HOME;
13
+ let tmpHome;
14
+ let tmpWork;
15
+ beforeEach(() => {
16
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-home-'));
17
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-work-'));
18
+ process.env.HOME = tmpHome;
19
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm-test');
14
20
  });
15
- it('routes opencode local skills correctly', () => {
16
- const result = (0, providers_1.getTargetPath)('skill', 'opencode', 'local');
17
- expect(result).toBe('.agents/skills');
21
+ afterEach(() => {
22
+ if (originalHome === undefined)
23
+ delete process.env.HOME;
24
+ else
25
+ process.env.HOME = originalHome;
26
+ if (originalAwmHome === undefined)
27
+ delete process.env.AWM_HOME;
28
+ else
29
+ process.env.AWM_HOME = originalAwmHome;
30
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
31
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
18
32
  });
19
- it('routes antigravity global workflows correctly', () => {
20
- const result = (0, providers_1.getTargetPath)('workflow', 'antigravity', 'global');
21
- expect(result).toBe(`${os_1.default.homedir()}/.gemini/antigravity/global_workflows`);
33
+ it('resolves Codex paths from the current HOME at call time', () => {
34
+ const first = fs_1.default.mkdtempSync(path_1.default.join(tmpWork, 'awm-home-a-'));
35
+ const second = fs_1.default.mkdtempSync(path_1.default.join(tmpWork, 'awm-home-b-'));
36
+ process.env.HOME = first;
37
+ expect((0, providers_1.getTargetPath)('skill', 'codex', 'global'))
38
+ .toBe(path_1.default.join(first, '.agents/skills'));
39
+ process.env.HOME = second;
40
+ expect((0, providers_1.getTargetPath)('agent', 'codex', 'global'))
41
+ .toBe(path_1.default.join(second, '.codex/agents'));
22
42
  });
23
- it('throws on opencode workflow', () => {
24
- expect(() => (0, providers_1.getTargetPath)('workflow', 'opencode', 'global')).toThrow('not supported');
43
+ it('returns a fresh provider graph using current HOME and AWM_HOME', () => {
44
+ const first = (0, providers_1.providers)();
45
+ const nextHome = path_1.default.join(tmpWork, 'next-home');
46
+ const nextAwmHome = path_1.default.join(tmpWork, 'next-awm');
47
+ process.env.HOME = nextHome;
48
+ process.env.AWM_HOME = nextAwmHome;
49
+ const second = (0, providers_1.providers)();
50
+ expect(second).not.toBe(first);
51
+ expect(second.codex.skill.global).toBe(path_1.default.join(nextHome, '.agents/skills'));
52
+ expect(second.codex.hooks?.scriptsDir).toBe(path_1.default.join(nextAwmHome, 'hooks/codex'));
53
+ expect(first.codex.skill.global).toBe(path_1.default.join(tmpHome, '.agents/skills'));
25
54
  });
26
- // ── New Claude Code tests ──
27
- it('routes claude-code global skills correctly', () => {
28
- const result = (0, providers_1.getTargetPath)('skill', 'claude-code', 'global');
29
- expect(result).toBe(`${os_1.default.homedir()}/.claude/skills`);
55
+ it('declares the complete Codex capability metadata', () => {
56
+ expect((0, providers_1.providerFor)('codex')).toEqual({
57
+ label: 'Codex',
58
+ minimumVersion: '0.145.0',
59
+ versionCommand: { command: 'codex', args: ['--version'] },
60
+ skill: {
61
+ global: path_1.default.join(tmpHome, '.agents/skills'),
62
+ local: '.agents/skills',
63
+ renderer: 'link',
64
+ },
65
+ workflow: null,
66
+ agent: {
67
+ global: path_1.default.join(tmpHome, '.codex/agents'),
68
+ local: '.codex/agents',
69
+ renderer: 'codex-agent-toml',
70
+ },
71
+ hooks: {
72
+ type: 'codex-hooks-json',
73
+ settingsPath: path_1.default.join(tmpHome, '.codex/hooks.json'),
74
+ scriptsDir: path_1.default.join(process.env.AWM_HOME, 'hooks/codex'),
75
+ matcher: 'startup|resume|clear|compact',
76
+ eventName: 'SessionStart',
77
+ },
78
+ injection: {
79
+ type: 'managed-agents-md',
80
+ globalPath: path_1.default.join(tmpHome, '.codex/AGENTS.md'),
81
+ localFile: 'AGENTS.md',
82
+ },
83
+ });
30
84
  });
31
- it('routes claude-code local skills correctly', () => {
32
- const result = (0, providers_1.getTargetPath)('skill', 'claude-code', 'local');
33
- expect(result).toBe('.claude/skills');
85
+ it('keeps Claude Code, OpenCode, and Antigravity contracts unchanged', () => {
86
+ const graph = (0, providers_1.providers)();
87
+ expect(graph.antigravity).toEqual({
88
+ label: 'Antigravity',
89
+ skill: {
90
+ global: path_1.default.join(tmpHome, '.gemini/antigravity/skills'),
91
+ local: '.agent/skills',
92
+ renderer: 'link',
93
+ },
94
+ workflow: {
95
+ global: path_1.default.join(tmpHome, '.gemini/antigravity/global_workflows'),
96
+ local: '.agent/workflows',
97
+ renderer: 'link',
98
+ },
99
+ agent: null,
100
+ });
101
+ expect(graph.opencode).toEqual({
102
+ label: 'OpenCode',
103
+ skill: {
104
+ global: path_1.default.join(tmpHome, '.agents/skills'),
105
+ local: '.agents/skills',
106
+ renderer: 'link',
107
+ },
108
+ workflow: null,
109
+ agent: {
110
+ global: path_1.default.join(tmpHome, '.config/opencode/agents'),
111
+ local: '.agents/profiles',
112
+ renderer: 'link',
113
+ },
114
+ injection: {
115
+ type: 'config-instructions',
116
+ configPath: path_1.default.join(tmpHome, '.config/opencode/opencode.json'),
117
+ field: 'instructions',
118
+ },
119
+ });
120
+ expect(graph['claude-code']).toEqual({
121
+ label: 'Claude Code',
122
+ skill: {
123
+ global: path_1.default.join(tmpHome, '.claude/skills'),
124
+ local: '.claude/skills',
125
+ renderer: 'link',
126
+ },
127
+ workflow: null,
128
+ agent: {
129
+ global: path_1.default.join(tmpHome, '.claude/agents'),
130
+ local: '.claude/agents',
131
+ renderer: 'link',
132
+ },
133
+ hooks: {
134
+ type: 'cc-settings-merge',
135
+ settingsPath: path_1.default.join(tmpHome, '.claude/settings.json'),
136
+ scriptsDir: path_1.default.join(process.env.AWM_HOME, 'hooks'),
137
+ matcher: 'startup|clear|compact',
138
+ eventName: 'SessionStart',
139
+ },
140
+ injection: { type: 'cc-settings-merge' },
141
+ });
34
142
  });
35
- it('routes claude-code global agents correctly', () => {
36
- const result = (0, providers_1.getTargetPath)('agent', 'claude-code', 'global');
37
- expect(result).toBe(`${os_1.default.homedir()}/.claude/agents`);
143
+ it('keeps Claude Code and OpenCode destinations unchanged', () => {
144
+ expect((0, providers_1.getTargetPath)('skill', 'claude-code', 'global'))
145
+ .toBe(path_1.default.join(process.env.HOME, '.claude/skills'));
146
+ expect((0, providers_1.getTargetPath)('skill', 'opencode', 'global'))
147
+ .toBe(path_1.default.join(process.env.HOME, '.agents/skills'));
38
148
  });
39
- it('routes claude-code local agents correctly', () => {
40
- const result = (0, providers_1.getTargetPath)('agent', 'claude-code', 'local');
41
- expect(result).toBe('.claude/agents');
149
+ it('uses AGENT_TARGETS as the single iterable target catalog', () => {
150
+ expect(providers_1.AGENT_TARGETS).toEqual(['antigravity', 'opencode', 'claude-code', 'codex']);
151
+ expect(Object.keys((0, providers_1.providers)())).toEqual([...providers_1.AGENT_TARGETS]);
42
152
  });
43
- it('throws on claude-code workflow', () => {
153
+ it('throws on unsupported artifacts', () => {
154
+ expect(() => (0, providers_1.getTargetPath)('workflow', 'opencode', 'global')).toThrow('not supported');
44
155
  expect(() => (0, providers_1.getTargetPath)('workflow', 'claude-code', 'global')).toThrow('not supported');
156
+ expect(() => (0, providers_1.getTargetPath)('agent', 'antigravity', 'local')).toThrow('not supported');
45
157
  });
46
- it('throws on unknown agent target', () => {
47
- expect(() => (0, providers_1.getTargetPath)('skill', 'unknown-agent', 'global')).toThrow('Unknown agent target');
48
- });
49
- // ── PROVIDERS map structure tests ──
50
- it('exports PROVIDERS with all three targets', () => {
51
- expect(Object.keys(providers_1.PROVIDERS)).toEqual(expect.arrayContaining(['antigravity', 'opencode', 'claude-code']));
52
- });
53
- it('marks unsupported artifact types as null', () => {
54
- expect(providers_1.PROVIDERS['antigravity'].agent).toBeNull();
55
- expect(providers_1.PROVIDERS['opencode'].workflow).toBeNull();
56
- expect(providers_1.PROVIDERS['claude-code'].workflow).toBeNull();
158
+ it('fails loudly for invalid runtime provider input', () => {
159
+ expect(() => (0, providers_1.providerFor)('unknown-agent')).toThrow('Unknown agent target');
160
+ expect(() => (0, providers_1.getTargetPath)('skill', 'unknown-agent', 'global'))
161
+ .toThrow('Unknown agent target');
57
162
  });
58
163
  });