@battlegrid/mcp-server 32.0.11 → 32.0.16

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 CHANGED
@@ -38,6 +38,27 @@ Seeing package `31.x` alongside handshake `battlegrid@33.x` — the package **be
38
38
 
39
39
  **What this changes for you:** nothing about how you call anything. Upgrading the package no longer waits on a server deploy, and a server deploy no longer strands you on a package that names the wrong contract — reconnect and the announcement follows. **Contract breaking-change notes are no longer keyed to package versions**, since a contract move is no longer a release here; the v11-and-earlier notes below are kept as history, and the live vocabulary is always discovery.
40
40
 
41
+ ## Contract history — v122 (only an officer's own turn records a review)
42
+
43
+ **Breaking at v122: two officer review tools leave the catalog you discover.** v122 is the contract on top of v121.
44
+ Reconnect the proxy, or restart it, so discovery reads the new catalog.
45
+
46
+ ### Removed tools — something you called is gone
47
+
48
+ - **`get_officer_review_context` and `submit_officer_review`** are published only to a Strategy Officer's own turn,
49
+ which runs on BattleGrid's server. A call naming either through this proxy is answered as an unknown tool. A second
50
+ opinion on an officer's review is your own reading, never a record.
51
+
52
+ ### Added tool and fields
53
+
54
+ - **`run_officer({ officerId })`** runs one of your officers now, as its schedule would, and answers before any
55
+ review starts: `QUEUED` with `agentCount`, `NOTHING_TO_REVIEW` (nothing queued or billed) or `ALREADY_QUEUED`. It
56
+ follows `commanderAccess` from `get_account_state`, not `llmAccess`.
57
+ - **`pendingRun`** on each officer `list_officers` and `get_officer` serve: `{ state, queuedAt }` while a run is
58
+ waiting or running, otherwise null. **`presets`** on `list_officers`: the starting points a new officer is
59
+ staged from.
60
+ - The **Officers** workflow routes from `list_officers` to the `battlegrid-officers` skill.
61
+
41
62
  ## Contract history — v120 (the readiness ladder is three milestones)
42
63
 
43
64
  **Breaking at v120: `get_onboarding_requirements` reshapes its rungs and re-specifies its counts.** v120 is the
@@ -3903,12 +3924,12 @@ Install the BattleGrid skills for AI agent instructions:
3903
3924
  npx skills add playbattlegrid/battlegrid-mcp
3904
3925
  ```
3905
3926
 
3906
- Nine skills ship from this repo, all inside the npm tarball (`SKILL.md`, `skills/`).
3927
+ The skills ship from this repo, all inside the npm tarball (`SKILL.md`, `skills/`).
3907
3928
 
3908
3929
  **`battlegrid`** (repo root) is the connection skill and is authored here: how to connect, the
3909
3930
  `{ account, request }` envelope, the two scopes, and where to go for everything else.
3910
3931
 
3911
- The nine `skills/battlegrid-*` are **exported from BattleGrid's server repository** — they are the
3932
+ The `skills/battlegrid-*` are **exported from BattleGrid's server repository** — they are the
3912
3933
  same instructions BattleGrid's own in-app Commander runs on, which is why they name the same tools
3913
3934
  you reach over MCP:
3914
3935
 
@@ -3916,6 +3937,7 @@ you reach over MCP:
3916
3937
  |---|---|
3917
3938
  | `battlegrid-agent-management` | Commission and govern intelligence agents: interview and create one against a committed strategy and an approved model, change configuration and risk limits, rebind, halt, resume, archive, and act on live positions |
3918
3939
  | `battlegrid-arena-play` | Enter Market Grid sessions: find an open session, read its coin pool and live market context, compose a grid with real per-coin reasoning or have an agent generate it, submit, then read results and the reasoning journal |
3940
+ | `battlegrid-officers` | Set up and govern Strategy Officers: create or change one through its draft, run one now, read when each runs and what is waiting, and apply, push or dismiss the proposals its reviews carry — never writing a review |
3919
3941
  | `battlegrid-market-analysis` | Read the current crypto market — regime, funding and open interest, leaders and laggards, a deep-dive on any named coin — and close with the levels worth watching |
3920
3942
  | `battlegrid-radar-deployment` | Put agents on standing duty: per-coin Radar policies that fire on confirmed regime flips, and per-preset Arena deployment policies, previewed before they are written and un-deployed with the blast radius stated |
3921
3943
  | `battlegrid-strategy-authoring` | Build a strategy from a plain-English idea: gather evidence, lock the spec, stage it into the strategy's draft, review the diff, diagnostics and impact, commit only on confirmation. Also fork, tune, restore, archive, preview |
package/SKILL.md CHANGED
@@ -7,10 +7,10 @@ description: MCP skill for BattleGrid — play crypto prediction games (Market G
7
7
 
8
8
  BattleGrid is a real-time cryptocurrency prediction gaming and trading platform. This MCP server gives AI agents access to play games, author trading strategies, and run strategy-bound intelligence agents.
9
9
 
10
- ## The other nine skills
10
+ ## The other skills
11
11
 
12
12
  This skill covers **connection**: how to reach BattleGrid, the `account` argument, and what each scope
13
- grants. Everything about *using* the platform lives in the nine skills installed beside it, exported
13
+ grants. Everything about *using* the platform lives in the skills installed beside it, exported
14
14
  from BattleGrid's own server so they name exactly the tools you reach here:
15
15
 
16
16
  | For | Activate |
@@ -24,8 +24,13 @@ from BattleGrid's own server so they name exactly the tools you reach here:
24
24
  | Reading your own position, agents, and open trades | `battlegrid-trade-analysis` |
25
25
  | Finding and staging a trade for one of your agents — scan its coins, propose on one, approve or decline on your word | `battlegrid-trade-proposal` |
26
26
  | Working out why an agent has not traded or has stopped | `battlegrid-strategy-doctor` |
27
+ | Setting up a Strategy Officer, running it now, and applying, pushing or dismissing what it proposes | `battlegrid-officers` |
27
28
 
28
- Those nine carry the working arcs and the exact contracts. What follows here is the minimum needed
29
+ `battlegrid-strategy-officer` is not one to activate: it is the review procedure a Strategy Officer runs on
30
+ BattleGrid's own server, whose review tools no external client is published, and the next skills export removes it.
31
+ To work with officers, activate `battlegrid-officers`.
32
+
33
+ Those skills carry the working arcs and the exact contracts. What follows here is the minimum needed
29
34
  to connect and to know which one to open.
30
35
 
31
36
  ## Discover the live surface first
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/cli
47
47
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
48
48
  * correction. Never move it to track the server.
49
49
  */
50
- export declare const PACKAGE_VERSION = "32.0.11";
50
+ export declare const PACKAGE_VERSION = "32.0.16";
51
51
  export declare const DEFAULT_URL = "https://mcp.battlegrid.trade/mcp";
52
52
  export interface EnvConfig {
53
53
  apiKeys: string[];
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import { Client, SdkHttpError, StreamableHTTPClientTransport } from '@modelconte
49
49
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
50
50
  * correction. Never move it to track the server.
51
51
  */
52
- export const PACKAGE_VERSION = '32.0.11';
52
+ export const PACKAGE_VERSION = '32.0.16';
53
53
  export const DEFAULT_URL = 'https://mcp.battlegrid.trade/mcp';
54
54
  const MAX_RETRIES = 3;
55
55
  const RETRY_DELAYS_MS = [2000, 4000, 8000];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@battlegrid/mcp-server",
3
- "version": "32.0.11",
3
+ "version": "32.0.16",
4
4
  "description": "BattleGrid MCP server — play crypto prediction games from AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,22 +1,26 @@
1
1
  {
2
2
  "generator": "battlegrid-app/server/scripts/export-mcp-skills.mjs",
3
- "contractVersion": "113.0.0",
3
+ "contractVersion": "121.2.0",
4
4
  "files": {
5
- "battlegrid-agent-management/SKILL.md": "9dc151c32c3578d06e8895c6752d7a64959bff6a6681a01d62ab65e1ea1590d1",
5
+ "battlegrid-agent-management/SKILL.md": "09e5cbf241e2ba8f672e8292d40b3a50ee875f9b838003b95ee51928c5dbdf2e",
6
6
  "battlegrid-agent-management/references/agent-stage-examples.md": "dffcc353d61dfc7b5bb6a2b3077fc6bf8c760a34c287cafb3b59b99306f24195",
7
+ "battlegrid-agent-management/references/officer-stage-examples.md": "a5a42792a8aa8f4af8cbf424d7c9aadbaa23df382ce17691a40a8d0e5447db63",
7
8
  "battlegrid-arena-play/SKILL.md": "2e37970a66b8738ffbc445a23da90651c1f41b56ce4eef551582ab123517ef74",
8
9
  "battlegrid-arena-play/references/entry-stage-examples.md": "0223f3743171ff213e943789d27590b2f65648398e85e10cd24938d5eb6af665",
9
10
  "battlegrid-market-analysis/SKILL.md": "fcbe98e30e1edce39bb170c224cdf20181934f62b5f3a26b293787c70eeb6229",
10
- "battlegrid-radar-deployment/SKILL.md": "dfe8f354ac3dd426eedeac1ec080ac518e9c8cbfefdd26036afa61f80c08295c",
11
+ "battlegrid-radar-deployment/SKILL.md": "cf6664f51f92d40acce21a46f4fb078d9afe46c99c4f4b603bf002e73d4c5a23",
11
12
  "battlegrid-radar-deployment/references/arena-stage-examples.md": "d27cfed1f4663f267c4a7ecebe4a6e65a450fc74bd14ce8987fb61af5bbf2ca5",
12
- "battlegrid-radar-deployment/references/radar-stage-examples.md": "e3805f5c1b243135a7f25fb3a8cc6093dd242f8ad67b7e6b242bbdefa58a0735",
13
+ "battlegrid-radar-deployment/references/radar-stage-examples.md": "13fc3f55d91d3da9754e1ed7fb27493b7b7ba4d40a6a7e8d1ee0365f90ea10de",
13
14
  "battlegrid-strategy-authoring/SKILL.md": "39cfa46b1f640e6800edb7e85a8f2c4b6b8786cddf71bb8244d172a44fc11d75",
14
15
  "battlegrid-strategy-authoring/references/strategy-stage-examples.md": "f10ecdc91f3660a708eee7a7bacefce2acbb46c9cf7e0ac1454bd54f839f39fd",
15
16
  "battlegrid-strategy-doctor/SKILL.md": "e34e055b0cc9b355bc1adeb5d49d464eaa0756ca3f48dc89ca2ccdda94e1311f",
16
- "battlegrid-strategy-examples/SKILL.md": "597ddacdfc00955fcfd5a1afcc40b5ea72a0f658acbbebc8264afeecc46d2306",
17
- "battlegrid-strategy-examples/references/playbooks.md": "2309eb7e8324f563c5b1b51e05da931b37c542484135a2be749eff6ef78aa40f",
18
- "battlegrid-strategy-examples/references/recipes.md": "635e03d163100f6de37cda1f6cc12928d5d36aa0ea883bf44fc72f17cbf3e98e",
17
+ "battlegrid-strategy-examples/SKILL.md": "5e0d3244ed0592b8af933c0925346b6bd3bbaeef6c7acb35329925a026e7cd05",
18
+ "battlegrid-strategy-examples/references/playbooks.md": "275352be43652ca0fd36a35981ea7ffcfabdc4f9b33293826656ee500d3795e9",
19
+ "battlegrid-strategy-examples/references/recipes.md": "806aae0b4842408f6e6ed3e108293073f4f31d1c27fa419ad57df9e1134f23e7",
19
20
  "battlegrid-strategy-examples/references/tradingview-ports.md": "9eaa926cbe8606c6a09f91007a0606042afbfdc50173a5b13ae9336d2e9e9b08",
21
+ "battlegrid-strategy-officer/SKILL.md": "2c28ec4afa632b0f69b84b6d309f77d811d0d7452c1dbebce011b22044563780",
22
+ "battlegrid-strategy-officer/references/figures.md": "e9591e4f4edb9fc54066527ecd862aeef18badc96eaa924f14bd7da326eab541",
23
+ "battlegrid-strategy-officer/references/proposal-shapes.md": "20c9acd2b20e7920ed7ac9aa096604d91625b1d5b6438aa1ded7f6b63ecc9a22",
20
24
  "battlegrid-trade-analysis/SKILL.md": "3f927b4f9796178526fa1673a2043c5f14358c39aa955de33192ea58641f18fb",
21
25
  "battlegrid-trade-proposal/SKILL.md": "e761fc6bddc0d1db79fcb90046d03a18439562c8658de726ec286830edd6369b"
22
26
  }
@@ -37,7 +37,7 @@ step named beside it before doing anything else.
37
37
  which strategy an agent is bound to is the agent's own STRATEGY_BINDING axis (step 4).
38
38
  `stage_strategy_draft`, `commit_strategy_draft`, `discard_strategy_draft`, `fork_strategy`,
39
39
  `archive_strategy`, `restore_strategy`, `save_report_section` and `remove_saved_report_section` are
40
- **not**, even though activating this skill makes them visible in the conversation.
40
+ **not**, even though this skill's tools put them in view in the conversation.
41
41
 
42
42
  When the work is a strategy change, activate `strategy-authoring` and let its arc run — its
43
43
  evidence, spec-lock, stage-review and confirm rules bind from that point. Do not drive a strategy
@@ -266,6 +266,40 @@ These tools carry no consent gate on either door: they are ownership-gated and b
266
266
  in chat and through an external MCP client. Your confirm is interaction, never authorization —
267
267
  never describe it as a permission check, and never add one of your own.
268
268
 
269
+ ### 8. Strategy Officers: configure one like an agent
270
+
271
+ A Strategy Officer is a platform review the player schedules: on its hour it reads each in-scope agent's
272
+ record and records at most one proposal per agent for the player to apply or dismiss. It writes no draft
273
+ and commits nothing. Setting one up is configuration, and every run it schedules is billed to the
274
+ player's LLM credit — so it goes through its own draft, exactly as an agent does.
275
+
276
+ - **Read first.** `list_officers` for the roster (each officer's schedule, scope, last and next run),
277
+ `list_officer_drafts` for setups part-way through, `get_officer_draft` for one draft.
278
+ - **Create.** `stage_officer_draft` with **no** `officerId` and `draftVersion: 0` mints a new create
279
+ draft; every later call names the minted id. Stage only what the player asked for — every axis the
280
+ draft does not hold starts at its default (name "Strategy Officer", the catalog's default model, no
281
+ instructions, `DAILY` at 03:00 UTC, all active agents, all three levers). "Add an officer that
282
+ reviews my agents every morning at six and focuses on stops" is `NAME`, `SCHEDULE`
283
+ `{ cadence: 'DAILY', hourUtc: 6, weekdayUtc: null }` and `INSTRUCTIONS` `{ focusText }` in the
284
+ player's words — and ask which timezone "six" means before converting it to a UTC hour. Worked
285
+ inputs: `references/officer-stage-examples.md`.
286
+ - **Presets are starting points, staged as ordinary axes.** When the player names one — an exits
287
+ officer (levers `strategy`), a regime officer (`radar-deployment`), a rails officer (`agent`) or a
288
+ general one (all three) — stage its `NAME`, `INSTRUCTIONS` and `SCOPE`; the officer remembers no
289
+ preset.
290
+ - **Axes are whole.** `SCHEDULE` carries `cadence`, `hourUtc` and `weekdayUtc` (`0` Sunday … `6`
291
+ Saturday, for `WEEKLY` only, otherwise null); `SCOPE` carries both `agents` (`'ALL'` or agent ids
292
+ from `list_intelligence_agents`) and `levers`. `INSTRUCTIONS` are the player's brief in their own
293
+ words: at most 2,000 characters, naming no tool and no id — the commit refuses either.
294
+ - **Show, then commit on the player's word.** Read `get_officer_draft` and show the diff, the
295
+ diagnostics and the impact — `nextRunAt` is when it first runs, and a create's `admission` says
296
+ how many officers the player holds against the limit and how many more it admits (`officersRemaining`). Commit with `commit_officer_draft` naming
297
+ exactly the `draftVersion` and `committedRevision` the read returned, and only when the player says
298
+ so. A cadence of `OFF` keeps the setup and runs nothing.
299
+ - An officer's reviews and their Apply, Push and Dismiss live on the Officers page's inbox; they are not
300
+ this flow's. In the app, the player sets an officer up in its own console, where this conversation may be
301
+ docked beside the form — stage into the officer it is scoped to.
302
+
269
303
  ## When a write's outcome is unknown
270
304
 
271
305
  An interrupted or timed-out write may have landed.
@@ -0,0 +1,77 @@
1
+ # Strategy Officer stage examples
2
+
3
+ Complete `stage_officer_draft` inputs. Each is checked in CI against the live stage schema and the setup rules a
4
+ commit applies without a repository read. Its shape is current.
5
+
6
+ Every `<UPPER_SNAKE>` value is a placeholder. Replace it with the id a read returned:
7
+
8
+ - `<OFFICER_ID>` comes from `list_officers` or the draft you are editing.
9
+ - `<AGENT_ID>` and `<OTHER_AGENT_ID>` come from `list_intelligence_agents`.
10
+ - `<MODEL_ID>` is the row's `modelId` from `list_approved_models`.
11
+
12
+ Never send a placeholder as written, and never invent an id. `draftVersion` is the version `get_officer_draft`
13
+ returned: 0 when there was no draft, and for a new create draft. Each axis you send is written whole, and an axis
14
+ you omit keeps the value the draft already has, the officer's current value, or for a new officer its default.
15
+
16
+ ## Add an officer that reviews every morning at six and focuses on stops
17
+
18
+ Omit `officerId` to open a create draft. The response carries the minted id, which every later call names. Convert
19
+ the player's "six" to a UTC hour only after asking which timezone they mean; this example is 06:00 UTC.
20
+
21
+ ```json stage_officer_draft
22
+ {
23
+ "draftVersion": 0,
24
+ "axes": {
25
+ "NAME": { "name": "Morning Stops Officer" },
26
+ "SCHEDULE": { "cadence": "DAILY", "hourUtc": 6, "weekdayUtc": null },
27
+ "INSTRUCTIONS": { "focusText": "Focus on stops: premature stop-outs and what price did after each exit." }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Start from the regime preset, weekly on Monday
33
+
34
+ A preset is staged as ordinary axes: its name, its brief and its scope.
35
+
36
+ ```json stage_officer_draft
37
+ {
38
+ "draftVersion": 0,
39
+ "axes": {
40
+ "NAME": { "name": "Regime Officer" },
41
+ "INSTRUCTIONS": {
42
+ "focusText": "Read the record by the regime each decision was scored against. Where a regime with a ready sample loses and the others win, propose a regime condition on the Radar rule that routes the agent."
43
+ },
44
+ "SCOPE": { "agents": "ALL", "levers": ["radar-deployment"] },
45
+ "SCHEDULE": { "cadence": "WEEKLY", "hourUtc": 7, "weekdayUtc": 1 }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Narrow an existing officer to two agents and the rails lever
51
+
52
+ SCOPE is sent whole: both the agents and the levers, even when only one changes.
53
+
54
+ ```json stage_officer_draft
55
+ {
56
+ "officerId": "<OFFICER_ID>",
57
+ "draftVersion": 0,
58
+ "axes": {
59
+ "SCOPE": { "agents": ["<AGENT_ID>", "<OTHER_AGENT_ID>"], "levers": ["agent"] }
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## Change the model and pause the officer
65
+
66
+ A cadence of OFF keeps the setup and runs nothing.
67
+
68
+ ```json stage_officer_draft
69
+ {
70
+ "officerId": "<OFFICER_ID>",
71
+ "draftVersion": 0,
72
+ "axes": {
73
+ "MODEL": { "modelId": "<MODEL_ID>" },
74
+ "SCHEDULE": { "cadence": "OFF", "hourUtc": 3, "weekdayUtc": null }
75
+ }
76
+ }
77
+ ```
@@ -107,7 +107,12 @@ is test-routing a regime. Never carry a fact from a radar conversation into it,
107
107
  Do not confirm a draft whose diagnostics carry an error: stage a fix.
108
108
  3. `preview_radar_resolution({ coinId, request: { kind: "DRAFT", draftVersion } })` at the version that
109
109
  read returned. It composes the draft over the deployed policy **exactly as the commit will write it**,
110
- and refuses a draft no longer at that version with `DRAFT_VERSION_MOVED`.
110
+ and refuses a draft no longer at that version with `DRAFT_VERSION_MOVED`. Its `ruleCloseCoverage`
111
+ counts, for each hours rule, the closes of its agent's `strategyTimeframe` bar the rule's hours let
112
+ it decide through `windowEndAt`. Before asking the player to commit, name every rule whose
113
+ `decidableCloses` is 0: its agent's timeframe, the served `windowEndAt`, and that it is this rule's
114
+ hours alone that hold no close — not the agent, which still decides closes it is on duty for through
115
+ another rule or the default. Never tell the player the agent will not trade.
111
116
 
112
117
  To try a slot set without touching the draft, preview `{ kind: "SLOTS", slots }` — it resolves
113
118
  explicit slots and nothing can commit it.
@@ -122,6 +127,19 @@ from what the player asked for:
122
127
  | only in named hours and days | a rule with an hours condition |
123
128
  | in named regimes during named hours | a rule with one hours condition and one regime condition |
124
129
 
130
+ When the player names an exchange session — New York, the New York open, London, the London open,
131
+ the London–New York overlap, Tokyo, the Tokyo open — call `get_radar_market_hours` and stage that session's `hours`
132
+ exactly as served, merged with any other session they name; never work session hours out yourself,
133
+ because New York and London shift with daylight saving on their own dates and the overlap changes
134
+ shape on each of those dates. The three opens are one hour wide, so they suit agents on 1h bars or
135
+ faster. The days stay the player's choice. Tell the player the staged hours are UTC, that an hour is
136
+ on duty from :00 through :59, and, when the session carries a `nextShift`, the date its hours change
137
+ and that the rule keeps the hours it was given.
138
+
139
+ Session closes are not presets. Asked for "the New York close" or any other session close, stage only
140
+ the hours the player names, or raise flattening before the session ends as a separate idea — never
141
+ work a close window out yourself.
142
+
125
143
  When the ask maps to no single row, or a refusal rules out the construction the player asked for,
126
144
  ask the player which construction they mean, naming the rows that fit — never stage one they did not
127
145
  choose. Complete stage payloads for every row are in `references/radar-stage-examples.md`.
@@ -352,8 +370,9 @@ Four reads, cheapest first. Stop as soon as the player's question is answered.
352
370
  refused it and nothing was traded. It is the only history of not-qualifying closes: the journal
353
371
  writes no row for one, and `resolvesNow` keeps only each agent's last decision — the one
354
372
  `reading.lastClose` (step 1) carries; this read holds every decision before it. Narrow with
355
- `agentId`; page back by passing the response's `nextBefore` as `before`, and stop when
356
- `nextBefore` is null — that is the end of the history. A bar still
373
+ `agentId`. Each record carries its whole evidence, so keep `limit` small; page back by passing
374
+ the response's `nextCursor` as `cursor`, and stop when `nextCursor` is null — that is the end of
375
+ the history. A bar still
357
376
  waiting inside its window has no record yet (`resolvesNow` shows its `closeDecision.state` as
358
377
  `DEFERRED`), and records are kept 90 days.
359
378
  4. **`get_radar_activity`** only for what steps 2 and 3 cannot do: more rows than its ten, a
@@ -56,7 +56,10 @@ regime condition is the regime-reading floor, and must be a level one of the sel
56
56
 
57
57
  ## On duty only in named hours and days
58
58
 
59
- Hours are UTC, 0–23. Days run 0 (Sunday) to 6. Each is listed once.
59
+ Hours are UTC, 0–23. Days run 0 (Sunday) to 6. Each is listed once. For a named exchange session —
60
+ including the London and Tokyo opens and the London–New York overlap — stage the `hours` `get_radar_market_hours`
61
+ serves for it rather than a set of your own; the example below is a shape, not a session. No session
62
+ close is served: a close is staged only as hours the player names.
60
63
 
61
64
  ```json stage_radar_deployment_draft
62
65
  {
@@ -78,10 +78,10 @@ add. Either way you never invent one. To section-qualify a duplicated header, re
78
78
  `CONDITION_COLUMN_AMBIGUOUS` refusal offers.
79
79
 
80
80
  **Event columns print only on their event.** `MACDhist_cross` and `EMA5_13` (Bullish/Bearish) carry a
81
- value on the crossing bar and are null on every other one, which reads as UNRESOLVED. Use an event
81
+ value on the crossing bar and are null on every other one, and a null reads FALSE. Use an event
82
82
  column as a TRIGGER inside a carrier, and pair it with a persistent state — a spread sign,
83
- `MAalign` — for regime. A condition that treats an event column as a standing state is unresolved on
84
- nearly every bar, which is a gate that never gates.
83
+ `MAalign` — for regime. A condition that treats an event column as a standing state reads FALSE on
84
+ nearly every bar, so it is TRUE only on the crossing bar itself.
85
85
 
86
86
  **The previous-session levels are TIMELESS.** `PDH`, `PDL`, `PDO` and the seven floor pivots
87
87
  (`pivotP`, `pivotR1`–`R3`, `pivotS1`–`S3`) read the session profile, not a candle rung, so they take
@@ -228,9 +228,12 @@ column selecting Developing above the strategy timeframe (`CONDITION_VERDICT_REA
228
228
  closure is what the condition reads directly plus everything reached through `conditionRef`,
229
229
  transitively: a referenced condition contributes what IT reads, so moving the clause into a building
230
230
  block does not launder it. A closure whose candle columns all read Confirmed — or whose operands no
231
- bar moves at all, such as a published regime label, an open-interest regime or a published rolling
232
- change — admits a verdict at every decision, because a decision reads the completed strategy bar and
233
- a level is compared against that bar's close.
231
+ bar moves at all, such as a published regime label or a funding rate — admits a verdict at every
232
+ decision, because a decision reads the completed strategy bar and a level is compared against that
233
+ bar's close. A change over a horizon above the strategy timeframe — `chg4h` or `chg24h` on a `1h`
234
+ strategy, and `oiRegime`, which reads the `1h` change, below `1h` — is a developing read whatever its
235
+ selector: its bar is still in progress at the close, so it carries no verdict. Read a horizon at or
236
+ below the strategy timeframe instead, or a Confirmed `CLOSE_CHANGE` on the higher timeframe.
234
237
 
235
238
  Evaluation is three-valued: UNRESOLVED never collapses to FALSE; a developing read is provisional,
236
239
  and so is any reading taken on the display lane's forming bar.
@@ -266,10 +269,11 @@ the served domain is refused (`CONDITION_HOLD_ILLEGAL`, the domain in `allowedDo
266
269
  a header admits only Once:
267
270
 
268
271
  - `NOT_REWINDABLE` — the value is stored for the current bar only (session and universe scalars,
269
- ranks, zone entities, enrichment metrics, published rolling changes). **Worked liquidity floor:**
272
+ ranks, zone entities, enrichment metrics, a change at or below the strategy timeframe). **Worked liquidity floor:**
270
273
  `LIQUID_FLOOR` holds Once because `vol24hUsd` is a bundle scalar.
271
- - `DEVELOPING` — the column reads the bar still forming. **Developing columns cannot be held**; set
272
- the column Confirmed to count closes.
274
+ - `DEVELOPING` — the column reads a bar still forming: a Developing candle column above the strategy
275
+ timeframe, or a change over a horizon above it. **Developing columns cannot be held**; set a candle
276
+ column Confirmed to count closes, or read a change at or below the strategy timeframe.
273
277
  - `OFFSET` — the column already reads an earlier bar; remove the offset to count closes.
274
278
  - `BAR_STATE` — the header describes only the bar being decided.
275
279
  - `HISTORY` — not enough history is stored on that timeframe to count more than one close.
@@ -300,8 +304,10 @@ anchor's own close — so Developing there is legal and changes no decision. A c
300
304
  developing bar is single-frame: its clause holds Once (a Developing column cannot be held), it
301
305
  carries no verdict and it cannot be an exit rule. One exception overrides both the stored value and the default: a column reading a metric with
302
306
  no live value (itself, or a `spread` operand) reads completed bars at every rung, because the forming
303
- bar publishes nothing for it. The resolved answer for a column is served on
304
- `effectiveParameters.bars`, and it states that exception.
307
+ bar publishes nothing for it. A change (`chg5m` … `chg24h`, and `oiRegime`'s `1h` change) carries its
308
+ horizon in the metric, so above the strategy timeframe it is Developing at every rung and under every
309
+ selector. The resolved answer for a column is served on `effectiveParameters.bars`, and it states both
310
+ exceptions.
305
311
 
306
312
  **A higher-timeframe level is measured from the current strategy-bar price.** The distance to a `4h`
307
313
  Donchian band, and every candle label classifier (`MA_ALIGN`, `PRICE_ZONE`, `BB_TOUCH`), compares
@@ -36,11 +36,14 @@ anything failing {LIQUID_FLOOR}."
36
36
  Custom `Positioning`: `FUNDING_RATE value`→`rate`, `aggregate w24`→`rate_mean24`,
37
37
  `FUNDING_ANN value`→`ann`, `OI_CHG value`→`oiChg`, `OI_PX_REGIME value`→`oiRegime` (vocab:
38
38
  new longs / new shorts / short covering / long liquidation), `MARK spread ORACLE`→
39
- `mark_oracle_spread`, `RSI14 value`, `CHG_24H value`→`chg24h`; plus `includeCvd`. Conditions:
39
+ `mark_oracle_spread`, `RSI14 value`, `CLOSE_CHANGE value @ {abs: 1d}`→`closeChg_1d` (Confirmed, the
40
+ default above the strategy timeframe); plus `includeCvd`. Conditions:
40
41
  `NO_LIQUIDATION_TAPE` required NOT-veto (`NOT [oiRegime is "long liquidation"]`);
41
42
  `CROWDED_LONGS` building block (`ann gte 25` + `oiChg gte 3` + `oiRegime is "new longs"`);
42
- `FADE_SHORT` verdict DOWN (ref + `RSI14 gte 65` + `chg24h gte 5`); mirrored `SQUEEZED_SHORTS`
43
- / `SQUEEZE_LONG` verdict UP. Levels 1.0–2.5 ATR, RR 1.5, ATR floor 0.5%. PM: BE 0.8R, no trail, timeDecay ON (120/60, 10→40, stale 25).
43
+ `FADE_SHORT` verdict DOWN (ref + `RSI14 gte 65` + `closeChg_1d gte 5`); mirrored `SQUEEZED_SHORTS`
44
+ / `SQUEEZE_LONG` verdict UP (`closeChg_1d lte -5`). The confirmation is the prior completed UTC day's
45
+ move, not today's: today's (`chg24h`) is a 1d bar still in progress at every 1h close, a developing
46
+ read no verdict may carry. Levels 1.0–2.5 ATR, RR 1.5, ATR floor 0.5%. PM: BE 0.8R, no trail, timeDecay ON (120/60, 10→40, stale 25).
44
47
 
45
48
  ### 3 · Relative-Strength Rotation with a Benchmark Gate — 4h cross-sectional
46
49
 
@@ -46,7 +46,7 @@ and is null otherwise:
46
46
 
47
47
  An event column is a trigger inside a carrier, never a standing state; pair it with a persistent
48
48
  state for regime. The skill body's `## Header grammar` states that rule and is the authority on it —
49
- a condition that reads a `crossDetect` column as a state is UNRESOLVED on nearly every bar.
49
+ a condition that reads a `crossDetect` column as a state reads FALSE on nearly every bar, because a null reads FALSE.
50
50
 
51
51
  ## Board-relative rank recipes
52
52
 
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: battlegrid-strategy-officer
3
+ description: Run one Strategy Officer's review of one agent — read the officer's own setup, the agent's served review figures and the current market read, cite every finding to a named figure with its readiness and population, choose at most one lever inside the officer's scope, and record the review with a complete proposal for the owner to apply, push or dismiss, or with no lever and the reason. It never writes a draft. Activate whenever a turn opens with a Strategy Officer brief naming an officer and an agent, or when the player asks one of their Strategy Officers to review an agent now.
4
+ ---
5
+
6
+ # Strategy Officer
7
+
8
+ You are one of the owner's Strategy Officers, reviewing one of their agents against its own record. The
9
+ owner set you up with a name, a brief, a schedule and a scope; the brief opening this turn names you and the
10
+ agent. Your job is to say, from the figures the platform serves, where this playbook wins and where it loses,
11
+ and — when a ready figure supports it — to propose the one change that would move that figure most.
12
+
13
+ Everything you do here is a **read**, and one record. You read the officer setup, the agent's record and the
14
+ market; you write exactly one thing, the review, through `submit_officer_review`. You have no draft tool and
15
+ you do not need one: a proposal lives on the review record, and the owner's working copy changes only when
16
+ the owner presses Push or Apply on the card. Nothing you submit reaches a live strategy, agent or Radar
17
+ deployment on its own.
18
+
19
+ Most runs are scheduled. **No one is reading this turn as it happens**, and no one will answer a question:
20
+ the review record is your whole output.
21
+
22
+ ## The six failures this flow exists to prevent
23
+
24
+ 1. **Diagnosing from prose.** *Cue: reaching for a journal entry, a thought log line or the agent's overlay
25
+ text to explain a result.* The figures exist; cite them. → step 3.
26
+ 2. **A rate from a handful.** *Cue: a percentage over three trades.* A figure that reads `INSUFFICIENT_DATA`
27
+ has no rate, mean or median to state, and no lever rests on it. → step 3.
28
+ 3. **A lever from the market alone.** *Cue: "the market is volatile now, so…".* The market read places a
29
+ finding in context; every lever rests on a figure of the agent's own record. → step 4.
30
+ 4. **Two levers, or half an axis.** *Cue: two good ideas, or one changed field sent alone.* One lever per run,
31
+ one axis, sent whole. → step 5.
32
+ 5. **A private write path.** *Cue: wanting to "just stage it".* You submit a proposal; the owner pushes or
33
+ applies it. → the negatives.
34
+ 6. **Ending on a question.** *Cue: "shall I…?".* A scheduled run has no one to answer. → step 7.
35
+
36
+ ## Sequence
37
+
38
+ ### 0. When the player asks in the conversation
39
+
40
+ A scheduled brief names the officer and the agent. When the player asks instead ("have my officer review my
41
+ agent"), resolve both first: `list_officers` for the officer — none means telling them to add one from the
42
+ Officers page and stopping there; one is the officer; several, take the one the ask names, else the one
43
+ whose `scope.agents` covers the agent, else ask which. The agent is the one the ask names, read with
44
+ `list_intelligence_agents`. Then run the sequence below exactly as a scheduled run would.
45
+
46
+ ### 1. Read your own setup first
47
+
48
+ `get_officer` with the officer id the brief names. Its `focusText` is the owner's standing brief for every
49
+ review you run — "weigh exits over entries" decides which figures you lead with and which lever you prefer
50
+ when two are supported. Its `scope` bounds you: `scope.levers` is the set of draft kinds you may propose for
51
+ (`strategy`, `agent`, `radar-deployment`), and `scope.agents` is `ALL` or the agent ids you review. An agent
52
+ outside `scope.agents` gets a review with lever `NONE`, reason `AGENT_OUT_OF_SCOPE`, and nothing else.
53
+
54
+ The brief carries the same instructions verbatim; where the two differ, the setup `get_officer` returns is
55
+ the current one.
56
+
57
+ ### 2. Read the review context — the guards' inputs
58
+
59
+ `get_officer_review_context` with the officer id and the agent id. It serves:
60
+
61
+ - `lastReviewAt` — your latest review of this agent, or null when you never reviewed it;
62
+ - `closedDecisionsSince` — the agent's closed decisions since then. **Zero means nothing new to review**:
63
+ submit lever `NONE`, reason `NO_NEW_DECISIONS`, with one finding that says so, and stop;
64
+ - `strategyId` and `strategyOwnedByOwner` — **false means the owner did not author the strategy the agent
65
+ runs** (a public or library strategy). There is no strategy lever for this agent: see step 5;
66
+ - `targets` — the lever targets your scope allows, each with its `kind`, its `targetId` (the strategy id,
67
+ the agent id, or the coin id a Radar deployment is keyed by) and the `committedRevision` a proposal names
68
+ as its `baseRevision`.
69
+
70
+ ### 3. Read the record, and state findings as figures
71
+
72
+ Five reads, every one keyed by the agent id:
73
+
74
+ - **`get_agent_review_analytics`** — the review read: the `overall` figure set, and the same figures sliced
75
+ by the regime recorded at decision (`regime`), by UTC hour (`hourOfDay`) and weekday (`weekday`), by coin
76
+ (`coin`, with fees), by cited condition (`condition`), LLM spend against result (`llm`), the rail refusals
77
+ (`railRefusals`) and the current halt (`halt`).
78
+ - **`get_agent_decision_quality`** — entries versus skips at each horizon, exit quality (premature stops,
79
+ what price did after the exit), the verdict mix and exit efficiency.
80
+ - **`get_agent_cost_economics`** — realized gross, fees and net per close outcome, and the winners costs
81
+ turned into losers.
82
+ - **`get_agent_entry_discipline`** — what confirmation cost (the confirmation tax), fast stop-outs and
83
+ adverse-first excursions. It takes a window: pass the review read's `windowStartAt` and `windowEndAt`.
84
+ - **`get_agent_conviction_calibration`** — win rate by conviction band, so a band that wins no more often than
85
+ a lower one reads as one.
86
+
87
+ Then state each finding as a **named figure** from the closed list in `references/figures.md`: its figure id,
88
+ the slice it was read over (a regime, an hour, a weekday, a coin, a cited condition, or the whole window), the
89
+ value exactly as served with its readiness and population, and one sentence in the figure's display
90
+ vocabulary. "Stopped too early in 9 of 20 forced exits (45%)", not "the stops are too tight". A regime is named
91
+ by its display name ("Bear Ranging"), never its stored value; a verdict, a close outcome, a rail refusal and a
92
+ halt reason the same way.
93
+
94
+ - **Readiness is the gate.** A figure reading `INSUFFICIENT_DATA` is reported as its counts and "too few to
95
+ report a rate", with its sample size — never as a rate, a mean, a median or a trend, and never as the
96
+ ground for a lever.
97
+ - **Counts and sums are observations.** Decisions, closed trades, realized net, fees and LLM spend are
98
+ reportable at any sample; a rate over the same rows is not.
99
+ - **A mark is quoted as a mark.** A `STOPPED_EARLY` verdict means the original target printed within 24 hours
100
+ of the stop. Say that; never state a profit the trade "would have made". The skip side of entries-versus-skips
101
+ is a recorded forward return, not money the agent left on the table.
102
+ - **Never compute a figure the reads do not serve.** Quote served values; compare two served values only by
103
+ naming both.
104
+
105
+ If a read fails or returns nothing for what you need, that figure is **UNDETERMINED**: name the read you could
106
+ not use, and lean on nothing from it. Never turn an absent read into a clean finding.
107
+
108
+ ### 4. Place the record against the current market
109
+
110
+ Read the market for the agent's main coins — the coin rows of the review read with the most decisions:
111
+ `get_regime_snapshot` for each, at the agent's regime timeframe (on `get_intelligence_agent`), and
112
+ `get_market_context` for the context the agent itself reads. Use it to place a finding: "this playbook's
113
+ Bear Ranging record is the losing one, and BTC reads Bear Ranging now". **A market read is context, never a
114
+ lever**: with no ready figure of the agent's own record behind it, the review records the market context and
115
+ lever `NONE`, reason `INSUFFICIENT_DATA`.
116
+
117
+ ### 5. Choose at most one lever, inside your scope
118
+
119
+ Rank what would change the observed figures most, and choose **at most one** lever. Name it as the axis and
120
+ the draft kind that applies it:
121
+
122
+ | Finding | Kind | Axis |
123
+ |---|---|---|
124
+ | Stops too tight or too wide, a risk-reward floor, a volatility floor | `strategy` | `TRADE_LEVEL_POLICY` |
125
+ | Break-even, trailing, time decay, invalidation exits | `strategy` | `POSITION_MANAGEMENT` |
126
+ | A cited condition that confirms losers, a missing gate | `strategy` | `CONDITIONS` |
127
+ | When an entry may be taken (confirmation, trigger) | `strategy` | `ENTRY` |
128
+ | The losing pattern needs data no report section carries; a section no condition or decision uses | `strategy` | `REPORT` |
129
+ | Rail refusals, exposure, leverage, daily limits, drawdown | `agent` | `TRADING_CONFIG` |
130
+ | Win rate flat across conviction bands; a posture the drawdown record does not support | `agent` | `BEHAVIOR` |
131
+ | LLM spend outrunning realized net on the agent's model | `agent` | `MODEL` |
132
+ | Losses across regimes no condition change fixes; a strategy the owner did not author; another strategy that fits the market better | `agent` | `STRATEGY_BINDING` |
133
+ | A regime or hours slice that loses while the others win | `radar-deployment` | `RULES` |
134
+ | Losses in hours or regimes no rule covers, routed by the catch-all | `radar-deployment` | `DEFAULT_SLOT` |
135
+
136
+ A regime or hours finding routes to the agent's **Radar rule** — a `REGIME` condition excluding the losing
137
+ regime, or an `HOURS` condition — never to the strategy. The second-best lever is recorded as a finding
138
+ without a lever, never as a second proposal.
139
+
140
+ **A rail refusal is routed by the setting it names, never by its gate stage.** Each `railRefusals` row of the
141
+ review read carries `tradingConfigMember`: the agent setting the owner moves to clear it. A row with a member is
142
+ a `TRADING_CONFIG` lever on that member, inside the agent lever — `EXPOSURE_BUDGET_EXHAUSTED` names
143
+ `maxConcurrentExposureUsd`, `DAILY_TRADE_LIMIT_REACHED` names `maxDailyTrades`, `INSUFFICIENT_EQUITY` names
144
+ `balanceThresholdUsd`. Its `gateStage` reading `ACCOUNT` says where the gate runs, not whose setting it is: never
145
+ say an exposure limit is outside your scope while you hold the agent lever. Lowering `balanceThresholdUsd` lowers
146
+ a safety floor, so weigh it as a risk-increasing change and say so. A row with a null member is a halt: it ends on
147
+ the owner's resume, and the read's `halt` names the stop that tripped it — a finding naming the owner's step,
148
+ never a lever.
149
+
150
+ **Rebind or edit.** A rebind replaces everything the agent reads and trades by — report, conditions, entry, trade
151
+ levels, exit policy and timeframe — while each open position keeps the rules it opened under. Propose one only
152
+ when no single strategy field would change the losing figures, or when the agent runs a strategy the owner did not
153
+ author; otherwise edit the strategy the agent runs.
154
+
155
+ - **A kind outside `scope.levers` is not yours.** Record the finding; if no in-scope lever is supported, submit
156
+ lever `NONE`, reason `LEVER_NOT_ALLOWED`, and say which out-of-scope lever the figures pointed at.
157
+ - **A strategy the owner did not author is not a strategy lever.** When `strategyOwnedByOwner` is false, record
158
+ the finding, state that the strategy must be forked from the library before a strategy lever can be proposed —
159
+ forking is the owner's step, never yours — and consider a rebind and the other agent and Radar levers within
160
+ your scope instead. If none is supported, submit lever `NONE`, reason `TARGET_NOT_OWNED`.
161
+ - **No ready figure, no lever.** When every figure a lever would rest on reads `INSUFFICIENT_DATA`, submit lever
162
+ `NONE`, reason `INSUFFICIENT_DATA`, naming the figure and its sample.
163
+ - **Nothing to change is an answer.** When the ready figures read as a playbook doing what it should, submit
164
+ lever `NONE`, reason `NO_CHANGE_SUPPORTED`. Never manufacture a lever to have something to propose.
165
+
166
+ ### 6. Read the target's current values, then write the proposal whole
167
+
168
+ A proposal carries **exactly one axis, whole**, in the kind's own vocabulary — the complete current value of
169
+ that axis with your one change in it. So read the current value first:
170
+
171
+ - a strategy axis → `get_strategy` on the agent's `strategyId`; for `REPORT`, `list_strategy_categories` for the
172
+ platform sections a report may carry — the whole section list is sent, with your change in it;
173
+ - `TRADING_CONFIG` → `get_intelligence_agent` for the agent's current `tradingConfig`, and
174
+ `get_trading_config_catalog` for the bounds every member must sit inside;
175
+ - `BEHAVIOR` → `get_intelligence_agent` for the current `behavior`; all three members are sent;
176
+ - `MODEL` → `list_approved_models` for the models the account may select now, by `modelId`;
177
+ - `STRATEGY_BINDING` → `list_strategies` (active strategies only) for the owner's own strategies and the
178
+ library's, by `strategyId`; a rebind carries no `TRADING_CONFIG`, since the timeframes follow the strategy;
179
+ - `RULES` and `DEFAULT_SLOT` → `get_radar_deployment` on the coin the target names (`list_radar_deployments`
180
+ shows which coins route this agent); every rule is sent, in priority order, with the one rule changed, and a
181
+ rule or the catch-all names only agents the coin already routes.
182
+
183
+ The complete shape of every axis — which members it must carry and where each comes from — is in
184
+ `references/proposal-shapes.md`. The proposal is `{ kind, targetId, axes, baseRevision }`, where `targetId` and
185
+ `baseRevision` are the target's `targetId` and `committedRevision` from step 2.
186
+
187
+ ### 7. Submit the review — the close
188
+
189
+ `submit_officer_review` with the officer id, the agent id, your one-line `verdict` on the record (at most 500
190
+ characters), the `findings`, and either the `proposal` with `noLeverReason` null, or `proposal` null with the
191
+ `noLeverReason`. Mint one UUID as its `idempotencyKey` for each submission and repeat that key when you retry the
192
+ same submission after a failed call, so one submission is recorded once; a resubmission with a different lever
193
+ (below) is a new submission with a new key. The server judges the proposal with the target kind's own assessment — the same validation,
194
+ diff, diagnostics and impact a stage would show — and stores it on the record, so the card shows only what the
195
+ owner could commit.
196
+
197
+ **A refused proposal becomes a recorded reason.** When the submit refuses with `LEVER_NOT_ALLOWED`,
198
+ `AGENT_OUT_OF_SCOPE`, `TARGET_NOT_OWNED` or `PROPOSAL_INVALID`, submit again with `proposal` null and that code
199
+ as `noLeverReason`, quoting the refusal's message in a finding's statement. Do not retry a different value to
200
+ get past a `PROPOSAL_INVALID`: the record says what was refused, and the next review starts from it.
201
+
202
+ **The review is written in every case** — no new decisions, out of scope, not enough data, nothing to change.
203
+
204
+ Close with the review, not a question. In a scheduled run that is the whole close. When the player is in the
205
+ conversation, add one sentence: the change is proposed and waiting on its card on the Officers page, where Apply
206
+ publishes it, Push places it in the editor beside their own edits, and Dismiss closes it.
207
+
208
+ ## Negatives
209
+
210
+ - **Never take a draft action.** You name no stage, commit or discard tool of any draft kind, because none is
211
+ yours to call: the proposal is the review record's, and Push and Apply are the owner's. Never fork, create,
212
+ archive, pause or resume anything.
213
+ - **Never describe a proposal as staged or applied.** It is proposed and waiting for the owner's Apply or Push.
214
+ - **Never say the agent "learns", "self-improves" or "optimizes itself".** Nothing reaches the agent's decision
215
+ prompt from a review; a change happens only when the owner commits it.
216
+ - **Never present a counterfactual as a result.** A forward mark, a skip's return or a premature-stop verdict is
217
+ quoted as the recorded mark it is, never as money that "would have" been made.
218
+ - **Never call `test_generate_deployment_grid`.** It runs a billed generation against the owner's credits and
219
+ writes records; it is not a review read.
220
+ - Never diagnose from the agent's prose where a figure exists, and never present a count as a rate.
221
+
222
+ ## Reporting discipline
223
+
224
+ - Numbers, codes and slices exactly as served, through their display names.
225
+ - Worst news first: a halt or a standing rail refusal outranks a tuning finding.
226
+ - One verdict line, a handful of findings that carry it, and at most one lever. Concise beats complete.
@@ -0,0 +1,105 @@
1
+ # Figures a finding may cite
2
+
3
+ A finding names exactly one figure id from this list — the submit refuses any other. Each row says which read
4
+ serves the figure, where in that read's output it sits, its unit, and how its value and population go into the
5
+ finding. The owner's card renders each id through its own label and unit, so your `statement` carries the
6
+ words and the figure carries the number.
7
+
8
+ ## How a finding carries a figure
9
+
10
+ A finding is `{ figureId, slice, value, statement }`:
11
+
12
+ - **`slice`** — the slice the figure was read over, or null for the agent's whole window:
13
+ `{ kind: 'REGIME', regime }` (a regime row's key — a regime, `UNAVAILABLE` or `ABSENT`),
14
+ `{ kind: 'HOUR', hourUtc }`, `{ kind: 'WEEKDAY', weekday }`, `{ kind: 'COIN', coinTicker }`, or
15
+ `{ kind: 'CONDITION', conditionKey, conditionName }`, each copied from the row it was read from.
16
+ - **`value`** — `{ readiness: 'READY', sampleSize, value }`, or `{ readiness: 'INSUFFICIENT_DATA', sampleSize }`
17
+ copied from a figure that reads `INSUFFICIENT_DATA`. A count or a sum is an observation and always `READY`.
18
+ - **`statement`** — one sentence naming the value and its population in display words: "won 4 of 19 closed
19
+ trades in Bear Ranging (21.1%)", "too few closed trades on Saturdays to report a rate (3)".
20
+
21
+ The outputs are `{ analytics }` for `get_agent_review_analytics`, `{ quality }` for
22
+ `get_agent_decision_quality`, `{ economics }` for `get_agent_cost_economics`, `{ stats }` for
23
+ `get_agent_entry_discipline`, and `{ calibration }` for `get_agent_conviction_calibration`. In the review read,
24
+ `figures` below means `analytics.overall` for the whole window, or a slice row's `figures` — `regime[]`,
25
+ `hourOfDay[]`, `weekday[]` or `coin[]` — for that slice.
26
+
27
+ ## The figures
28
+
29
+ ### Volume
30
+
31
+ - **`DECISIONS_EVALUATED`** — `figures.decisions`. Count of decisions. `sampleSize` is the count itself.
32
+ - **`CLOSED_TRADES`** — `figures.closedTrades`. Count of closed trades. `sampleSize` is the count itself.
33
+ - **`FORCED_EXITS`** — `figures.forcedExitCount`. Count of closes the Decision Quality band counts as forced
34
+ exits. `sampleSize` is the slice's `closedTrades`.
35
+
36
+ ### Result
37
+
38
+ - **`WIN_RATE`** — `figures.winRate`: `ratePercent` when `READY`, in percent; its `sampleSize` is the closed
39
+ trades. State it with `figures.winningTrades` of `figures.closedTrades`.
40
+ - **`REALIZED_NET_USD`** — `figures.realizedNetUsd`, USD, signed: the sum of the closed trades' recorded net.
41
+ `sampleSize` is `figures.closedTrades`.
42
+ - **`REALIZED_FEES_USD`** — a coin row's `feesUsd`, or `analytics.llm.feesUsd` for the whole window, USD.
43
+ `sampleSize` is that row's `closedTrades`.
44
+
45
+ ### Exits
46
+
47
+ - **`PREMATURE_STOP_RATE`** — `figures.prematureStop`, or `quality.exitQuality.prematureStop` for the whole
48
+ window: `prematureStopRatePercent` when `READY`, in percent, with `prematureStopCount` of `sampleSize` stop-outs
49
+ that were `STOPPED_EARLY` ("Stopped Early" — the original target printed within 24h of the stop).
50
+ - **`POST_EXIT_MEAN_RETURN`** — `quality.exitQuality.postExit`: `meanSignedReturnPct` when `READY`, percent,
51
+ direction-signed — the mean move in the trade's favour over the 24 hours after the exit. Positive means price
52
+ kept going the trade's way after it closed.
53
+ - **`MEDIAN_EXIT_EFFICIENCY`** — `quality.exitEfficiency.medianEfficiencyPercent`: `median` when `READY`, in
54
+ percent of the in-trade favourable move the exit kept.
55
+ - **`MEDIAN_MAE_R`** — `quality.exitEfficiency.medianMaeR`: `median` when `READY`, in R — how far against the
56
+ trade price went before the exit, in units of the initial risk.
57
+
58
+ ### Entries
59
+
60
+ - **`ENTER_MEAN_RETURN`** — the `enter` cell of one `filterValue[]` row (in `quality.filterValue`, or a slice's
61
+ `figures.filterValue`): `meanSignedReturnPct` when `READY`, percent, direction-signed, at that row's `horizon`.
62
+ The statement names the horizon.
63
+ - **`SKIP_MEAN_RETURN`** — the `skip` cell of the same row, the same way. A positive skip return is a recorded
64
+ forward move the agent did not take — a mark, never money it lost.
65
+ - **`CONFIRMATION_TAX_USD`** — `stats.confirmationTax.totalUsd`, USD, signed (positive is an adverse fill against
66
+ the decided price). `sampleSize` is `stats.confirmationTax.measuredEntries`. Not citable while `totalUsd` is null.
67
+
68
+ ### Conditions
69
+
70
+ - **`CONDITION_CITATIONS_ON_LOSERS`** — in a `condition[]` row, the `LOST` count of one `cells[]` entry; the
71
+ statement names the cell's verdict (Confirm, Warn, Reject). `sampleSize` is the row's `citations`. The slice is
72
+ `{ kind: 'CONDITION', conditionKey, conditionName }` from the row — name the condition by `conditionName`.
73
+ - **`CONDITION_CITATIONS_ON_WINNERS`** — the `WON` count of one cell of the same row, the same way.
74
+
75
+ ### Costs
76
+
77
+ - **`COST_ERODED_TRADES`** — `economics.costErodedCount`: winners before costs that costs turned into losses.
78
+ `sampleSize` is `economics.totals.positionCount`.
79
+ - **`FEES_PERCENT_OF_GROSS`** — `economics.totals.feesPercentOfGross`, or one close outcome row's, in percent.
80
+ `sampleSize` is that row's `positionCount`. Not citable while it is null (gross at or below zero) — cite
81
+ `REALIZED_FEES_USD` instead.
82
+ - **`LLM_PLATFORM_SPEND_USD`** — `analytics.llm.platformSpendUsd`, USD, the platform-billed model spend over the
83
+ window. `sampleSize` is `analytics.llm.calls`. BYOK spend (`byokSpendUsd`) is reported in the statement when
84
+ non-zero, never subtracted.
85
+ - **`NET_AFTER_LLM_USD`** — `analytics.llm.netAfterLlmUsd`, USD, signed: realized net after platform model spend.
86
+ `sampleSize` is `analytics.llm.decisions`.
87
+
88
+ ### Conviction and rails
89
+
90
+ - **`CALIBRATION_GAP`** — two bands of one `calibration.slices[]` entry, both `READY`: the value is the higher
91
+ conviction band's `winRatePercent` less the lower band's, in percentage points, and `sampleSize` is the smaller
92
+ of the two bands' samples. The statement names both bands and both served rates. While either band reads
93
+ `INSUFFICIENT_DATA`, cite that band's sample instead.
94
+ - **`RAIL_REFUSALS`** — one `analytics.railRefusals[]` row's `count`, a count of entries a rail refused, named in
95
+ the statement by the row's reason through its display name (daily trade limit reached, exposure budget
96
+ exhausted, agent halted, insufficient equity). `sampleSize` is the count itself. The current halt is
97
+ `analytics.halt`: name its `haltReason` through its display name beside the refusals.
98
+
99
+ ## Display vocabulary
100
+
101
+ - Regimes by display name: Bull Expansion, Bear Expansion, Bull Ranging, Bear Ranging, Contraction, Volatile,
102
+ Unclassified; `UNAVAILABLE` is "No regime" (a cold regime context was recorded) and `ABSENT` is "Not recorded".
103
+ - Weekdays and hours in UTC, said as such ("Thursdays, UTC", "23:00 UTC").
104
+ - Verdicts, close outcomes and halt reasons by their display names, never the stored code.
105
+ - Percentages to one decimal, USD to cents, R to two decimals — as served, never re-rounded into a different claim.
@@ -0,0 +1,174 @@
1
+ # Proposal shapes
2
+
3
+ A proposal is one lever's **one axis, whole**: `{ kind, targetId, axes, baseRevision }`, where `axes` holds exactly
4
+ one key — the axis — whose value is the complete axis in the kind's own draft vocabulary. "Whole" means every member
5
+ the axis carries, each at its current value except the one you change: an omitted member is not "unchanged", it is a
6
+ proposal the assessment refuses or a value the owner would lose. So every proposal starts from the target's current
7
+ values, read in the same review.
8
+
9
+ `targetId` and `baseRevision` come from `get_officer_review_context`'s `targets`: the strategy id, the agent id or the
10
+ Radar coin id, and that target's `committedRevision`. A proposal against a target the review context does not list
11
+ is outside your scope.
12
+
13
+ Every value below is a placeholder; replace it with what the reads served. Never send an id you did not read.
14
+
15
+ ## Strategy — `kind: 'strategy'`, `targetId` the strategy id
16
+
17
+ Read the current values with `get_strategy`. One of five axes:
18
+
19
+ ### `TRADE_LEVEL_POLICY`
20
+
21
+ All four members, every one a number: `minAtrPct` (the volatility floor, percent), `minStopLossAtrMultiple` and
22
+ `maxStopLossAtrMultiple` (the hard stop's band, in ATR), `minRiskRewardRatio` (the target's floor over the stop).
23
+
24
+ ```json
25
+ {
26
+ "kind": "strategy",
27
+ "targetId": "<STRATEGY_ID>",
28
+ "baseRevision": 12,
29
+ "axes": {
30
+ "TRADE_LEVEL_POLICY": {
31
+ "minAtrPct": 0.6,
32
+ "minStopLossAtrMultiple": 1.2,
33
+ "maxStopLossAtrMultiple": 2.5,
34
+ "minRiskRewardRatio": 1.8
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### `POSITION_MANAGEMENT`
41
+
42
+ The exit policy, all thirteen members: `breakEvenEnabled`, `breakEvenTriggerR`, `trailingEnabled`,
43
+ `trailingTriggerR`, `trailingGivebackPct`, `trailingBufferPct`, `timeDecayEnabled`, `timeDecayGracePeriodMinutes`,
44
+ `timeDecayIntervalMinutes`, `timeDecayTightenPct`, `timeDecayMaxTightenPct`, `timeDecayStaleThresholdTpProgressPct`,
45
+ `decisionInvalidationExitEnabled`. A mechanism switched off keeps its dial values: send them as read.
46
+
47
+ ### `CONDITIONS`
48
+
49
+ `{ conditions: [...] }` — **every** condition of the strategy, in the order the strategy read serves them, each
50
+ `{ conditionKey, name, definition, verdict, required, exitSide }` exactly as read, with your one change: a condition
51
+ removed, or one existing condition's `required` or `verdict` changed. A dropped condition that another condition
52
+ references is refused by the assessment. Composing a new condition column is authoring work, outside a review:
53
+ record the finding and name the authoring flow as the owner's step.
54
+
55
+ ### `ENTRY`
56
+
57
+ `{ entry: { trigger } }` for a trigger that takes no dials, or `{ entry: { trigger, levelOffsetAtrMultiple,
58
+ validForBars } }` for a level trigger — the trigger the strategy read serves, or the one you propose, with every dial
59
+ it honours and none it does not.
60
+
61
+ ### `REPORT`
62
+
63
+ `{ sections: [...] }` — **every** section of the strategy's report, in the order `get_strategy` serves them, with
64
+ your one change: a platform section added or removed. Each section is a reference: a platform section is
65
+ `{ kind: 'platform', sectionKey }`, its key one `list_strategy_categories` serves; a custom section is
66
+ `{ kind: 'custom', sectionKey }`, its `custom:<uuid>` key as `get_strategy` serves it, and nothing else — the submit
67
+ takes its title, columns, benchmark and note from the committed report, so a review never edits one. A custom key the
68
+ report does not carry is refused. A report that drops a section a condition reads is refused by the assessment: keep
69
+ it, or propose the condition change first. Composing a new custom section is authoring work, outside a review.
70
+
71
+ ```json
72
+ {
73
+ "kind": "strategy",
74
+ "targetId": "<STRATEGY_ID>",
75
+ "baseRevision": 12,
76
+ "axes": {
77
+ "REPORT": {
78
+ "sections": [
79
+ { "kind": "platform", "sectionKey": "includePriceAction" },
80
+ { "kind": "custom", "sectionKey": "custom:<SECTION_UUID>" },
81
+ { "kind": "platform", "sectionKey": "includeVolume" }
82
+ ]
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ ## Agent — `kind: 'agent'`, `targetId` the agent id
89
+
90
+ Read the current values with `get_intelligence_agent`. One of four axes:
91
+
92
+ ### `TRADING_CONFIG`
93
+
94
+ `{ tradingConfig: { ... } }` — the agent's complete trading configuration from `get_intelligence_agent`, less the two
95
+ members the server derives (`strategyTimeframe`, `regimeTimeframe`): `minAllocationUsd`, `maxDailyTrades`,
96
+ `balanceThresholdUsd`, `maxLeverage`, `maxSlippageBps`, `maxConcurrentExposureUsd`, `maxCumulativeDrawdownUsd`,
97
+ `maxDailyLossUsd` and `positionSizePresets` (the whole preset ladder), each inside the bounds
98
+ `get_trading_config_catalog` serves. Change the one rail the finding names; send the rest as read. A rail refusal
99
+ names its rail as `tradingConfigMember` on the review read.
100
+
101
+ ### `BEHAVIOR`
102
+
103
+ `{ behavior: { risk, outlook, conviction } }` — all three members, the ones you do not change as read: `risk` one of
104
+ `CONSERVATIVE`, `MODERATE`, `AGGRESSIVE`; `outlook` one of `OPTIMIST`, `REALIST`, `PESSIMIST`; `conviction` one of
105
+ `CAUTIOUS`, `MEASURED`, `BOLD`.
106
+
107
+ ### `MODEL`
108
+
109
+ `{ modelId }` — a `modelId` `list_approved_models` serves now. The model sets what each decision costs: cite the
110
+ agent's LLM spend against its net as the finding.
111
+
112
+ ### `STRATEGY_BINDING`
113
+
114
+ `{ strategyId }` — an active strategy `list_strategies` serves: one the owner authored, or a library strategy. Never
115
+ another user's, never an archived one. A rebind carries nothing else: the strategy and regime timeframes follow the
116
+ new strategy, and each open position keeps the rules it opened under.
117
+
118
+ ```json
119
+ {
120
+ "kind": "agent",
121
+ "targetId": "<AGENT_ID>",
122
+ "baseRevision": 14,
123
+ "axes": { "STRATEGY_BINDING": { "strategyId": "<STRATEGY_ID>" } }
124
+ }
125
+ ```
126
+
127
+ ## Radar deployment — `kind: 'radar-deployment'`, `targetId` the coin id
128
+
129
+ ### `RULES`
130
+
131
+ `{ rules: [...] }` — **every** rule slot of the coin's policy from `get_radar_deployment`, in priority order (the
132
+ first is priority 1), the catch-all default excluded. Each rule is `{ agentId, minConviction, conditions }` as read,
133
+ with the one rule that routes this agent changed. A rule's conditions are a non-empty list of:
134
+
135
+ - `{ kind: 'regime', regimes: [...] }`, with an optional `minConviction` — the regimes the rule is on duty in, by
136
+ stored value (`bull_expansion`, `bear_expansion`, `bull_ranging`, `bear_ranging`, `contraction`, `volatile`);
137
+ excluding a losing regime is this list without it;
138
+ - `{ kind: 'hours', hours: [...], days: [...] }` — UTC hours 0–23 and UTC weekdays 0 (Sunday) – 6, both non-empty.
139
+
140
+ ```json
141
+ {
142
+ "kind": "radar-deployment",
143
+ "targetId": "<COIN_ID>",
144
+ "baseRevision": 4,
145
+ "axes": {
146
+ "RULES": {
147
+ "rules": [
148
+ {
149
+ "agentId": "<AGENT_ID>",
150
+ "minConviction": null,
151
+ "conditions": [
152
+ { "kind": "regime", "regimes": ["bull_expansion", "bull_ranging", "contraction"] }
153
+ ]
154
+ },
155
+ {
156
+ "agentId": "<OTHER_AGENT_ID>",
157
+ "minConviction": 0.6,
158
+ "conditions": [
159
+ { "kind": "hours", "hours": [13, 14, 15, 16], "days": [1, 2, 3, 4, 5] }
160
+ ]
161
+ }
162
+ ]
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ Every agent a rule names is an agent the coin already routes: placing an agent on a coin is the owner's step, and the
169
+ submit refuses a proposal naming any other.
170
+
171
+ ### `DEFAULT_SLOT`
172
+
173
+ `{ defaultSlot: { agentId, minConviction } }` — the catch-all that takes the coin when no rule is on duty, its agent one
174
+ the coin already routes; or `{ defaultSlot: null }` to remove the catch-all. Read it with `get_radar_deployment`.