agentic-workflow-manager 3.1.0 → 3.2.1

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 (92) 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/hooks/claude.js +192 -0
  6. package/dist/src/commands/hooks/codex.js +191 -0
  7. package/dist/src/commands/hooks/index.js +23 -8
  8. package/dist/src/commands/hooks/install.js +11 -107
  9. package/dist/src/commands/hooks/resync.js +28 -18
  10. package/dist/src/commands/hooks/shared.js +94 -0
  11. package/dist/src/commands/hooks/status.js +13 -64
  12. package/dist/src/commands/hooks/uninstall.js +11 -47
  13. package/dist/src/commands/init.js +90 -28
  14. package/dist/src/commands/registry/index.js +1 -1
  15. package/dist/src/commands/sync.js +113 -0
  16. package/dist/src/commands/update.js +107 -0
  17. package/dist/src/core/agent-targets.js +50 -0
  18. package/dist/src/core/artifact-state.js +65 -0
  19. package/dist/src/core/atomic-file.js +72 -0
  20. package/dist/src/core/bundle-install.js +60 -38
  21. package/dist/src/core/bundles.js +21 -0
  22. package/dist/src/core/context/managed-block.js +190 -0
  23. package/dist/src/core/context/orchestrator.js +3 -1
  24. package/dist/src/core/context/project-constitution-inject.js +1 -1
  25. package/dist/src/core/context/regenerate.js +3 -3
  26. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  27. package/dist/src/core/diagnostics/checks.js +16 -0
  28. package/dist/src/core/diagnostics/context.js +37 -8
  29. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  30. package/dist/src/core/executor.js +30 -9
  31. package/dist/src/core/init/mutation-targets.js +139 -0
  32. package/dist/src/core/init/provider-facts.js +179 -0
  33. package/dist/src/core/init/steps.js +65 -9
  34. package/dist/src/core/install-planner.js +206 -0
  35. package/dist/src/core/install-transaction.js +405 -0
  36. package/dist/src/core/profile.js +1 -1
  37. package/dist/src/core/provider-artifacts.js +54 -0
  38. package/dist/src/core/provider-version.js +36 -0
  39. package/dist/src/core/reconciliation.js +61 -0
  40. package/dist/src/core/registries.js +10 -0
  41. package/dist/src/core/renderers/canonical-agent.js +27 -0
  42. package/dist/src/core/renderers/codex-agent.js +60 -0
  43. package/dist/src/core/skill-integrity.js +4 -3
  44. package/dist/src/index.js +97 -289
  45. package/dist/src/providers/index.js +143 -39
  46. package/dist/src/ui/provider-preflight.js +26 -0
  47. package/dist/src/utils/config.js +101 -9
  48. package/dist/tests/commands/agent.test.js +103 -0
  49. package/dist/tests/commands/backup.test.js +109 -0
  50. package/dist/tests/commands/doctor.test.js +65 -4
  51. package/dist/tests/commands/hooks/codex.test.js +230 -0
  52. package/dist/tests/commands/hooks/install.test.js +20 -1
  53. package/dist/tests/commands/hooks/resync.test.js +53 -6
  54. package/dist/tests/commands/hooks/status.test.js +10 -0
  55. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  56. package/dist/tests/commands/init.test.js +195 -8
  57. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  58. package/dist/tests/core/agent-targets.test.js +45 -0
  59. package/dist/tests/core/artifact-state.test.js +144 -0
  60. package/dist/tests/core/atomic-file.test.js +96 -0
  61. package/dist/tests/core/bundle-install.test.js +208 -17
  62. package/dist/tests/core/bundles.test.js +12 -0
  63. package/dist/tests/core/context/managed-block.test.js +90 -0
  64. package/dist/tests/core/context/orchestrator.test.js +48 -3
  65. package/dist/tests/core/context/regenerate.test.js +1 -1
  66. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  67. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  68. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  69. package/dist/tests/core/diagnostics/context.test.js +74 -0
  70. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  71. package/dist/tests/core/executor.test.js +20 -0
  72. package/dist/tests/core/export/engine.test.js +3 -2
  73. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  74. package/dist/tests/core/init/orchestrator.test.js +1 -1
  75. package/dist/tests/core/init/provider-facts.test.js +177 -0
  76. package/dist/tests/core/init/steps.test.js +106 -2
  77. package/dist/tests/core/install-planner.test.js +224 -0
  78. package/dist/tests/core/install-transaction.test.js +257 -0
  79. package/dist/tests/core/provider-artifacts.test.js +64 -0
  80. package/dist/tests/core/provider-version.test.js +53 -0
  81. package/dist/tests/core/reconciliation.test.js +165 -0
  82. package/dist/tests/core/registries-sync.test.js +4 -4
  83. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  84. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  85. package/dist/tests/core/versioning.test.js +1 -1
  86. package/dist/tests/integration/codex-provider-isolated.test.js +235 -0
  87. package/dist/tests/providers/hooks-config.test.js +43 -25
  88. package/dist/tests/providers/index.test.js +144 -39
  89. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  90. package/dist/tests/ui/provider-preflight.test.js +25 -0
  91. package/dist/tests/utils/config.test.js +178 -18
  92. package/package.json +1 -1
@@ -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
  });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ // Structural regression guard for the recurring class of bug found across
3
+ // multiple review rounds in this repo's history (harness-retro,
4
+ // 2026-07-25 — codex-cli-provider-plan): the Codex TOML renderer's
5
+ // character-escaping was fixed piecemeal, one control character at a time
6
+ // (quote runs, then DEL, then the rest of the C0 range), because each fix
7
+ // only tested the specific character it addressed rather than the full
8
+ // class. This test exercises the ENTIRE character class the renderer must
9
+ // escape in one pass, so a future partial regression (e.g. someone "fixing"
10
+ // tomlMultiline for a new case and narrowing the character class by
11
+ // accident) fails immediately instead of waiting for another review round
12
+ // to notice one more missed code point.
13
+ //
14
+ // Deliberately does not import the renderer's private helpers (tomlString/
15
+ // tomlMultiline aren't exported) — it drives the same public entry point
16
+ // (`renderCodexAgent`) real callers use, and inspects the actual TOML text.
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ const codex_agent_1 = require("../../src/core/renderers/codex-agent");
19
+ // Every C0 control character other than tab (0x09) and newline (0x0A), plus
20
+ // DEL (0x7F) — the full set TOML requires escaping in both single-line and
21
+ // multi-line basic strings.
22
+ const REQUIRES_ESCAPE = [
23
+ ...Array.from({ length: 0x09 }, (_, i) => i), // 0x00-0x08
24
+ ...Array.from({ length: 0x1f - 0x0b + 1 }, (_, i) => 0x0b + i), // 0x0B-0x1F
25
+ 0x7f,
26
+ ];
27
+ function canonicalAgentWith(instructions, description = 'd') {
28
+ return `---\nname: test-agent\ndescription: ${description}\n---\n\n${instructions}\n`;
29
+ }
30
+ // A run of embedded quotes long enough to exercise the original critical bug
31
+ // (a naive `/"""/g` replace mis-escaped runs whose length was >3 and not a
32
+ // multiple of 3 — e.g. 4, 5, 7, 8 consecutive quotes).
33
+ const QUOTE_RUN_LENGTHS = [1, 3, 4, 5, 7, 8];
34
+ describe('codex-agent renderer — escaping completeness (structural)', () => {
35
+ it('escapes every C0-minus-tab/newline control character and DEL as \\uXXXX in developer_instructions', () => {
36
+ const body = REQUIRES_ESCAPE.map((code) => `x${String.fromCharCode(code)}y`).join('|');
37
+ const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith(body));
38
+ for (const code of REQUIRES_ESCAPE) {
39
+ const raw = String.fromCharCode(code);
40
+ expect(toml).not.toContain(raw); // no literal control byte survives
41
+ const escape = `\\u${code.toString(16).padStart(4, '0')}`;
42
+ expect(toml).toContain(escape);
43
+ }
44
+ });
45
+ it('never leaves a run of 3+ unescaped double quotes for any embedded quote-run length', () => {
46
+ for (const length of QUOTE_RUN_LENGTHS) {
47
+ const body = `before ${'"'.repeat(length)} after`;
48
+ const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith(body));
49
+ // Strip the legitimate opening/closing """ delimiters, then confirm
50
+ // no 3-consecutive-unescaped-quote run remains inside the body —
51
+ // that would prematurely close a TOML multi-line basic string.
52
+ const startMarker = 'developer_instructions = """\n';
53
+ const start = toml.indexOf(startMarker) + startMarker.length;
54
+ const end = toml.lastIndexOf('\n"""');
55
+ const rendered = toml.slice(start, end);
56
+ expect(rendered).not.toMatch(/(?<!\\)"""/);
57
+ }
58
+ });
59
+ it('tab and newline stay raw (legitimately unescaped in a TOML multi-line basic string)', () => {
60
+ const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith('line one\twith tab\nline two'));
61
+ expect(toml).toContain('line one\twith tab\nline two');
62
+ });
63
+ it('produces output parseable as a TOML multi-line basic string boundary (no stray raw quote runs at all)', () => {
64
+ // Combine control chars + quotes + backslashes in one body — the
65
+ // realistic worst case, not just each hazard in isolation.
66
+ const body = `a\\b"""c${String.fromCharCode(0x00)}d${String.fromCharCode(0x1f)}e${String.fromCharCode(0x7f)}f`;
67
+ const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith(body));
68
+ const startMarker = 'developer_instructions = """\n';
69
+ const start = toml.indexOf(startMarker) + startMarker.length;
70
+ const end = toml.lastIndexOf('\n"""');
71
+ const rendered = toml.slice(start, end);
72
+ expect(rendered).not.toMatch(/(?<!\\)"""/);
73
+ expect(rendered).not.toContain(String.fromCharCode(0x00));
74
+ expect(rendered).not.toContain(String.fromCharCode(0x1f));
75
+ expect(rendered).not.toContain(String.fromCharCode(0x7f));
76
+ });
77
+ });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const provider_preflight_1 = require("../../src/ui/provider-preflight");
4
+ describe.each(['--all', 'regular'])('%s add preflight boundary', (flow) => {
5
+ it('reports the exact error and stops before install or preference writes', () => {
6
+ const error = jest.fn();
7
+ const exit = jest.fn();
8
+ const installArtifact = jest.fn();
9
+ const savePreferences = jest.fn();
10
+ const pairs = [
11
+ { agent: 'codex', artifact: { type: 'skill' } },
12
+ { agent: 'codex', artifact: { type: 'agent' } },
13
+ ];
14
+ const proceed = (0, provider_preflight_1.preflightLinkArtifactsForCli)(pairs, { error, exit });
15
+ if (proceed) {
16
+ installArtifact(flow);
17
+ savePreferences(flow);
18
+ }
19
+ expect(proceed).toBe(false);
20
+ expect(error).toHaveBeenCalledWith("Renderer 'codex-agent-toml' for codex agent artifacts is not implemented yet");
21
+ expect(exit).toHaveBeenCalledWith(1);
22
+ expect(installArtifact).not.toHaveBeenCalled();
23
+ expect(savePreferences).not.toHaveBeenCalled();
24
+ });
25
+ });
@@ -3,34 +3,194 @@ 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/utils/config.test.ts
7
- const config_1 = require("../../src/utils/config");
8
6
  const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
7
  const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
11
9
  describe('Preferences Manager', () => {
12
- let tmpDir;
10
+ let tmpHome;
11
+ let tmpWork;
12
+ let originalHome;
13
+ let originalAwmHome;
13
14
  beforeEach(() => {
14
- tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-prefs-'));
15
- process.env.AWM_HOME = tmpDir;
15
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-prefs-home-'));
16
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-prefs-work-'));
17
+ originalHome = process.env.HOME;
18
+ originalAwmHome = process.env.AWM_HOME;
19
+ process.env.HOME = tmpHome;
20
+ process.env.AWM_HOME = tmpHome;
21
+ jest.resetModules();
16
22
  });
17
23
  afterEach(() => {
18
- delete process.env.AWM_HOME;
19
- fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
24
+ if (originalHome === undefined)
25
+ delete process.env.HOME;
26
+ else
27
+ process.env.HOME = originalHome;
28
+ if (originalAwmHome === undefined)
29
+ delete process.env.AWM_HOME;
30
+ else
31
+ process.env.AWM_HOME = originalAwmHome;
32
+ jest.resetModules();
33
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
34
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
20
35
  });
21
- it('creates default preferences if none exist (default agent is claude-code)', () => {
22
- const prefs = (0, config_1.getPreferences)();
23
- expect(prefs.defaultAgent).toBe('claude-code');
36
+ it('creates default preferences if none exist', () => {
37
+ const { getPreferences } = require('../../src/utils/config');
38
+ expect(getPreferences()).toEqual({
39
+ defaultAgent: 'claude-code',
40
+ enabledAgents: ['claude-code'],
41
+ installMethod: 'symlink',
42
+ defaultScope: 'local',
43
+ });
24
44
  });
25
45
  it('preferencesExist reflects whether the file is on disk', () => {
26
- expect((0, config_1.preferencesExist)()).toBe(false);
27
- (0, config_1.getPreferences)(); // side-effect: persists defaults
28
- expect((0, config_1.preferencesExist)()).toBe(true);
46
+ const { getPreferences, preferencesExist } = require('../../src/utils/config');
47
+ expect(preferencesExist()).toBe(false);
48
+ getPreferences();
49
+ expect(preferencesExist()).toBe(true);
29
50
  });
30
51
  it('saves and loads preferences correctly', () => {
31
- (0, config_1.savePreferences)({ defaultAgent: 'opencode', installMethod: 'copy', defaultScope: 'local' });
32
- const prefs = (0, config_1.getPreferences)();
33
- expect(prefs.defaultAgent).toBe('opencode');
34
- expect(prefs.installMethod).toBe('copy');
52
+ const { getPreferences, savePreferences } = require('../../src/utils/config');
53
+ savePreferences({
54
+ defaultAgent: 'opencode',
55
+ enabledAgents: ['opencode'],
56
+ installMethod: 'copy',
57
+ defaultScope: 'local',
58
+ });
59
+ expect(getPreferences()).toEqual({
60
+ defaultAgent: 'opencode',
61
+ enabledAgents: ['opencode'],
62
+ installMethod: 'copy',
63
+ defaultScope: 'local',
64
+ });
65
+ });
66
+ it('migrates defaultAgent without losing optional preferences', () => {
67
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify({
68
+ defaultAgent: 'claude-code',
69
+ installMethod: 'symlink',
70
+ defaultScope: 'local',
71
+ baseRemote: 'https://example.test/baseline.git',
72
+ channel: 'dev',
73
+ pins: { baseline: '1.4.0' },
74
+ futurePreference: { mode: 'preview' },
75
+ }));
76
+ const { getPreferences } = require('../../src/utils/config');
77
+ const prefs = getPreferences();
78
+ expect(prefs.enabledAgents).toEqual(['claude-code']);
79
+ expect(prefs.baseRemote).toBe('https://example.test/baseline.git');
80
+ expect(prefs.channel).toBe('dev');
81
+ expect(prefs.pins).toEqual({ baseline: '1.4.0' });
82
+ expect(prefs.futurePreference)
83
+ .toEqual({ mode: 'preview' });
84
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, 'preferences.json'), 'utf8')))
85
+ .toEqual(prefs);
86
+ });
87
+ it('does not overwrite malformed preferences', () => {
88
+ const file = path_1.default.join(tmpHome, 'preferences.json');
89
+ fs_1.default.writeFileSync(file, '{"defaultAgent":');
90
+ const before = fs_1.default.readFileSync(file, 'utf8');
91
+ const { getPreferences } = require('../../src/utils/config');
92
+ expect(() => getPreferences()).toThrow('preferences.json is not valid JSON');
93
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe(before);
94
+ });
95
+ it('deduplicates enabled agents and requires the default to remain enabled', () => {
96
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify({
97
+ defaultAgent: 'claude-code',
98
+ enabledAgents: ['claude-code', 'codex', 'codex'],
99
+ installMethod: 'symlink',
100
+ defaultScope: 'local',
101
+ }));
102
+ const { getPreferences } = require('../../src/utils/config');
103
+ expect(getPreferences().enabledAgents).toEqual(['claude-code', 'codex']);
104
+ });
105
+ it('rejects preferences whose default agent is disabled', () => {
106
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify({
107
+ defaultAgent: 'claude-code',
108
+ enabledAgents: ['codex'],
109
+ installMethod: 'symlink',
110
+ defaultScope: 'local',
111
+ }));
112
+ const { getPreferences } = require('../../src/utils/config');
113
+ expect(() => getPreferences())
114
+ .toThrow('preferences.json defaultAgent must be included in enabledAgents');
115
+ });
116
+ it.each([
117
+ ['a non-object value', [], 'preferences.json must contain a JSON object'],
118
+ ['an invalid defaultAgent', {
119
+ defaultAgent: 'unknown',
120
+ installMethod: 'symlink',
121
+ defaultScope: 'local',
122
+ }, 'preferences.json has an invalid defaultAgent'],
123
+ ['an invalid installMethod', {
124
+ defaultAgent: 'claude-code',
125
+ installMethod: 'hardlink',
126
+ defaultScope: 'local',
127
+ }, 'preferences.json has an invalid installMethod'],
128
+ ['an invalid defaultScope', {
129
+ defaultAgent: 'claude-code',
130
+ installMethod: 'symlink',
131
+ defaultScope: 'workspace',
132
+ }, 'preferences.json has an invalid defaultScope'],
133
+ ['an invalid enabledAgents field', {
134
+ defaultAgent: 'claude-code',
135
+ enabledAgents: ['claude-code', 'unknown'],
136
+ installMethod: 'symlink',
137
+ defaultScope: 'local',
138
+ }, 'preferences.json has an invalid enabledAgents'],
139
+ ])('rejects %s', (_name, value, message) => {
140
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify(value));
141
+ const { getPreferences } = require('../../src/utils/config');
142
+ expect(() => getPreferences()).toThrow(message);
143
+ });
144
+ it.each([
145
+ ['an empty baseRemote', { baseRemote: '' }, 'preferences.json has an invalid baseRemote'],
146
+ ['a non-string baseRemote', { baseRemote: 42 }, 'preferences.json has an invalid baseRemote'],
147
+ ['an invalid channel', { channel: 'preview' }, 'preferences.json has an invalid channel'],
148
+ ['an array pins value', { pins: [] }, 'preferences.json has invalid pins'],
149
+ ['a null pins value', { pins: null }, 'preferences.json has invalid pins'],
150
+ ['an empty pin name', { pins: { '': '1.2.3' } }, 'preferences.json has invalid pins'],
151
+ ['an empty pin value', { pins: { baseline: '' } }, 'preferences.json has invalid pins'],
152
+ ['a non-string pin value', { pins: { baseline: 123 } }, 'preferences.json has invalid pins'],
153
+ ])('rejects %s without overwriting the file', (_name, invalidField, message) => {
154
+ const file = path_1.default.join(tmpHome, 'preferences.json');
155
+ const before = JSON.stringify({
156
+ defaultAgent: 'claude-code',
157
+ enabledAgents: ['claude-code'],
158
+ installMethod: 'symlink',
159
+ defaultScope: 'local',
160
+ ...invalidField,
161
+ });
162
+ fs_1.default.writeFileSync(file, before);
163
+ const { getPreferences } = require('../../src/utils/config');
164
+ expect(() => getPreferences()).toThrow(message);
165
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe(before);
166
+ });
167
+ it('rejects incomplete updates without dropping enabled agents', () => {
168
+ const config = require('../../src/utils/config');
169
+ config.savePreferences({
170
+ defaultAgent: 'claude-code',
171
+ enabledAgents: ['claude-code', 'codex'],
172
+ installMethod: 'symlink',
173
+ defaultScope: 'local',
174
+ });
175
+ const incomplete = {
176
+ defaultAgent: 'opencode',
177
+ installMethod: 'copy',
178
+ defaultScope: 'global',
179
+ };
180
+ expect(() => {
181
+ // @ts-expect-error enabledAgents is required for every preference update
182
+ config.savePreferences(incomplete);
183
+ }).toThrow('preferences.json has an invalid enabledAgents');
184
+ expect(config.getPreferences().enabledAgents).toEqual(['claude-code', 'codex']);
185
+ });
186
+ it('validates preferences before saving them', () => {
187
+ const { savePreferences } = require('../../src/utils/config');
188
+ expect(() => savePreferences({
189
+ defaultAgent: 'codex',
190
+ enabledAgents: ['claude-code'],
191
+ installMethod: 'symlink',
192
+ defaultScope: 'local',
193
+ })).toThrow('preferences.json defaultAgent must be included in enabledAgents');
194
+ expect(fs_1.default.existsSync(path_1.default.join(tmpHome, 'preferences.json'))).toBe(false);
35
195
  });
36
196
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"