chesspal 0.2.1 → 0.2.3

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.
@@ -1,7 +1,7 @@
1
1
  import { type CurrentGame } from "../games/index.ts";
2
2
  import type { Context } from "./context.ts";
3
3
  import type { GameSummary } from "./game-summary.ts";
4
- /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
4
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code or Codex, with its agent in the player's folder (D-89, D-98). */
5
5
  export declare function analystRuns(context: Context): boolean;
6
6
  /**
7
7
  * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
@@ -12,8 +12,10 @@ export declare function withAnalyst(summary: GameSummary, context: Context, over
12
12
  export declare function analystLine(game?: string): string;
13
13
  /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
14
14
  export declare function reviewedLine(game?: string): string;
15
- /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
15
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code or Codex, with its agent in the player's folder (D-80, D-88, D-98). */
16
16
  export declare function coachRuns(context: Context): boolean;
17
+ /** Whether one of Chesspal's agents runs beside this chat: in a host that starts agents, with its file in the player's folder. */
18
+ export declare function agentRuns(context: Context, agent: string): boolean;
17
19
  /**
18
20
  * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
19
21
  * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
@@ -1,11 +1,11 @@
1
- // The agents that work beside the chat in Claude Code: the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
1
+ // The agents that work beside the chat in Claude Code and Codex (D-98): the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
2
2
  import { isReviewed } from "../core/index.js";
3
3
  import { archivedGame, archivedName, coachStarted, gameInAnalysis } from "../games/index.js";
4
- import { analystAgent, coachAgent, hasAgent } from "../host-files/index.js";
4
+ import { agentHosts, analystAgent, coachAgent, hasAgent } from "../host-files/index.js";
5
5
  import { gameFolder, helpsIn } from "../profiles/index.js";
6
- /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
6
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code or Codex, with its agent in the player's folder (D-89, D-98). */
7
7
  export function analystRuns(context) {
8
- return context.host?.() === "claude-code" && hasAgent(context.folder, analystAgent);
8
+ return agentRuns(context, analystAgent);
9
9
  }
10
10
  /**
11
11
  * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
@@ -28,9 +28,14 @@ export function reviewedLine(game) {
28
28
  return "The analyst agent has gone over this game, or is at it: never start it again. When the player wants it gone over, go over it with them from what "
29
29
  + `it told you, or, in a chat without it, from its review on the board page, which analyse_game ${game === undefined ? "" : `{"game": "${game}"} `}gives.`;
30
30
  }
31
- /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
31
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code or Codex, with its agent in the player's folder (D-80, D-88, D-98). */
32
32
  export function coachRuns(context) {
33
- return context.host?.() === "claude-code" && hasAgent(context.folder, coachAgent);
33
+ return agentRuns(context, coachAgent);
34
+ }
35
+ /** Whether one of Chesspal's agents runs beside this chat: in a host that starts agents, with its file in the player's folder. */
36
+ export function agentRuns(context, agent) {
37
+ const host = context.host?.();
38
+ return host !== undefined && agentHosts.includes(host) && hasAgent(context.folder, agent, host);
34
39
  }
35
40
  /**
36
41
  * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { boardText, drawingsText, moods, moodTag } from "../core/index.js";
3
3
  import { commentWithPlan, talkLimits } from "../games/index.js";
4
- import { coachAgent, hasAgent, ponderAgentFor } from "../host-files/index.js";
4
+ import { coachAgent, ponderAgentFor } from "../host-files/index.js";
5
5
  import { gameFolder, helpsIn } from "../profiles/index.js";
6
- import { agentsLine, coachRuns, withAnalyst } from "./agents.js";
6
+ import { agentRuns, agentsLine, coachRuns, withAnalyst } from "./agents.js";
7
7
  import { closingLines, moveLines, shownMove } from "./answer-lines.js";
8
8
  import { arrowsInput, drawingsOf, squaresInput } from "./drawings.js";
9
9
  import {} from "./game-summary.js";
@@ -71,15 +71,12 @@ function refuseWithoutHelps(folder, { comment, arrows, squares }) {
71
71
  }
72
72
  }
73
73
  /**
74
- * The agents to start after the move, in Claude Code, in a folder init set up with them: the one that thinks for the AI while the game goes on (D-72), then
74
+ * The agents to start after the move, in Claude Code or Codex, in a folder init set up with them: the one that thinks for the AI while the game goes on (D-72), then
75
75
  * the coach with the helps on, which judges the player's move even when the AI's ends the game (D-88).
76
76
  */
77
77
  function agentsAfter(context, folder, played) {
78
- if (context.host?.() !== "claude-code") {
79
- return [];
80
- }
81
78
  const ponder = ponderAgentFor(played.game.level);
82
- const thinks = played.game.end === undefined && hasAgent(context.folder, ponder);
79
+ const thinks = played.game.end === undefined && agentRuns(context, ponder);
83
80
  const coaches = helpsIn(folder) && coachRuns(context);
84
81
  return [...(thinks ? [ponder] : []), ...(coaches ? [coachAgent] : [])];
85
82
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { badges, Game, leadText, levelOf, materialOf, withSanLetters } from "../core/index.js";
3
3
  import { archiveCoached, coachWaitingSince, coachWords, endOf, keepHint, keepJudgment, keepLead, loadGame, startCoach } from "../games/index.js";
4
- import { coachAgent } from "../host-files/index.js";
4
+ import { agentHosts, coachAgent } from "../host-files/index.js";
5
5
  import { activeProfile, gameFolder, helpsIn } from "../profiles/index.js";
6
6
  import { clockLine, clockSummaryOf } from "./clock.js";
7
7
  import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
@@ -40,7 +40,7 @@ export const coach = defineCommand({
40
40
  done: z.boolean().optional().describe("true once you have kept your words, who is ahead always while the game goes on: it waits for the player's move"),
41
41
  }),
42
42
  byAgents: [coachAgent],
43
- host: "claude-code",
43
+ hosts: agentHosts,
44
44
  run: async ({ game, position, ahead, badge, note, hint, arrows, squares, done = false }, context) => {
45
45
  const folder = gameFolder(context.home());
46
46
  const current = loadGame(folder);
@@ -30,8 +30,6 @@ export interface Context {
30
30
  heard?: () => void;
31
31
  /** Told when the chat calls a tool, before it runs: the board page hears the chat listens, and the AI's clock, held while nobody did, starts. */
32
32
  calls?: () => void;
33
- /** Told when the AI starts thinking on the player's time (D-72), with the position, and when it is done: the board page shows it. */
34
- ponders?: (position: SeenPosition | undefined) => void;
35
33
  /** The game the chat saw when its last tool ended, its folder and its stamp: the doorbell wakes it only for a game changed since. */
36
34
  seen?: string | undefined;
37
35
  /**
@@ -29,7 +29,7 @@ interface Start {
29
29
  }
30
30
  /**
31
31
  * The AI's thinking on the player's time (D-72), for the agent that does it: the position, the view after a reply, its answer kept for its move, and its
32
- * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to Claude Code alone, which runs the agent.
32
+ * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to the hosts that run the agent (D-98).
33
33
  */
34
34
  export declare const ponder: import("./registry.ts").Command<z.ZodObject<{
35
35
  position: z.ZodOptional<z.ZodInt>;
@@ -46,6 +46,7 @@ export declare const ponder: import("./registry.ts").Command<z.ZodObject<{
46
46
  export declare function waitForPlayer(context: {
47
47
  now: () => Date;
48
48
  pause?: (milliseconds: number) => Promise<void>;
49
+ host?: () => string | undefined;
49
50
  }, since: Date, gone: () => boolean): Promise<"moved" | "waited" | "waiting">;
50
51
  /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
51
52
  export declare function wordsOf(comment: string | undefined): string | undefined;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { Game, levelOf, moodAt, sees, timeLeft } from "../core/index.js";
3
- import { currentPlan, endOf, keepThought, loadGame, talkLimits, thoughtKey, waitingSince } from "../games/index.js";
4
- import { ponderAgents } from "../host-files/index.js";
3
+ import { currentPlan, endOf, keepThought, loadGame, notePondering, talkLimits, thoughtKey, waitingSince } from "../games/index.js";
4
+ import { agentHosts, isCodex, ponderAgents } from "../host-files/index.js";
5
5
  import { gameFolder } from "../profiles/index.js";
6
6
  import { positionLines } from "./answer-lines.js";
7
7
  import { summaryOf } from "./game-summary.js";
@@ -11,7 +11,7 @@ import { say as sayWords } from "./say.js";
11
11
  import { boardView, colourName, compactView } from "./view.js";
12
12
  /**
13
13
  * The AI's thinking on the player's time (D-72), for the agent that does it: the position, the view after a reply, its answer kept for its move, and its
14
- * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to Claude Code alone, which runs the agent.
14
+ * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to the hosts that run the agent (D-98).
15
15
  */
16
16
  export const ponder = defineCommand({
17
17
  name: "ponder",
@@ -28,7 +28,7 @@ export const ponder = defineCommand({
28
28
  done: z.boolean().optional().describe("true when you have no more replies to think on: it waits for the player's move"),
29
29
  }),
30
30
  byAgents: ponderAgents,
31
- host: "claude-code",
31
+ hosts: agentHosts,
32
32
  run: async ({ position, reply, move, why, say, done = false }, context) => {
33
33
  const folder = gameFolder(context.home());
34
34
  const current = toPonder(folder, context.now(), position);
@@ -39,7 +39,7 @@ export const ponder = defineCommand({
39
39
  if (reply !== undefined || move !== undefined || why !== undefined || say !== undefined || done) {
40
40
  throw new Error("Give the position's number with it: call ponder {} first, which gives it. Nothing changed.");
41
41
  }
42
- context.ponders?.({ started: current.started, plies: current.game.record.moves.length });
42
+ notePondering(folder, thoughtKey(current), true);
43
43
  return { start: startOf(current) };
44
44
  }
45
45
  if ((say !== undefined || done) && (reply !== undefined || move !== undefined || why !== undefined || (say !== undefined && done))) {
@@ -50,7 +50,7 @@ export const ponder = defineCommand({
50
50
  return { said: position };
51
51
  }
52
52
  if (done) {
53
- context.ponders?.(undefined);
53
+ notePondering(folder, thoughtKey(current), false);
54
54
  return await waitForMove(folder, context, current);
55
55
  }
56
56
  if (reply === undefined) {
@@ -106,14 +106,15 @@ export const ponder = defineCommand({
106
106
  export async function waitForPlayer(context, since, gone) {
107
107
  const pause = context.pause ?? ((milliseconds) => new Promise((done) => setTimeout(done, milliseconds)));
108
108
  const start = context.now().getTime();
109
- const end = Math.min(start + playerWait.most, since.getTime() + playerWait.inAll);
109
+ const most = isCodex(context.host?.()) ? playerWait.mostInCodex : playerWait.most;
110
+ const end = Math.min(start + most, since.getTime() + playerWait.inAll);
110
111
  while (context.now().getTime() < end) {
111
112
  await pause(playerWait.every);
112
113
  if (gone()) {
113
114
  return "moved";
114
115
  }
115
116
  }
116
- return end < start + playerWait.most ? "waited" : "waiting";
117
+ return end < start + most ? "waited" : "waiting";
117
118
  }
118
119
  /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
119
120
  export function wordsOf(comment) {
@@ -122,9 +123,10 @@ export function wordsOf(comment) {
122
123
  }
123
124
  /**
124
125
  * How long one call waits for the player's move at most, under the minute a host may give a tool (Priya), how often it looks, and how long the agent
125
- * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya).
126
+ * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya). Codex hands its agent's code back to the model after 30
127
+ * seconds: a call shorter than that is one turn of the model, not several (Priya).
126
128
  */
127
- const playerWait = { most: 50_000, every: 500, inAll: 10 * 60_000 };
129
+ const playerWait = { most: 50_000, mostInCodex: 25_000, every: 500, inAll: 10 * 60_000 };
128
130
  /** The game whose position the AI thinks on, read and never written: none once the player has moved, the game is over or their flag has fallen, or the position given is another. */
129
131
  function toPonder(folder, now, position) {
130
132
  const current = loadGame(folder);
@@ -23,8 +23,8 @@ export interface Command<Schema extends z.ZodObject = z.ZodObject, Result = unkn
23
23
  byAgents?: readonly string[];
24
24
  /** The agents that call it besides the chat, as the analyst analyse_game (D-89): their calls leave the doorbell's record of what the chat has read as it was. */
25
25
  sharedWith?: readonly string[];
26
- /** The only host the MCP server offers it to, by the client's name, as ponder to Claude Code: elsewhere its description would cost tokens for nothing. */
27
- host?: string;
26
+ /** The only hosts the MCP server offers it to, by the client's name, as ponder to those that run agents: elsewhere its description would cost tokens for nothing. */
27
+ hosts?: readonly string[];
28
28
  /** Only the command line has it, as init: the MCP server leaves it out, and its description costs the AI no tokens. */
29
29
  cliOnly?: boolean;
30
30
  /** Fields the command line takes as positional arguments, in order. */
@@ -23,6 +23,18 @@ export interface Pin {
23
23
  readonly by: Square;
24
24
  readonly to: Square;
25
25
  }
26
+ /**
27
+ * A king, queen or rook attacked by a bishop, rook or queen with a piece of its side worth less behind it on the line, when the front piece must move (it
28
+ * is the king, the attacker is worth less, or it is undefended) and the attacker is worth less than the piece behind or that piece is undefended without
29
+ * counting the front piece (Anatolij).
30
+ */
31
+ export interface Skewer {
32
+ /** The piece in front, attacked. */
33
+ readonly square: Square;
34
+ readonly by: Square;
35
+ /** The piece behind it. */
36
+ readonly to: Square;
37
+ }
26
38
  /** The facts of a position: data for the AI and the page, never a judgment (D-19). */
27
39
  export interface Facts {
28
40
  /** Every piece, White's first, each side's from the king down: K Q R B N P. */
@@ -38,6 +50,7 @@ export interface Facts {
38
50
  /** Pieces neither attacked nor defended, kings left out: the first targets of a tactic. */
39
51
  readonly loose: readonly Square[];
40
52
  readonly pins: readonly Pin[];
53
+ readonly skewers: readonly Skewer[];
41
54
  /** The checks of the side to move, in SAN. */
42
55
  readonly checks: readonly string[];
43
56
  /** The checks the other side would have if it were to move; left out when the side to move is in check, since it could not be. */
@@ -1,4 +1,4 @@
1
- // What a position shows, computed without judging it (D-19, D-26): who attacks and defends each piece, what is at risk, the pins, the checks, the moves of each piece.
1
+ // What a position shows, computed without judging it (D-19, D-26): who attacks and defends each piece, what is at risk, the pins and skewers, the checks, the moves of each piece.
2
2
  import { Chess } from "chess.js";
3
3
  /** The facts of a chess.js board, whose legal moves are given: computed once for each position, by its key, and frozen. */
4
4
  export function factsOf(chess, legal) {
@@ -51,6 +51,7 @@ function computed(chess, legal) {
51
51
  const defended = atRisk.filter((piece) => piece.defenders.length > 0);
52
52
  const byCheaper = defended.filter((piece) => Math.min(...piece.attackers.map(valueAt)) < valueAt(piece.square));
53
53
  const relativePins = placed.filter((piece) => piece.type === "q" || piece.type === "r").flatMap((target) => pinsTo(chess, target).filter((pin) => valueAt(pin.by) < values[target.type] || factsAt.get(target.square)?.defenders.length === 0));
54
+ const skewers = placed.filter((piece) => piece.type === "k" || piece.type === "q" || piece.type === "r").flatMap((front) => skewersOf(chess, front, factsAt));
54
55
  const king = pieces.find((piece) => piece.piece === "K" && piece.side === sideOf(turn));
55
56
  const facts = {
56
57
  pieces,
@@ -60,6 +61,7 @@ function computed(chess, legal) {
60
61
  outnumbered: defended.filter((piece) => !byCheaper.includes(piece) && piece.attackers.length > piece.defenders.length).map((piece) => piece.square),
61
62
  loose: officers.filter((piece) => piece.attackers.length === 0 && piece.defenders.length === 0).map((piece) => piece.square),
62
63
  pins: [...kingPins, ...relativePins].sort((left, right) => compare(left.square, right.square)),
64
+ skewers: skewers.sort((left, right) => compare(left.square, right.square)),
63
65
  checks: legal.filter((move) => /[+#]$/.test(move.san)).map((move) => move.san),
64
66
  };
65
67
  return chess.inCheck() ? facts : { ...facts, otherChecks: otherChecks(chess) };
@@ -128,6 +130,30 @@ function pinsTo(chess, target) {
128
130
  }
129
131
  return pins;
130
132
  }
133
+ /**
134
+ * The skewers of a king, queen or rook: a rook, bishop or queen that attacks it as the facts count, and a piece of its side worth less right behind it.
135
+ * The piece behind is counted without the front one among its defenders: the front piece leaves the line when it moves (Anatolij).
136
+ */
137
+ function skewersOf(chess, front, factsAt) {
138
+ const skewers = [];
139
+ const facts = factsAt.get(front.square);
140
+ for (const [fileStep, rankStep, slider] of lines) {
141
+ const by = rayFrom(front.square, fileStep, rankStep).find((square) => chess.get(square) !== undefined);
142
+ const to = rayFrom(front.square, -fileStep, -rankStep).find((square) => chess.get(square) !== undefined);
143
+ const attacker = by === undefined ? undefined : chess.get(by);
144
+ const behind = to === undefined ? undefined : chess.get(to);
145
+ if (attacker === undefined || behind === undefined || attacker.color === front.color || behind.color !== front.color
146
+ || (attacker.type !== slider && attacker.type !== "q") || values[behind.type] >= values[front.type] || !facts.attackers.includes(by)) {
147
+ continue;
148
+ }
149
+ const mustMove = front.type === "k" || values[attacker.type] < values[front.type] || facts.defenders.length === 0;
150
+ const behindDefended = factsAt.get(to).defenders.some((square) => square !== front.square);
151
+ if (mustMove && (values[attacker.type] < values[behind.type] || !behindDefended)) {
152
+ skewers.push({ square: front.square, by: by, to: to });
153
+ }
154
+ }
155
+ return skewers;
156
+ }
131
157
  /** The pawn that can be taken en passant, and the square a pawn takes it on; none without an en passant square. */
132
158
  function enPassantVictim(chess) {
133
159
  const to = chess.fen().split(" ")[3];
@@ -1,7 +1,7 @@
1
1
  export { boardText, figurinesOf } from "./board-text.ts";
2
2
  export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.ts";
3
3
  export type { Clock, TimeControl } from "./clock.ts";
4
- export type { Facts, PieceFacts, PieceLetter, Pin } from "./facts.ts";
4
+ export type { Facts, PieceFacts, PieceLetter, Pin, Skewer } from "./facts.ts";
5
5
  export { Game, withSanLetters } from "./game.ts";
6
6
  export type { Ending, Move, Outcome, Played, Position, Promotion, Refusal, Side, Square } from "./game.ts";
7
7
  export type { Illegal, IllegalReason } from "./illegal.ts";
@@ -8,4 +8,4 @@ export { makeFolder, renameOver, writeAtomically } from "./files.ts";
8
8
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.ts";
9
9
  export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk } from "./table-talk.ts";
10
10
  export { type Heard, heardLines, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
11
- export { keepThought, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
11
+ export { keepThought, notePondering, ponderingOn, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
@@ -9,4 +9,4 @@ export { makeFolder, renameOver, writeAtomically } from "./files.js";
9
9
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.js";
10
10
  export { endTableTalk, loadTableTalk, saveTableTalk } from "./table-talk.js";
11
11
  export { heardLines, talkLimits, talkLine, unheardWords, wakesTheChat, WordsRefused } from "./talk.js";
12
- export { keepThought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
12
+ export { keepThought, notePondering, ponderingOn, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
@@ -13,3 +13,10 @@ export declare function keepThought(folder: string, key: string, thought: Though
13
13
  export declare function thoughtForLastMove(folder: string, current: CurrentGame): Thought | undefined;
14
14
  /** Since when the AI has waited for the player's move in a position: the first time it asked, `now` then, kept with its thoughts. */
15
15
  export declare function waitingSince(folder: string, key: string, now: Date): Date;
16
+ /**
17
+ * Notes whether the AI thinks on the player's time in a position now, from its first look until it waits for their move, for the page's sign: in the file,
18
+ * since in Codex the agent that thinks has a process of its own (D-98).
19
+ */
20
+ export declare function notePondering(folder: string, key: string, pondering: boolean): void;
21
+ /** Whether the AI thinks on the player's time in a position now, as noted. */
22
+ export declare function ponderingOn(folder: string, key: string): boolean;
@@ -33,6 +33,21 @@ export function waitingSince(folder, key, now) {
33
33
  write(folder, { ...kept, since: now.toISOString() });
34
34
  return now;
35
35
  }
36
+ /**
37
+ * Notes whether the AI thinks on the player's time in a position now, from its first look until it waits for their move, for the page's sign: in the file,
38
+ * since in Codex the agent that thinks has a process of its own (D-98).
39
+ */
40
+ export function notePondering(folder, key, pondering) {
41
+ const { pondering: before, ...kept } = keptAt(folder, key);
42
+ if ((before === true) !== pondering) {
43
+ write(folder, pondering ? { ...kept, pondering: true } : kept);
44
+ }
45
+ }
46
+ /** Whether the AI thinks on the player's time in a position now, as noted. */
47
+ export function ponderingOn(folder, key) {
48
+ const kept = read(folder);
49
+ return kept?.key === key && kept.pondering === true;
50
+ }
36
51
  /** The most thoughts kept for a position: a few likely replies, not every legal one. */
37
52
  const mostThoughts = 8;
38
53
  const fileName = "thoughts.json";
@@ -40,6 +55,7 @@ const thoughtsFile = z.object({
40
55
  key: z.string(),
41
56
  thoughts: z.array(z.object({ reply: z.string(), move: z.string(), why: z.string() })),
42
57
  since: z.iso.datetime().optional(),
58
+ pondering: z.boolean().optional(),
43
59
  });
44
60
  /** What is kept for a position: nothing yet when the file is for another, or missing. */
45
61
  function keptAt(folder, key) {
@@ -1,4 +1,8 @@
1
1
  import type { AiLevel } from "../core/index.ts";
2
+ /** The hosts whose chat starts Chesspal's agents, by the names they give the server (D-98). */
3
+ export declare const agentHosts: readonly string[];
4
+ /** Whether the host is Codex, which runs each agent in a process of its own and gives it every tool (D-98). */
5
+ export declare function isCodex(host: string | undefined): boolean;
2
6
  /** The agents that think for the AI, by their names. */
3
7
  export declare const ponderAgents: readonly string[];
4
8
  /** The coach's agent (D-88). */
@@ -9,9 +13,11 @@ export declare const analystAgent = "chesspal-analyst";
9
13
  export declare function ponderAgentFor(level: AiLevel): string;
10
14
  /** Whether the AI's thoughts on the player's time come from a quicker model than its own at a level. */
11
15
  export declare function onQuickModel(level: AiLevel): boolean;
12
- /** Whether the player's folder has the agent, as init writes it: a folder set up before it has none, and the AI then plays as before. */
13
- export declare function hasAgent(folder: string, agent: string): boolean;
14
- /** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
16
+ /** Whether the player's folder has the agent for the host, as init writes it: a folder set up before it has none, and the AI then plays as before. */
17
+ export declare function hasAgent(folder: string, agent: string, host: string | undefined): boolean;
18
+ /** The tools of one of Chesspal's agents, by its name; undefined for another agent. Codex gives its agents every tool, so the server keeps them to theirs (D-98). */
19
+ export declare function agentTools(agent: string): readonly string[] | undefined;
20
+ /** The agents' files, as Claude Code reads them from .claude/agents and Codex from .codex/agents: Chesspal's whole, rewritten by init, for both hosts. */
15
21
  export declare function agentFiles(folder: string): {
16
22
  path: string;
17
23
  text: string;
@@ -1,9 +1,16 @@
1
- // The agents that work in the background in Claude Code: those that think for the opponent on the player's time (D-72), one on the host's quick model for the
2
- // lower levels, one on the AI's own for the higher; the coach, with the helps on (D-88), and the analyst of a game over (D-89), on the AI's own model.
1
+ // The agents that work in the background in Claude Code and Codex (D-98): those that think for the opponent on the player's time (D-72), one on the host's
2
+ // quick model for the lower levels, one on the AI's own for the higher; the coach, with the helps on (D-88), and the analyst of a game over (D-89), on the AI's own.
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  const quickAgent = "chesspal-ponder-quick";
6
6
  const ownAgent = "chesspal-ponder";
7
+ const codexHost = "codex-mcp-client";
8
+ /** The hosts whose chat starts Chesspal's agents, by the names they give the server (D-98). */
9
+ export const agentHosts = ["claude-code", codexHost];
10
+ /** Whether the host is Codex, which runs each agent in a process of its own and gives it every tool (D-98). */
11
+ export function isCodex(host) {
12
+ return host === codexHost;
13
+ }
7
14
  /** The agents that think for the AI, by their names. */
8
15
  export const ponderAgents = [quickAgent, ownAgent];
9
16
  /** The coach's agent (D-88). */
@@ -18,36 +25,74 @@ export function ponderAgentFor(level) {
18
25
  export function onQuickModel(level) {
19
26
  return level === "beginner" || level === "club";
20
27
  }
21
- /** Whether the player's folder has the agent, as init writes it: a folder set up before it has none, and the AI then plays as before. */
22
- export function hasAgent(folder, agent) {
23
- return existsSync(agentPath(folder, agent));
28
+ /** Whether the player's folder has the agent for the host, as init writes it: a folder set up before it has none, and the AI then plays as before. */
29
+ export function hasAgent(folder, agent, host) {
30
+ return existsSync(isCodex(host) ? codexAgentPath(folder, agent) : claudeAgentPath(folder, agent));
31
+ }
32
+ /** The tools of one of Chesspal's agents, by its name; undefined for another agent. Codex gives its agents every tool, so the server keeps them to theirs (D-98). */
33
+ export function agentTools(agent) {
34
+ return toolsOf[agent];
24
35
  }
25
- /** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
36
+ /** The agents' files, as Claude Code reads them from .claude/agents and Codex from .codex/agents: Chesspal's whole, rewritten by init, for both hosts. */
26
37
  export function agentFiles(folder) {
38
+ const agents = agentSpecs();
39
+ return [...agents.map((agent) => claudeAgentFile(folder, agent)), ...agents.map((agent) => codexAgentFile(folder, agent))];
40
+ }
41
+ /** Instructions for the AI, by their file's name in the package's instructions folder. */
42
+ export function instructions(name) {
43
+ return readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
44
+ }
45
+ const toolsOf = {
46
+ [quickAgent]: ["ponder"],
47
+ [ownAgent]: ["ponder"],
48
+ [coachAgent]: ["coach"],
49
+ [analystAgent]: ["analyse_game", "annotate_game", "look", "position"],
50
+ };
51
+ // Codex's quick model for the lower levels: written in its agent, since Codex ignores a model the chat names when it starts one that has it (Priya).
52
+ const codexQuickModel = "gpt-6-luna";
53
+ function agentSpecs() {
27
54
  const ponder = instructions("ponder");
28
55
  const thinking = "Chesspal's opponent thinking on the player's time. Started by the AI in the background after its move, as ai_move says.";
29
56
  return [
30
- { agent: quickAgent, model: "haiku", description: thinking, tools: ["ponder"], text: ponder },
31
- { agent: ownAgent, model: "inherit", description: thinking, tools: ["ponder"], text: ponder },
57
+ { agent: quickAgent, claudeModel: "haiku", codexModel: codexQuickModel, description: thinking, text: ponder },
58
+ { agent: ownAgent, claudeModel: "inherit", description: thinking, text: ponder },
32
59
  // Its description tells the chat it starts the agent only as ai_move says, never to ask it about the game (Priya).
33
- { agent: coachAgent, model: "inherit", description: "Chesspal's coach. Started only as Chesspal's answers say, in the background, never to answer the player.", tools: ["coach"], text: instructions("coach") },
60
+ {
61
+ agent: coachAgent,
62
+ claudeModel: "inherit",
63
+ description: "Chesspal's coach. Started only as Chesspal's answers say, in the background, never to answer the player.",
64
+ text: instructions("coach"),
65
+ },
34
66
  {
35
67
  agent: analystAgent,
36
- model: "inherit",
68
+ claudeModel: "inherit",
37
69
  description: "Chesspal's analyst. Started only as Chesspal's answers say, in the background, to review a game over on the board page; never to answer the player.",
38
- tools: ["analyse_game", "annotate_game", "look", "position"],
39
70
  text: `${instructions("analyst-agent")}\n${instructions("analysis")}`,
40
71
  },
41
- ].map(({ agent, model, description, tools, text }) => ({
42
- path: agentPath(folder, agent),
43
- // The tools it works with: Claude Code gives an agent its MCP tools loaded, with no search first (Priya).
44
- text: ["---", `name: ${agent}`, `description: ${description}`, `tools: ${tools.map((tool) => `mcp__chesspal__${tool}`).join(", ")}`, `model: ${model}`, "---", "", text, ""].join("\n"),
45
- }));
72
+ ];
46
73
  }
47
- /** Instructions for the AI, by their file's name in the package's instructions folder. */
48
- export function instructions(name) {
49
- return readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
74
+ function claudeAgentFile(folder, { agent, claudeModel, description, text }) {
75
+ // The tools it works with: Claude Code gives an agent its MCP tools loaded, with no search first (Priya).
76
+ const tooled = `tools: ${(toolsOf[agent] ?? []).map((tool) => `mcp__chesspal__${tool}`).join(", ")}`;
77
+ return { path: claudeAgentPath(folder, agent), text: ["---", `name: ${agent}`, `description: ${description}`, tooled, `model: ${claudeModel}`, "---", "", text, ""].join("\n") };
78
+ }
79
+ /**
80
+ * Codex's agent: its instructions in a literal string; no effort, which the chat could no longer change, and no model but the quick one's, so that the
81
+ * others play on the AI's own (Priya). Codex gives an agent all the chat's MCP servers: its instructions name its own tools.
82
+ */
83
+ function codexAgentFile(folder, { agent, codexModel, description, text }) {
84
+ if (text.includes("'''")) {
85
+ throw new Error(`The instructions of ${agent} hold ''', which would end Codex's literal string early: write them another way.`);
86
+ }
87
+ const model = codexModel === undefined ? [] : [`model = ${JSON.stringify(codexModel)}`];
88
+ // Its tools by the names Codex gives them, which spare the agent a search for them first (Priya).
89
+ const tools = (toolsOf[agent] ?? []).map((tool) => `tools.mcp__chesspal__${tool}`).join(", ");
90
+ const lines = [`name = ${JSON.stringify(agent)}`, `description = ${JSON.stringify(description)}`, ...model, "developer_instructions = '''", text, "", `Your tools: ${tools}.`, "'''", ""];
91
+ return { path: codexAgentPath(folder, agent), text: lines.join("\n") };
50
92
  }
51
- function agentPath(folder, agent) {
93
+ function claudeAgentPath(folder, agent) {
52
94
  return join(folder, ".claude", "agents", `${agent}.md`);
53
95
  }
96
+ function codexAgentPath(folder, agent) {
97
+ return join(folder, ".codex", "agents", `${agent}.toml`);
98
+ }
@@ -41,15 +41,15 @@ const endMark = "<!-- chesspal: end -->";
41
41
  const markedBlock = /<!-- chesspal: (?!end)[^\n]*\n[\s\S]*?<!-- chesspal: end -->/;
42
42
  /** What init writes for each host, missing or changed: what the player wrote there stays. */
43
43
  function hostChanges(folder, platform) {
44
- const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...ponderAgentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
44
+ const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...agentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
45
45
  return changes.filter((change) => change !== undefined);
46
46
  }
47
47
  /** How a host starts the server: on Windows through cmd, which finds npm's shim. */
48
48
  function chesspalServer(platform) {
49
49
  return platform === "win32" ? { command: "cmd", args: ["/c", "chesspal", "mcp"] } : { command: "chesspal", args: ["mcp"] };
50
50
  }
51
- /** Claude Code's agents that work on the player's time, thinking for the opponent and coaching, Chesspal's whole: written when they are missing or changed. */
52
- function ponderAgentChanges(folder) {
51
+ /** The agents of Claude Code and Codex, Chesspal's whole, whichever host the player has (Stefano): written when they are missing or changed. */
52
+ function agentChanges(folder) {
53
53
  return agentFiles(folder).filter(({ path, text }) => textOf(path) !== text);
54
54
  }
55
55
  /** .mcp.json with the chesspal server, beside the servers already there, keeping the environment the player gave it, as CHESSPAL_HOME in a playtest. */
@@ -1,2 +1,2 @@
1
- export { analystAgent, coachAgent, hasAgent, instructions, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
1
+ export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructions, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
2
2
  export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.ts";
@@ -1,3 +1,3 @@
1
1
  // The files through which an AI host finds Chesspal in the player's folder: the server, the opponent's instructions, the hosts' settings.
2
- export { analystAgent, coachAgent, hasAgent, instructions, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
2
+ export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructions, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
3
3
  export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.js";
@@ -3,10 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
3
3
  // with two clicks or by dragging a piece, which lifts off the board, in Zoe's design.
4
4
  import { useMemo, useRef } from "preact/hooks";
5
5
  import { BadgeIcon } from "../game/badges.js";
6
- import { riskOn } from "../helps/view.js";
6
+ import { pinOn, riskOn, skewerOn } from "../helps/view.js";
7
7
  import { Piece } from "../pieces/pieces.js";
8
8
  import { bubblePlace, keptSquares } from "./explain.js";
9
- import { Arrows, Badge, Bubble, ControlCount, controlOf, kingSigns, Lifted, noSigns, Notch, Pins, PromotionChoice } from "./marks.js";
9
+ import { Arrows, Badge, Bubble, ControlCount, controlOf, kingSigns, Lifted, noSigns, Notch, PinsAndSkewers, PromotionChoice, Pushpin, Skewer } from "./marks.js";
10
10
  import { backOf, motionOf, noMotion, playedOn, rearranged } from "./motion.js";
11
11
  import { useBoardPointer } from "./pointer.js";
12
12
  import { files, kingToMove, piecesOf } from "./squares.js";
@@ -104,12 +104,17 @@ export function Board({ fen, bottom, lastMove, pointed, helps, control, bare, ba
104
104
  "--dy": ranks.indexOf(Number(from[1])) - row,
105
105
  };
106
106
  const risk = signsOfHelps === undefined ? undefined : riskOn(signsOfHelps, square);
107
+ // A pinned piece's pushpin and a skewered one's skewer go with their lines, so not over the control map.
108
+ const pin = signsOfHelps === undefined || control !== undefined ? undefined : pinOn(signsOfHelps, square);
109
+ const skewer = signsOfHelps === undefined || control !== undefined ? undefined : skewerOn(signsOfHelps, square);
107
110
  return (_jsxs("div", { class: classes, role: "button", "aria-label": label, "aria-pressed": own(square) ? selected === square : undefined, tabIndex: square === focused ? 0 : -1, "data-square": square, onKeyDown: (event) => key(event, square), onFocus: () => setCursor(square),
108
111
  // A click with no pointer behind it, as a screen reader's in browse mode, presses the square as Enter does.
109
112
  onClick: (event) => event.detail === 0 && pressKey(square), style: away, children: [column === 0 ? _jsx("span", { class: "coordinate rank", children: rank }) : null, row === 7 ? _jsx("span", { class: "coordinate file", children: file }) : null, _jsx("span", { class: "square-name", "aria-hidden": "true", children: square }), motion.taken?.square === square
110
113
  ? (_jsx("span", { class: "taken-piece", children: _jsx(Piece, { letter: motion.taken.letter }) }, `taken ${fen}`))
111
- : null, motion.gone?.filter((gone) => gone.square === square).map((gone) => (_jsx("span", { class: "gone-piece", children: _jsx(Piece, { letter: gone.letter }) }, `gone ${fen}`))), control === undefined ? null : _jsx("span", { class: `control-ground ${controlOf(control[square])}`, "aria-hidden": "true" }), piece === undefined ? null : _jsx(Piece, { letter: piece }, moving ? fen : "still"), sign === undefined ? null : _jsx(Badge, { kind: sign.badge }), risk === undefined ? null : _jsx(Notch, { tone: risk }), control === undefined ? null : _jsx(ControlCount, { counts: control[square] }), badge === undefined || lastMove?.to !== square ? null : _jsx(BadgeIcon, { badge: badge.badge, title: badge.name })] }, square));
112
- })), signsOfHelps === undefined || signsOfHelps.pins.length === 0 || control !== undefined ? null : _jsx(Pins, { pins: signsOfHelps.pins, at: centreOf })] }), _jsx(Arrows, { arrows: aiArrows, at: centreOf, occupied: (square) => pieces.has(square) }), _jsx(Arrows, { arrows: ownArrows, at: centreOf }), notice?.explanation?.arrow === undefined ? null : _jsx(Arrows, { arrows: [notice.explanation.arrow], at: centreOf, explain: { occupied: pieces.has(notice.explanation.arrow.to) } }), drag === undefined || !pieces.has(drag.from) ? null : _jsx(Lifted, { drag: drag, letter: pieces.get(drag.from) }), promotion === undefined
114
+ : null, motion.gone?.filter((gone) => gone.square === square).map((gone) => (_jsx("span", { class: "gone-piece", children: _jsx(Piece, { letter: gone.letter }) }, `gone ${fen}`))), control === undefined ? null : _jsx("span", { class: `control-ground ${controlOf(control[square])}`, "aria-hidden": "true" }), piece === undefined ? null : _jsx(Piece, { letter: piece }, moving ? fen : "still"), sign === undefined ? null : _jsx(Badge, { kind: sign.badge }), risk === undefined ? null : _jsx(Notch, { tone: risk }), pin === undefined ? null : _jsx(Pushpin, { tone: pin }), skewer === undefined ? null : _jsx(Skewer, { tone: skewer }), control === undefined ? null : _jsx(ControlCount, { counts: control[square] }), badge === undefined || lastMove?.to !== square ? null : _jsx(BadgeIcon, { badge: badge.badge, title: badge.name })] }, square));
115
+ })), signsOfHelps === undefined || signsOfHelps.pins.length + signsOfHelps.skewers.length === 0 || control !== undefined
116
+ ? null
117
+ : _jsx(PinsAndSkewers, { pins: signsOfHelps.pins, skewers: signsOfHelps.skewers, at: centreOf })] }), _jsx(Arrows, { arrows: aiArrows, at: centreOf, occupied: (square) => pieces.has(square) }), _jsx(Arrows, { arrows: ownArrows, at: centreOf }), notice?.explanation?.arrow === undefined ? null : _jsx(Arrows, { arrows: [notice.explanation.arrow], at: centreOf, explain: { occupied: pieces.has(notice.explanation.arrow.to) } }), drag === undefined || !pieces.has(drag.from) ? null : _jsx(Lifted, { drag: drag, letter: pieces.get(drag.from) }), promotion === undefined
113
118
  ? null
114
119
  : _jsx(PromotionChoice, { side: promotion.side, column: columns.indexOf(promotion.to[0]), down: ranks[0] === Number(promotion.to[1]), onPick: promote, text: text }), notice === undefined ? null : (_jsx(Bubble, { notice: notice, place: notice.square === undefined ? undefined : seen(notice.square), where: notice.square === undefined
115
120
  ? "above"