Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-09
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# agent-codex-fix-cue-aliases-and-skills-tests-2026-06-09-10-24 (minimal / T1)

Branch: `agent/codex/fix-cue-aliases-and-skills-tests-2026-06-09-10-24`

Keep Claude account aliases launching through Cue while separating Soul skill
profile from Cue profile. Make skill-profile tests self-contained with a temp
Soul fixture so they pass on hosts without `~/Documents/soul`.

## Handoff

- Handoff: change=`agent-codex-fix-cue-aliases-and-skills-tests-2026-06-09-10-24`; branch=`agent/codex/fix-cue-aliases-and-skills-tests-2026-06-09-10-24`; scope=`src/commands/parallel.ts, src/tests/json-parity.test.ts, src/tests/skills-profile.test.ts`; action=`finish via PR`.
- Verification: `npm run build`; `node dist/tests/skills-profile.test.js`; `node dist/tests/json-parity.test.js`; `npm test` (195/195 passing); live smoke `claude-account1 --version`, `claude-account2 --version`.
- Live config: refreshed `/home/deadpool/.bashrc` aliases; `claude-account1` and `claude-account2` now expand to `__authmux_claude_account '<account>' 'base' 'core'`.

## Cleanup

- [ ] Run: `gx branch finish --branch agent/codex/fix-cue-aliases-and-skills-tests-2026-06-09-10-24 --base main --via-pr --wait-for-merge --cleanup`
- [ ] Record PR URL + `MERGED` state in the completion handoff.
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).
14 changes: 8 additions & 6 deletions src/commands/parallel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,19 +200,21 @@ export default class ClaudeParallel extends Command {
"# Claude Code parallel accounts (managed by agent-auth)",
"__authmux_claude_account() {",
" local name=\"$1\"",
` local profile="\${2:-${DEFAULT_CUE_PROFILE}}"`,
" shift 2",
" local skill_profile=\"${2:-base}\"",
` local cue_profile="\${3:-${DEFAULT_CUE_PROFILE}}"`,
" shift 3",
" local dir=\"$HOME/.claude-accounts/$name\"",
" command authmux skills activate \"$profile\" --agent claude --target \"$dir/skills\" >/dev/null 2>&1 || true",
" command authmux skills activate \"$skill_profile\" --agent claude --target \"$dir/skills\" >/dev/null 2>&1 || true",
" if command -v cue >/dev/null 2>&1 && [ -z \"${AUTHMUX_SKIP_CUE_LAUNCH:-}\" ]; then",
" CLAUDE_CONFIG_DIR=\"$dir\" cue launch claude --cue-profile \"$profile\" \"$@\"",
" CLAUDE_CONFIG_DIR=\"$dir\" cue launch claude --cue-profile \"$cue_profile\" \"$@\"",
" else",
" CLAUDE_CONFIG_DIR=\"$dir\" command claude \"$@\"",
" fi",
"}",
...profiles.map((p) => {
const profile = readCueProfile(p) ?? readSkillProfile(p) ?? DEFAULT_CUE_PROFILE;
return `alias claude-${p}="__authmux_claude_account ${shellQuote(p)} ${shellQuote(profile)}"`;
const skillProfile = readSkillProfile(p) ?? "base";
const cueProfile = readCueProfile(p) ?? DEFAULT_CUE_PROFILE;
return `alias claude-${p}="__authmux_claude_account ${shellQuote(p)} ${shellQuote(skillProfile)} ${shellQuote(cueProfile)}"`;
}),
];
return lines.join("\n");
Expand Down
8 changes: 6 additions & 2 deletions src/tests/json-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,15 @@ test("parallel aliases pass explicit cue profile per Claude account", async () =
assert.match(parsedAliases.data.aliases, /__authmux_claude_account\(\)/);
assert.match(
parsedAliases.data.aliases,
/cue launch claude --cue-profile "\$profile"/,
/authmux skills activate "\$skill_profile" --agent claude/,
);
assert.match(
parsedAliases.data.aliases,
/alias claude-account2="__authmux_claude_account 'account2' 'frontend'"/,
/cue launch claude --cue-profile "\$cue_profile"/,
);
assert.match(
parsedAliases.data.aliases,
/alias claude-account2="__authmux_claude_account 'account2' 'frontend' 'frontend'"/,
);
});
});
120 changes: 89 additions & 31 deletions src/tests/skills-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,58 @@ import {
resolveDefaultSkillTarget,
} from "../lib/skills/profile";

async function withTempSoulHome<T>(fn: (soulHome: string) => Promise<T> | T): Promise<T> {
const soulHome = await fsp.mkdtemp(path.join(os.tmpdir(), "authmux-soul-"));
const profilesRoot = path.join(soulHome, "skills", "profiles");
const scriptsRoot = path.join(soulHome, "skills", "scripts");
const activatorPath = path.join(scriptsRoot, "activate-profile.sh");
const previous = {
AUTHMUX_SOUL_HOME: process.env.AUTHMUX_SOUL_HOME,
SOUL_HOME: process.env.SOUL_HOME,
AUTHMUX_SOUL_SKILL_ACTIVATOR: process.env.AUTHMUX_SOUL_SKILL_ACTIVATOR,
};

await fsp.mkdir(profilesRoot, { recursive: true });
await fsp.mkdir(scriptsRoot, { recursive: true });
await Promise.all([
fsp.writeFile(path.join(profilesRoot, "base.json"), "{}\n", "utf8"),
fsp.writeFile(path.join(profilesRoot, "all.json"), "{}\n", "utf8"),
fsp.writeFile(
activatorPath,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
"target=\"\"",
"while [[ $# -gt 0 ]]; do",
" case \"$1\" in",
" --target) target=\"$2\"; shift 2 ;;",
" *) shift ;;",
" esac",
"done",
"printf 'target=%s skills=10\\n' \"$target\"",
].join("\n"),
"utf8",
),
]);
await fsp.chmod(activatorPath, 0o755);

process.env.AUTHMUX_SOUL_HOME = soulHome;
delete process.env.SOUL_HOME;
delete process.env.AUTHMUX_SOUL_SKILL_ACTIVATOR;

try {
return await fn(soulHome);
} finally {
if (previous.AUTHMUX_SOUL_HOME === undefined) delete process.env.AUTHMUX_SOUL_HOME;
else process.env.AUTHMUX_SOUL_HOME = previous.AUTHMUX_SOUL_HOME;
if (previous.SOUL_HOME === undefined) delete process.env.SOUL_HOME;
else process.env.SOUL_HOME = previous.SOUL_HOME;
if (previous.AUTHMUX_SOUL_SKILL_ACTIVATOR === undefined) delete process.env.AUTHMUX_SOUL_SKILL_ACTIVATOR;
else process.env.AUTHMUX_SOUL_SKILL_ACTIVATOR = previous.AUTHMUX_SOUL_SKILL_ACTIVATOR;
await fsp.rm(soulHome, { recursive: true, force: true });
}
}

test("normalizeSkillProfileName accepts simple profile names", () => {
assert.equal(normalizeSkillProfileName("frontend"), "frontend");
assert.equal(normalizeSkillProfileName("medusa-v2"), "medusa-v2");
Expand All @@ -21,27 +73,31 @@ test("normalizeSkillProfileName rejects path-like names", () => {
assert.throws(() => normalizeSkillProfileName("../all"), /Invalid skill profile/);
});

test("listAvailableSkillProfiles reads Soul profile JSON files", () => {
const profiles = listAvailableSkillProfiles();
assert.ok(profiles.includes("base"));
assert.ok(profiles.includes("all"));
test("listAvailableSkillProfiles reads Soul profile JSON files", async () => {
await withTempSoulHome(() => {
const profiles = listAvailableSkillProfiles();
assert.ok(profiles.includes("base"));
assert.ok(profiles.includes("all"));
});
});

test("activateSkillProfile delegates to the Soul activator", async (t) => {
const targetRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "authmux-skills-profile-"));
t.after(async () => {
await fsp.rm(targetRoot, { recursive: true, force: true });
});
await withTempSoulHome(async () => {
const targetRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "authmux-skills-profile-"));
t.after(async () => {
await fsp.rm(targetRoot, { recursive: true, force: true });
});

const result = activateSkillProfile({
profile: "base",
agent: "codex",
target: path.join(targetRoot, "skills"),
});
const result = activateSkillProfile({
profile: "base",
agent: "codex",
target: path.join(targetRoot, "skills"),
});

assert.equal(result.activated, true);
assert.equal(result.profile, "base");
assert.equal(result.skillCount, 10);
assert.equal(result.activated, true);
assert.equal(result.profile, "base");
assert.equal(result.skillCount, 10);
});
});

test("isSkillAgent narrows to known agents", () => {
Expand Down Expand Up @@ -72,20 +128,22 @@ test("resolveDefaultSkillTarget returns undefined for codex and claude", () => {
});

test("activateSkillProfile fills hermes target from env when not given", async (t) => {
const targetRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "authmux-skills-hermes-"));
t.after(async () => {
await fsp.rm(targetRoot, { recursive: true, force: true });
await withTempSoulHome(async () => {
const targetRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "authmux-skills-hermes-"));
t.after(async () => {
await fsp.rm(targetRoot, { recursive: true, force: true });
});
const previous = process.env.AUTHMUX_HERMES_HOME;
process.env.AUTHMUX_HERMES_HOME = targetRoot;
try {
const result = activateSkillProfile({ profile: "base", agent: "hermes" });
assert.equal(result.activated, true);
assert.equal(result.agent, "hermes");
assert.equal(result.target, path.join(targetRoot, "skills"));
assert.equal(result.skillCount, 10);
} finally {
if (previous === undefined) delete process.env.AUTHMUX_HERMES_HOME;
else process.env.AUTHMUX_HERMES_HOME = previous;
}
});
const previous = process.env.AUTHMUX_HERMES_HOME;
process.env.AUTHMUX_HERMES_HOME = targetRoot;
try {
const result = activateSkillProfile({ profile: "base", agent: "hermes" });
assert.equal(result.activated, true);
assert.equal(result.agent, "hermes");
assert.equal(result.target, path.join(targetRoot, "skills"));
assert.equal(result.skillCount, 10);
} finally {
if (previous === undefined) delete process.env.AUTHMUX_HERMES_HOME;
else process.env.AUTHMUX_HERMES_HOME = previous;
}
});
Loading