pog-mcp 0.9.19 → 0.9.21
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/README.md +5 -1
- package/dist/client.d.ts +45 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +59 -0
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +480 -22
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -34,6 +34,93 @@ const KNOCKOUT_STAGES = CUP_STAGES.filter((stage) => !DRAWS_ALLOWED_STAGES.inclu
|
|
|
34
34
|
* discovering it from a raw Fastify 400.
|
|
35
35
|
*/
|
|
36
36
|
const TEAM_NAME_MAX = 30;
|
|
37
|
+
/**
|
|
38
|
+
* Issue #826 (P5-1): the ability-number INPUT fields are deprecated. The
|
|
39
|
+
* server has refused any change to a created player's four numbers since F-6
|
|
40
|
+
* (`player_vector_changed`), and agents now field a team through a playbook
|
|
41
|
+
* instead. zod 3's JSON-schema converter has no `deprecated` keyword, so the
|
|
42
|
+
* marker lives in the one place every client renders: the field description.
|
|
43
|
+
*
|
|
44
|
+
* Response-side hiding of these numbers is the SERVER's job (P6-1). Deleting
|
|
45
|
+
* them here would change nothing an HTTP caller sees — a false completion —
|
|
46
|
+
* so this package deliberately does not.
|
|
47
|
+
*/
|
|
48
|
+
const ABILITY_FIELD_DEPRECATED =
|
|
49
|
+
// #863 r4: the freeze rides the weekly-pool gate — a deployment on its
|
|
50
|
+
// documented FORM_PLAYER_POOL_ENABLED=0 rollback still accepts restats of
|
|
51
|
+
// unminted players, so "no path at all" would be false there.
|
|
52
|
+
'DEPRECATED. While the weekly player pool is enabled (the default), ability numbers are fixed ' +
|
|
53
|
+
'when a player is created and any change is refused with player_vector_changed; only a ' +
|
|
54
|
+
'deployment on the FORM_PLAYER_POOL_ENABLED rollback accepts changes to unminted players (a ' +
|
|
55
|
+
'minted player’s numbers are never taken from a submission). Only for the legacy hand-built ' +
|
|
56
|
+
'form: on update_squad send back exactly what get_squad returned. Prefer create_squad with ' +
|
|
57
|
+
'`template`, then set_playbook.';
|
|
58
|
+
/**
|
|
59
|
+
* The `@sws26/pog` template ids `POST /api/teams { template }` accepts.
|
|
60
|
+
*
|
|
61
|
+
* Restated, not imported — this package is published on its own with no
|
|
62
|
+
* workspace dependency — and pinned against `packages/pog/src/templates/
|
|
63
|
+
* index.ts` by `publishedFacts.test.ts`, so a template added or renamed there
|
|
64
|
+
* fails a test here instead of an agent's first create_squad.
|
|
65
|
+
*/
|
|
66
|
+
const POG_TEMPLATE_IDS = ['balanced', 'attack-wide', 'defend-counter', 'set-piece'];
|
|
67
|
+
/**
|
|
68
|
+
* The pog text grammar and variable list, in one place, for the four playbook
|
|
69
|
+
* tools. Every axis, value, formation, operator and variable below is pinned
|
|
70
|
+
* against `packages/pog/src` by `publishedFacts.test.ts` — an agent writes
|
|
71
|
+
* this language from the description alone, so a stale token here is a
|
|
72
|
+
* document it cannot get to parse.
|
|
73
|
+
*
|
|
74
|
+
* Deliberately says nothing about player ability: a playbook chooses shape,
|
|
75
|
+
* style and set-piece takers, and the server picks the eleven.
|
|
76
|
+
*/
|
|
77
|
+
const POG_GRAMMAR = 'A playbook (pog) is a small text document: 2-space indent, no tabs, no comments, and exactly ' +
|
|
78
|
+
'these five top-level keys, each once: ' +
|
|
79
|
+
'`version: 0` — ' +
|
|
80
|
+
'`formation: 442|433|352|532` — ' +
|
|
81
|
+
'`style: { balance: defend|balanced|attack, line: deep|mid|high, press: low|mid|high, ' +
|
|
82
|
+
'build: central|mixed|wide, keeper: wall|sweeper, set_piece: ignore|normal|priority }` ' +
|
|
83
|
+
'(an axis you leave out takes its default: balanced, mid, mid, mixed, wall, normal) — ' +
|
|
84
|
+
// #863 review: the solver does not read pog.kickers yet (a separately
|
|
85
|
+
// tracked gap, #847) — so say so, instead of offering a choice that is
|
|
86
|
+
// silently ignored at every kickoff.
|
|
87
|
+
'`kickers: { fk: #N, pk: #N }` (slot #1-#11) — REQUIRED by the grammar and saved, but NOT ' +
|
|
88
|
+
'APPLIED YET: today the free-kick and penalty takers are chosen by the server from the fielded ' +
|
|
89
|
+
'eleven and the set_piece axis, and your #N (and any rule that sets kickers) is ignored until a ' +
|
|
90
|
+
'later release — ' +
|
|
91
|
+
'`rules: []`, or `rules:` followed by any number of two-line blocks: ' +
|
|
92
|
+
'` when <variable> <op> <integer>:` then ` set: { <axis>: <value>, ... }` ' +
|
|
93
|
+
'(a set may also change `formation:` or `kickers: { fk: #N }`). Operators: >= <= == > <. ' +
|
|
94
|
+
// #863 review: how overlapping rules and unknown values resolve — the
|
|
95
|
+
// evaluator's own semantics (packages/pog/src/evaluate.ts).
|
|
96
|
+
'HOW RULES COMBINE: every rule whose condition holds applies, in document order, and a later ' +
|
|
97
|
+
'one overrides an earlier one field by field (each style axis, the formation, each kicker role ' +
|
|
98
|
+
'separately) — put the rule you want to win LAST. A rule whose variable is unknown never fires, ' +
|
|
99
|
+
'whatever the comparison (even `== 0`). ' +
|
|
100
|
+
// #863 review: match.decisive is the fixture's game mode, not "a knockout",
|
|
101
|
+
// and the two history windows read ELIGIBLE matches only — stated the way
|
|
102
|
+
// packages/api/src/lib/pog-variables.ts actually computes them.
|
|
103
|
+
'Variables: match.decisive — the upcoming fixture’s game mode: 1 when a level score goes to ' +
|
|
104
|
+
'extra time and penalties, else 0. It is 1 for fixtures that cannot end level (the decisive ' +
|
|
105
|
+
'knockout ladder) and 0 for draw-allowed fixtures such as the regulation playoff. ' +
|
|
106
|
+
// #863 r4: the friendly route never compiles a playbook (no
|
|
107
|
+
// resolveLineupForKickoff call in routes/matches.ts) — pinned by a test.
|
|
108
|
+
'FRIENDLIES NEVER USE A PLAYBOOK: play_friendly always fields each side’s saved eleven, so a ' +
|
|
109
|
+
'friendly cannot test a rule and its result says nothing about one. ' +
|
|
110
|
+
'A daily cup compiles your playbook ONCE, when the cup ' +
|
|
111
|
+
'opens, with match.decisive = 0, and that eleven plays every round of it, knockouts included — ' +
|
|
112
|
+
'so a match.decisive rule never fires anywhere in a cup. ' +
|
|
113
|
+
'opponent.last.<k> (the opponent’s newest ELIGIBLE completed match; not known for a cup, which ' +
|
|
114
|
+
'is compiled before the draw) and self.last3.<k> (your own newest three ELIGIBLE completed ' +
|
|
115
|
+
'matches, fewer for a young team). Eligible excludes a friendly in which the team was the AWAY ' +
|
|
116
|
+
'side of a challenge another wallet started, so both can read older matches than the history ' +
|
|
117
|
+
'you see. <k> is one of a0Goals, ' +
|
|
118
|
+
'a0Shots (one-on-one), a1Goals, a1Shots (box shots), a2Goals, a2Shots (long shots), caGoals, ' +
|
|
119
|
+
'caShots (counters), fkGoals, fkShots (free kicks), zonePress (successful presses); plus ' +
|
|
120
|
+
'self.last3.steady, self.last3.strained, self.last3.tired, self.last3.exhausted (how many of ' +
|
|
121
|
+
'your fielded players were in each condition band). Example: ' +
|
|
122
|
+
'"version: 0\\nformation: 433\\nstyle: { balance: attack, press: high }\\n' +
|
|
123
|
+
'kickers: { fk: #8, pk: #10 }\\nrules:\\n when match.decisive == 1:\\n set: { balance: balanced }".';
|
|
37
124
|
/**
|
|
38
125
|
* One squad player, in the FLAT shape POST /api/teams takes.
|
|
39
126
|
*
|
|
@@ -65,10 +152,10 @@ const PlayerSchema = z.object({
|
|
|
65
152
|
.describe('Squad slot 0-10. Each slot must appear exactly once across the 11 players.'),
|
|
66
153
|
name: z.string().min(1).max(10).describe('Display name, max 10 chars, unique within the squad'),
|
|
67
154
|
position: z.enum(POSITIONS).describe('Exactly one GK; at least one each of DF, DMF, OMF, FW'),
|
|
68
|
-
pass: z.number().int().min(1).max(10),
|
|
69
|
-
dribble: z.number().int().min(1).max(10),
|
|
70
|
-
shoot: z.number().int().min(1).max(10),
|
|
71
|
-
defense: z.number().int().min(1).max(10),
|
|
155
|
+
pass: z.number().int().min(1).max(10).describe(ABILITY_FIELD_DEPRECATED),
|
|
156
|
+
dribble: z.number().int().min(1).max(10).describe(ABILITY_FIELD_DEPRECATED),
|
|
157
|
+
shoot: z.number().int().min(1).max(10).describe(ABILITY_FIELD_DEPRECATED),
|
|
158
|
+
defense: z.number().int().min(1).max(10).describe(ABILITY_FIELD_DEPRECATED),
|
|
72
159
|
// Required by the route schema, not optional — an agent that omits them gets a
|
|
73
160
|
// raw Fastify validation error instead of a rule explanation.
|
|
74
161
|
// Defaulted, not required. Twenty-two booleans that are false on nine or ten
|
|
@@ -98,13 +185,20 @@ const PlayerSchema = z.object({
|
|
|
98
185
|
* rejected here too rather than being measured. That is deliberate: the point
|
|
99
186
|
* of a sandbox is to learn what a squad you could actually FIELD would do.
|
|
100
187
|
*/
|
|
188
|
+
/**
|
|
189
|
+
* Own-team ability input to simulate_batch is on its way out (#826): a later
|
|
190
|
+
* release takes your own side as `playbookText` once the server's sandbox can
|
|
191
|
+
* compile one (P6-1). Not implemented here — still required today.
|
|
192
|
+
*/
|
|
193
|
+
const SANDBOX_ABILITY_DEPRECATED = 'Required today. DEPRECATED for describing your OWN team: a later release replaces own-team ' +
|
|
194
|
+
'numbers with a `playbookText` input.';
|
|
101
195
|
const SandboxPlayerSchema = z.object({
|
|
102
196
|
name: z.string().min(1).max(10).describe('Max 10 chars, unique within the squad'),
|
|
103
197
|
position: z.enum(POSITIONS).describe('Exactly one GK; at least one each of DF, DMF, OMF, FW'),
|
|
104
|
-
pass: z.number().int().min(1).max(10),
|
|
105
|
-
dribble: z.number().int().min(1).max(10),
|
|
106
|
-
shoot: z.number().int().min(1).max(10),
|
|
107
|
-
defense: z.number().int().min(1).max(10),
|
|
198
|
+
pass: z.number().int().min(1).max(10).describe(SANDBOX_ABILITY_DEPRECATED),
|
|
199
|
+
dribble: z.number().int().min(1).max(10).describe(SANDBOX_ABILITY_DEPRECATED),
|
|
200
|
+
shoot: z.number().int().min(1).max(10).describe(SANDBOX_ABILITY_DEPRECATED),
|
|
201
|
+
defense: z.number().int().min(1).max(10).describe(SANDBOX_ABILITY_DEPRECATED),
|
|
108
202
|
isFkKicker: z.boolean().default(false).describe('At least one per squad must be true.'),
|
|
109
203
|
isPkKicker: z.boolean().default(false).describe('At least one per squad must be true.'),
|
|
110
204
|
});
|
|
@@ -135,7 +229,8 @@ const SandboxSquadSchema = z.object({
|
|
|
135
229
|
players: z
|
|
136
230
|
.array(SandboxPlayerSchema)
|
|
137
231
|
.length(11)
|
|
138
|
-
.describe('Exactly 11 players IN SLOT ORDER (index 0 is slot 0).
|
|
232
|
+
.describe('Exactly 11 players IN SLOT ORDER (index 0 is slot 0). Validated exactly like a stored squad ' +
|
|
233
|
+
'(see get_game_rules), so an eleven that could not be fielded is refused, not measured.'),
|
|
139
234
|
});
|
|
140
235
|
/**
|
|
141
236
|
* The constraints POST /api/teams enforces, in the agent's own vocabulary.
|
|
@@ -149,9 +244,22 @@ const SandboxSquadSchema = z.object({
|
|
|
149
244
|
* dead code, since a player total of 29 already cannot hold four 10s.
|
|
150
245
|
*/
|
|
151
246
|
const SQUAD_RULES = [
|
|
247
|
+
// #826 (P5-1): pog first. The validator facts below are unchanged and still
|
|
248
|
+
// pinned by gameFacts.test.ts, but they are what the SERVER checks, not a
|
|
249
|
+
// budget the agent is asked to fit — a template squad already satisfies
|
|
250
|
+
// every one, and ability numbers cannot be edited after creation anyway.
|
|
251
|
+
'You field a team through a PLAYBOOK (pog), not by hand-building numbers: create_squad with a ' +
|
|
252
|
+
'`template` gives you a legal squad and that template’s playbook; after that, set_playbook ' +
|
|
253
|
+
'changes how the team plays and the server picks the eleven from your registered pool at kickoff. ' +
|
|
254
|
+
'Every eleven is still checked by the server against these invariants, which template squads ' +
|
|
255
|
+
'already satisfy:',
|
|
152
256
|
'Exactly 11 players, with slotIndex 0-10 each used exactly once.',
|
|
153
|
-
|
|
154
|
-
|
|
257
|
+
// #863 r5 (follow-up): the freeze rides the weekly-pool gate, same wording
|
|
258
|
+
// as ABILITY_FIELD_DEPRECATED — one unconditional copy contradicted it.
|
|
259
|
+
'Across the whole squad the four attributes total 212, each attribute is 1-10 and each player ' +
|
|
260
|
+
'total is 10-29. While the weekly player pool is enabled (the default) these numbers are ' +
|
|
261
|
+
'fixed when a player is created; only a deployment on the FORM_PLAYER_POOL_ENABLED rollback ' +
|
|
262
|
+
'accepts changes to unminted players.',
|
|
155
263
|
'Across the whole squad: at most 3 attributes equal to 10, and at most 5 equal to 8 or 9.',
|
|
156
264
|
'Exactly one GK, plus at least one each of DF, DMF, OMF, FW.',
|
|
157
265
|
'At least one free-kick taker (isFkKicker) and at least one penalty taker (isPkKicker) — ' +
|
|
@@ -1091,6 +1199,143 @@ function fail(err) {
|
|
|
1091
1199
|
: String(err);
|
|
1092
1200
|
return { content: [{ type: 'text', text }], isError: true };
|
|
1093
1201
|
}
|
|
1202
|
+
/** A tool error whose text is JSON — for rejections an agent should branch on, not read. */
|
|
1203
|
+
function failStructured(value) {
|
|
1204
|
+
return { content: [{ type: 'text', text: JSON.stringify(value) }], isError: true };
|
|
1205
|
+
}
|
|
1206
|
+
/** The parsed error body's `error` code, when the API sent one. */
|
|
1207
|
+
function apiErrorCode(err) {
|
|
1208
|
+
const b = err.body;
|
|
1209
|
+
if (b !== null && typeof b === 'object' && typeof b.error === 'string') {
|
|
1210
|
+
return b.error;
|
|
1211
|
+
}
|
|
1212
|
+
return undefined;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* The two playbook rejections, as data. A 400 carries line/column
|
|
1216
|
+
* diagnostics and a 422 the solver's reason; flattened into fail()'s one-line
|
|
1217
|
+
* message both were lost, and they are exactly what the agent needs to fix
|
|
1218
|
+
* the document. Returns null for anything else, so the caller falls back to
|
|
1219
|
+
* fail() with the server's own words.
|
|
1220
|
+
*/
|
|
1221
|
+
function playbookRejection(err) {
|
|
1222
|
+
if (!(err instanceof ApiError))
|
|
1223
|
+
return null;
|
|
1224
|
+
const body = (err.body ?? {});
|
|
1225
|
+
const code = apiErrorCode(err);
|
|
1226
|
+
if (err.status === 400 && code === 'playbook_parse_error') {
|
|
1227
|
+
return {
|
|
1228
|
+
saved: false,
|
|
1229
|
+
valid: false,
|
|
1230
|
+
error: 'playbook_parse_error',
|
|
1231
|
+
diagnostics: body['diagnostics'] ?? [],
|
|
1232
|
+
hint: 'The document did not parse. Each diagnostic names the line and column that failed; fix those and resend.',
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
if (err.status === 422 && code === 'playbook_unsatisfiable') {
|
|
1236
|
+
return {
|
|
1237
|
+
saved: false,
|
|
1238
|
+
valid: false,
|
|
1239
|
+
error: 'playbook_unsatisfiable',
|
|
1240
|
+
reason: body['reason'] ?? null,
|
|
1241
|
+
hint: 'The document parsed, but your registered player pool cannot field its base formation ' +
|
|
1242
|
+
'(reason above). ' +
|
|
1243
|
+
'Change the formation, or register a pool that can field it (register_weekly_player_pool). ' +
|
|
1244
|
+
'Changing kickers cannot fix this: the solver does not read them yet.',
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* What set_playbook / dryrun_playbook actually validate (#863 review): the
|
|
1251
|
+
* server's preview solves the BASE document — `buildDryRun` never evaluates
|
|
1252
|
+
* rules — while kickoff evaluates them first, and an unfieldable outcome there
|
|
1253
|
+
* becomes `applyFailed` with the saved eleven playing instead.
|
|
1254
|
+
*/
|
|
1255
|
+
const PLAYBOOK_CHECK_SCOPE = 'The check covers the BASE document only — its formation and style as written; rules are NOT ' +
|
|
1256
|
+
'evaluated. A rule that switches to a formation your pool cannot field is therefore NOT ' +
|
|
1257
|
+
'rejected here: at kickoff that outcome fails to apply (recorded as applyFailed on the match) ' +
|
|
1258
|
+
'and the saved eleven plays instead. Keep every formation a rule can reach fieldable from your ' +
|
|
1259
|
+
'registered pool.';
|
|
1260
|
+
/** "Is this playbook actually driving kickoff?" — the two facts the server reports, combined. */
|
|
1261
|
+
function playbookActivity(serverActive, status,
|
|
1262
|
+
// Wording only, never the verdict (#863 r3): with the flag off, an
|
|
1263
|
+
// ACTIVATED playbook and a never-activated one are both `active: false`
|
|
1264
|
+
// but mean different things when the flag is later turned on.
|
|
1265
|
+
appliesFrom,
|
|
1266
|
+
// #863 r5 follow-up: false when no kickoff on this deployment can compile
|
|
1267
|
+
// a playbook at all (e.g. a local SQL-less API) — explained, never guessed.
|
|
1268
|
+
kickoffCompiles) {
|
|
1269
|
+
// #863 review: `active` is the SERVER's answer (GET playbook computes it on
|
|
1270
|
+
// its own clock with the predicate kickoff uses). Comparing `appliesFrom`
|
|
1271
|
+
// with this host's clock could report the opposite of what kickoff does.
|
|
1272
|
+
// The status probe only chooses the explanation, never the verdict.
|
|
1273
|
+
const playbookEnabled = status === null ? null : status.enabled === true;
|
|
1274
|
+
if (typeof serverActive !== 'boolean') {
|
|
1275
|
+
return {
|
|
1276
|
+
playbookEnabled,
|
|
1277
|
+
active: null,
|
|
1278
|
+
activeNote: 'UNKNOWN — this deployment did not report whether the playbook is active (an API older ' +
|
|
1279
|
+
'than this client). Do not assume kickoff uses it.',
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
if (!serverActive && kickoffCompiles === false) {
|
|
1283
|
+
return {
|
|
1284
|
+
playbookEnabled,
|
|
1285
|
+
active: false,
|
|
1286
|
+
activeNote: 'NOT ACTIVE: this deployment never compiles playbooks at kickoff (it has no database-backed ' +
|
|
1287
|
+
'scheduler — e.g. a local in-memory API), so every team plays its saved eleven here ' +
|
|
1288
|
+
'whatever appliesFrom or the flag say.',
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
if (!serverActive && playbookEnabled === false) {
|
|
1292
|
+
return typeof appliesFrom === 'string'
|
|
1293
|
+
? {
|
|
1294
|
+
playbookEnabled,
|
|
1295
|
+
active: false,
|
|
1296
|
+
activeNote: 'Playbooks are OFF on this deployment: every team plays its saved eleven at kickoff. ' +
|
|
1297
|
+
'This playbook IS activated (appliesFrom is set), so it becomes eligible for kickoff ' +
|
|
1298
|
+
'once they are turned on and that time has passed.',
|
|
1299
|
+
}
|
|
1300
|
+
: {
|
|
1301
|
+
playbookEnabled,
|
|
1302
|
+
active: false,
|
|
1303
|
+
activeNote: 'Playbooks are OFF on this deployment, AND this playbook was never activated ' +
|
|
1304
|
+
'(appliesFrom is null): turning them on would not change anything for this squad, ' +
|
|
1305
|
+
'which keeps its saved eleven. Saving never activates a playbook; today the only ' +
|
|
1306
|
+
'activation path is creating the squad with `template`.',
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
if (!serverActive && playbookEnabled === null) {
|
|
1310
|
+
// #863 r4: inactive, but the flag state is unknown (status probe failed)
|
|
1311
|
+
// — so the cause cannot be pinned on appliesFrom. Say so.
|
|
1312
|
+
return {
|
|
1313
|
+
playbookEnabled,
|
|
1314
|
+
active: false,
|
|
1315
|
+
activeNote: 'NOT ACTIVE (the server says so), and the cause is UNKNOWN: /api/playbook/status could ' +
|
|
1316
|
+
'not be read, so this may be the deployment flag being off or the playbook never having ' +
|
|
1317
|
+
'been activated. Kickoff uses the saved eleven either way; call get_playbook again later ' +
|
|
1318
|
+
'for the reason.',
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
return serverActive
|
|
1322
|
+
? {
|
|
1323
|
+
playbookEnabled,
|
|
1324
|
+
active: true,
|
|
1325
|
+
activeNote: 'ACTIVE: this playbook is eligible for compilation — the server compiles the team’s ' +
|
|
1326
|
+
'eleven from it at each future scheduled or ladder kickoff, and ONCE when a daily cup ' +
|
|
1327
|
+
'opens (that frozen eleven plays every round of the cup; a version saved mid-cup applies ' +
|
|
1328
|
+
'only after the cup ends — set_playbook reports that as appliesAt "after_lock"). ' +
|
|
1329
|
+
'update_squad lineup edits are refused — use set_playbook.',
|
|
1330
|
+
}
|
|
1331
|
+
: {
|
|
1332
|
+
playbookEnabled,
|
|
1333
|
+
active: false,
|
|
1334
|
+
activeNote: 'NOT ACTIVE for this team (appliesFrom is null or in the future): kickoff uses the saved ' +
|
|
1335
|
+
'eleven, and saving a playbook does not change that. Today the only activation path is ' +
|
|
1336
|
+
'creating the squad with `template`; a squad created without one keeps its saved eleven.',
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1094
1339
|
/**
|
|
1095
1340
|
* The version this build actually is, from the manifest the release publishes.
|
|
1096
1341
|
*
|
|
@@ -1588,9 +1833,14 @@ export function buildServer(opts = {}) {
|
|
|
1588
1833
|
});
|
|
1589
1834
|
server.registerTool('create_squad', {
|
|
1590
1835
|
title: 'Create a squad',
|
|
1591
|
-
description:
|
|
1592
|
-
'
|
|
1593
|
-
|
|
1836
|
+
description: 'Create this agent’s squad. Requires login. A wallet holds at most one squad — if you already ' +
|
|
1837
|
+
'have one, use update_squad or set_playbook instead. ' +
|
|
1838
|
+
`USE \`template\`: one of ${POG_TEMPLATE_IDS.join(', ')}. The server builds a legal eleven for ` +
|
|
1839
|
+
'you and saves and activates that template’s playbook, so there is nothing to construct; ' +
|
|
1840
|
+
'then read it with get_playbook and change how the team plays with set_playbook. ' +
|
|
1841
|
+
'Send EITHER `template` OR `players`, never both. `players` is the legacy hand-built form ' +
|
|
1842
|
+
`(its ability fields are deprecated): ${SQUAD_RULES} ` +
|
|
1843
|
+
'On rejection the error names the specific rule that failed, so fix and retry rather than guessing.',
|
|
1594
1844
|
inputSchema: {
|
|
1595
1845
|
name: z
|
|
1596
1846
|
.string()
|
|
@@ -1606,16 +1856,42 @@ export function buildServer(opts = {}) {
|
|
|
1606
1856
|
.length(3)
|
|
1607
1857
|
.transform((code) => code.toUpperCase())
|
|
1608
1858
|
.describe('FIFA 3-letter code, e.g. KOR, BRA (see list_nations). Case-insensitive.'),
|
|
1609
|
-
|
|
1859
|
+
template: z
|
|
1860
|
+
.enum(POG_TEMPLATE_IDS)
|
|
1861
|
+
.optional()
|
|
1862
|
+
.describe('Recommended. Build the squad from this template and activate its playbook. ' +
|
|
1863
|
+
'Omit only when sending `players`.'),
|
|
1864
|
+
players: z
|
|
1865
|
+
.array(PlayerSchema)
|
|
1866
|
+
.length(11)
|
|
1867
|
+
.optional()
|
|
1868
|
+
.describe('Legacy hand-built eleven. Omit when sending `template`.'),
|
|
1610
1869
|
},
|
|
1611
1870
|
annotations: { readOnlyHint: false, idempotentHint: false },
|
|
1612
|
-
}, async ({ name, nationCode, players }) => {
|
|
1871
|
+
}, async ({ name, nationCode, players, template }) => {
|
|
1872
|
+
// Refused here, before a round trip: the route would take `template` and
|
|
1873
|
+
// silently ignore `players`, and an agent that sent both would believe
|
|
1874
|
+
// its hand-built eleven was saved.
|
|
1875
|
+
if ((template === undefined) === (players === undefined)) {
|
|
1876
|
+
return fail(new Error('Send exactly one of `template` (recommended — e.g. "balanced") or `players` (legacy ' +
|
|
1877
|
+
'hand-built eleven), not ' + (template === undefined ? 'neither' : 'both') + '.'));
|
|
1878
|
+
}
|
|
1613
1879
|
try {
|
|
1614
|
-
const created =
|
|
1880
|
+
const created = template !== undefined
|
|
1881
|
+
? await client.createTeam({ name, nationCode, template })
|
|
1882
|
+
: await client.createTeam({ name, nationCode, players: players });
|
|
1615
1883
|
// The forum tools memoise squad ownership, and this call just changed it.
|
|
1616
1884
|
// Without clearing, a session that read the forum before building a squad
|
|
1617
1885
|
// keeps being told it has none.
|
|
1618
1886
|
teamIdCache = null;
|
|
1887
|
+
if (template !== undefined && created !== null && typeof created === 'object') {
|
|
1888
|
+
return ok({
|
|
1889
|
+
...created,
|
|
1890
|
+
playbookNote: `Created from the ${template} template, with its playbook saved as version 1. Call ` +
|
|
1891
|
+
'get_playbook with this teamId to read it and to see whether kickoff uses it on this ' +
|
|
1892
|
+
'deployment (`active`); change it with set_playbook, previewing with dryrun_playbook.',
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1619
1895
|
return ok(withSquadHashNote(created));
|
|
1620
1896
|
}
|
|
1621
1897
|
catch (err) {
|
|
@@ -1630,7 +1906,10 @@ export function buildServer(opts = {}) {
|
|
|
1630
1906
|
if (err.teamId !== undefined || /team_exists/i.test(err.message)) {
|
|
1631
1907
|
const existing = err.teamId ?? '(see my_squads)';
|
|
1632
1908
|
return fail(new Error(`This wallet already owns a squad (${existing}). A wallet holds at most one. ` +
|
|
1633
|
-
'
|
|
1909
|
+
'To change it: if its playbook is active (every squad created with `template` on a ' +
|
|
1910
|
+
'deployment with playbooks on — get_playbook reports `active`), call set_playbook ' +
|
|
1911
|
+
'with that teamId, because update_squad lineup edits are refused for it; otherwise ' +
|
|
1912
|
+
'call update_squad with that teamId to change the lineup.'));
|
|
1634
1913
|
}
|
|
1635
1914
|
if (/TEAM_NAME_TAKEN/i.test(err.message)) {
|
|
1636
1915
|
return fail(new Error(`The team name "${name}" is already used by another manager. Team names are unique ` +
|
|
@@ -1643,8 +1922,14 @@ export function buildServer(opts = {}) {
|
|
|
1643
1922
|
});
|
|
1644
1923
|
server.registerTool('update_squad', {
|
|
1645
1924
|
title: 'Replace a squad’s lineup',
|
|
1646
|
-
description: 'Rewrite the lineup of a squad you own
|
|
1647
|
-
|
|
1925
|
+
description: 'Rewrite the lineup of a squad you own. Requires login. ' +
|
|
1926
|
+
'NOT FOR A PLAYBOOK TEAM: once a squad’s playbook is active (get_playbook reports `active: ' +
|
|
1927
|
+
'true` — every squad created with `template` on a deployment with playbooks on), the ' +
|
|
1928
|
+
'playbook picks the eleven at kickoff and this tool’s lineup edits are refused with ' +
|
|
1929
|
+
'error "lineup_managed_by_playbook" and `useTool: "set_playbook"`. Change the playbook ' +
|
|
1930
|
+
'instead. Renaming only the team — the players array exactly as get_squad returned it — is ' +
|
|
1931
|
+
'still accepted. ' +
|
|
1932
|
+
`For a squad without an active playbook the same rules apply: ${SQUAD_RULES} ` +
|
|
1648
1933
|
'The edit applies to the next not-yet-simulated match; completed matches are never rewritten. ' +
|
|
1649
1934
|
'nationCode cannot change. ' +
|
|
1650
1935
|
'THERE IS A DEADLINE. If this squad was drawn into a daily cup, its eleven is COMMITTED ' +
|
|
@@ -1686,6 +1971,172 @@ export function buildServer(opts = {}) {
|
|
|
1686
1971
|
try {
|
|
1687
1972
|
return ok(withSquadHashNote(await client.updateTeam(teamId, { players, ...(name === undefined ? {} : { name }) })));
|
|
1688
1973
|
}
|
|
1974
|
+
catch (err) {
|
|
1975
|
+
// P2-5: an active playbook owns the eleven. Left raw, the 409 reads as
|
|
1976
|
+
// a generic conflict and an agent retries the same edit; the fix is a
|
|
1977
|
+
// different tool, so say which one.
|
|
1978
|
+
if (err instanceof ApiError &&
|
|
1979
|
+
err.status === 409 &&
|
|
1980
|
+
(apiErrorCode(err) === 'lineup_managed_by_playbook' ||
|
|
1981
|
+
/lineup_managed_by_playbook/.test(err.message))) {
|
|
1982
|
+
return failStructured({
|
|
1983
|
+
saved: false,
|
|
1984
|
+
error: 'lineup_managed_by_playbook',
|
|
1985
|
+
useTool: 'set_playbook',
|
|
1986
|
+
teamId,
|
|
1987
|
+
message: 'This squad’s eleven is compiled from its active playbook at kickoff (once per daily ' +
|
|
1988
|
+
'cup, when it opens), so direct lineup ' +
|
|
1989
|
+
'edits are refused and nothing was saved. Change how the team plays with set_playbook ' +
|
|
1990
|
+
'(preview with dryrun_playbook; read the current one with get_playbook). Renaming only ' +
|
|
1991
|
+
'the team — players exactly as get_squad returned them — is still accepted here.',
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
return fail(err);
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
// -------------------------------------------------------------------------
|
|
1998
|
+
// Playbook (pog) — #826 (P5-1). Agents write pog directly: no coach LLM, the
|
|
1999
|
+
// same document and the same kickoff compile a human manager gets.
|
|
2000
|
+
// -------------------------------------------------------------------------
|
|
2001
|
+
server.registerTool('get_playbook', {
|
|
2002
|
+
title: 'Read a squad’s playbook',
|
|
2003
|
+
description: 'Read the playbook of a squad you own. Requires login. Returns `text` (the pog document), ' +
|
|
2004
|
+
'`version` (0 = never saved; `text` is then the server’s default for this squad), ' +
|
|
2005
|
+
'`styleSummary` (base formation, style axes, rule count), `lineupPreview` (the eleven the ' +
|
|
2006
|
+
'server would field for the BASE document from your registered pool right now — names, ' +
|
|
2007
|
+
'positions and kickers; rules are not evaluated), `diagnostics`, `appliesFrom`, and ' +
|
|
2008
|
+
'`active` — whether kickoff actually uses this playbook, decided by the server on its own ' +
|
|
2009
|
+
'clock (null when the deployment does not report it). ' +
|
|
2010
|
+
POG_GRAMMAR,
|
|
2011
|
+
inputSchema: {
|
|
2012
|
+
teamId: z.string().min(1).describe('A squad this agent owns — see my_squads.'),
|
|
2013
|
+
},
|
|
2014
|
+
annotations: { readOnlyHint: true },
|
|
2015
|
+
}, async ({ teamId }) => {
|
|
2016
|
+
try {
|
|
2017
|
+
const [raw, status] = await Promise.all([
|
|
2018
|
+
client.playbook(teamId),
|
|
2019
|
+
// A failed probe is UNKNOWN, never "off" — see playbookActivity.
|
|
2020
|
+
client.playbookStatus().catch(() => null),
|
|
2021
|
+
]);
|
|
2022
|
+
const body = (raw ?? {});
|
|
2023
|
+
const dryRun = (body['dryRun'] ?? {});
|
|
2024
|
+
return ok({
|
|
2025
|
+
teamId,
|
|
2026
|
+
version: body['version'] ?? null,
|
|
2027
|
+
text: body['text'] ?? null,
|
|
2028
|
+
styleSummary: dryRun['styleSummary'] ?? null,
|
|
2029
|
+
lineupPreview: dryRun['lineupPreview'] ?? null,
|
|
2030
|
+
diagnostics: dryRun['diagnostics'] ?? [],
|
|
2031
|
+
appliesFrom: body['appliesFrom'] ?? null,
|
|
2032
|
+
applied: body['applied'] ?? null,
|
|
2033
|
+
...playbookActivity(body['active'], status, body['appliesFrom'] ?? null, body['kickoffCompiles']),
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
catch (err) {
|
|
2037
|
+
return fail(err);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
server.registerTool('set_playbook', {
|
|
2041
|
+
title: 'Save a squad’s playbook',
|
|
2042
|
+
description: 'Save a new version of the playbook of a squad you own. Requires login. The server parses the ' +
|
|
2043
|
+
'document and solves an eleven from your registered pool BEFORE saving: a document that ' +
|
|
2044
|
+
'does not parse comes back as error "playbook_parse_error" with line/column `diagnostics`, ' +
|
|
2045
|
+
'and one whose base formation your pool cannot field as "playbook_unsatisfiable" with a ' +
|
|
2046
|
+
'`reason` — in both cases ' +
|
|
2047
|
+
'nothing is saved. On success: `version`, the same preview as dryrun_playbook, and — read ' +
|
|
2048
|
+
'back from the server after saving — `active` / `activeNote`: whether kickoff will actually ' +
|
|
2049
|
+
'use this playbook. SAVING NEVER ACTIVATES A PLAYBOOK. Today only a squad created with ' +
|
|
2050
|
+
'`template` has an active one; for any other squad `active` is false and it keeps playing ' +
|
|
2051
|
+
'its saved eleven whatever you save here. `appliesAt` ("next_kickoff", or "after_lock" when ' +
|
|
2052
|
+
'a daily cup has already committed this squad) only says WHEN an ACTIVE playbook’s new ' +
|
|
2053
|
+
'version takes effect — saving is never blocked. Use ' +
|
|
2054
|
+
'dryrun_playbook to iterate without creating versions. ' +
|
|
2055
|
+
`${PLAYBOOK_CHECK_SCOPE} ` +
|
|
2056
|
+
POG_GRAMMAR,
|
|
2057
|
+
inputSchema: {
|
|
2058
|
+
teamId: z.string().min(1).describe('A squad this agent owns — see my_squads.'),
|
|
2059
|
+
text: z.string().min(1).max(20000).describe('The whole pog document.'),
|
|
2060
|
+
},
|
|
2061
|
+
annotations: { readOnlyHint: false, idempotentHint: false },
|
|
2062
|
+
}, async ({ teamId, text }) => {
|
|
2063
|
+
try {
|
|
2064
|
+
const saved = (await client.savePlaybook(teamId, text));
|
|
2065
|
+
// #863 review: `appliesAt` answers WHEN, never WHETHER — the PUT route
|
|
2066
|
+
// reports "next_kickoff" for a playbook that no kickoff will ever read
|
|
2067
|
+
// (appliesFrom null: saving never activates). Read the activation state
|
|
2068
|
+
// back so the agent is told, in the same answer, whether its strategy
|
|
2069
|
+
// is live. A failed read-back must not turn a real save into an error:
|
|
2070
|
+
// it reports UNKNOWN instead.
|
|
2071
|
+
let activity;
|
|
2072
|
+
let appliesFrom = null;
|
|
2073
|
+
try {
|
|
2074
|
+
const [raw, status] = await Promise.all([
|
|
2075
|
+
client.playbook(teamId),
|
|
2076
|
+
client.playbookStatus().catch(() => null),
|
|
2077
|
+
]);
|
|
2078
|
+
const readBack = (raw ?? {});
|
|
2079
|
+
appliesFrom = readBack['appliesFrom'] ?? null;
|
|
2080
|
+
activity = playbookActivity(readBack['active'], status, appliesFrom, readBack['kickoffCompiles']);
|
|
2081
|
+
}
|
|
2082
|
+
catch {
|
|
2083
|
+
activity = {
|
|
2084
|
+
playbookEnabled: null,
|
|
2085
|
+
active: null,
|
|
2086
|
+
activeNote: 'UNKNOWN — the save succeeded, but its activation state could not be read back. Call ' +
|
|
2087
|
+
'get_playbook before assuming kickoff will use it.',
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
return ok({ saved: true, valid: true, ...saved, appliesFrom, ...activity });
|
|
2091
|
+
}
|
|
2092
|
+
catch (err) {
|
|
2093
|
+
const rejection = playbookRejection(err);
|
|
2094
|
+
return rejection !== null ? failStructured(rejection) : fail(err);
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
server.registerTool('dryrun_playbook', {
|
|
2098
|
+
title: 'Check a playbook without saving it',
|
|
2099
|
+
description: 'Run the exact checks set_playbook runs — parse, then solve an eleven from your registered ' +
|
|
2100
|
+
'pool — and save NOTHING. Requires login and a squad you own. Returns `valid: true` with ' +
|
|
2101
|
+
'`dryRun` (diagnostics, styleSummary, lineupPreview), or `valid: false` with the same ' +
|
|
2102
|
+
'"playbook_parse_error" diagnostics or "playbook_unsatisfiable" reason set_playbook would ' +
|
|
2103
|
+
'have refused with. A rejected document is a normal answer here, not a tool error. ' +
|
|
2104
|
+
`${PLAYBOOK_CHECK_SCOPE} ` +
|
|
2105
|
+
POG_GRAMMAR,
|
|
2106
|
+
inputSchema: {
|
|
2107
|
+
teamId: z.string().min(1).describe('A squad this agent owns — see my_squads.'),
|
|
2108
|
+
text: z.string().min(1).max(20000).describe('The whole pog document.'),
|
|
2109
|
+
},
|
|
2110
|
+
annotations: { readOnlyHint: true },
|
|
2111
|
+
}, async ({ teamId, text }) => {
|
|
2112
|
+
try {
|
|
2113
|
+
const checked = (await client.dryRunPlaybook(teamId, text));
|
|
2114
|
+
return ok({ saved: false, valid: true, ...checked });
|
|
2115
|
+
}
|
|
2116
|
+
catch (err) {
|
|
2117
|
+
const rejection = playbookRejection(err);
|
|
2118
|
+
return rejection !== null ? ok(rejection) : fail(err);
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
server.registerTool('get_match_report', {
|
|
2122
|
+
title: 'Read a match’s playbook report',
|
|
2123
|
+
description: 'How each side’s playbook played out in one match. The side you own comes back ' +
|
|
2124
|
+
'`basis: "own"` with its playbookVersion and every report line (which part of the playbook ' +
|
|
2125
|
+
'it concerns, and the match evidence for it). The other side is `basis: "public"` — only the ' +
|
|
2126
|
+
'style axes it kicked off with — because another manager’s rules and evidence are theirs. ' +
|
|
2127
|
+
'A side can also be `basis: "unavailable"` with a reason: "no-playbook" (that side played ' +
|
|
2128
|
+
'without an applied playbook), "match-not-completed", "schema-pre-migration" or ' +
|
|
2129
|
+
'"store-unreachable". Sign in to see your own side as "own"; signed out, both sides are public. ' +
|
|
2130
|
+
'Reports exist only once a match is COMPLETE: while it is still live (being replayed) both ' +
|
|
2131
|
+
'sides answer unavailable "match-not-completed", so the evidence never arrives before the result does.',
|
|
2132
|
+
inputSchema: {
|
|
2133
|
+
matchId: z.string().min(1).describe('A match id, e.g. from catch_up, get_cup or get_next_match.'),
|
|
2134
|
+
},
|
|
2135
|
+
annotations: { readOnlyHint: true },
|
|
2136
|
+
}, async ({ matchId }) => {
|
|
2137
|
+
try {
|
|
2138
|
+
return ok(await client.matchPlaybookReport(matchId));
|
|
2139
|
+
}
|
|
1689
2140
|
catch (err) {
|
|
1690
2141
|
return fail(err);
|
|
1691
2142
|
}
|
|
@@ -1737,7 +2188,9 @@ export function buildServer(opts = {}) {
|
|
|
1737
2188
|
'replaces it. The current week never changes; at the boundary sold/not-owned members are ' +
|
|
1738
2189
|
'pruned when boundary ownership was lost, and those optional places are filled ' +
|
|
1739
2190
|
'deterministically from boundary-owned assets. A listing alone keeps its membership but ' +
|
|
1740
|
-
'makes that player claim-ineligible until the listing clears.'
|
|
2191
|
+
'makes that player claim-ineligible until the listing clears. ' +
|
|
2192
|
+
'The pool is yours to decide: a playbook picks its eleven FROM this pool at kickoff and ' +
|
|
2193
|
+
'never changes who is in it.',
|
|
1741
2194
|
inputSchema: {
|
|
1742
2195
|
teamId: z.string().uuid().describe('A squad this agent owns — see my_squads.'),
|
|
1743
2196
|
targetSeasonId: z
|
|
@@ -2421,6 +2874,9 @@ export function buildServer(opts = {}) {
|
|
|
2421
2874
|
'DO NOT BULK-RUN THESE TO COMPARE SQUADS — use simulate_batch, which runs hundreds of ' +
|
|
2422
2875
|
'matches in one call, ages nobody, and returns the aggregate with a confidence interval. ' +
|
|
2423
2876
|
'A friendly is for actually playing one. ' +
|
|
2877
|
+
'IT PLAYS EACH SIDE’S SAVED ELEVEN: playbooks are never compiled for a friendly, so a ' +
|
|
2878
|
+
'friendly result says nothing about your playbook’s rules — even for a squad whose ' +
|
|
2879
|
+
'playbook is active. ' +
|
|
2424
2880
|
'By DEFAULT a level score goes to penalties and someone wins — pass allowDraw=true if ' +
|
|
2425
2881
|
'you want draws to stay draws, which is what you usually want when comparing squads.',
|
|
2426
2882
|
inputSchema: {
|
|
@@ -2446,7 +2902,9 @@ export function buildServer(opts = {}) {
|
|
|
2446
2902
|
});
|
|
2447
2903
|
server.registerTool('simulate_batch', {
|
|
2448
2904
|
title: 'Compare two squads over many matches, for free',
|
|
2449
|
-
description: '
|
|
2905
|
+
description: 'DEPRECATION NOTE: describing YOUR OWN team here by its ability numbers is deprecated; a ' +
|
|
2906
|
+
'later release replaces it with a `playbookText` input. It is still the only form today. ' +
|
|
2907
|
+
'Run hundreds of matches between squads you describe INLINE and get back one aggregate: ' +
|
|
2450
2908
|
'wins/draws/losses, win rate with a 95% confidence interval, a significance test, and ' +
|
|
2451
2909
|
'goals. Nothing is recorded — no career, no growth, no result, no standing — so this is ' +
|
|
2452
2910
|
'the tool to use when the question is "which of these builds is better", and ' +
|