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.
- package/dist/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +179 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +97 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +3 -2
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +177 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +235 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- 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,235 @@
|
|
|
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('initializes Codex on a machine that has never run a Claude Code init', async () => {
|
|
195
|
+
// The E2E above inits claude-code FIRST, so ~/.awm/hooks always exists
|
|
196
|
+
// by the time Codex runs — which is precisely why it never caught this.
|
|
197
|
+
// A fresh Codex Cloud box has no Claude install at all, and installing
|
|
198
|
+
// the Codex hook creates ~/.awm/hooks as a side effect of the recursive
|
|
199
|
+
// mkdir for its own nested ~/.awm/hooks/codex. That flipped Claude's
|
|
200
|
+
// R19 baseline digest from absent to present-but-empty and aborted init
|
|
201
|
+
// with "Claude Code baseline changed during a non-Claude init".
|
|
202
|
+
seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
|
|
203
|
+
const { runInit } = require('../../src/commands/init');
|
|
204
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/hooks'))).toBe(false);
|
|
205
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.claude'))).toBe(false);
|
|
206
|
+
const codex = await runInit({
|
|
207
|
+
cwd: tmpWork,
|
|
208
|
+
yes: true,
|
|
209
|
+
agent: 'codex',
|
|
210
|
+
machineOnly: true,
|
|
211
|
+
assertProviderSupported: () => ({ provider: 'codex', version: '0.150.0' }),
|
|
212
|
+
});
|
|
213
|
+
// Exit 2 is the internal-error path the R19 guard aborts through.
|
|
214
|
+
expect(codex).toBeLessThanOrEqual(1);
|
|
215
|
+
expect(readPrefs().enabledAgents).toEqual(['codex']);
|
|
216
|
+
// The Codex hook really installed, and Claude gained no content of its own.
|
|
217
|
+
const hooksJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.codex/hooks.json'), 'utf8'));
|
|
218
|
+
expect(hooksJson.hooks.SessionStart).toHaveLength(1);
|
|
219
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/hooks/codex/session-start'))).toBe(true);
|
|
220
|
+
expect(fs_1.default.readdirSync(path_1.default.join(tmpHome, '.awm/hooks'))).toEqual(['codex']);
|
|
221
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.claude/settings.json'))).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
it('doctor reports the isolated Codex install as supported/healthy, not against the real ~/.awm', () => {
|
|
224
|
+
seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
|
|
225
|
+
writePrefs(prefsWith(['claude-code', 'codex']));
|
|
226
|
+
const { runDoctor } = require('../../src/commands/doctor');
|
|
227
|
+
// R20: doctor resolves a single explicit provider without needing every
|
|
228
|
+
// enabled agent to be independently initialized first.
|
|
229
|
+
runDoctor({ cwd: tmpWork, json: true, agent: 'codex' });
|
|
230
|
+
const report = JSON.parse(stdout());
|
|
231
|
+
expect(report.providers.map((p) => p.id)).toEqual(['codex']);
|
|
232
|
+
const codexReport = report.providers[0];
|
|
233
|
+
expect(codexReport.checks.find((c) => c.id === 'binary.version').state).toBe('missing');
|
|
234
|
+
});
|
|
235
|
+
});
|