agentic-workflow-manager 3.1.0 → 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 (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 +164 -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 +129 -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 +206 -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,165 @@
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
+ // tests/core/reconciliation.test.ts
7
+ //
8
+ // Exercises the REAL planReconciliation (no mocks) end-to-end: seeds a real
9
+ // registry content root with a baseline bundle, an ambient bundle, and a
10
+ // project-scope bundle, then verifies the computed plan only reconciles the
11
+ // machine-scope (baseline + ambient) artifacts — and, applied via the real
12
+ // applyInstallPlan, actually fixes both a MISSING target and a STALE target.
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const path_1 = __importDefault(require("path"));
16
+ function writeBundle(contentRoot, name, scope, skills, dependsOn = []) {
17
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'bundles', name), { recursive: true });
18
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'bundles', name, 'bundle.json'), JSON.stringify({
19
+ name, version: '1.0.0', scope, dependsOn,
20
+ skills: skills.map((s) => ({ name: s })), workflows: [], agents: [],
21
+ }));
22
+ for (const skill of skills) {
23
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills', skill), { recursive: true });
24
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'skills', skill, 'SKILL.md'), `# ${skill}`);
25
+ }
26
+ }
27
+ function addToCatalog(contentRoot, name, scope) {
28
+ const catalogFile = path_1.default.join(contentRoot, 'catalog.json');
29
+ const catalog = JSON.parse(fs_1.default.readFileSync(catalogFile, 'utf-8'));
30
+ catalog.bundles.push({ name, source: `./bundles/${name}`, version: '1.0.0', scope });
31
+ fs_1.default.writeFileSync(catalogFile, JSON.stringify(catalog));
32
+ }
33
+ describe('planReconciliation (real, unmocked)', () => {
34
+ let tmpHome;
35
+ let contentRoot;
36
+ let originalHome;
37
+ let originalAwmHome;
38
+ beforeEach(() => {
39
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-reconcile-'));
40
+ originalHome = process.env.HOME;
41
+ originalAwmHome = process.env.AWM_HOME;
42
+ process.env.HOME = tmpHome;
43
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
44
+ jest.resetModules();
45
+ contentRoot = path_1.default.join(tmpHome, 'registry');
46
+ fs_1.default.mkdirSync(contentRoot, { recursive: true });
47
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'catalog.json'), JSON.stringify({
48
+ version: 1,
49
+ bundles: [
50
+ { name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' },
51
+ { name: 'ambient-x', source: './bundles/ambient-x', version: '1.0.0', scope: 'ambient' },
52
+ { name: 'proj-y', source: './bundles/proj-y', version: '1.0.0', scope: 'project' },
53
+ ],
54
+ }));
55
+ writeBundle(contentRoot, 'dev', 'baseline', ['brainstorming']);
56
+ writeBundle(contentRoot, 'ambient-x', 'ambient', ['ambient-skill']);
57
+ writeBundle(contentRoot, 'proj-y', 'project', ['proj-skill']);
58
+ });
59
+ afterEach(() => {
60
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
61
+ if (originalHome === undefined)
62
+ delete process.env.HOME;
63
+ else
64
+ process.env.HOME = originalHome;
65
+ if (originalAwmHome === undefined)
66
+ delete process.env.AWM_HOME;
67
+ else
68
+ process.env.AWM_HOME = originalAwmHome;
69
+ });
70
+ it('plans only the machine-scope (baseline + ambient) artifacts, excluding project-scope bundles', () => {
71
+ const { planReconciliation } = require('../../src/core/reconciliation');
72
+ const plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
73
+ const names = plan.operations.map((op) => op.name).sort();
74
+ expect(names).toEqual(['ambient-skill', 'brainstorming']); // proj-skill excluded (R — project bundles are `awm sync`'s job, not `awm update`'s)
75
+ const claudeSkillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
76
+ const targets = plan.operations.map((op) => op.targetPath).sort();
77
+ expect(targets).toEqual([
78
+ path_1.default.join(claudeSkillsDir, 'ambient-skill'),
79
+ path_1.default.join(claudeSkillsDir, 'brainstorming'),
80
+ ]);
81
+ });
82
+ it('reconciles drift end-to-end when applied: installs a MISSING target and replaces a STALE one', () => {
83
+ const claudeSkillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
84
+ fs_1.default.mkdirSync(claudeSkillsDir, { recursive: true });
85
+ // Drift scenario 1 — stale: 'brainstorming' exists but as a plain file
86
+ // with the wrong content (as if it were copied once and the registry
87
+ // has since diverged), not a symlink to the registry source.
88
+ fs_1.default.writeFileSync(path_1.default.join(claudeSkillsDir, 'brainstorming'), 'stale, wrong content');
89
+ // Drift scenario 2 — missing: 'ambient-skill' was never installed at all.
90
+ expect(fs_1.default.existsSync(path_1.default.join(claudeSkillsDir, 'ambient-skill'))).toBe(false);
91
+ const { planReconciliation } = require('../../src/core/reconciliation');
92
+ const { applyInstallPlan } = require('../../src/core/install-transaction');
93
+ const plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
94
+ const summary = applyInstallPlan(plan);
95
+ expect(summary.installed.sort()).toEqual([
96
+ 'ambient-skill → claude-code',
97
+ 'brainstorming → claude-code',
98
+ ]);
99
+ // Stale target is now a real symlink to the registry source, not the old file.
100
+ const brainstormingStat = fs_1.default.lstatSync(path_1.default.join(claudeSkillsDir, 'brainstorming'));
101
+ expect(brainstormingStat.isSymbolicLink()).toBe(true);
102
+ expect(fs_1.default.realpathSync(path_1.default.join(claudeSkillsDir, 'brainstorming')))
103
+ .toBe(fs_1.default.realpathSync(path_1.default.join(contentRoot, 'skills', 'brainstorming')));
104
+ // Missing target now exists as a symlink too.
105
+ const ambientStat = fs_1.default.lstatSync(path_1.default.join(claudeSkillsDir, 'ambient-skill'));
106
+ expect(ambientStat.isSymbolicLink()).toBe(true);
107
+ expect(fs_1.default.realpathSync(path_1.default.join(claudeSkillsDir, 'ambient-skill')))
108
+ .toBe(fs_1.default.realpathSync(path_1.default.join(contentRoot, 'skills', 'ambient-skill')));
109
+ });
110
+ it('scopes to the selected agent target only', () => {
111
+ const { planReconciliation } = require('../../src/core/reconciliation');
112
+ const plan = planReconciliation({ targets: ['opencode'], roots: [contentRoot] });
113
+ const owners = new Set(plan.operations.flatMap((op) => op.owners));
114
+ expect(owners).toEqual(new Set(['opencode']));
115
+ });
116
+ // NOTE on the two tests below: `resolveBundleClosure` (bundles.ts) already
117
+ // treats a `dependsOn` entry naming a bundle that isn't in the catalog as
118
+ // silently absent from the closure — it does NOT throw (verified directly:
119
+ // a baseline bundle with `dependsOn: ['does-not-exist']` resolves its own
120
+ // artifacts fine; the ghost name is just dropped). So a plain dangling
121
+ // `dependsOn` string never reaches `planReconciliation`'s
122
+ // `try { expandBundleArtifacts(...) } catch { continue; }` guard at all —
123
+ // the first test below confirms that directly. To actually force the catch
124
+ // branch (and check it skips only the one broken bundle, not the whole
125
+ // batch, and doesn't silently eat some other bug), the second test injects
126
+ // a bundle whose `dependsOn` field is malformed in a way that genuinely
127
+ // breaks closure resolution (`dependsOn` is not an array, so
128
+ // `resolveBundleClosure`'s `for (const dep of b.dependsOn)` throws) —
129
+ // mirroring the kind of real, unresolvable-closure error the catch is
130
+ // meant to guard against.
131
+ it('tolerates a plain dangling dependsOn reference without needing the catch — the bundle installs normally', () => {
132
+ writeBundle(contentRoot, 'dangling-dep', 'baseline', ['dangling-skill'], ['does-not-exist']);
133
+ addToCatalog(contentRoot, 'dangling-dep', 'baseline');
134
+ const { planReconciliation } = require('../../src/core/reconciliation');
135
+ const plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
136
+ const names = plan.operations.map((op) => op.name).sort();
137
+ // 'dangling-skill' installs fine even though its bundle's dependsOn
138
+ // points nowhere — resolveBundleClosure just drops the missing dep.
139
+ expect(names).toEqual(['ambient-skill', 'brainstorming', 'dangling-skill']);
140
+ });
141
+ it('skips a bundle whose closure genuinely fails to resolve, without aborting reconciliation for the rest', () => {
142
+ // Malformed dependsOn (not an array) makes resolveBundleClosure throw
143
+ // when it tries to iterate it — a real "unresolvable closure" error,
144
+ // unlike a plain dangling name reference (see test above).
145
+ writeBundle(contentRoot, 'broken', 'baseline', ['broken-skill'], 5);
146
+ addToCatalog(contentRoot, 'broken', 'baseline');
147
+ const { planReconciliation } = require('../../src/core/reconciliation');
148
+ let plan;
149
+ expect(() => {
150
+ plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
151
+ }).not.toThrow();
152
+ // The healthy bundles (baseline 'dev' + ambient 'ambient-x') still
153
+ // reconcile correctly.
154
+ const names = plan.operations.map((op) => op.name).sort();
155
+ expect(names).toEqual(['ambient-skill', 'brainstorming']);
156
+ // The broken bundle is cleanly absent — no partial/broken entry for
157
+ // 'broken-skill' or any operation touching it.
158
+ expect(names).not.toContain('broken-skill');
159
+ expect(plan.operations.some((op) => op.sourcePath?.includes('broken'))).toBe(false);
160
+ // Applying the plan end-to-end still works (no dangling reference to
161
+ // the broken bundle leaked into the plan).
162
+ const { applyInstallPlan } = require('../../src/core/install-transaction');
163
+ expect(() => applyInstallPlan(plan)).not.toThrow();
164
+ });
165
+ });
@@ -93,7 +93,7 @@ describe('syncRegistries (git fixtures locales)', () => {
93
93
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
94
94
  const awmDir = path_1.default.join(tmpHome, '.awm');
95
95
  fs_1.default.mkdirSync(awmDir, { recursive: true });
96
- fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '9.9.9' } }));
96
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '9.9.9' } }));
97
97
  const results = await m.syncRegistries();
98
98
  expect(results[0].action).toBe('error');
99
99
  // El dir no debe quedar en disco tras el fallo
@@ -110,7 +110,7 @@ describe('syncRegistries (git fixtures locales)', () => {
110
110
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
111
111
  const awmDir = path_1.default.join(tmpHome, '.awm');
112
112
  fs_1.default.mkdirSync(awmDir, { recursive: true });
113
- fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
113
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
114
114
  const results = await m.syncRegistries();
115
115
  expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
116
116
  });
@@ -174,7 +174,7 @@ describe('syncRegistries (git fixtures locales)', () => {
174
174
  expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0' }]);
175
175
  // establece pin a la versión anterior
176
176
  const awmDir = path_1.default.join(tmpHome, '.awm');
177
- fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
177
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
178
178
  // segunda sync con pin: retrocede a v1.0.0
179
179
  const r2 = await m.syncRegistries();
180
180
  expect(r2).toEqual([{ name: 'personal', action: 'pulled', version: 'v1.0.0' }]);
@@ -188,7 +188,7 @@ describe('syncRegistries (git fixtures locales)', () => {
188
188
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
189
189
  const awmDir = path_1.default.join(tmpHome, '.awm');
190
190
  fs_1.default.mkdirSync(awmDir, { recursive: true });
191
- fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
191
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
192
192
  // primera sync en canal dev: queda en HEAD (no en el tag)
193
193
  const r1 = await m.syncRegistries();
194
194
  expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const canonical_agent_1 = require("../../../src/core/renderers/canonical-agent");
4
+ // Real canonical agent content from the awm-baseline-registry (registries/baseline/agents/development-process.md),
5
+ // which the installer materializes at ~/.awm/registries/baseline/agents/development-process.md.
6
+ const canonicalWithModePrimary = `---
7
+ name: development-process
8
+ description: Use as agent profile to orchestrate the development lifecycle - invokes the development-process skill which contains the full orchestration logic
9
+ mode: primary
10
+ ---
11
+
12
+ # Development Process Orchestrator
13
+
14
+ You are a development orchestrator. You do NOT write code directly.
15
+
16
+ ## On Every Conversation Start
17
+
18
+ 1. **Invoke the \`development-process\` skill.** This skill contains the complete orchestration logic: state detection, lifecycle phases, decision rules, and the full catalog of available skills.
19
+ 2. Follow the skill's instructions exactly - it will guide you through identifying project state, recommending the next phase, and delegating to the correct skill.
20
+
21
+ ## Rules
22
+
23
+ - NEVER start writing code without first invoking \`development-process\`
24
+ - NEVER duplicate orchestration logic here - the skill is the single source of truth
25
+ - NEVER invoke a downstream skill without user approval
26
+ `;
27
+ it('parses name, description, and instructions from valid frontmatter', () => {
28
+ const source = `---
29
+ name: my-agent
30
+ description: Does a thing
31
+ ---
32
+
33
+ Body text here.
34
+ `;
35
+ expect((0, canonical_agent_1.parseCanonicalAgent)(source)).toEqual({
36
+ name: 'my-agent',
37
+ description: 'Does a thing',
38
+ instructions: 'Body text here.',
39
+ });
40
+ });
41
+ it('ignores provider-only mode while retaining canonical instructions', () => {
42
+ const parsed = (0, canonical_agent_1.parseCanonicalAgent)(canonicalWithModePrimary);
43
+ expect(parsed).toEqual({
44
+ name: 'development-process',
45
+ description: 'Use as agent profile to orchestrate the development lifecycle - invokes the development-process skill which contains the full orchestration logic',
46
+ instructions: expect.stringContaining('You do NOT write code directly.'),
47
+ }); // verifies R8, R9
48
+ });
49
+ it.each([
50
+ ['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
51
+ ['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
52
+ ['---\nname: ok\ndescription: x\n---\n', 'non-empty instruction body'],
53
+ ['name: ok', 'frontmatter'],
54
+ ['---\nname: ok\nnot a field\n---\nbody', 'invalid canonical agent frontmatter line'],
55
+ ['---\nname: ok\ndescription: first\ndescription: second\n---\nbody', 'duplicate canonical agent frontmatter key: description'],
56
+ ])('rejects invalid canonical agent sources', (source, message) => {
57
+ expect(() => (0, canonical_agent_1.parseCanonicalAgent)(source)).toThrow(message); // verifies R17
58
+ });
@@ -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
+ });