@coinrithm/mcp-trading 0.7.12 → 0.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,9 @@
1
1
  import { CoinRithmClient, ProvenanceReport } from "./client.js";
2
2
  import { ProposedAction, AgentTrace, ApiResult, Observation, QuoteEvidence } from "./types.js";
3
- export declare function fetchQuote(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace?: AgentTrace): Promise<QuoteEvidence | undefined>;
4
- export declare function executeAction(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace: AgentTrace, idempotencyKey: string, provenance?: ProvenanceReport): Promise<ApiResult>;
3
+ /** Configured policy values the server enforces independently of the model's
4
+ * action fields (2026-09-24: the PM entry floor). */
5
+ export interface ExecutionPolicy {
6
+ pmMinEntryProbabilityPct?: number;
7
+ }
8
+ export declare function fetchQuote(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace?: AgentTrace, policy?: ExecutionPolicy): Promise<QuoteEvidence | undefined>;
9
+ export declare function executeAction(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace: AgentTrace, idempotencyKey: string, provenance?: ProvenanceReport, policy?: ExecutionPolicy): Promise<ApiResult>;
package/dist/agent/act.js CHANGED
@@ -1,13 +1,45 @@
1
1
  // Act phase: fetch the quote evidence for an open (the runner does this, never
2
2
  // the model) and execute a validated action (futures / spot / PM) with an
3
3
  // idempotency key.
4
- import { asObj, asNum } from "./extract.js";
4
+ import { asObj, asNum, asStr } from "./extract.js";
5
5
  import { freshnessOf } from "./pmContext.js";
6
+ function fundingQuoteEvidence(value) {
7
+ if (value === null)
8
+ return null;
9
+ if (typeof value !== "object" || value === null)
10
+ return undefined;
11
+ const f = asObj(value);
12
+ const estimatedPerIntervalMusd = Object.prototype.hasOwnProperty.call(f, "estimatedPerIntervalMusd")
13
+ ? (asNum(f.estimatedPerIntervalMusd) ??
14
+ (f.estimatedPerIntervalMusd === null ? null : undefined))
15
+ : undefined;
16
+ return {
17
+ ...(asStr(f.venue) !== undefined ? { venue: asStr(f.venue) } : {}),
18
+ ...(asStr(f.symbol) !== undefined ? { symbol: asStr(f.symbol) } : {}),
19
+ ...(asNum(f.rate) !== undefined ? { rate: asNum(f.rate) } : {}),
20
+ ...(asNum(f.intervalHours) !== undefined
21
+ ? { intervalHours: asNum(f.intervalHours) }
22
+ : {}),
23
+ ...(asStr(f.nextFundingTime) !== undefined
24
+ ? { nextFundingTime: asStr(f.nextFundingTime) }
25
+ : {}),
26
+ ...(asStr(f.asOf) !== undefined ? { asOf: asStr(f.asOf) } : {}),
27
+ ...(estimatedPerIntervalMusd !== undefined
28
+ ? { estimatedPerIntervalMusd }
29
+ : {}),
30
+ ...(asNum(f.annualizedRate) !== undefined
31
+ ? { annualizedRate: asNum(f.annualizedRate) }
32
+ : {}),
33
+ };
34
+ }
6
35
  function coinIdFor(observation, symbol) {
7
36
  return (observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined);
8
37
  }
9
- // Read-only quote BEFORE any open. Returns ineligible (never throws) on error.
10
- export async function fetchQuote(client, action, observation, trace) {
38
+ const pmFloorBody = (policy) => typeof policy?.pmMinEntryProbabilityPct === "number" &&
39
+ Number.isFinite(policy.pmMinEntryProbabilityPct)
40
+ ? { minEntryProbabilityPct: policy.pmMinEntryProbabilityPct }
41
+ : {};
42
+ export async function fetchQuote(client, action, observation, trace, policy) {
11
43
  let r;
12
44
  if (action.type === "futures_open") {
13
45
  const coinId = coinIdFor(observation, action.symbol);
@@ -32,6 +64,10 @@ export async function fetchQuote(client, action, observation, trace) {
32
64
  slug: action.slug,
33
65
  outcomeExternalMarketId: action.outcomeExternalMarketId,
34
66
  stakeMusd: action.stakeMusd,
67
+ ...(action.forecastProbability != null
68
+ ? { forecastProbability: action.forecastProbability }
69
+ : {}),
70
+ ...pmFloorBody(policy),
35
71
  }, trace);
36
72
  }
37
73
  else {
@@ -62,6 +98,9 @@ export async function fetchQuote(client, action, observation, trace) {
62
98
  futuresFeeBps: asNum(asObj(d.executionModel).feeBps),
63
99
  estimatedEntryFeeMusd: asNum(asObj(d.executionModel).estimatedEntryFeeMusd),
64
100
  cashRequiredMusd: asNum(d.cashRequiredMusd),
101
+ ...(Object.prototype.hasOwnProperty.call(d, "funding")
102
+ ? { funding: fundingQuoteEvidence(d.funding) }
103
+ : {}),
65
104
  }
66
105
  : {}),
67
106
  // Freshness lives in the response's `observation` block (anti-look-ahead).
@@ -76,7 +115,7 @@ export async function executeAction(client, action, observation, trace, idempote
76
115
  // Optional SELF-REPORTED runner provenance, attached to a pm_open only (the one
77
116
  // durable-artifact write path here). Absent => the request is byte-identical to
78
117
  // pre-provenance behavior.
79
- provenance) {
118
+ provenance, policy) {
80
119
  if (action.type === "futures_open") {
81
120
  const coinId = coinIdFor(observation, action.symbol);
82
121
  if (!coinId)
@@ -146,6 +185,10 @@ provenance) {
146
185
  ...(action.forecastProbability != null
147
186
  ? { forecastProbability: action.forecastProbability }
148
187
  : {}),
188
+ ...(action.thesis?.summary ? { thesis: action.thesis.summary } : {}),
189
+ // The CONFIGURED entry floor rides along so the server re-checks it inside
190
+ // its locked open transaction (absent => byte-identical request).
191
+ ...pmFloorBody(policy),
149
192
  // Attach runner provenance only when present (byte-identical to before when absent).
150
193
  ...(provenance ? { provenance } : {}),
151
194
  agentTrace: trace,
@@ -18,5 +18,6 @@ export declare function cmdRun(path: string, opts?: {
18
18
  once?: boolean;
19
19
  live?: boolean;
20
20
  stateFile?: string;
21
+ expectDefinition?: string;
21
22
  }): Promise<CmdResult>;
22
23
  export declare function main(argv: string[]): Promise<number>;
package/dist/agent/cli.js CHANGED
@@ -10,6 +10,7 @@ import { resolve as resolvePath, dirname, join, basename } from "node:path";
10
10
  import { parse as parseYaml } from "yaml";
11
11
  import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource, hostedProseBudget, HOSTED_PROSE_MAX_CHARS, } from "./resolve.js";
12
12
  import { buildSpec, loadAgent } from "./skill.js";
13
+ import { buildAgentDefinitionSnapshot } from "./definitionSnapshot.js";
13
14
  import { validateSkill } from "./skillValidator.js";
14
15
  import { strictLint } from "./strictLint.js";
15
16
  import { checkCapabilityDrift } from "./capabilityGuard.js";
@@ -227,6 +228,9 @@ export function cmdInspect(path, json = false) {
227
228
  resolvedConfig: resolved.rawFrontmatter,
228
229
  provenance: resolved.provenance,
229
230
  contentHashes: resolved.contentHashes,
231
+ // The local runtime consumes these exact compiled inputs. In particular,
232
+ // skill ablation changes prose, not the source manifest or hard caps.
233
+ compiledDefinition: buildAgentDefinitionSnapshot(spec, runtimeProse(resolved)),
230
234
  validation: { valid: v.valid, issues: v.issues, lint },
231
235
  };
232
236
  if (json) {
@@ -244,11 +248,17 @@ export function cmdInspect(path, json = false) {
244
248
  `model: ${spec.model ? `${spec.model.provider}/${spec.model.name}` : "(host free-tier)"}`,
245
249
  `risk: maxLeverage=${spec.risk.maxLeverage} perTradeMargin=${spec.risk.perTradeMarginMusd} requireStopLoss=${spec.risk.requireStopLoss}`,
246
250
  `sources: ${Object.keys(resolved.contentHashes).length} file(s)`,
251
+ `definition: ${output.compiledDefinition.definitionHash}`,
247
252
  `validation: ${v.valid ? "valid" : "INVALID"}${lint.length ? ` (+${lint.length} lint note(s))` : ""}`,
248
253
  ...output.warnings.map((warning) => `⚠ ${warning}`),
249
254
  ];
250
255
  return { ok: v.valid, code: 0, lines, data: output };
251
256
  }
257
+ function runtimeProse(resolved) {
258
+ return envFlag(process.env.COINRITHM_AGENT_DISABLE_SKILLS)
259
+ ? mergeProseParts(resolved.proseParts.filter((part) => !isSkillProseSource(part.source)))
260
+ : resolved.mergedProse;
261
+ }
252
262
  // Is a process still alive? signal 0 probes without sending — ESRCH means it's
253
263
  // gone, EPERM means it exists but we can't signal it (still alive). Unknown PIDs
254
264
  // (NaN / non-positive) are treated as alive so we never reclaim a malformed lock.
@@ -333,6 +343,16 @@ export async function cmdRun(path, opts = {}) {
333
343
  return issuesResult(e.issues, "resolve failed");
334
344
  throw e;
335
345
  }
346
+ const mergedProse = runtimeProse(loaded.resolved);
347
+ const definition = buildAgentDefinitionSnapshot(loaded.spec, mergedProse);
348
+ if (opts.expectDefinition !== undefined &&
349
+ opts.expectDefinition !== definition.definitionHash) {
350
+ return fail([
351
+ "compiled agent definition does not match --expect-definition; no model or account call was made",
352
+ `current definition: ${definition.definitionHash}`,
353
+ "inspect the change before choosing a new baseline",
354
+ ]);
355
+ }
336
356
  const apiKey = process.env.COINRITHM_API_KEY;
337
357
  if (!apiKey)
338
358
  return fail([
@@ -386,14 +406,12 @@ export async function cmdRun(path, opts = {}) {
386
406
  const live = !!opts.live;
387
407
  const lines = [
388
408
  `run ${live ? "LIVE (paper trades WILL be placed)" : "DRY-RUN (no writes; set --live or LIVE=1)"} — ${loaded.spec.name}`,
409
+ `definition: ${definition.definitionHash}`,
389
410
  ];
390
411
  // Skills ablation kill-switch: drop tactic-skill prose from the prompt for
391
412
  // token-cost control or A/B testing. Affects ONLY the run-time prompt — the
392
413
  // resolver, manifest, and caps are untouched (the spec is still enforced).
393
414
  const disableSkills = envFlag(process.env.COINRITHM_AGENT_DISABLE_SKILLS);
394
- const mergedProse = disableSkills
395
- ? mergeProseParts(loaded.resolved.proseParts.filter((p) => !isSkillProseSource(p.source)))
396
- : loaded.body;
397
415
  if (disableSkills) {
398
416
  const dropped = loaded.resolved.proseParts.filter((p) => isSkillProseSource(p.source)).length;
399
417
  lines.push(`skills DISABLED via COINRITHM_AGENT_DISABLE_SKILLS — ${dropped} tactic skill(s) dropped from the prompt (caps unchanged)`);
@@ -442,6 +460,8 @@ function parseFlags(args) {
442
460
  out.preset = args[++i];
443
461
  else if (a === "--state")
444
462
  out.state = args[++i];
463
+ else if (a === "--expect-definition")
464
+ out.expectDefinition = args[++i] ?? "";
445
465
  else
446
466
  out._.push(a);
447
467
  }
@@ -455,7 +475,7 @@ function usageLines() {
455
475
  " inspect <path> [--json]",
456
476
  " eject <agent.md | dir>",
457
477
  " lock <path>",
458
- " run <path> [--once] [--live] [--dry-run] [--state <file>] (dry-run by default)",
478
+ " run <path> [--once] [--live] [--dry-run] [--state <file>] [--expect-definition sha256:...] (dry-run by default)",
459
479
  ];
460
480
  }
461
481
  export async function main(argv) {
@@ -489,6 +509,7 @@ export async function main(argv) {
489
509
  once: flags.once,
490
510
  live,
491
511
  stateFile: flags.state,
512
+ expectDefinition: flags.expectDefinition,
492
513
  });
493
514
  break;
494
515
  }
@@ -91,6 +91,9 @@ export declare class CoinRithmClient {
91
91
  slug: string;
92
92
  outcomeExternalMarketId: string;
93
93
  stakeMusd: number;
94
+ side?: "yes" | "no";
95
+ forecastProbability?: number;
96
+ bankrollMusd?: number;
94
97
  }, trace?: AgentTrace): Promise<ApiResult>;
95
98
  openFutures(body: {
96
99
  coinId: string;
@@ -133,6 +136,7 @@ export declare class CoinRithmClient {
133
136
  stakeMusd: number;
134
137
  idempotencyKey: string;
135
138
  forecastProbability?: number;
139
+ thesis?: string;
136
140
  provenance?: ProvenanceReport;
137
141
  agentTrace?: AgentTrace;
138
142
  }): Promise<ApiResult>;
@@ -19,9 +19,9 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
19
19
  marginMusd?: any;
20
20
  stopLossPrice?: any;
21
21
  takeProfitPrice?: any;
22
+ thesis?: import("./types.js").Thesis | undefined;
22
23
  confidence?: any;
23
24
  rationaleSummary?: string | undefined;
24
- thesis?: import("./types.js").Thesis | undefined;
25
25
  }, {
26
26
  symbol: string;
27
27
  side: "long" | "short";
@@ -30,9 +30,9 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
30
30
  marginMusd?: unknown;
31
31
  stopLossPrice?: unknown;
32
32
  takeProfitPrice?: unknown;
33
+ thesis?: any;
33
34
  confidence?: unknown;
34
35
  rationaleSummary?: string | undefined;
35
- thesis?: any;
36
36
  }>, z.ZodObject<{
37
37
  type: z.ZodLiteral<"futures_close">;
38
38
  positionId: z.ZodEffects<z.ZodTypeAny, any, unknown>;
@@ -44,16 +44,16 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
44
44
  type: "futures_close";
45
45
  positionId?: any;
46
46
  fraction?: any;
47
+ thesis?: import("./types.js").Thesis | undefined;
47
48
  confidence?: any;
48
49
  rationaleSummary?: string | undefined;
49
- thesis?: import("./types.js").Thesis | undefined;
50
50
  }, {
51
51
  type: "futures_close";
52
52
  positionId?: unknown;
53
53
  fraction?: unknown;
54
+ thesis?: any;
54
55
  confidence?: unknown;
55
56
  rationaleSummary?: string | undefined;
56
- thesis?: any;
57
57
  }>, z.ZodObject<{
58
58
  type: z.ZodLiteral<"futures_set_sltp">;
59
59
  positionId: z.ZodEffects<z.ZodTypeAny, any, unknown>;
@@ -67,17 +67,17 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
67
67
  stopLossPrice?: any;
68
68
  takeProfitPrice?: any;
69
69
  positionId?: any;
70
+ thesis?: import("./types.js").Thesis | undefined;
70
71
  confidence?: any;
71
72
  rationaleSummary?: string | undefined;
72
- thesis?: import("./types.js").Thesis | undefined;
73
73
  }, {
74
74
  type: "futures_set_sltp";
75
75
  stopLossPrice?: unknown;
76
76
  takeProfitPrice?: unknown;
77
77
  positionId?: unknown;
78
+ thesis?: any;
78
79
  confidence?: unknown;
79
80
  rationaleSummary?: string | undefined;
80
- thesis?: any;
81
81
  }>, z.ZodObject<{
82
82
  type: z.ZodLiteral<"spot_order">;
83
83
  symbol: z.ZodString;
@@ -97,9 +97,9 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
97
97
  quantity?: any;
98
98
  limitPrice?: any;
99
99
  stopPrice?: any;
100
+ thesis?: import("./types.js").Thesis | undefined;
100
101
  confidence?: any;
101
102
  rationaleSummary?: string | undefined;
102
- thesis?: import("./types.js").Thesis | undefined;
103
103
  }, {
104
104
  symbol: string;
105
105
  side: "buy" | "sell";
@@ -108,9 +108,9 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
108
108
  quantity?: unknown;
109
109
  limitPrice?: unknown;
110
110
  stopPrice?: unknown;
111
+ thesis?: any;
111
112
  confidence?: unknown;
112
113
  rationaleSummary?: string | undefined;
113
- thesis?: any;
114
114
  }>, z.ZodObject<{
115
115
  type: z.ZodLiteral<"spot_cancel">;
116
116
  orderId: z.ZodEffects<z.ZodTypeAny, any, unknown>;
@@ -119,16 +119,16 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
119
119
  thesis: z.ZodOptional<z.ZodEffects<z.ZodAny, import("./types.js").Thesis | undefined, any>>;
120
120
  }, "strict", z.ZodTypeAny, {
121
121
  type: "spot_cancel";
122
+ thesis?: import("./types.js").Thesis | undefined;
122
123
  confidence?: any;
123
124
  rationaleSummary?: string | undefined;
124
125
  orderId?: any;
125
- thesis?: import("./types.js").Thesis | undefined;
126
126
  }, {
127
127
  type: "spot_cancel";
128
+ thesis?: any;
128
129
  confidence?: unknown;
129
130
  rationaleSummary?: string | undefined;
130
131
  orderId?: unknown;
131
- thesis?: any;
132
132
  }>, z.ZodObject<{
133
133
  type: z.ZodLiteral<"pm_open">;
134
134
  ref: z.ZodOptional<z.ZodString>;
@@ -147,9 +147,9 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
147
147
  outcomeExternalMarketId?: string | undefined;
148
148
  stakeMusd?: any;
149
149
  forecastProbability?: number | undefined;
150
+ thesis?: import("./types.js").Thesis | undefined;
150
151
  confidence?: any;
151
152
  rationaleSummary?: string | undefined;
152
- thesis?: import("./types.js").Thesis | undefined;
153
153
  ref?: string | undefined;
154
154
  }, {
155
155
  type: "pm_open";
@@ -158,9 +158,9 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
158
158
  outcomeExternalMarketId?: string | undefined;
159
159
  stakeMusd?: unknown;
160
160
  forecastProbability?: any;
161
+ thesis?: any;
161
162
  confidence?: unknown;
162
163
  rationaleSummary?: string | undefined;
163
- thesis?: any;
164
164
  ref?: string | undefined;
165
165
  }>]>;
166
166
  export declare const DECISION_JSON_SCHEMA: {
@@ -138,6 +138,31 @@ export function buildDecisionInputRecord(input) {
138
138
  "sentimentBullishPct",
139
139
  ]),
140
140
  freshness: freshness(r.freshness),
141
+ ...(r.indicatorContext
142
+ ? {
143
+ indicatorContext: {
144
+ range: code(obj(r.indicatorContext).range, ["1D"]),
145
+ asOf: sourceTimestamp(obj(r.indicatorContext).asOf) ?? null,
146
+ intervalStatus: code(obj(r.indicatorContext).intervalStatus, [
147
+ "regular",
148
+ "irregular",
149
+ "unknown",
150
+ ]),
151
+ ...numeric(obj(r.indicatorContext), [
152
+ "nominalIntervalSeconds",
153
+ "barCount",
154
+ "timestampedBarCount",
155
+ "checkedIntervalCount",
156
+ "irregularIntervalCount",
157
+ "maxGapSeconds",
158
+ ]),
159
+ recent15: {
160
+ ...numeric(obj(obj(r.indicatorContext).recent15), ["barCount"]),
161
+ intervalStatus: code(obj(obj(r.indicatorContext).recent15).intervalStatus, ["regular", "irregular", "unknown"]),
162
+ },
163
+ },
164
+ }
165
+ : {}),
141
166
  indicators: {
142
167
  ...numeric(obj(r.indicators), [
143
168
  "asOfClose",
@@ -334,6 +359,7 @@ const LIST_KEYS = {
334
359
  "sentimentBullishPct",
335
360
  "freshness",
336
361
  "indicators",
362
+ "indicatorContext",
337
363
  "fundamentals",
338
364
  ],
339
365
  futuresPositions: [
@@ -379,6 +405,19 @@ const LIST_KEYS = {
379
405
  universeMovers: ["symbol", "change24hPct", "priceUsd"],
380
406
  };
381
407
  const NESTED_KEYS = {
408
+ indicatorContext: [
409
+ "range",
410
+ "asOf",
411
+ "nominalIntervalSeconds",
412
+ "barCount",
413
+ "timestampedBarCount",
414
+ "checkedIntervalCount",
415
+ "irregularIntervalCount",
416
+ "maxGapSeconds",
417
+ "intervalStatus",
418
+ "recent15",
419
+ ],
420
+ recent15: ["barCount", "intervalStatus"],
382
421
  freshness: ["status", "ageSeconds", "asOf", "basis"],
383
422
  quality: [
384
423
  "decisionEligible",
@@ -432,6 +471,10 @@ function validRow(value, keys) {
432
471
  return sourceTimestamp(v) === v;
433
472
  if (key === "basis")
434
473
  return code(v, FRESHNESS_BASES) !== null;
474
+ if (key === "range")
475
+ return code(v, ["1D"]) !== null;
476
+ if (key === "intervalStatus")
477
+ return code(v, ["regular", "irregular", "unknown"]) !== null;
435
478
  if (key === "policyVersion")
436
479
  return typeof v === "string" && /^pm-quality-\d{1,3}$/.test(v);
437
480
  if (key === "warningReasons" || key === "blockReasons") {
@@ -17,6 +17,44 @@ const PM_MIN_FORECAST_EDGE_POINTS = (() => {
17
17
  return Number.isFinite(raw) && raw >= 0 ? raw : 2;
18
18
  })();
19
19
  const PM_MIN_STAKE_MUSD = 10; // server minimum prediction-market stake
20
+ // Match backend-v2 validateSlTpPrices, including its boundary tolerance. This
21
+ // preflight uses observed evidence only; the API still checks a fresh mark and
22
+ // locked position at execution. Older observations can omit these prices.
23
+ const TRIGGER_PRICE_EPS = 1e-8;
24
+ function validateTriggerUpdate(action, pos) {
25
+ // executeAction omits null as well as undefined; neither clears a trigger in
26
+ // this runner. Check the resulting state, including retained protection.
27
+ const sl = action.stopLossPrice ?? pos.stopLossPrice;
28
+ const tp = action.takeProfitPrice ?? pos.takeProfitPrice;
29
+ const positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
30
+ if (sl != null && !positive(sl))
31
+ return fail("invalid_stop_loss_price", `position ${pos.id}: stop loss must be finite and positive`);
32
+ if (tp != null && !positive(tp))
33
+ return fail("invalid_take_profit_price", `position ${pos.id}: take profit must be finite and positive`);
34
+ if (pos.side !== "long" && pos.side !== "short")
35
+ return ok();
36
+ const long = pos.side === "long";
37
+ const mark = pos.markPrice;
38
+ const liq = pos.liquidationPrice;
39
+ if (sl != null) {
40
+ if (positive(mark) &&
41
+ (long ? sl >= mark - TRIGGER_PRICE_EPS : sl <= mark + TRIGGER_PRICE_EPS)) {
42
+ return fail(long ? "stop_loss_not_below_mark" : "stop_loss_not_above_mark", `position ${pos.id}: ${pos.side} effective stop ${sl} must be ${long ? "below" : "above"} observed mark ${mark}; omitted triggers keep their existing values`);
43
+ }
44
+ if (positive(liq) &&
45
+ (long ? sl <= liq + TRIGGER_PRICE_EPS : sl >= liq - TRIGGER_PRICE_EPS)) {
46
+ return fail(long
47
+ ? "stop_loss_not_above_liquidation"
48
+ : "stop_loss_not_below_liquidation", `position ${pos.id}: ${pos.side} effective stop ${sl} must be ${long ? "above" : "below"} observed liquidation ${liq}; omitted triggers keep their existing values`);
49
+ }
50
+ }
51
+ if (tp != null &&
52
+ positive(mark) &&
53
+ (long ? tp <= mark + TRIGGER_PRICE_EPS : tp >= mark - TRIGGER_PRICE_EPS)) {
54
+ return fail(long ? "take_profit_not_above_mark" : "take_profit_not_below_mark", `position ${pos.id}: ${pos.side} effective take profit ${tp} must be ${long ? "above" : "below"} observed mark ${mark}; omitted triggers keep their existing values`);
55
+ }
56
+ return ok();
57
+ }
20
58
  // Entry budgets are exposure budgets, not emergency-action budgets. Closing a
21
59
  // futures position, updating its protection, cancelling an order, or selling
22
60
  // spot reduces/contains risk and must remain available after an entry cap.
@@ -219,6 +257,7 @@ export function validateAction(action, ctx) {
219
257
  if (!hasTrigger) {
220
258
  return fail("sltp_no_op", "futures_set_sltp must set at least one positive stopLossPrice or takeProfitPrice");
221
259
  }
260
+ return validateTriggerUpdate(action, pos);
222
261
  }
223
262
  return ok();
224
263
  }
@@ -316,6 +355,41 @@ export function validateAction(action, ctx) {
316
355
  if (!ctx.quote.freshness || ctx.quote.freshness.status !== "fresh") {
317
356
  return fail("stale_quote", `quote freshness ${ctx.quote.freshness?.status ?? "missing"} (need fresh)`);
318
357
  }
358
+ // Hard entry-price floor (opt-in, 2026-09-24): compares the CHOSEN
359
+ // outcome's raw market probability at entry (the quote's entryProbability,
360
+ // points) with risk.pmMinEntryProbabilityPct. Fees are deliberately left
361
+ // out: a 19-point outcome fails a 20-point floor even when fees push its
362
+ // all-in cost above 20 (fees belong to the edge check below). No
363
+ // discovery-price fallback: with a floor set and no finite quoted
364
+ // probability the open is rejected, never waved through.
365
+ const entryFloor = spec.risk.pmMinEntryProbabilityPct;
366
+ if (entryFloor !== undefined) {
367
+ // A present-but-invalid floor never silently means "no floor".
368
+ if (typeof entryFloor !== "number" ||
369
+ !Number.isFinite(entryFloor) ||
370
+ entryFloor < 0 ||
371
+ entryFloor > 100) {
372
+ return fail("pm_entry_floor_invalid", `risk.pmMinEntryProbabilityPct ${JSON.stringify(entryFloor)} is not a number between 0 and 100`);
373
+ }
374
+ const market = ctx.quote.entryProbability;
375
+ if (typeof market !== "number" ||
376
+ !Number.isFinite(market) ||
377
+ market < 0 ||
378
+ market > 100) {
379
+ return fail("pm_entry_price_unavailable", `entry floor ${entryFloor}pt is set but the quote carries no usable market probability for the chosen outcome (${JSON.stringify(market ?? null)})`);
380
+ }
381
+ if (market + 1e-9 < entryFloor) {
382
+ const stake = ctx.quote.stakeMusd;
383
+ const shares = ctx.quote.sharesEstimate;
384
+ const cost = typeof stake === "number" &&
385
+ typeof shares === "number" &&
386
+ stake > 0 &&
387
+ shares > 0
388
+ ? ` (fee-inclusive cost ${((stake / shares) * 100).toFixed(1)}pt)`
389
+ : "";
390
+ return fail("pm_entry_below_floor", `market ${market.toFixed(1)}pt is below the ${entryFloor}pt entry floor for the chosen outcome${cost}`);
391
+ }
392
+ }
319
393
  // Forecast consistency. By prompt contract forecastProbability is the
320
394
  // model's own probability (1-99) that the outcome IT IS BACKING wins, so
321
395
  // buying that outcome only makes sense when the forecast clears what the
@@ -0,0 +1,22 @@
1
+ import type { AgentSpec } from "./types.js";
2
+ /** Private, portable definition; deliberately excludes mutable account state. */
3
+ export interface AgentDefinitionSnapshot {
4
+ schema: "coinrithm.agent-definition.v1";
5
+ engine: {
6
+ packageVersion: string;
7
+ resolverVersion: string;
8
+ indicatorVersion: string;
9
+ apiContractVersion: string;
10
+ };
11
+ /** Pass the actual post-overlay spec, without rebuilding it from frontmatter. */
12
+ spec: AgentSpec;
13
+ /** Exact strategy prose supplied to runCycle, after any skills ablation. */
14
+ mergedProse: string;
15
+ definitionHash: string;
16
+ }
17
+ /**
18
+ * Binds the compiled definition and declared engine versions, not the original
19
+ * source folder alone. This is an integrity/comparison hash, not a signature,
20
+ * complete replay record, or attestation of a particular deployed binary.
21
+ */
22
+ export declare function buildAgentDefinitionSnapshot(spec: AgentSpec, mergedProse: string): AgentDefinitionSnapshot;
@@ -0,0 +1,28 @@
1
+ import { INDICATOR_VERSION } from "./indicators.js";
2
+ import { COINRITHM_API, RESOLVER_VERSION } from "./version.js";
3
+ import { sha256, stableStringify } from "./util.js";
4
+ /**
5
+ * Binds the compiled definition and declared engine versions, not the original
6
+ * source folder alone. This is an integrity/comparison hash, not a signature,
7
+ * complete replay record, or attestation of a particular deployed binary.
8
+ */
9
+ export function buildAgentDefinitionSnapshot(spec, mergedProse) {
10
+ const definition = {
11
+ schema: "coinrithm.agent-definition.v1",
12
+ engine: {
13
+ packageVersion: COINRITHM_API.mcpVersion,
14
+ resolverVersion: RESOLVER_VERSION,
15
+ indicatorVersion: INDICATOR_VERSION,
16
+ apiContractVersion: COINRITHM_API.openapiVersion,
17
+ },
18
+ spec,
19
+ mergedProse,
20
+ };
21
+ // Detach from caller-owned spec: later mutations must not invalidate a saved
22
+ // artifact while leaving its old hash attached to it.
23
+ const serialized = stableStringify(definition);
24
+ return {
25
+ ...JSON.parse(serialized),
26
+ definitionHash: sha256(serialized),
27
+ };
28
+ }
@@ -1,6 +1,7 @@
1
1
  export { runCycle, type RunnerDeps } from "./runner.js";
2
+ export { buildAgentDefinitionSnapshot, type AgentDefinitionSnapshot, } from "./definitionSnapshot.js";
2
3
  export { DECISION_INPUT_MAX_BYTES, sanitizeDecisionInputRecord, type DecisionInputRecord, } from "./decisionReceipt.js";
3
- export { selectProvider, providerForRoute, type ProviderEnv, type Provider, type DecideInput, type DecideResult, type DecideRouteAttempt, type DecideRouteMeta, } from "./providers.js";
4
+ export { selectProvider, providerForRoute, classifyProviderFailure, type ProviderEnv, type Provider, type DecideInput, type DecideResult, type DecideRouteAttempt, type DecideRouteMeta, } from "./providers.js";
4
5
  export { parseDecision } from "./decision.js";
5
6
  export { chatShapeFor, buildChatBody, type ChatShape, } from "./providerCapabilities.js";
6
7
  export { probeDecisionContract, type ProbeRoute, type ProbeDecisionResult, } from "./decisionProbe.js";
@@ -6,8 +6,9 @@
6
6
  // This barrel is the ONE import a host scheduler needs; it re-exports only the
7
7
  // stable engine pieces, never the CLI.
8
8
  export { runCycle } from "./runner.js";
9
+ export { buildAgentDefinitionSnapshot, } from "./definitionSnapshot.js";
9
10
  export { DECISION_INPUT_MAX_BYTES, sanitizeDecisionInputRecord, } from "./decisionReceipt.js";
10
- export { selectProvider, providerForRoute, } from "./providers.js";
11
+ export { selectProvider, providerForRoute, classifyProviderFailure, } from "./providers.js";
11
12
  export { parseDecision } from "./decision.js";
12
13
  // Reliability slice A: the declarative request-capability table and the
13
14
  // representative decision probe (route eligibility = a REAL parsed decision,
@@ -1,5 +1,5 @@
1
1
  import { ResolveIssue } from "./types.js";
2
- export type CapDirection = "lower" | "higher" | "true";
2
+ export type CapDirection = "lower" | "lower_zero_off" | "higher" | "true";
3
3
  export declare const RISK_CAPS: Record<string, CapDirection>;
4
4
  export declare const LIMIT_CAPS: Record<string, CapDirection>;
5
5
  export declare function isAtLeastAsRestrictive(dir: CapDirection, candidate: unknown, base: unknown): boolean;
@@ -8,6 +8,9 @@
8
8
  //
9
9
  // Normal (non-cap) config uses simple precedence (inline > $ref > defaults),
10
10
  // handled in the resolver; this module owns only the cap arithmetic.
11
+ // 0 is "no cap" for lower_zero_off; every other value compares as itself.
12
+ const zeroOffLimit = (n) => (n === 0 ? Infinity : n);
13
+ const isZeroOffValue = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
11
14
  // risk.* hard caps a tactic module may tighten.
12
15
  export const RISK_CAPS = {
13
16
  maxLeverage: "lower",
@@ -17,9 +20,9 @@ export const RISK_CAPS = {
17
20
  };
18
21
  // limits.* throughput/spend caps a tactic module may tighten.
19
22
  export const LIMIT_CAPS = {
20
- maxTradesPerDay: "lower",
23
+ maxTradesPerDay: "lower_zero_off",
21
24
  maxWritesPerCycle: "lower",
22
- maxDailyLossMusd: "lower",
25
+ maxDailyLossMusd: "lower_zero_off",
23
26
  maxOpenMarginMusd: "lower",
24
27
  };
25
28
  // Is `candidate` at least as restrictive as `base` (i.e. a legal tightening)?
@@ -29,6 +32,11 @@ export function isAtLeastAsRestrictive(dir, candidate, base) {
29
32
  // legal: base=false→candidate any; base=true→candidate must be true.
30
33
  return base === true ? candidate === true : true;
31
34
  }
35
+ if (dir === "lower_zero_off") {
36
+ if (!isZeroOffValue(candidate) || !isZeroOffValue(base))
37
+ return false;
38
+ return zeroOffLimit(candidate) <= zeroOffLimit(base);
39
+ }
32
40
  if (typeof candidate !== "number" || typeof base !== "number") {
33
41
  // a non-numeric cap candidate where a number is expected is not a legal
34
42
  // tightening (caller will surface a type issue separately).
@@ -40,6 +48,13 @@ export function isAtLeastAsRestrictive(dir, candidate, base) {
40
48
  export function mostRestrictive(dir, a, b) {
41
49
  if (dir === "true")
42
50
  return a === true || b === true ? true : false;
51
+ if (dir === "lower_zero_off") {
52
+ if (!isZeroOffValue(a))
53
+ return b;
54
+ if (!isZeroOffValue(b))
55
+ return a;
56
+ return zeroOffLimit(a) <= zeroOffLimit(b) ? a : b;
57
+ }
43
58
  if (typeof a !== "number")
44
59
  return b;
45
60
  if (typeof b !== "number")