claude-codex-pair 0.3.1 → 0.4.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/DESIGN.md +15 -1
- package/README.md +22 -3
- package/dist/pair.js +234 -65
- package/package.json +1 -1
package/DESIGN.md
CHANGED
|
@@ -74,7 +74,21 @@ active threads.
|
|
|
74
74
|
freshness to the channel and lane states. Freshness compares the marked pair section in
|
|
75
75
|
`AGENTS.md` and the generated Claude skill against the same templates used by `init`. It is
|
|
76
76
|
read-only and describes files on disk, not instructions already loaded into a running session.
|
|
77
|
-
The
|
|
77
|
+
The comparison ignores the `<!-- pair x.y.z -->` stamp that records which version wrote a file.
|
|
78
|
+
|
|
79
|
+
`send`, `wait`, `history`, `clean`, `register` and `unregister` run the same check once at startup,
|
|
80
|
+
not on each poll, and rewrite stale files. They skip files stamped by a newer pair, so worktrees
|
|
81
|
+
shared by two versions don't flip back and forth. A write failure is a warning on stderr, so a
|
|
82
|
+
read-only sandbox still delivers messages. `status` stays read-only, and `mcp` never refreshes:
|
|
83
|
+
it runs unsandboxed and only reads. `status` and `init` also report a newer npm version. They check
|
|
84
|
+
at most once a day, cache the answer under the state directory, and stay quiet on failure.
|
|
85
|
+
`init` also keeps files a newer pair wrote; deleting them is the way to downgrade. The refresh note goes
|
|
86
|
+
to stderr after the command's own output, so `pair wait`'s first line is still the delivery or
|
|
87
|
+
timeout line agents read. `pair upgrade` installs with npm, then runs the `init` of the package it
|
|
88
|
+
just installed (found with `npm root --global`), not the first `pair` on PATH, because both this
|
|
89
|
+
process and an older copy on PATH would write the old templates. It asks `npm view` first, so it uses
|
|
90
|
+
the same registry as the install. Already on the latest version, it only runs the in-process
|
|
91
|
+
refresh. If the check fails, it goes ahead with the install, which reports the actual npm error.
|
|
78
92
|
|
|
79
93
|
Channel operations report `EACCES` and `EPERM` with the affected path and the sandbox
|
|
80
94
|
configuration printed by `init`. Either ordinary permissions or sandbox restrictions can
|
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ lines. Pair needs no extra sandbox config, because every channel lives under `~/
|
|
|
158
158
|
|
|
159
159
|
## Commands
|
|
160
160
|
|
|
161
|
-
The agents run `send` and `wait` themselves. You'll mostly use `init`, `status`, `history` and `
|
|
161
|
+
The agents run `send` and `wait` themselves. You'll mostly use `init`, `status`, `history`, `clean` and `upgrade`.
|
|
162
162
|
|
|
163
163
|
```
|
|
164
164
|
pair send <request|review|response> --as <claude|codex> [--verdict approve|changes] <file|->
|
|
@@ -167,6 +167,7 @@ pair status
|
|
|
167
167
|
pair history [thread-id] # list finished threads (latest first), or print one (any unique part of its id)
|
|
168
168
|
pair clean --keep N # delete all but the N newest finished threads
|
|
169
169
|
pair init
|
|
170
|
+
pair upgrade # install the latest pair from npm, then refresh this worktree's instructions
|
|
170
171
|
pair uninit # undo pair init in this worktree (keeps history; see "Uninstall")
|
|
171
172
|
pair register | unregister # run by Codex in its shell: let pair_listen notify this session
|
|
172
173
|
pair mcp # stdio MCP server for Codex (see "Let pair wake Codex")
|
|
@@ -189,11 +190,28 @@ codex notifications: session 01a13922-c08f-79e3-a41f-6780876fd9c5, registered 20
|
|
|
189
190
|
last listener event: 2026-10-14T08:05:12.345Z notified 01a13922-… about /Users/you/.pair/…/001-request.md
|
|
190
191
|
```
|
|
191
192
|
|
|
192
|
-
Instruction states are `current`, `stale`, `missing`, or `unreadable`. The check compares only
|
|
193
|
+
Instruction states are `current`, `stale`, `newer`, `missing`, or `unreadable`. The check compares only
|
|
193
194
|
the marked pair section in `AGENTS.md`, so your other instructions don't affect it. Status
|
|
194
195
|
doesn't change files. Run `pair init` to refresh missing or stale instructions, then reload
|
|
195
196
|
agent sessions; fix access to unreadable files first. Current files on disk don't guarantee
|
|
196
|
-
that a running agent has loaded them.
|
|
197
|
+
that a running agent has loaded them. `newer` means a newer pair wrote them: run `pair upgrade`.
|
|
198
|
+
|
|
199
|
+
`pair status` and `pair init` also print a line when npm has a newer version. They check at most
|
|
200
|
+
once a day and cache the answer in `~/.local/state/pair/latest.json`. The check stays quiet when
|
|
201
|
+
offline or sandboxed.
|
|
202
|
+
|
|
203
|
+
## Upgrading
|
|
204
|
+
|
|
205
|
+
Run `pair upgrade` in your own terminal, not through an agent: agent sandboxes block the network and
|
|
206
|
+
global installs. If npm has a newer version, it runs `npm install -g claude-codex-pair@latest`, then
|
|
207
|
+
the new `pair init` in the current worktree if pair is set up there, and prints only the files that
|
|
208
|
+
`init` wrote or kept. Already on the latest version, it installs nothing and only refreshes this
|
|
209
|
+
worktree's stale instructions. Other worktrees refresh their instructions on the next
|
|
210
|
+
`pair send`, `wait`, `history` or `clean`. pair owns these files, so edits inside them are
|
|
211
|
+
overwritten. pair, including `pair init`, never rewrites instructions that a newer pair wrote, so
|
|
212
|
+
worktrees shared by different versions don't flip back and forth. To downgrade, delete them first. Restart Claude Code and Codex afterwards. Running
|
|
213
|
+
sessions keep the instructions they loaded at start, and Codex keeps its old `pair mcp` until
|
|
214
|
+
it restarts.
|
|
197
215
|
|
|
198
216
|
If a channel read or write fails with `EACCES` or `EPERM`, the command names the path and prints
|
|
199
217
|
sandbox configuration snippets using your actual channel directory. Check ordinary filesystem
|
|
@@ -206,6 +224,7 @@ so a cleanup problem doesn't prevent delivering a review or starting the next th
|
|
|
206
224
|
| `PAIR_AGENT` | (none) | Default for `--as` |
|
|
207
225
|
| `PAIR_MAX_ROUNDS` | `3` | Number of review rounds before escalation |
|
|
208
226
|
| `PAIR_KEEP` | `100` | Finished threads to keep, by finish time. Older ones are deleted whenever a thread finishes. `all` keeps everything, `0` keeps none |
|
|
227
|
+
| `PAIR_NO_UPDATE_CHECK` | (none) | Any value turns off the daily npm version check |
|
|
209
228
|
|
|
210
229
|
## Uninstall
|
|
211
230
|
|
package/dist/pair.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { accessSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -24,6 +24,12 @@ const PAIR_SECTIONS = /<!-- pair:start -->[\s\S]*?<!-- pair:end -->(?:\r?\n)?/g;
|
|
|
24
24
|
// SKILL.md as released versions wrote it (0.1.0 and 0.1.1), so `uninit` can tell it from an edited one.
|
|
25
25
|
const RELEASED_SKILL_SHA256 = ["e9afb64c53d5e90f935bcb66406601a31a91b37758467cf867713e245f8af288"];
|
|
26
26
|
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
const PACKAGE = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
28
|
+
// Which pair wrote an instruction file, so an older pair never rolls back what a newer one wrote.
|
|
29
|
+
const STAMP = /<!-- pair (\S+) -->\n/;
|
|
30
|
+
// Commands that refresh stale instructions. Not status (a read-only diagnosis) or mcp (unsandboxed, so it only reads).
|
|
31
|
+
const REFRESHING_COMMANDS = ["send", "wait", "history", "clean", "register", "unregister"];
|
|
32
|
+
const UPDATE_CHECK_MS = 24 * 60 * 60 * 1000;
|
|
27
33
|
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; // any UUID version; Codex uses v7
|
|
28
34
|
const SESSION_FILE = "codex.session"; // the Codex session that pair_listen may notify, per channel
|
|
29
35
|
const LISTEN_LOG = "codex.listen.log";
|
|
@@ -506,9 +512,12 @@ function rotateLog(path) {
|
|
|
506
512
|
* any check and the write that follows, so the unsandboxed server only ever reads channels.
|
|
507
513
|
*/
|
|
508
514
|
function stateDir(channel) {
|
|
515
|
+
return join(stateHome(), basename(channel));
|
|
516
|
+
}
|
|
517
|
+
function stateHome() {
|
|
509
518
|
const configured = process.env.XDG_STATE_HOME;
|
|
510
519
|
const root = configured && isAbsolute(configured) ? configured : join(homedir(), ".local", "state");
|
|
511
|
-
return join(resolve(root), "pair"
|
|
520
|
+
return join(resolve(root), "pair");
|
|
512
521
|
}
|
|
513
522
|
function logListen(channel, text) {
|
|
514
523
|
try {
|
|
@@ -817,7 +826,11 @@ function instructionState(path, expected, sectionOnly = false) {
|
|
|
817
826
|
if (actual === undefined) {
|
|
818
827
|
return "missing"; // AGENTS.md exists but has no complete pair section
|
|
819
828
|
}
|
|
820
|
-
|
|
829
|
+
if (unstamped(actual) === unstamped(expected)) {
|
|
830
|
+
return "current";
|
|
831
|
+
}
|
|
832
|
+
const writer = STAMP.exec(actual)?.[1];
|
|
833
|
+
return writer && isNewer(writer, PACKAGE.version) ? `newer (pair ${writer})` : "stale";
|
|
821
834
|
}
|
|
822
835
|
catch (error) {
|
|
823
836
|
if (isMissing(error)) {
|
|
@@ -855,13 +868,12 @@ function notificationStatus(channel) {
|
|
|
855
868
|
}
|
|
856
869
|
function status(channel, root) {
|
|
857
870
|
const lanes = channelAccess(channel, () => AGENTS.map((agent) => describe(channel, agent)));
|
|
858
|
-
const { name, version } = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
859
871
|
const agents = instructionState(join(root, "AGENTS.md"), codexSection(), true);
|
|
860
872
|
const skill = instructionState(join(root, ".claude", "skills", "pair", "SKILL.md"), claudeSkill());
|
|
861
873
|
return [
|
|
862
874
|
`channel: ${channel}`,
|
|
863
875
|
`worktree: ${root}`,
|
|
864
|
-
`version: ${name} ${version}`,
|
|
876
|
+
`version: ${PACKAGE.name} ${PACKAGE.version}`,
|
|
865
877
|
...lanes,
|
|
866
878
|
"instructions on disk (does not verify what running agents loaded):",
|
|
867
879
|
` AGENTS.md (pair section): ${agents}`,
|
|
@@ -871,8 +883,137 @@ function status(channel, root) {
|
|
|
871
883
|
? ["Run `pair init` to refresh missing or stale instructions, then reload agent sessions."] : []),
|
|
872
884
|
...(agents.startsWith("unreadable") || skill.startsWith("unreadable")
|
|
873
885
|
? ["Check permissions on the unreadable instruction paths before refreshing them."] : []),
|
|
886
|
+
...(agents.startsWith("newer") || skill.startsWith("newer")
|
|
887
|
+
? ["A newer pair wrote these instructions: run `pair upgrade` in your own terminal."] : []),
|
|
874
888
|
].join("\n");
|
|
875
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Rewrites instructions an older pair wrote; leaves missing, unreadable and newer ones for `pair status` to report.
|
|
892
|
+
* Returns a note for after the command's output: agents read `pair wait`'s first line, with stderr merged.
|
|
893
|
+
*/
|
|
894
|
+
function refreshInstructions(root) {
|
|
895
|
+
try {
|
|
896
|
+
return refreshedNote(refreshStale(root));
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
// A read-only sandbox: message delivery must still work, so report and go on.
|
|
900
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
901
|
+
return `pair: couldn't refresh instructions (${reason}); run \`pair init\` in your own terminal`;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/** The stale instruction files it rewrote. A write failure throws. */
|
|
905
|
+
function refreshStale(root) {
|
|
906
|
+
const skillPath = join(root, ".claude", "skills", "pair", "SKILL.md");
|
|
907
|
+
const agentsPath = join(root, "AGENTS.md");
|
|
908
|
+
const refreshed = [];
|
|
909
|
+
if (instructionState(skillPath, claudeSkill()) === "stale") {
|
|
910
|
+
writeFileSync(skillPath, claudeSkill());
|
|
911
|
+
refreshed.push(skillPath);
|
|
912
|
+
}
|
|
913
|
+
if (instructionState(agentsPath, codexSection(), true) === "stale") {
|
|
914
|
+
writeFileSync(agentsPath, readFileSync(agentsPath, "utf8").replace(PAIR_SECTION, () => codexSection()));
|
|
915
|
+
refreshed.push(`${agentsPath} (pair section)`);
|
|
916
|
+
}
|
|
917
|
+
return refreshed;
|
|
918
|
+
}
|
|
919
|
+
function refreshedNote(refreshed) {
|
|
920
|
+
return refreshed.length > 0
|
|
921
|
+
? `pair: refreshed ${refreshed.join(", ")} to pair ${PACKAGE.version}; reload agent sessions to load them`
|
|
922
|
+
: undefined;
|
|
923
|
+
}
|
|
924
|
+
function unstamped(text) {
|
|
925
|
+
return text.replace(STAMP, "");
|
|
926
|
+
}
|
|
927
|
+
function isNewer(version, than) {
|
|
928
|
+
return version.localeCompare(than, "en", { numeric: true }) > 0;
|
|
929
|
+
}
|
|
930
|
+
/** The latest version on npm, checked at most once a day; undefined when opted out or the check fails. */
|
|
931
|
+
async function latestVersion() {
|
|
932
|
+
if (process.env.PAIR_NO_UPDATE_CHECK) {
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
const cache = join(stateHome(), "latest.json");
|
|
936
|
+
try {
|
|
937
|
+
const cached = JSON.parse(readFileSync(cache, "utf8"));
|
|
938
|
+
if (Date.now() - cached.checkedAt < UPDATE_CHECK_MS && typeof cached.version === "string") {
|
|
939
|
+
return cached.version;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
// no cache yet, or a corrupt one: check again
|
|
944
|
+
}
|
|
945
|
+
let version;
|
|
946
|
+
try {
|
|
947
|
+
// ponytail: the public registry, not npm's configured one; read `npm config get registry` if mirrors matter
|
|
948
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE.name}/latest`, { signal: AbortSignal.timeout(2000) });
|
|
949
|
+
version = (await response.json()).version;
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return undefined; // offline or sandboxed: the notice is best-effort
|
|
953
|
+
}
|
|
954
|
+
if (typeof version !== "string") {
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
mkdirSync(dirname(cache), { recursive: true });
|
|
959
|
+
writeFileSync(cache, JSON.stringify({ checkedAt: Date.now(), version }));
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
// a read-only state dir only means checking again next time
|
|
963
|
+
}
|
|
964
|
+
return version;
|
|
965
|
+
}
|
|
966
|
+
async function updateNotice() {
|
|
967
|
+
const latest = await latestVersion();
|
|
968
|
+
return latest && isNewer(latest, PACKAGE.version)
|
|
969
|
+
? [`update available: ${PACKAGE.name} ${PACKAGE.version} → ${latest}. Run \`pair upgrade\` in your own terminal.`]
|
|
970
|
+
: [];
|
|
971
|
+
}
|
|
972
|
+
/** Installs the latest pair, then lets the new version refresh this worktree's instructions. */
|
|
973
|
+
function upgrade(root) {
|
|
974
|
+
// npm's own answer, from the registry it installs from; if it fails, the install reports why.
|
|
975
|
+
const view = spawnSync("npm", ["view", PACKAGE.name, "version"], { encoding: "utf8" });
|
|
976
|
+
const latest = view.status === 0 ? view.stdout.trim() : undefined;
|
|
977
|
+
if (latest && !isNewer(latest, PACKAGE.version)) {
|
|
978
|
+
let note;
|
|
979
|
+
try {
|
|
980
|
+
note = refreshedNote(refreshStale(root)); // refreshing is all that is left to do, so a failure fails upgrade
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
984
|
+
throw new PairError(`already up to date, but couldn't refresh instructions (${reason})`);
|
|
985
|
+
}
|
|
986
|
+
console.log(`already up to date: ${PACKAGE.name} ${PACKAGE.version}${note ? `\n${note}` : ""}`);
|
|
987
|
+
return 0;
|
|
988
|
+
}
|
|
989
|
+
const install = spawnSync("npm", ["install", "--global", `${PACKAGE.name}@latest`], { stdio: "inherit" });
|
|
990
|
+
if (install.error) {
|
|
991
|
+
throw new PairError(`couldn't run npm: ${install.error.message}`);
|
|
992
|
+
}
|
|
993
|
+
if (install.status !== 0) {
|
|
994
|
+
return install.status ?? 1;
|
|
995
|
+
}
|
|
996
|
+
const states = [
|
|
997
|
+
instructionState(join(root, ".claude", "skills", "pair", "SKILL.md"), claudeSkill()),
|
|
998
|
+
instructionState(join(root, "AGENTS.md"), codexSection(), true),
|
|
999
|
+
];
|
|
1000
|
+
if (states.some((state) => state !== "missing")) {
|
|
1001
|
+
// The package just installed, not whatever `pair` comes first on PATH, which may be an older copy.
|
|
1002
|
+
const globalRoot = spawnSync("npm", ["root", "--global"], { encoding: "utf8" });
|
|
1003
|
+
if (globalRoot.error || globalRoot.status !== 0) {
|
|
1004
|
+
throw new PairError("installed, but `npm root --global` failed; run `pair init` yourself");
|
|
1005
|
+
}
|
|
1006
|
+
const installed = join(globalRoot.stdout.trim(), PACKAGE.name, "dist", "pair.js");
|
|
1007
|
+
const init = spawnSync(process.execPath, [installed, "init"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
1008
|
+
if (init.error || init.status !== 0) {
|
|
1009
|
+
throw new PairError(`installed, but the new \`pair init\` failed${init.error ? `: ${init.error.message}` : ""}; run it yourself`);
|
|
1010
|
+
}
|
|
1011
|
+
// Only what it did to the files: the sandbox and `codex mcp add` setup it also prints is already done.
|
|
1012
|
+
console.log(init.stdout.split("\n").filter((line) => /^(?:wrote|kept): /.test(line)).join("\n"));
|
|
1013
|
+
}
|
|
1014
|
+
console.log(`upgraded: ${PACKAGE.name} ${PACKAGE.version} → ${latest ?? "latest"}. Restart Claude Code and Codex sessions to load it; other worktrees refresh on their next pair send or wait.`);
|
|
1015
|
+
return 0;
|
|
1016
|
+
}
|
|
876
1017
|
/** Finished threads, most recently finished first. Entries that aren't archived threads are ignored. */
|
|
877
1018
|
function listArchived(channel) {
|
|
878
1019
|
const archiveDir = join(channel, "archive");
|
|
@@ -953,25 +1094,42 @@ function renderPrompt(agent) {
|
|
|
953
1094
|
}
|
|
954
1095
|
function claudeSkill() {
|
|
955
1096
|
const header = `---\nname: pair\ndescription: Request a code review from codex after finishing a task, or act as codex's reviewer ("be my reviewer", "listen"), via the pair CLI.\n---\n\n`;
|
|
956
|
-
return header
|
|
1097
|
+
return `${header}<!-- pair ${PACKAGE.version} -->\n${renderPrompt("claude")}`;
|
|
957
1098
|
}
|
|
958
1099
|
function codexSection() {
|
|
959
|
-
return `<!-- pair:start -->\n${renderPrompt("codex").trimEnd()}\n<!-- pair:end -->\n`;
|
|
1100
|
+
return `<!-- pair:start -->\n<!-- pair ${PACKAGE.version} -->\n${renderPrompt("codex").trimEnd()}\n<!-- pair:end -->\n`;
|
|
960
1101
|
}
|
|
961
1102
|
function init(channel, root) {
|
|
962
1103
|
channelAccess(channel, () => mkdirSync(channel, { recursive: true }));
|
|
1104
|
+
// Current files keep their stamp too: a newer pair may have written the same text.
|
|
1105
|
+
const kept = (state) => state === "current" ? " (current)" : state.startsWith("newer") ? " (a newer pair wrote it; run `pair upgrade`, or delete it to downgrade)" : undefined;
|
|
1106
|
+
const done = [];
|
|
963
1107
|
const skillDir = join(root, ".claude", "skills", "pair");
|
|
964
|
-
|
|
965
|
-
|
|
1108
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
1109
|
+
const skillKept = kept(instructionState(skillPath, claudeSkill()));
|
|
1110
|
+
if (skillKept) {
|
|
1111
|
+
done.push(`kept: ${skillPath}${skillKept}`);
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1115
|
+
writeFileSync(skillPath, claudeSkill());
|
|
1116
|
+
done.push(`wrote: ${skillPath}`);
|
|
1117
|
+
}
|
|
966
1118
|
const agentsPath = join(root, "AGENTS.md");
|
|
967
1119
|
const section = codexSection();
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
1120
|
+
const agentsKept = kept(instructionState(agentsPath, section, true));
|
|
1121
|
+
if (agentsKept) {
|
|
1122
|
+
done.push(`kept: ${agentsPath} (pair section)${agentsKept}`);
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
const existing = existsSync(agentsPath) ? readFileSync(agentsPath, "utf8") : "";
|
|
1126
|
+
const updated = PAIR_SECTION.test(existing) ? existing.replace(PAIR_SECTION, () => section) : `${existing}${existing ? "\n" : ""}${section}`;
|
|
1127
|
+
writeFileSync(agentsPath, updated);
|
|
1128
|
+
done.push(`wrote: ${agentsPath} (pair section)`);
|
|
1129
|
+
}
|
|
971
1130
|
return [
|
|
972
1131
|
`channel: ${channel}`,
|
|
973
|
-
|
|
974
|
-
`wrote: ${agentsPath} (pair section)`,
|
|
1132
|
+
...done,
|
|
975
1133
|
"",
|
|
976
1134
|
sandboxConfig(channel),
|
|
977
1135
|
"",
|
|
@@ -1017,7 +1175,7 @@ function uninit(channel, root) {
|
|
|
1017
1175
|
const skillPath = join(skillDir, "SKILL.md");
|
|
1018
1176
|
if (existsSync(skillPath)) {
|
|
1019
1177
|
const skill = readFileSync(skillPath, "utf8");
|
|
1020
|
-
if (skill === claudeSkill() || RELEASED_SKILL_SHA256.includes(createHash("sha256").update(skill).digest("hex"))) {
|
|
1178
|
+
if (unstamped(skill) === unstamped(claudeSkill()) || RELEASED_SKILL_SHA256.includes(createHash("sha256").update(skill).digest("hex"))) {
|
|
1021
1179
|
rmSync(skillPath);
|
|
1022
1180
|
removed.push(skillPath);
|
|
1023
1181
|
// Drop the directories init created, up to the first one that holds something else.
|
|
@@ -1240,6 +1398,7 @@ const USAGE = `usage:
|
|
|
1240
1398
|
pair history [thread-id] (list finished threads, or show one)
|
|
1241
1399
|
pair clean --keep N (delete all but the N newest finished threads)
|
|
1242
1400
|
pair init
|
|
1401
|
+
pair upgrade (install the latest pair from npm, then refresh this worktree)
|
|
1243
1402
|
pair uninit (remove what pair init wrote in this worktree; keeps history)
|
|
1244
1403
|
pair register | unregister (in Codex's shell: let pair_listen notify this session)
|
|
1245
1404
|
pair mcp (stdio MCP server for Codex, providing pair_listen)
|
|
@@ -1254,57 +1413,67 @@ async function main(argv) {
|
|
|
1254
1413
|
const root = worktreeRoot();
|
|
1255
1414
|
const channel = channelDir(root);
|
|
1256
1415
|
const agent = () => oneOf(values.as ?? process.env.PAIR_AGENT, AGENTS, "--as (or PAIR_AGENT)");
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
autoKeep(); // reject a bad PAIR_KEEP before touching any thread
|
|
1269
|
-
const me = agent();
|
|
1270
|
-
const timeout = positiveNumber(values.timeout ?? String(DEFAULT_WAIT_SECONDS), "--timeout");
|
|
1271
|
-
if (!(await wait(channel, me, timeout))) {
|
|
1272
|
-
// Exit 0: a timeout is routine, and agent UIs show any non-zero exit as a failure. No "run it
|
|
1273
|
-
// again" here: whether to wait again differs by agent (Codex must not poll), so the prompt says.
|
|
1274
|
-
console.log(`pair: no message for ${me} yet.`);
|
|
1416
|
+
const refreshNote = REFRESHING_COMMANDS.includes(command) ? refreshInstructions(root) : undefined;
|
|
1417
|
+
try {
|
|
1418
|
+
switch (command) {
|
|
1419
|
+
case "send": {
|
|
1420
|
+
autoKeep(); // reject a bad PAIR_KEEP before touching any thread
|
|
1421
|
+
const type = oneOf(rest[0], MESSAGE_TYPES, "message type");
|
|
1422
|
+
const verdict = values.verdict === undefined ? undefined : oneOf(values.verdict, VERDICTS, "--verdict");
|
|
1423
|
+
const me = agent();
|
|
1424
|
+
const body = readBody(rest[1]);
|
|
1425
|
+
console.log(`sent: ${channelAccess(channel, () => send(channel, me, type, body, verdict))}`);
|
|
1426
|
+
return 0;
|
|
1275
1427
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1428
|
+
case "wait": {
|
|
1429
|
+
autoKeep(); // reject a bad PAIR_KEEP before touching any thread
|
|
1430
|
+
const me = agent();
|
|
1431
|
+
const timeout = positiveNumber(values.timeout ?? String(DEFAULT_WAIT_SECONDS), "--timeout");
|
|
1432
|
+
if (!(await wait(channel, me, timeout))) {
|
|
1433
|
+
// Exit 0: a timeout is routine, and agent UIs show any non-zero exit as a failure. No "run it
|
|
1434
|
+
// again" here: whether to wait again differs by agent (Codex must not poll), so the prompt says.
|
|
1435
|
+
console.log(`pair: no message for ${me} yet.`);
|
|
1436
|
+
}
|
|
1437
|
+
return 0;
|
|
1438
|
+
}
|
|
1439
|
+
case "status":
|
|
1440
|
+
console.log([status(channel, root), ...(await updateNotice())].join("\n"));
|
|
1441
|
+
return 0;
|
|
1442
|
+
case "history":
|
|
1443
|
+
console.log(channelAccess(channel, () => history(channel, rest[0])));
|
|
1444
|
+
return 0;
|
|
1445
|
+
case "clean":
|
|
1446
|
+
console.log(channelAccess(channel, () => clean(channel, values.keep)));
|
|
1447
|
+
return 0;
|
|
1448
|
+
case "init":
|
|
1449
|
+
console.log([init(channel, root), ...(await updateNotice())].join("\n"));
|
|
1450
|
+
return 0;
|
|
1451
|
+
case "upgrade":
|
|
1452
|
+
return upgrade(root);
|
|
1453
|
+
case "uninit":
|
|
1454
|
+
console.log(uninit(channel, root));
|
|
1455
|
+
return 0;
|
|
1456
|
+
case "register":
|
|
1457
|
+
console.log(channelAccess(channel, () => register(channel, root)));
|
|
1458
|
+
return 0;
|
|
1459
|
+
case "unregister":
|
|
1460
|
+
console.log(channelAccess(channel, () => unregister(channel)));
|
|
1461
|
+
return 0;
|
|
1462
|
+
case "mcp":
|
|
1463
|
+
await serveMcp();
|
|
1464
|
+
return 0;
|
|
1465
|
+
case "ps":
|
|
1466
|
+
console.log(ps(values.verbose ?? false));
|
|
1467
|
+
return 0;
|
|
1468
|
+
default:
|
|
1469
|
+
console.error(USAGE);
|
|
1470
|
+
return 1;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
finally {
|
|
1474
|
+
if (refreshNote) {
|
|
1475
|
+
console.error(refreshNote);
|
|
1476
|
+
}
|
|
1308
1477
|
}
|
|
1309
1478
|
}
|
|
1310
1479
|
main(process.argv.slice(2)).then((code) => {
|