Skip to content

Commit dbe6872

Browse files
feat(run): basou run codex launcher with pre-spawn orientation channel
Wrap the Codex CLI as a Basou-tracked session, the twin of `basou run claude-code`. Two grips beyond plain tracking: inject `-c shell_environment_policy.inherit=all` so Codex tool calls can reach `basou` on PATH, and re-render THIS workspace's orientation into ~/.codex/AGENTS.md just before spawn so the interactive Codex auto-loads the current position (correcting the global channel's last-refreshed-workspace semantics). The pre-spawn render is best-effort and surfaces the last-refreshed orientation without re-importing. Generalizes runClaudeCode's lifecycle into a shared runTrackedTool(args, options, ctx, adapter) parametrized by per-tool seams (command resolver, source metadata, arg transform, pre-spawn hook); runClaudeCode / runCodex are thin wrappers, and the run-group subcommand dispatch is shared. Adds the codex-adapter session-source kind (regenerated published JSON Schema) plus codexAdapterMetadata / resolveCodexCommand in core. Claude-code run tests are unchanged and pass; a RunContext.codexChannelPath seam keeps the suite off the real home-global file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7a3c37a commit dbe6872

13 files changed

Lines changed: 393 additions & 98 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ All notable changes to **basou** are recorded here. The project follows
77

88
### Added
99

10+
- `basou run codex` — wrap the Codex CLI as a Basou-tracked session, the twin of
11+
`basou run claude-code`. Two grips beyond plain tracking: it injects
12+
`-c shell_environment_policy.inherit=all` so Codex's own tool calls can reach
13+
`basou` on PATH, and just before spawn it re-renders THIS workspace's
14+
orientation into the Codex context face (`~/.codex/AGENTS.md`) so the
15+
about-to-start interactive Codex auto-loads the current position even when the
16+
global channel last reflected a different workspace. The pre-spawn render is
17+
best-effort (a failure never blocks the launch) and surfaces the
18+
last-refreshed orientation without re-importing. The session is attributed to
19+
the new `codex-adapter` source kind.
20+
1021
- Codex context channel — `basou refresh` now renders the regenerated
1122
orientation into `~/.codex/AGENTS.md` (a marker-delimited `BASOU:ORIENTATION`
1223
block), the file Codex auto-loads at startup. Codex exposes no SessionStart
@@ -42,6 +53,15 @@ All notable changes to **basou** are recorded here. The project follows
4253

4354
### Internal
4455

56+
- Added the `codex-adapter` session-source kind to `SessionSourceKindSchema`
57+
(and the regenerated published JSON Schema) for live `basou run codex`
58+
sessions — distinct from the after-the-fact `codex-import`. Added
59+
`codexAdapterMetadata` / `resolveCodexCommand` to the core codex adapter, and
60+
generalized `runClaudeCode`'s lifecycle into a shared `runTrackedTool(args,
61+
options, ctx, adapter)` parametrized by per-tool seams (command resolver,
62+
source metadata, arg transform, pre-spawn hook); `runClaudeCode` / `runCodex`
63+
are thin wrappers. Claude-code behavior is preserved (its run tests are
64+
unchanged and pass).
4565
- Added `ORIENTATION_START` / `ORIENTATION_END` markers to core and a shared
4666
`context-channel` lib (`syncMarkerBlock` / `removeMarkerBlock` /
4767
`assertNoMarkerLine`) that owns the symlink guard, append/replace, one-time

packages/cli/src/commands/refresh.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import {
2-
assertBasouRootSafe,
3-
type BasouPaths,
4-
basouPaths,
5-
findErrorCode,
6-
readMarkdownFile,
7-
} from "@basou/core";
1+
import { assertBasouRootSafe, type BasouPaths, basouPaths, findErrorCode } from "@basou/core";
82
import { type Command, InvalidArgumentError } from "commander";
9-
import { syncOrientationChannel } from "../lib/context-channel.js";
3+
import { renderOrientationToCodexChannel } from "../lib/context-channel.js";
104
import { isVerbose, renderCliError } from "../lib/error-render.js";
115
import { loadPortfolioConfig } from "../lib/portfolio-config.js";
126
import { type ImportOutcome, type RefreshResult, refreshAll } from "../lib/provenance-actions.js";
@@ -320,13 +314,11 @@ async function syncCodexOrientationChannel(
320314
channelPath?: string,
321315
): Promise<string | null> {
322316
try {
323-
const body = await readMarkdownFile(paths.files.orientation);
324-
if (body === null) return null;
325-
const { action } = await syncOrientationChannel({
326-
body,
327-
...(channelPath !== undefined ? { target: channelPath } : {}),
317+
const rendered = await renderOrientationToCodexChannel({
318+
orientationPath: paths.files.orientation,
319+
...(channelPath !== undefined ? { channelPath } : {}),
328320
});
329-
return `codex channel: orientation ${action} in ${channelPath ?? "~/.codex/AGENTS.md"}`;
321+
return rendered === null ? null : rendered.line;
330322
} catch (error: unknown) {
331323
return `codex channel skipped: ${error instanceof Error ? error.message : String(error)}`;
332324
}

packages/cli/src/commands/run.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@basou/core";
1717
import { Command } from "commander";
1818
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19-
import { type RunContext, registerRunCommand, runClaudeCode } from "./run.js";
19+
import { type RunContext, registerRunCommand, runClaudeCode, runCodex } from "./run.js";
2020

2121
const execFileAsync = promisify(execFile);
2222

@@ -693,3 +693,84 @@ describe("runClaudeCode", () => {
693693
expect(yaml).toContain("dirty-untracked.txt");
694694
});
695695
});
696+
697+
describe("runCodex", () => {
698+
const codexResolve = async (): Promise<{ command: string }> => ({ command: "codex" });
699+
700+
// codexChannelPath is ALWAYS overridden in these tests so the pre-spawn
701+
// channel render never touches the real ~/.codex/AGENTS.md.
702+
it("records a codex-adapter session and injects the env-policy flag into the child argv", async () => {
703+
const repo = await setupInitedRepo();
704+
const runner = makeFakeRunner({ exit_code: 0 });
705+
const exitCode = await runCodex(
706+
["resume"],
707+
{ cwd: repo, snapshot: false },
708+
{
709+
runner,
710+
now: () => FIXED_DATE,
711+
resolveCodexCommand: codexResolve,
712+
codexChannelPath: join(repo, "codex-AGENTS.md"),
713+
},
714+
);
715+
expect(exitCode).toBe(0);
716+
// The env-policy flag is prepended to whatever the user passed.
717+
expect(runner.lastArgs).toEqual(["-c", "shell_environment_policy.inherit=all", "resume"]);
718+
const sessionId = await findOnlySessionId(repo);
719+
const lines = await readEventsLines(repo, sessionId);
720+
expect(JSON.parse(lines[0] ?? "{}").source).toBe("codex-adapter");
721+
// The recorded invocation reflects what actually ran (flag included).
722+
const yaml = await readFile(join(basouPaths(repo).sessions, sessionId, "session.yaml"), "utf8");
723+
expect(yaml).toContain("shell_environment_policy.inherit=all");
724+
expect(yaml).toContain("codex-adapter");
725+
});
726+
727+
it("renders this workspace's orientation into the overridden codex channel before spawn", async () => {
728+
const repo = await setupInitedRepo();
729+
// The launcher surfaces the LAST-rendered orientation (it does not refresh);
730+
// seed one so the pre-spawn render has something to push.
731+
await writeFile(basouPaths(repo).files.orientation, "# Orientation\n\nyou are right here\n");
732+
const channelPath = join(repo, "codex-AGENTS.md");
733+
const runner = makeFakeRunner({ exit_code: 0 });
734+
await runCodex(
735+
[],
736+
{ cwd: repo, snapshot: false },
737+
{
738+
runner,
739+
now: () => FIXED_DATE,
740+
resolveCodexCommand: codexResolve,
741+
codexChannelPath: channelPath,
742+
},
743+
);
744+
const channel = await readFile(channelPath, "utf8");
745+
expect(channel).toContain("BASOU:ORIENTATION:START");
746+
expect(channel).toContain("you are right here");
747+
});
748+
749+
it("launches even when there is no orientation to render (best-effort channel)", async () => {
750+
const repo = await setupInitedRepo();
751+
// No orientation.md written → the pre-spawn render is a no-op, but the
752+
// launch must still proceed and the channel file is never created.
753+
const channelPath = join(repo, "codex-AGENTS.md");
754+
const runner = makeFakeRunner({ exit_code: 0 });
755+
const exitCode = await runCodex(
756+
[],
757+
{ cwd: repo, snapshot: false },
758+
{
759+
runner,
760+
now: () => FIXED_DATE,
761+
resolveCodexCommand: codexResolve,
762+
codexChannelPath: channelPath,
763+
},
764+
);
765+
expect(exitCode).toBe(0);
766+
await expect(access(channelPath)).rejects.toThrow();
767+
});
768+
769+
it("registers a `codex` subcommand under `run`", () => {
770+
const program = new Command();
771+
registerRunCommand(program);
772+
const run = program.commands.find((c) => c.name() === "run");
773+
const subs = run?.commands.map((c) => c.name()) ?? [];
774+
expect(subs).toEqual(expect.arrayContaining(["claude-code", "codex"]));
775+
});
776+
});

0 commit comments

Comments
 (0)