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
@@ -0,0 +1,185 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const codex_agents_1 = require("../../../../src/core/context/strategies/codex-agents");
10
+ const provider_1 = require("../../../../src/core/context/provider");
11
+ describe('CodexAgentsStrategy', () => {
12
+ let tmpHome;
13
+ let tmpWork;
14
+ let originalHome;
15
+ let originalAwmHome;
16
+ beforeEach(() => {
17
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-codex-home-'));
18
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-codex-work-'));
19
+ originalHome = process.env.HOME;
20
+ originalAwmHome = process.env.AWM_HOME;
21
+ process.env.HOME = tmpHome;
22
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
23
+ });
24
+ afterEach(() => {
25
+ if (originalHome === undefined)
26
+ delete process.env.HOME;
27
+ else
28
+ process.env.HOME = originalHome;
29
+ if (originalAwmHome === undefined)
30
+ delete process.env.AWM_HOME;
31
+ else
32
+ process.env.AWM_HOME = originalAwmHome;
33
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
34
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
35
+ jest.restoreAllMocks();
36
+ });
37
+ it('injects global AWM bootstrap without changing user rules', () => {
38
+ fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.codex'), { recursive: true });
39
+ const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
40
+ fs_1.default.writeFileSync(file, '# Personal\n\nDo not delete.\n');
41
+ const result = new codex_agents_1.CodexAgentsStrategy().injectGlobal({
42
+ markdown: '# AWM\n\nUse `development-process`.',
43
+ });
44
+ expect(result).toBe('injected');
45
+ const written = fs_1.default.readFileSync(file, 'utf8');
46
+ expect(written).toContain('# Personal\n\nDo not delete.\n');
47
+ expect(written).toContain('Use `development-process`.');
48
+ });
49
+ it('creates the global file and returns unchanged on an idempotent repeat', () => {
50
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
51
+ const context = { markdown: '# AWM\n\nBootstrap.' };
52
+ expect(strategy.injectGlobal(context)).toBe('injected');
53
+ expect(strategy.injectGlobal(context)).toBe('unchanged');
54
+ expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.codex/AGENTS.md'))).toBe(true);
55
+ });
56
+ it('uses HOME at call time', () => {
57
+ const firstHome = tmpHome;
58
+ const secondHome = path_1.default.join(tmpWork, 'second-home');
59
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
60
+ strategy.injectGlobal({ markdown: 'first' });
61
+ process.env.HOME = secondHome;
62
+ strategy.injectGlobal({ markdown: 'second' });
63
+ expect(fs_1.default.readFileSync(path_1.default.join(firstHome, '.codex/AGENTS.md'), 'utf8')).toContain('first');
64
+ expect(fs_1.default.readFileSync(path_1.default.join(secondHome, '.codex/AGENTS.md'), 'utf8')).toContain('second');
65
+ });
66
+ it('updates only its global managed block', () => {
67
+ const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
68
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
69
+ fs_1.default.writeFileSync(file, 'before\n');
70
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
71
+ strategy.injectGlobal({ markdown: 'old' });
72
+ fs_1.default.appendFileSync(file, 'after\n');
73
+ strategy.injectGlobal({ markdown: 'new' });
74
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe('before\n\n<!-- AWM:START -->\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\nnew\n<!-- AWM:END -->\nafter\n');
75
+ });
76
+ it('fails on ambiguous global markers without changing the file', () => {
77
+ const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
78
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
79
+ const ambiguous = '<!-- AWM:START -->\nuser';
80
+ fs_1.default.writeFileSync(file, ambiguous);
81
+ expect(() => new codex_agents_1.CodexAgentsStrategy().injectGlobal({ markdown: 'new' })).toThrow('unmatched');
82
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe(ambiguous);
83
+ });
84
+ it('rejects inline marker examples without changing the global file', () => {
85
+ const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
86
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
87
+ const ambiguous = '`<!-- AWM:START -->` example and `<!-- AWM:END -->` example';
88
+ fs_1.default.writeFileSync(file, ambiguous);
89
+ expect(() => new codex_agents_1.CodexAgentsStrategy().injectGlobal({ markdown: 'new' })).toThrow('standalone');
90
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe(ambiguous);
91
+ });
92
+ it.each([
93
+ '<!-- AWM:BOUNDARY prefix=9 suffix=1 -->',
94
+ '<!-- AWM:BOUNDARY prefix=1 suffix=2 -->',
95
+ '<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\n<!-- AWM:BOUNDARY prefix=0 suffix=1 -->',
96
+ 'owned\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->',
97
+ '<!-- AWM:BOUNDARY prefix=1',
98
+ 'inline <!-- AWM:BOUNDARY prefix=1 suffix=1 -->',
99
+ ])('rejects ambiguous boundary metadata without changing the file: %j', (metadata) => {
100
+ const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
101
+ const materialized = path_1.default.join(tmpWork, 'awm-context.md');
102
+ const markdown = '# AWM\n\nExpected.';
103
+ const original = `<!-- AWM:START -->\n${metadata}\nowned\n<!-- AWM:END -->`;
104
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
105
+ fs_1.default.writeFileSync(file, original);
106
+ fs_1.default.writeFileSync(materialized, markdown);
107
+ const provider = codexProvider(file);
108
+ const input = injectionInput(materialized, markdown);
109
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
110
+ expect(() => strategy.inject(input, provider)).toThrow('malformed AWM boundary metadata');
111
+ expect(() => strategy.remove(input, provider)).toThrow('malformed AWM boundary metadata');
112
+ expect(() => strategy.status(input, provider)).toThrow('malformed AWM boundary metadata');
113
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe(original);
114
+ });
115
+ it('injects project constitution guidance without owning the whole AGENTS.md', () => {
116
+ const project = path_1.default.join(tmpWork, 'repo');
117
+ fs_1.default.mkdirSync(project, { recursive: true });
118
+ fs_1.default.writeFileSync(path_1.default.join(project, 'CONSTITUTION.md'), '# Rules\n');
119
+ fs_1.default.writeFileSync(path_1.default.join(project, 'AGENTS.md'), '# Repo-owned rules\n');
120
+ const result = new codex_agents_1.CodexAgentsStrategy().injectProject(project);
121
+ expect(result).toBe('injected');
122
+ const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
123
+ expect(written).toContain('# Repo-owned rules');
124
+ expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
125
+ });
126
+ it('creates project guidance even before CONSTITUTION.md exists and is idempotent', () => {
127
+ const project = path_1.default.join(tmpWork, 'repo');
128
+ fs_1.default.mkdirSync(project, { recursive: true });
129
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
130
+ expect(strategy.injectProject(project)).toBe('injected');
131
+ expect(strategy.injectProject(project)).toBe('unchanged');
132
+ expect(fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8'))
133
+ .toContain('when that file exists');
134
+ });
135
+ it('implements global status and remove while preserving user bytes', () => {
136
+ const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
137
+ const materialized = path_1.default.join(tmpWork, 'awm-context.md');
138
+ const markdown = '# AWM\n\nExpected.';
139
+ fs_1.default.writeFileSync(materialized, markdown);
140
+ const provider = codexProvider(file);
141
+ const input = injectionInput(materialized, markdown);
142
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
143
+ expect(strategy.status(input, provider)).toBe('absent');
144
+ expect(strategy.inject(input, provider)).toBe('injected');
145
+ expect(strategy.status(input, provider)).toBe('injected');
146
+ const staleInput = {
147
+ ...input,
148
+ ref: { ...input.ref, contentHash: (0, provider_1.sha256)('# AWM\n\nUpdated.') },
149
+ };
150
+ expect(strategy.status(staleInput, provider)).toBe('stale');
151
+ fs_1.default.appendFileSync(file, 'user suffix\n');
152
+ strategy.remove(input, provider);
153
+ expect(fs_1.default.readFileSync(file, 'utf8')).toBe('user suffix\n');
154
+ expect(strategy.status(input, provider)).toBe('absent');
155
+ });
156
+ it('validates public inputs and never writes outside the configured roots', () => {
157
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
158
+ expect(() => strategy.injectGlobal({ markdown: '' })).toThrow('markdown');
159
+ expect(() => strategy.injectGlobal(null)).toThrow('context');
160
+ expect(() => strategy.injectProject('')).toThrow('projectRoot');
161
+ const open = jest.spyOn(fs_1.default, 'openSync');
162
+ strategy.injectGlobal({ markdown: 'safe' });
163
+ strategy.injectProject(path_1.default.join(tmpWork, 'safe-project'));
164
+ const opened = open.mock.calls.map((call) => String(call[0]));
165
+ expect(opened.every((file) => file.startsWith(tmpHome) || file.startsWith(tmpWork))).toBe(true);
166
+ });
167
+ });
168
+ function codexProvider(globalPath) {
169
+ return {
170
+ label: 'Codex',
171
+ skill: { global: '', local: '', renderer: 'link' },
172
+ workflow: null,
173
+ agent: null,
174
+ injection: { type: 'managed-agents-md', globalPath, localFile: 'AGENTS.md' },
175
+ };
176
+ }
177
+ function injectionInput(absPath, markdown) {
178
+ return {
179
+ ref: { absPath, scope: 'global', contentHash: (0, provider_1.sha256)(markdown) },
180
+ registryRoot: '/registry',
181
+ installMethod: 'copy',
182
+ agent: 'codex',
183
+ scope: 'global',
184
+ };
185
+ }
@@ -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: '' }, workflow: null, agent: null,
21
+ label: 'OpenCode', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
22
22
  injection: { type: 'config-instructions', configPath, field: 'instructions' },
23
23
  };
24
24
  const input = {
@@ -184,6 +184,25 @@ describe('machineChecks — global skill integrity', () => {
184
184
  expect(row?.remedy).toEqual({ kind: 'command', value: 'awm init' });
185
185
  });
186
186
  });
187
+ describe('computeProviderOverall (Task 9)', () => {
188
+ const CHECK_IDS = ['binary.version', 'skills.global', 'agents.native', 'hook.trust', 'context.global'];
189
+ function providerWith(states) {
190
+ return {
191
+ id: 'codex',
192
+ label: 'Codex',
193
+ checks: states.map((state, i) => ({ id: CHECK_IDS[i % CHECK_IDS.length], state })),
194
+ };
195
+ }
196
+ it('healthy when every check is in an ok/pending/warn state', () => {
197
+ expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust', 'stale'])])).toBe('healthy');
198
+ });
199
+ it('degrades on a single broken/missing/absent/unsupported/conflict check', () => {
200
+ expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'absent'])])).toBe('degraded');
201
+ });
202
+ it('healthy for an empty provider list', () => {
203
+ expect((0, checks_1.computeProviderOverall)([])).toBe('healthy');
204
+ });
205
+ });
187
206
  describe('machine.context.<agent> checks', () => {
188
207
  function machineWith(contextInjection) {
189
208
  return { ...healthyMachine(), contextInjection };
@@ -75,6 +75,34 @@ describe('gatherContext', () => {
75
75
  expect(ctx.machine.devCore.present).toBe(true);
76
76
  expect(ctx.machine.devCore.brokenLinks).toContain('brainstorming');
77
77
  });
78
+ // Regression coverage for the second BLOCKER-adjacent bug found while
79
+ // building the real Codex+OpenCode coexistence E2E test: OpenCode and
80
+ // Codex share ~/.agents/skills, but agent-type artifacts are per-agent
81
+ // (never shared, R12/R13). Before this fix, devCorePresent only looked
82
+ // at the skill link, so once OpenCode's own init had already linked the
83
+ // shared skill, Codex's gatherContext wrongly reported devCore as fully
84
+ // present/no-broken-links even though Codex's own native .toml had never
85
+ // been rendered — causing stepDevCore to skip forever.
86
+ it('machine: devCore surfaces a missing per-agent artifact even when the shared skill link is already present', () => {
87
+ const bundleWithAgent = () => ({
88
+ name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
89
+ dependsOn: [], skills: [{ name: 'development-process', onSignal: false }],
90
+ workflows: [], agents: ['development-process'],
91
+ });
92
+ // Simulate OpenCode having already linked the shared ~/.agents/skills dir.
93
+ const sharedSkillsDir = path_1.default.join(tmpHome, '.agents', 'skills');
94
+ fs_1.default.mkdirSync(sharedSkillsDir, { recursive: true });
95
+ const target = path_1.default.join(tmpHome, 'targets', 'development-process');
96
+ fs_1.default.mkdirSync(target, { recursive: true });
97
+ fs_1.default.symlinkSync(target, path_1.default.join(sharedSkillsDir, 'development-process'), 'dir');
98
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
99
+ const ctx = gatherContext({ cwd: tmpHome, bundles: [bundleWithAgent()], agent: 'codex' });
100
+ // The skill itself is linked (shared with OpenCode)...
101
+ expect(ctx.machine.devCore.present).toBe(true);
102
+ // ...but Codex's own native agent artifact was never installed, so it
103
+ // must surface as a broken/missing link — NOT a clean, skippable state.
104
+ expect(ctx.machine.devCore.brokenLinks).toContain('development-process.toml');
105
+ });
78
106
  it('machine: ambient wanted read from ~/.awm/config.json, installed reflects links', () => {
79
107
  fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
80
108
  fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'config.json'), JSON.stringify({ ambient: ['personal-notion'] }));
@@ -144,6 +172,52 @@ describe('gatherContext', () => {
144
172
  expect(ctx.project.context).toEqual({ present: true, file: 'CLAUDE.md' });
145
173
  });
146
174
  });
175
+ describe('gatherContext — providers matrix (Task 9)', () => {
176
+ let tmpHome;
177
+ let originalHome;
178
+ let originalAwmHome;
179
+ beforeEach(() => {
180
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-providers-'));
181
+ originalHome = process.env.HOME;
182
+ originalAwmHome = process.env.AWM_HOME;
183
+ process.env.HOME = tmpHome;
184
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
185
+ jest.resetModules();
186
+ });
187
+ afterEach(() => {
188
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
189
+ if (originalHome === undefined)
190
+ delete process.env.HOME;
191
+ else
192
+ process.env.HOME = originalHome;
193
+ if (originalAwmHome === undefined)
194
+ delete process.env.AWM_HOME;
195
+ else
196
+ process.env.AWM_HOME = originalAwmHome;
197
+ });
198
+ function healthySharedSkills() {
199
+ return { valid: ['development-process'], repairable: [], dead: [] };
200
+ }
201
+ it('reports shared skills for both owners without scanning twice', () => {
202
+ // OpenCode and Codex both read/write ~/.agents/skills (providers/index.ts) —
203
+ // the physical directory must be scanned once and attributed to both owners.
204
+ const scan = jest.fn(() => healthySharedSkills());
205
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
206
+ const report = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills: scan });
207
+ expect(scan).toHaveBeenCalledTimes(1);
208
+ expect(report.providers.every((provider) => provider.checks.some((check) => check.state === 'shared'))).toBe(true);
209
+ });
210
+ it('marks skills.global healthy (not shared) for a single unshared provider', () => {
211
+ const scan = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
212
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
213
+ const report = gatherContext({ cwd: tmpHome, bundles: [], agents: ['claude-code'], scanSkills: scan });
214
+ expect(scan).toHaveBeenCalledTimes(1);
215
+ const claude = report.providers.find((p) => p.id === 'claude-code');
216
+ const skillsCheck = claude.checks.find((c) => c.id === 'skills.global');
217
+ expect(skillsCheck.state).not.toBe('shared');
218
+ expect(skillsCheck.owners).toBeUndefined();
219
+ });
220
+ });
147
221
  describe('gatherMachine — agnostic skill health (#4)', () => {
148
222
  it('classifies the target agent skills dir, not always Claude', () => {
149
223
  const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
@@ -0,0 +1,252 @@
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/core/diagnostics/provider-checks.test.ts
7
+ //
8
+ // Code-quality review follow-up on commit 8f02a00. Two real, reproduced defects:
9
+ //
10
+ // 1. CRITICAL — agentsNativeCheck/hookTrustCheck/contextGlobalCheck returned
11
+ // `state: 'unsupported'` to mean "doesn't structurally apply to this
12
+ // provider" (Antigravity has no agent/hooks/injection config, OpenCode has
13
+ // no hooks, Claude Code's context.global is covered by its hook). But
14
+ // 'unsupported' is ALSO what binaryVersionCheck uses for a genuine
15
+ // version-incompatibility failure, and both doctor's glyph logic and
16
+ // checks.ts's DEGRADING_PROVIDER_STATES treat it as a real failure. A
17
+ // fully-healthy claude-code-only (or opencode-only, or antigravity-only)
18
+ // `awm doctor` run could therefore never report `overall: 'healthy'`. Fixed
19
+ // by having those three functions return `null` (omitted entirely from
20
+ // `checks[]`) instead of a fabricated 'unsupported' row — this file proves
21
+ // the fix with realistic, fully-initialized single-provider fixtures, not
22
+ // just synthetic state-literal inputs.
23
+ //
24
+ // 2. IMPORTANT — skillsGlobalCheck set `state: 'shared'` unconditionally once
25
+ // a skills directory had more than one owner, even when it had broken/dead
26
+ // symlinks — masking real breakage behind a green checkmark and never
27
+ // degrading `overall`. Fixed by checking broken status BEFORE falling back
28
+ // to 'shared'.
29
+ const fs_1 = __importDefault(require("fs"));
30
+ const os_1 = __importDefault(require("os"));
31
+ const path_1 = __importDefault(require("path"));
32
+ describe('gatherProviderChecks — realistic fully-healthy single-provider fixtures', () => {
33
+ let tmpHome;
34
+ let tmpWork;
35
+ let originalHome;
36
+ let originalAwmHome;
37
+ let writeSpy;
38
+ beforeEach(() => {
39
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-home-'));
40
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-work-'));
41
+ originalHome = process.env.HOME;
42
+ originalAwmHome = process.env.AWM_HOME;
43
+ process.env.HOME = tmpHome;
44
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
45
+ jest.resetModules();
46
+ writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
47
+ });
48
+ afterEach(() => {
49
+ writeSpy.mockRestore();
50
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
51
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
52
+ if (originalHome === undefined)
53
+ delete process.env.HOME;
54
+ else
55
+ process.env.HOME = originalHome;
56
+ if (originalAwmHome === undefined)
57
+ delete process.env.AWM_HOME;
58
+ else
59
+ process.env.AWM_HOME = originalAwmHome;
60
+ });
61
+ /** Seeds a registry content root with a baseline bundle carrying a skill
62
+ * and (optionally) a canonical agent artifact. Mirrors the fixture in
63
+ * tests/integration/codex-provider-isolated.test.ts. */
64
+ function seedRegistry(root, opts) {
65
+ if (opts.withHooks) {
66
+ fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
67
+ fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
68
+ fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/run-hook.cmd'), '#!/bin/sh\nexec sh "$1"\n', { mode: 0o755 });
69
+ }
70
+ fs_1.default.mkdirSync(path_1.default.join(root, 'skills/using-awm'), { recursive: true });
71
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.');
72
+ fs_1.default.mkdirSync(path_1.default.join(root, 'skills/development-process'), { recursive: true });
73
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\n---\nBody.');
74
+ const agents = [];
75
+ if (opts.withAgent) {
76
+ agents.push('development-process');
77
+ fs_1.default.mkdirSync(path_1.default.join(root, 'agents'), { recursive: true });
78
+ fs_1.default.writeFileSync(path_1.default.join(root, 'agents/development-process.md'), '---\nname: development-process\ndescription: Orchestrates the dev lifecycle.\n---\nBody.');
79
+ }
80
+ fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
81
+ version: 1,
82
+ bundles: [{ name: 'dev-core', source: 'bundles/dev-core', version: '1.0.0', scope: 'baseline', visibility: 'public' }],
83
+ }));
84
+ fs_1.default.mkdirSync(path_1.default.join(root, 'bundles/dev-core'), { recursive: true });
85
+ fs_1.default.writeFileSync(path_1.default.join(root, 'bundles/dev-core/bundle.json'), JSON.stringify({
86
+ name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
87
+ dependsOn: [], skills: ['development-process'], workflows: [], agents,
88
+ }));
89
+ fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
90
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
91
+ }
92
+ it('claude-code-only, fully initialized via a real `awm init`, reports overall: healthy', async () => {
93
+ seedRegistry(path_1.default.join(tmpHome, '.awm/registries/baseline'), { withAgent: true, withHooks: true });
94
+ const { runInit } = require('../../../src/commands/init');
95
+ const code = await runInit({ cwd: tmpWork, yes: true });
96
+ expect(code).toBeLessThanOrEqual(1);
97
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
98
+ const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
99
+ const ctx = gatherContext({ cwd: tmpWork, agents: ['claude-code'] });
100
+ expect(computeProviderOverall(ctx.providers)).toBe('healthy');
101
+ const claude = ctx.providers[0];
102
+ // Claude's context.global is deliberately omitted (covered by hook.trust),
103
+ // not rendered as a fabricated 'unsupported' row.
104
+ expect(claude.checks.map((c) => c.id)).not.toContain('context.global');
105
+ expect(claude.checks.every((c) => !['unsupported', 'missing', 'broken', 'absent', 'conflict'].includes(c.state))).toBe(true);
106
+ });
107
+ it('opencode-only, fully installed via the real install/injection pipeline, reports overall: healthy', () => {
108
+ seedRegistry(path_1.default.join(tmpHome, '.awm/registries/baseline'), { withAgent: true, withHooks: false });
109
+ // OpenCode has no hook config — routed directly through the same real
110
+ // installBundle/InjectionOrchestrator functions `awm init` itself calls,
111
+ // rather than through runInit's step orchestration (a separate,
112
+ // pre-existing gap — stepHook unconditionally attempts a hook install
113
+ // for every agent, including ones with no hook mechanism at all — is out
114
+ // of scope for this fix and reported separately).
115
+ const { installBundle } = require('../../../src/core/bundle-install');
116
+ const { discoverAllBundles } = require('../../../src/core/bundles');
117
+ const { contentRoots } = require('../../../src/core/registries');
118
+ const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
119
+ const bundles = discoverAllBundles();
120
+ const contentDir = contentRoots()[0];
121
+ installBundle({ bundleName: 'dev-core', bundles, agents: ['opencode'], method: 'symlink', projectRoot: tmpWork, contentDir });
122
+ new InjectionOrchestrator().installContext({
123
+ agent: 'opencode', scope: 'global', registryRoot: contentDir, installMethod: 'symlink', profileExtensions: [],
124
+ });
125
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
126
+ const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
127
+ const ctx = gatherContext({ cwd: tmpWork, agents: ['opencode'] });
128
+ expect(computeProviderOverall(ctx.providers)).toBe('healthy');
129
+ const opencode = ctx.providers[0];
130
+ // OpenCode has no hooks — omitted, not a fabricated 'unsupported' row.
131
+ expect(opencode.checks.map((c) => c.id)).not.toContain('hook.trust');
132
+ });
133
+ it('antigravity-only, fully installed via the real install pipeline, reports overall: healthy', () => {
134
+ seedRegistry(path_1.default.join(tmpHome, '.awm/registries/baseline'), { withAgent: false, withHooks: false });
135
+ const { installBundle } = require('../../../src/core/bundle-install');
136
+ const { discoverAllBundles } = require('../../../src/core/bundles');
137
+ const { contentRoots } = require('../../../src/core/registries');
138
+ const bundles = discoverAllBundles();
139
+ installBundle({ bundleName: 'dev-core', bundles, agents: ['antigravity'], method: 'symlink', projectRoot: tmpWork, contentDir: contentRoots()[0] });
140
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
141
+ const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
142
+ const ctx = gatherContext({ cwd: tmpWork, agents: ['antigravity'] });
143
+ expect(computeProviderOverall(ctx.providers)).toBe('healthy');
144
+ const antigravity = ctx.providers[0];
145
+ // Antigravity has no agent/hooks/injection config at all — all three
146
+ // checks omitted, not fabricated 'unsupported' rows. Only binary.version
147
+ // and skills.global structurally apply.
148
+ expect(antigravity.checks.map((c) => c.id).sort())
149
+ .toEqual(['binary.version', 'skills.global']);
150
+ });
151
+ });
152
+ describe('gatherProviderChecks — shared skills.global does not mask broken links', () => {
153
+ let tmpHome;
154
+ let originalHome;
155
+ let originalAwmHome;
156
+ beforeEach(() => {
157
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-shared-'));
158
+ originalHome = process.env.HOME;
159
+ originalAwmHome = process.env.AWM_HOME;
160
+ process.env.HOME = tmpHome;
161
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
162
+ jest.resetModules();
163
+ });
164
+ afterEach(() => {
165
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
166
+ if (originalHome === undefined)
167
+ delete process.env.HOME;
168
+ else
169
+ process.env.HOME = originalHome;
170
+ if (originalAwmHome === undefined)
171
+ delete process.env.AWM_HOME;
172
+ else
173
+ process.env.AWM_HOME = originalAwmHome;
174
+ });
175
+ it('reports a degraded (not shared) skills.global state when the shared dir has broken links', () => {
176
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
177
+ const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
178
+ // OpenCode and Codex share ~/.agents/skills (providers/index.ts). Stub the
179
+ // scan to report 2 broken/dead links on that shared directory.
180
+ const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: ['stale-skill'], dead: ['ghost-skill'] }));
181
+ const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
182
+ expect(scanSkills).toHaveBeenCalledTimes(1);
183
+ for (const provider of ctx.providers) {
184
+ const skillsCheck = provider.checks.find((c) => c.id === 'skills.global');
185
+ expect(skillsCheck.state).toBe('broken'); // NOT 'shared' — must not mask the breakage
186
+ expect(skillsCheck.owners).toEqual(['opencode', 'codex']); // still reports the shared ownership
187
+ expect(skillsCheck.detail).toContain('2 broken links');
188
+ expect(skillsCheck.remediationCode).toBe('repair-global-skills');
189
+ }
190
+ expect(computeProviderOverall(ctx.providers)).toBe('degraded');
191
+ });
192
+ it('still reports shared (healthy, non-degrading) when the shared dir has no broken links', () => {
193
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
194
+ const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
195
+ const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: [], dead: [] }));
196
+ const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
197
+ for (const provider of ctx.providers) {
198
+ const skillsCheck = provider.checks.find((c) => c.id === 'skills.global');
199
+ expect(skillsCheck.state).toBe('shared');
200
+ }
201
+ // Other checks (binary.version missing codex, hook.trust absent, etc.) may
202
+ // still degrade `overall` on a bare HOME — this test only pins that a
203
+ // HEALTHY shared skills dir itself reports 'shared', not a false positive.
204
+ const skillsStates = ctx.providers.map((p) => p.checks.find((c) => c.id === 'skills.global').state);
205
+ expect(skillsStates).toEqual(['shared', 'shared']);
206
+ });
207
+ });
208
+ describe('gatherProviderChecks — agents.native reports broken on a malformed Codex .toml', () => {
209
+ let tmpHome;
210
+ let originalHome;
211
+ let originalAwmHome;
212
+ beforeEach(() => {
213
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-broken-agent-'));
214
+ originalHome = process.env.HOME;
215
+ originalAwmHome = process.env.AWM_HOME;
216
+ process.env.HOME = tmpHome;
217
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
218
+ jest.resetModules();
219
+ });
220
+ afterEach(() => {
221
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
222
+ if (originalHome === undefined)
223
+ delete process.env.HOME;
224
+ else
225
+ process.env.HOME = originalHome;
226
+ if (originalAwmHome === undefined)
227
+ delete process.env.AWM_HOME;
228
+ else
229
+ process.env.AWM_HOME = originalAwmHome;
230
+ });
231
+ it('reports state: broken and remediationCode: reinstall-native-agents for a hand-edited/truncated .toml', () => {
232
+ // Codex's agent renderer is 'codex-agent-toml' (providers/index.ts) — its global
233
+ // dir is ~/.codex/agents. tomlAgentsHealthy (provider-checks.ts) treats a .toml as
234
+ // broken when it can't be read, or when it's missing the `developer_instructions = `
235
+ // key that renderCodexAgent (Task 4) always emits — i.e. hand-edited/truncated.
236
+ const agentsDir = path_1.default.join(tmpHome, '.codex/agents');
237
+ fs_1.default.mkdirSync(agentsDir, { recursive: true });
238
+ fs_1.default.writeFileSync(path_1.default.join(agentsDir, 'development-process.toml'), 'name = "development-process"\n# missing the developer_instructions key entirely\n');
239
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
240
+ const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
241
+ const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['codex'], scanSkills });
242
+ const codex = ctx.providers.find((p) => p.id === 'codex');
243
+ const agentsNative = codex.checks.find((c) => c.id === 'agents.native');
244
+ expect(agentsNative).toMatchObject({
245
+ id: 'agents.native',
246
+ state: 'broken',
247
+ target: agentsDir,
248
+ detail: '1 malformed .toml',
249
+ remediationCode: 'reinstall-native-agents',
250
+ });
251
+ });
252
+ });
@@ -41,4 +41,24 @@ describe('Executor Engine', () => {
41
41
  const dest = path_1.default.join(targetDir, 'does-not-exist');
42
42
  expect(() => (0, executor_1.removeArtifact)(dest)).toThrow('Artifact not found');
43
43
  });
44
+ it('never removes the live target before staging succeeds', () => {
45
+ const missingSource = path_1.default.join(sourceDir, 'does-not-exist-source');
46
+ const target = path_1.default.join(targetDir, 'target');
47
+ fs_1.default.mkdirSync(target, { recursive: true });
48
+ fs_1.default.writeFileSync(path_1.default.join(target, 'sentinel'), 'before');
49
+ expect(() => (0, executor_1.installArtifact)(missingSource, target, 'copy')).toThrow('Source path does not exist');
50
+ expect(fs_1.default.readFileSync(path_1.default.join(target, 'sentinel'), 'utf8')).toBe('before'); // verifies R17
51
+ });
52
+ it('stageArtifact stages next to the target without touching it, replaceArtifact swaps it in', () => {
53
+ const target = path_1.default.join(targetDir, 'staged-target');
54
+ fs_1.default.mkdirSync(target, { recursive: true });
55
+ fs_1.default.writeFileSync(path_1.default.join(target, 'sentinel'), 'before');
56
+ const staged = (0, executor_1.stageArtifact)(sourceDir, target, 'copy');
57
+ expect(fs_1.default.readFileSync(path_1.default.join(target, 'sentinel'), 'utf8')).toBe('before'); // untouched pre-replace
58
+ expect(fs_1.default.existsSync(path_1.default.join(staged, 'test.txt'))).toBe(true);
59
+ (0, executor_1.replaceArtifact)(staged, target);
60
+ expect(fs_1.default.existsSync(path_1.default.join(target, 'sentinel'))).toBe(false);
61
+ expect(fs_1.default.existsSync(path_1.default.join(target, 'test.txt'))).toBe(true);
62
+ expect(fs_1.default.existsSync(staged)).toBe(false);
63
+ });
44
64
  });
@@ -91,9 +91,10 @@ describe('runExport (engine end-to-end)', () => {
91
91
  const originalCwd = process.cwd();
92
92
  process.chdir(cwdTmp);
93
93
  try {
94
+ const actualCwd = process.cwd();
94
95
  const summary = (0, export_1.runExport)({ name: 'mermaid', roots: [root], zip: okZip }); // sin `out`
95
- expect(summary.outDir).toBe(path_1.default.join(cwdTmp, 'awm-export', 'claude-ai'));
96
- expect(fs_1.default.existsSync(path_1.default.join(cwdTmp, 'awm-export/claude-ai/mermaid/SKILL.md'))).toBe(true);
96
+ expect(summary.outDir).toBe(path_1.default.join(actualCwd, 'awm-export', 'claude-ai'));
97
+ expect(fs_1.default.existsSync(path_1.default.join(actualCwd, 'awm-export/claude-ai/mermaid/SKILL.md'))).toBe(true);
97
98
  }
98
99
  finally {
99
100
  process.chdir(originalCwd);