Skip to content

Commit c1231a7

Browse files
NagyViktNagyVikt
andauthored
Specialize authmux launches by skill profile (#35)
Codex startup context was inflated by exposing every Soul skill to every session. This adds account/profile metadata, profile activation commands, Codex hook activation, and Claude parallel alias activation so new sessions can launch with a small skill surface. Constraint: Soul remains the source of truth for skill directories and may be absent on machines without the local Soul checkout. Rejected: Always exposing all Soul skills | keeps the 98-skill context cost on every new session. Confidence: high Scope-risk: moderate Directive: Keep missing Soul activators non-fatal in shell hooks so auth switching never blocks agent startup. Tested: git diff --check; npm run build; npm test; openspec validate agent-codex-authmux-skill-profiles-2026-05-19-08-28 --type change --strict; openspec validate --specs; Soul profile activation all/base/deploy/design/frontend/medusa/orchestration/review. Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 57ae7a5 commit c1231a7

22 files changed

Lines changed: 579 additions & 10 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-19
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Why
2+
3+
- Codex startup context is inflated by exposing all Soul skills to every session.
4+
- The user needs authmux to launch Codex and Claude Code with small, role-specific skill sets so new sessions do not burn context on unrelated skills.
5+
6+
## What Changes
7+
8+
- Add authmux account metadata for `skillProfile`.
9+
- Add `authmux skills` commands to list, inspect, save, and activate Soul skill profiles.
10+
- Activate the current skill profile from the Codex shell hook before `command codex`.
11+
- Extend Claude parallel profiles so generated aliases activate a per-profile skills directory before launching Claude Code.
12+
13+
## Impact
14+
15+
- Affects authmux registry shape, `save`, `login`, `use`, `list`, `parallel`, and generated shell hooks.
16+
- Defaults to the `base` profile when no account-specific profile is configured.
17+
- If Soul is not installed, profile activation is skipped with a warning for explicit commands and silently ignored by shell hooks.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Account Skill Profile Metadata
4+
authmux SHALL allow a saved Codex account to store an optional Soul skill profile name.
5+
6+
#### Scenario: Saving an account profile
7+
- **WHEN** a user runs `authmux save <name> --skill-profile frontend`
8+
- **THEN** the registry entry for `<name>` stores `skillProfile=frontend`
9+
- **AND** later account listings can expose that profile.
10+
11+
### Requirement: Current Skill Profile Resolution
12+
authmux SHALL resolve the current skill profile from explicit environment override, active account metadata, then the `base` default.
13+
14+
#### Scenario: Activating current profile
15+
- **WHEN** `authmux skills activate-current --agent codex` runs for an active account without metadata
16+
- **THEN** authmux activates the `base` Soul profile.
17+
18+
### Requirement: Codex Launch Hook Profile Activation
19+
The generated Codex shell hook SHALL activate the current skill profile before launching Codex.
20+
21+
#### Scenario: Starting Codex through the hook
22+
- **WHEN** the shell function `codex` is invoked
23+
- **THEN** it restores the authmux session
24+
- **AND** runs `authmux skills activate-current --agent codex`
25+
- **AND** then runs `command codex`.
26+
27+
### Requirement: Claude Parallel Profile Activation
28+
authmux SHALL allow Claude parallel profiles to carry a Soul skill profile and SHALL activate that profile in generated aliases.
29+
30+
#### Scenario: Generating a Claude alias
31+
- **WHEN** a Claude parallel profile has `skillProfile=frontend`
32+
- **THEN** its generated alias activates the `frontend` profile into that Claude profile's skills directory before launching `claude`.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Definition of Done
2+
3+
This change is complete only when **all** of the following are true:
4+
5+
- Every checkbox below is checked.
6+
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
7+
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.
8+
9+
## Handoff
10+
11+
- Handoff: change=`agent-codex-authmux-skill-profiles-2026-05-19-08-28`; branch=`agent/codex/authmux-skill-profiles-2026-05-19-08-28`; scope=`Soul profile activation plus authmux profile wiring`; action=`finish PR/merge cleanup if takeover is required`.
12+
- Copy prompt: Continue `agent-codex-authmux-skill-profiles-2026-05-19-08-28` on branch `agent/codex/authmux-skill-profiles-2026-05-19-08-28`. Work inside the existing sandbox, review `openspec/changes/agent-codex-authmux-skill-profiles-2026-05-19-08-28/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/authmux-skill-profiles-2026-05-19-08-28 --base main --via-pr --wait-for-merge --cleanup`.
13+
14+
## 1. Specification
15+
16+
- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-authmux-skill-profiles-2026-05-19-08-28`.
17+
- [x] 1.2 Define normative requirements in `specs/authmux-skill-profiles/spec.md`.
18+
19+
## 2. Implementation
20+
21+
- [x] 2.1 Implement scoped behavior changes.
22+
- [x] 2.2 Add/update focused regression coverage.
23+
24+
## 3. Verification
25+
26+
- [x] 3.1 Run targeted project verification commands.
27+
- [x] 3.2 Run `openspec validate agent-codex-authmux-skill-profiles-2026-05-19-08-28 --type change --strict`.
28+
- [x] 3.3 Run `openspec validate --specs`.
29+
30+
## 4. Cleanup (mandatory; run before claiming completion)
31+
32+
- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/authmux-skill-profiles-2026-05-19-08-28 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
33+
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
34+
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).

scripts/postinstall-login-hook.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function renderHookBlock() {
3434
"codex() {",
3535
" if command -v authmux >/dev/null 2>&1; then",
3636
" command authmux restore-session >/dev/null 2>&1 || true",
37+
" command authmux skills activate-current --agent codex >/dev/null 2>&1 || true",
3738
" fi",
3839
" command codex \"$@\"",
3940
" local __codex_exit=$?",

src/commands/list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default class ListCommand extends BaseCommand {
5656
` email=${account.email ?? "-"} account=${account.accountId ?? "-"} user=${account.userId ?? "-"}`,
5757
);
5858
this.log(
59-
` type=${formatAccountType(account.planType)} plan=${account.planType ?? "-"} usage=${account.usageSource ?? "-"} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)} lastUsageAt=${account.lastUsageAt ?? "-"}`,
59+
` type=${formatAccountType(account.planType)} plan=${account.planType ?? "-"} skillProfile=${account.skillProfile ?? "-"} usage=${account.usageSource ?? "-"} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)} lastUsageAt=${account.lastUsageAt ?? "-"}`,
6060
);
6161
}
6262
});

src/commands/login.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export default class LoginCommand extends BaseCommand {
3030
"Force overwrite when the existing snapshot name belongs to a different detected account identity",
3131
default: false,
3232
}),
33+
"skill-profile": Flags.string({
34+
description: "Attach a Soul skill profile to this account",
35+
}),
3336
} as const;
3437

3538
async run(): Promise<void> {
@@ -54,6 +57,7 @@ export default class LoginCommand extends BaseCommand {
5457
const forceOverwrite = Boolean(flags.force || resolvedName.forceOverwrite);
5558
const savedName = await this.accounts.saveAccount(resolvedName.name, {
5659
force: forceOverwrite,
60+
skillProfile: flags["skill-profile"],
5761
});
5862

5963
const suffix =
@@ -65,6 +69,9 @@ export default class LoginCommand extends BaseCommand {
6569
? " (reused saved account name)"
6670
: " (inferred from auth email)";
6771
this.log(`Saved current Codex auth tokens as "${savedName}"${suffix}.`);
72+
if (flags["skill-profile"]) {
73+
this.log(`Attached skill profile "${flags["skill-profile"]}".`);
74+
}
6875
} catch (error) {
6976
if (autoSwitchWasEnabled) {
7077
try {

src/commands/parallel.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "../lib/cli/json-envelope";
1414

1515
const CLAUDE_PARALLEL_DIR = path.join(os.homedir(), ".claude-accounts");
16+
const SKILL_PROFILE_FILE = ".authmux-skill-profile";
1617

1718
function getProfiles(): string[] {
1819
if (!fs.existsSync(CLAUDE_PARALLEL_DIR)) return [];
@@ -28,6 +29,22 @@ function shellRcPath(): string {
2829
return path.join(os.homedir(), ".bashrc");
2930
}
3031

32+
function shellQuote(value: string): string {
33+
return `'${value.replace(/'/g, `'\\''`)}'`;
34+
}
35+
36+
function readSkillProfile(name: string): string | undefined {
37+
const file = path.join(CLAUDE_PARALLEL_DIR, name, SKILL_PROFILE_FILE);
38+
if (!fs.existsSync(file)) return undefined;
39+
const profile = fs.readFileSync(file, "utf8").trim();
40+
return profile.length > 0 ? profile : undefined;
41+
}
42+
43+
function writeSkillProfile(name: string, skillProfile: string): void {
44+
const file = path.join(CLAUDE_PARALLEL_DIR, name, SKILL_PROFILE_FILE);
45+
fs.writeFileSync(file, `${skillProfile.trim()}\n`);
46+
}
47+
3148
export default class ClaudeParallel extends Command {
3249
static description = "Manage parallel Claude Code accounts via CLAUDE_CONFIG_DIR";
3350

@@ -37,6 +54,7 @@ export default class ClaudeParallel extends Command {
3754
aliases: Flags.boolean({ description: "Print shell aliases for all profiles" }),
3855
install: Flags.boolean({ description: "Install aliases into shell rc file" }),
3956
list: Flags.boolean({ char: "l", description: "List profiles" }),
57+
"skill-profile": Flags.string({ description: "Soul skill profile for this Claude profile" }),
4058
json: Flags.boolean({
4159
description: "Emit a single JSON envelope to stdout (Theme X4).",
4260
default: false,
@@ -45,6 +63,7 @@ export default class ClaudeParallel extends Command {
4563

4664
static examples = [
4765
"agent-auth parallel --add work",
66+
"agent-auth parallel --add frontend --skill-profile frontend",
4867
"agent-auth parallel --add personal",
4968
"agent-auth parallel --list",
5069
"agent-auth parallel --aliases",
@@ -58,7 +77,7 @@ export default class ClaudeParallel extends Command {
5877
this.jsonMode = Boolean(flags.json);
5978

6079
if (flags.add) {
61-
this.addProfile(flags.add);
80+
this.addProfile(flags.add, flags["skill-profile"]);
6281
} else if (flags.remove) {
6382
this.removeProfile(flags.remove);
6483
} else if (flags.install) {
@@ -70,29 +89,37 @@ export default class ClaudeParallel extends Command {
7089
}
7190
}
7291

73-
private addProfile(name: string): void {
92+
private addProfile(name: string, skillProfile?: string): void {
7493
const dir = path.join(CLAUDE_PARALLEL_DIR, name);
7594
const existed = fs.existsSync(dir);
7695
if (!existed) {
7796
fs.mkdirSync(dir, { recursive: true });
7897
}
98+
if (skillProfile) {
99+
writeSkillProfile(name, skillProfile);
100+
}
79101

80102
if (this.jsonMode) {
81103
writeJsonEnvelope(jsonSuccess({
82104
action: "add" as const,
83105
profile: name,
84106
dir,
85107
created: !existed,
108+
skillProfile: skillProfile ?? readSkillProfile(name) ?? "base",
86109
}));
87110
return;
88111
}
89112

90113
if (existed) {
91114
this.log(`Profile "${name}" already exists at ${dir}`);
115+
if (skillProfile) {
116+
this.log(` Skill profile: ${skillProfile}`);
117+
}
92118
return;
93119
}
94120
this.log(`Created profile: ${name}`);
95121
this.log(` Config dir: ${dir}`);
122+
this.log(` Skill profile: ${skillProfile ?? "base"}`);
96123
this.log(` Run: CLAUDE_CONFIG_DIR=${dir} claude`);
97124
this.log(`\nTo install shell aliases: agent-auth parallel --install`);
98125
}
@@ -120,6 +147,7 @@ export default class ClaudeParallel extends Command {
120147
const entries = profiles.map((p) => ({
121148
name: p,
122149
configDir: path.join(CLAUDE_PARALLEL_DIR, p),
150+
skillProfile: readSkillProfile(p) ?? "base",
123151
}));
124152

125153
if (this.jsonMode) {
@@ -137,7 +165,7 @@ export default class ClaudeParallel extends Command {
137165
}
138166
this.log("Claude Code parallel profiles:\n");
139167
for (const p of entries) {
140-
this.log(` • ${p.name}${p.configDir}`);
168+
this.log(` • ${p.name}${p.configDir} skillProfile=${p.skillProfile}`);
141169
}
142170
this.log(`\nRun any profile: claude-<name> (after installing aliases)`);
143171
}
@@ -147,9 +175,19 @@ export default class ClaudeParallel extends Command {
147175
if (!profiles.length) return "";
148176
const lines = [
149177
"# Claude Code parallel accounts (managed by agent-auth)",
150-
...profiles.map((p) =>
151-
`alias claude-${p}="CLAUDE_CONFIG_DIR=${path.join(CLAUDE_PARALLEL_DIR, p)} command claude"`
152-
),
178+
...profiles.map((p) => {
179+
const dir = path.join(CLAUDE_PARALLEL_DIR, p);
180+
const profile = readSkillProfile(p) ?? "base";
181+
const activate = [
182+
"command authmux skills activate",
183+
shellQuote(profile),
184+
"--agent claude",
185+
"--target",
186+
shellQuote(path.join(dir, "skills")),
187+
">/dev/null 2>&1 || true",
188+
].join(" ");
189+
return `alias claude-${p}="${activate}; CLAUDE_CONFIG_DIR=${shellQuote(dir)} command claude"`;
190+
}),
153191
];
154192
return lines.join("\n");
155193
}

src/commands/save.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export default class SaveCommand extends BaseCommand {
2020
"Force overwrite when the existing snapshot name belongs to a different email account",
2121
default: false,
2222
}),
23+
"skill-profile": Flags.string({
24+
description: "Attach a Soul skill profile to this account",
25+
}),
2326
...BaseCommand.jsonFlag,
2427
} as const;
2528

@@ -34,13 +37,15 @@ export default class SaveCommand extends BaseCommand {
3437
: await this.accounts.resolveDefaultAccountNameFromCurrentAuth();
3538
const savedName = await this.accounts.saveAccount(resolvedName.name, {
3639
force: Boolean(flags.force || resolvedName.forceOverwrite),
40+
skillProfile: flags["skill-profile"],
3741
});
3842

3943
this.emit(
4044
{
4145
saved: savedName,
4246
source: resolvedName.source,
4347
forced: Boolean(flags.force || resolvedName.forceOverwrite),
48+
skillProfile: flags["skill-profile"] ?? null,
4449
},
4550
(data) => {
4651
const suffix =
@@ -52,6 +57,9 @@ export default class SaveCommand extends BaseCommand {
5257
? " (reused saved account name)"
5358
: " (inferred from auth email)";
5459
this.log(`Saved current Codex auth tokens as "${data.saved}"${suffix}.`);
60+
if (data.skillProfile) {
61+
this.log(`Attached skill profile "${data.skillProfile}".`);
62+
}
5563
},
5664
);
5765
});

0 commit comments

Comments
 (0)