@coinrithm/mcp-trading 0.2.0 → 0.4.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/dist/agent/act.d.ts +4 -0
  4. package/dist/agent/act.js +58 -9
  5. package/dist/agent/capabilityGuard.d.ts +2 -0
  6. package/dist/agent/capabilityGuard.js +131 -0
  7. package/dist/agent/cli.d.ts +21 -0
  8. package/dist/agent/cli.js +28 -6
  9. package/dist/agent/client.d.ts +107 -0
  10. package/dist/agent/client.js +26 -0
  11. package/dist/agent/decision.d.ts +137 -0
  12. package/dist/agent/decision.js +37 -4
  13. package/dist/agent/decisionValidator.d.ts +16 -0
  14. package/dist/agent/decisionValidator.js +118 -10
  15. package/dist/agent/engine.d.ts +10 -0
  16. package/dist/agent/engine.js +16 -0
  17. package/dist/agent/extract.d.ts +4 -0
  18. package/dist/agent/frontmatter.d.ts +5 -0
  19. package/dist/agent/index.d.ts +2 -0
  20. package/dist/agent/indicators.d.ts +44 -0
  21. package/dist/agent/indicators.js +135 -0
  22. package/dist/agent/manifest.d.ts +15 -0
  23. package/dist/agent/mergeRules.d.ts +11 -0
  24. package/dist/agent/observe.d.ts +7 -0
  25. package/dist/agent/observe.js +142 -8
  26. package/dist/agent/prompt.d.ts +3 -0
  27. package/dist/agent/prompt.js +39 -8
  28. package/dist/agent/providers.d.ts +25 -0
  29. package/dist/agent/providers.js +10 -1
  30. package/dist/agent/resolve.d.ts +11 -0
  31. package/dist/agent/resolve.js +40 -3
  32. package/dist/agent/runEvidence.d.ts +6 -0
  33. package/dist/agent/runner.d.ts +19 -0
  34. package/dist/agent/runner.js +102 -19
  35. package/dist/agent/scorecard.d.ts +24 -0
  36. package/dist/agent/scorecard.js +177 -0
  37. package/dist/agent/skill.d.ts +12 -0
  38. package/dist/agent/skill.js +6 -2
  39. package/dist/agent/skillValidator.d.ts +7 -0
  40. package/dist/agent/state.d.ts +7 -0
  41. package/dist/agent/state.js +3 -1
  42. package/dist/agent/strictLint.d.ts +3 -0
  43. package/dist/agent/strictLint.js +2 -1
  44. package/dist/agent/templates.d.ts +14 -0
  45. package/dist/agent/types.d.ts +286 -0
  46. package/dist/agent/types.js +39 -1
  47. package/dist/agent/util.d.ts +13 -0
  48. package/dist/agent/util.js +4 -0
  49. package/dist/agent/version.d.ts +11 -0
  50. package/dist/agent/version.js +1 -1
  51. package/dist/client.d.ts +162 -0
  52. package/dist/http.d.ts +2 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/tools.d.ts +3 -0
  55. package/dist/version.d.ts +1 -0
  56. package/package.json +7 -2
@@ -1,14 +1,15 @@
1
- // The execution loop: observe -> decide (BYO model) -> validate -> act. v1
2
- // futures only. Dry-run never writes. Live uses idempotency keys + agentTrace
3
- // and exports run evidence. The client + provider are injected so the loop is
4
- // fully unit-testable with no network and no model calls.
1
+ // The execution loop: observe -> decide (BYO model) -> validate -> act across
2
+ // spot, futures, and prediction markets. Dry-run never writes. Live uses
3
+ // idempotency keys + agentTrace and exports run evidence. The client + provider
4
+ // are injected so the loop is fully unit-testable with no network/model calls.
5
+ import { spotBuyCost, } from "./types.js";
5
6
  import { observe } from "./observe.js";
6
7
  import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
7
8
  import { parseDecision } from "./decision.js";
8
9
  import { validateAction } from "./decisionValidator.js";
9
10
  import { fetchQuote, executeAction } from "./act.js";
10
11
  import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
11
- import { rollDay, checkKillSwitch, accrueRealized, saveState } from "./state.js";
12
+ import { rollDay, checkKillSwitch, accrueRealized, saveState, } from "./state.js";
12
13
  import { asObj, asNum, asStr } from "./extract.js";
13
14
  import { parseCadenceMs, sleep } from "./util.js";
14
15
  // A stable idempotency-key component per distinct intent (so a lost response
@@ -23,7 +24,33 @@ function intentKeyOf(action) {
23
24
  if (action.type === "futures_set_sltp") {
24
25
  return `sltp:${action.positionId}`;
25
26
  }
26
- return `other:${action.type}`; // unreachable: only futures actions are executed
27
+ if (action.type === "spot_order") {
28
+ return `spot:${action.symbol.toUpperCase()}:${action.side}:${action.orderType}:${action.quantity}:${action.limitPrice ?? ""}:${action.stopPrice ?? ""}`;
29
+ }
30
+ if (action.type === "spot_cancel") {
31
+ return `cancel:${action.orderId}`;
32
+ }
33
+ if (action.type === "pm_open") {
34
+ return `pm:${action.source.toLowerCase()}:${action.slug.toLowerCase()}:${action.outcomeExternalMarketId}:${action.stakeMusd}`;
35
+ }
36
+ return "other"; // unreachable: every action type is handled above
37
+ }
38
+ // Estimated cash a successful action consumes (for the running-cash guard):
39
+ // futures margin, spot buy notional, or a PM stake. Closes/cancels/sells free
40
+ // cash or are neutral, so they consume nothing here. Spot buys use the SAME
41
+ // `spotBuyCost` helper the validator gates on, so the gate and this decrement
42
+ // never diverge. The `?? 0` is unreachable for an EXECUTED buy: the validator
43
+ // fails closed (missing_quote_price) on any buy whose cost can't be sized, so
44
+ // nothing with an undefined cost ever reaches execution to be decremented.
45
+ function cashConsumed(action, quote) {
46
+ if (action.type === "futures_open")
47
+ return action.marginMusd;
48
+ if (action.type === "pm_open")
49
+ return action.stakeMusd;
50
+ if (action.type === "spot_order" && action.side === "buy") {
51
+ return spotBuyCost(action, quote) ?? 0;
52
+ }
53
+ return 0;
27
54
  }
28
55
  export async function runCycle(deps) {
29
56
  const { client, provider, spec, mergedProse, state, live, stateFile } = deps;
@@ -37,7 +64,13 @@ export async function runCycle(deps) {
37
64
  state.disabledReason = tripped;
38
65
  saveState(stateFile, state);
39
66
  log(`disabled: ${tripped}`);
40
- return { decision: "skip", planned: [], disabled: true, disabledReason: tripped, live };
67
+ return {
68
+ decision: "skip",
69
+ planned: [],
70
+ disabled: true,
71
+ disabledReason: tripped,
72
+ live,
73
+ };
41
74
  }
42
75
  const runId = state.runId;
43
76
  const decisionId = makeDecisionId(state.cyclesRun);
@@ -55,12 +88,19 @@ export async function runCycle(deps) {
55
88
  // not only realized losses.
56
89
  const unrealized = observation.openPositions.reduce((s, p) => s + (p.unrealizedPnlMusd ?? 0), 0);
57
90
  if (spec.killSwitch.maxDrawdownMusd > 0 &&
58
- state.peakRealizedMusd - (state.realizedPnlMusd + unrealized) >= spec.killSwitch.maxDrawdownMusd) {
91
+ state.peakRealizedMusd - (state.realizedPnlMusd + unrealized) >=
92
+ spec.killSwitch.maxDrawdownMusd) {
59
93
  state.disabled = true;
60
94
  state.disabledReason = `equity drawdown >= ${spec.killSwitch.maxDrawdownMusd}`;
61
95
  saveState(stateFile, state);
62
96
  log(`disabled: ${state.disabledReason}`);
63
- return { decision: "skip", planned: [], disabled: true, disabledReason: state.disabledReason, live };
97
+ return {
98
+ decision: "skip",
99
+ planned: [],
100
+ disabled: true,
101
+ disabledReason: state.disabledReason,
102
+ live,
103
+ };
64
104
  }
65
105
  if (obs.skip) {
66
106
  state.consecutiveRejectCycles += 1;
@@ -77,14 +117,26 @@ export async function runCycle(deps) {
77
117
  state.consecutiveModelFailures += 1;
78
118
  saveState(stateFile, state);
79
119
  log(`model error: ${res.error}`);
80
- return { decision: "skip", skipReason: `model error: ${res.error}`, planned: [], modelFailed: true, live };
120
+ return {
121
+ decision: "skip",
122
+ skipReason: `model error: ${res.error}`,
123
+ planned: [],
124
+ modelFailed: true,
125
+ live,
126
+ };
81
127
  }
82
128
  const parsed = parseDecision(res.text);
83
129
  if (!parsed.ok) {
84
130
  state.consecutiveModelFailures += 1;
85
131
  saveState(stateFile, state);
86
132
  log(`model output invalid: ${parsed.error}`);
87
- return { decision: "skip", skipReason: `model output invalid: ${parsed.error}`, planned: [], modelFailed: true, live };
133
+ return {
134
+ decision: "skip",
135
+ skipReason: `model output invalid: ${parsed.error}`,
136
+ planned: [],
137
+ modelFailed: true,
138
+ live,
139
+ };
88
140
  }
89
141
  state.consecutiveModelFailures = 0;
90
142
  const decision = parsed.decision;
@@ -92,7 +144,12 @@ export async function runCycle(deps) {
92
144
  state.consecutiveRejectCycles += 1;
93
145
  saveState(stateFile, state);
94
146
  log(`model chose skip${decision.reason ? `: ${decision.reason}` : ""}`);
95
- return { decision: "skip", skipReason: decision.reason ?? "model chose skip", planned: [], live };
147
+ return {
148
+ decision: "skip",
149
+ skipReason: decision.reason ?? "model chose skip",
150
+ planned: [],
151
+ live,
152
+ };
96
153
  }
97
154
  // VALIDATE (+ ACT when live). Quote evidence is fetched by the runner.
98
155
  const planned = [];
@@ -105,6 +162,7 @@ export async function runCycle(deps) {
105
162
  let cashAvailableMusd = observation.cashAvailableMusd;
106
163
  const realizedLossTodayMusd = Math.max(0, -state.realizedPnlTodayMusd);
107
164
  const targetedPositionIds = [];
165
+ const targetedOrderIds = [];
108
166
  let anyAccepted = false;
109
167
  let anyExecuted = false;
110
168
  let anyExecFailed = false;
@@ -112,6 +170,10 @@ export async function runCycle(deps) {
112
170
  const quote = await fetchQuote(client, action, observation, baseTrace);
113
171
  const ctx = {
114
172
  spec,
173
+ // Inherit the decision-level confidence so the per-action abstention gate
174
+ // doesn't reject a model that reports conviction on the decision (the
175
+ // output contract) rather than on each action.
176
+ decisionConfidence: decision.confidence,
115
177
  observation,
116
178
  quote,
117
179
  writesThisCycle,
@@ -121,10 +183,17 @@ export async function runCycle(deps) {
121
183
  openMarginMusd,
122
184
  realizedLossTodayMusd,
123
185
  targetedPositionIds,
186
+ targetedOrderIds,
124
187
  };
125
188
  const v = validateAction(action, ctx);
126
189
  if (!v.valid) {
127
- planned.push({ action, accepted: false, code: v.code, reason: v.reason, quote });
190
+ planned.push({
191
+ action,
192
+ accepted: false,
193
+ code: v.code,
194
+ reason: v.reason,
195
+ quote,
196
+ });
128
197
  log(`reject ${action.type}: ${v.code} (${v.reason})`);
129
198
  continue;
130
199
  }
@@ -132,6 +201,9 @@ export async function runCycle(deps) {
132
201
  if (action.type === "futures_close" || action.type === "futures_set_sltp") {
133
202
  targetedPositionIds.push(action.positionId);
134
203
  }
204
+ if (action.type === "spot_cancel") {
205
+ targetedOrderIds.push(action.orderId);
206
+ }
135
207
  if (!live) {
136
208
  planned.push({ action, accepted: true, quote, executed: false });
137
209
  log(`DRY-RUN: would ${action.type}`);
@@ -143,9 +215,15 @@ export async function runCycle(deps) {
143
215
  const seq = state.intentSeq[intentKey] ?? 0;
144
216
  const idem = `${runId}:${intentKey}:${seq}`;
145
217
  const meta = action;
146
- const trace = makeTrace(runId, decisionId, spec, meta.confidence, meta.rationaleSummary);
218
+ const trace = makeTrace(runId, decisionId, spec, meta.confidence ?? decision.confidence, meta.rationaleSummary);
147
219
  const r = await executeAction(client, action, observation, trace, idem);
148
- planned.push({ action, accepted: true, quote, executed: r.ok, result: r.data });
220
+ planned.push({
221
+ action,
222
+ accepted: true,
223
+ quote,
224
+ executed: r.ok,
225
+ result: r.data,
226
+ });
149
227
  if (r.ok) {
150
228
  anyExecuted = true;
151
229
  state.intentSeq[intentKey] = seq + 1;
@@ -154,9 +232,11 @@ export async function runCycle(deps) {
154
232
  if (action.type === "futures_open") {
155
233
  openCount += 1;
156
234
  openMarginMusd += action.marginMusd;
157
- if (cashAvailableMusd != null)
158
- cashAvailableMusd -= action.marginMusd;
159
235
  }
236
+ // Decrement running cash by what this action consumed (futures margin /
237
+ // spot buy notional / PM stake) so a later action this cycle sees it spent.
238
+ if (cashAvailableMusd != null)
239
+ cashAvailableMusd -= cashConsumed(action, quote);
160
240
  }
161
241
  else {
162
242
  anyExecFailed = true;
@@ -167,8 +247,11 @@ export async function runCycle(deps) {
167
247
  // live write is not progress, or a persistently failing live agent would
168
248
  // never trip the kill-switch.
169
249
  const progressed = live ? anyExecuted : anyAccepted;
170
- state.consecutiveRejectCycles = progressed ? 0 : state.consecutiveRejectCycles + 1;
171
- state.consecutiveExecFailures = anyExecFailed && !anyExecuted ? state.consecutiveExecFailures + 1 : 0;
250
+ state.consecutiveRejectCycles = progressed
251
+ ? 0
252
+ : state.consecutiveRejectCycles + 1;
253
+ state.consecutiveExecFailures =
254
+ anyExecFailed && !anyExecuted ? state.consecutiveExecFailures + 1 : 0;
172
255
  state.rateLimitHits = client.rateLimitHits ?? state.rateLimitHits;
173
256
  saveState(stateFile, state);
174
257
  if (live && anyExecuted)
@@ -0,0 +1,24 @@
1
+ export interface ScorecardInput {
2
+ realizedPnls: number[];
3
+ cumulative?: number[];
4
+ returns?: number[];
5
+ annualizationFactor?: number;
6
+ trials?: number;
7
+ predictions?: Array<{
8
+ p: number;
9
+ outcome: 0 | 1;
10
+ }>;
11
+ gates?: {
12
+ stopCoverage?: number;
13
+ evidenceCoverage?: number;
14
+ leakageClean?: boolean;
15
+ };
16
+ }
17
+ export interface Scorecard {
18
+ schema: "coinrithm.agent.scorecard.v1";
19
+ sampleSize: number;
20
+ returnsBasis: "returns" | "realized_pnl";
21
+ metrics: Record<string, number | null>;
22
+ contentHash: string;
23
+ }
24
+ export declare function computeScorecard(input: ScorecardInput): Scorecard;
@@ -0,0 +1,177 @@
1
+ // Deterministic scorecard engine — pure math over an agent's realized track
2
+ // record. The reproducible-evaluation half of coinrithm.agent.scorecard.v1
3
+ // (see examples/agents/_shared/scorecard.metrics.yaml + DECISIONS D17).
4
+ //
5
+ // DETERMINISM CONTRACT: the same inputs always yield the same metrics AND the
6
+ // same contentHash (sha256 of the canonicalized result), mirroring
7
+ // meta/manifest.lock.json. The engine NEVER calls the network or the model — it
8
+ // reads the run-evidence ledger export + realized equity curve (fetched by the
9
+ // caller) and computes, so tuning-to-the-metric is structurally impossible
10
+ // (leakage separation, arXiv 2512.02227). Every function returns null when there
11
+ // is too little data, so a thin record reports "n/a" rather than a fake number.
12
+ //
13
+ // SCIENTIFIC BASIS: risk-adjusted ratios (Sharpe/Sortino), skill-vs-luck
14
+ // deflation (probabilistic + deflated Sharpe, Bailey & Lopez de Prado), and
15
+ // calibration (Brier/ECE) for probabilistic calls — the reproducible-evaluation
16
+ // layer the field lacks (arXiv 2605.19337).
17
+ import { createHash } from "node:crypto";
18
+ const round = (n, d = 6) => {
19
+ const f = 10 ** d;
20
+ return Math.round(n * f) / f;
21
+ };
22
+ const sum = (xs) => xs.reduce((a, b) => a + b, 0);
23
+ const mean = (xs) => (xs.length ? sum(xs) / xs.length : 0);
24
+ // Sample standard deviation (n-1). null for < 2 points (undefined dispersion).
25
+ function sampleStd(xs) {
26
+ if (xs.length < 2)
27
+ return null;
28
+ const m = mean(xs);
29
+ const v = sum(xs.map((x) => (x - m) ** 2)) / (xs.length - 1);
30
+ return Math.sqrt(v);
31
+ }
32
+ // Downside deviation about a 0 minimum-acceptable-return (Sortino denominator).
33
+ function downsideDev(xs) {
34
+ if (xs.length < 2)
35
+ return null;
36
+ const sq = xs.map((x) => (x < 0 ? x * x : 0));
37
+ return Math.sqrt(sum(sq) / xs.length);
38
+ }
39
+ // Population moments used by the (probabilistic) Sharpe formula.
40
+ function moment(xs, k) {
41
+ const m = mean(xs);
42
+ return sum(xs.map((x) => (x - m) ** k)) / xs.length;
43
+ }
44
+ // Standard normal CDF via an Abramowitz & Stegun erf approximation (max err ~1e-7).
45
+ function normalCdf(z) {
46
+ const t = 1 / (1 + 0.2316419 * Math.abs(z));
47
+ const d = 0.3989422804014327 * Math.exp(-(z * z) / 2);
48
+ const p = d *
49
+ t *
50
+ (0.31938153 +
51
+ t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
52
+ return z >= 0 ? 1 - p : p;
53
+ }
54
+ // Max peak-to-trough drawdown (mUSD, >= 0) on a cumulative series.
55
+ function maxDrawdown(cumulative) {
56
+ let peak = -Infinity;
57
+ let maxDd = 0;
58
+ for (const c of cumulative) {
59
+ if (!Number.isFinite(c))
60
+ continue;
61
+ peak = Math.max(peak, c);
62
+ maxDd = Math.max(maxDd, peak - c);
63
+ }
64
+ return Number.isFinite(maxDd) ? maxDd : 0;
65
+ }
66
+ // Per-observation Sharpe (mean/std), the basis for the probabilistic SR test.
67
+ function rawSharpe(rs) {
68
+ const sd = sampleStd(rs);
69
+ if (sd == null || sd === 0)
70
+ return null;
71
+ return mean(rs) / sd;
72
+ }
73
+ // Probabilistic / deflated Sharpe (Bailey & Lopez de Prado, approximated).
74
+ // PSR(SR0) = Phi( (SR - SR0) * sqrt(n-1) / sqrt(1 - skew*SR + ((kurt-1)/4)*SR^2) ).
75
+ // Deflation: SR0 = sqrt(2*ln(trials)) / sqrt(n) — the expected max per-obs Sharpe
76
+ // of `trials` random strategies (extreme-value heuristic). trials=1 -> SR0=0, so
77
+ // it reduces to the probabilistic Sharpe (already penalizing short, skewed tracks).
78
+ function deflatedSharpe(rs, trials) {
79
+ const n = rs.length;
80
+ if (n < 3)
81
+ return null;
82
+ const sr = rawSharpe(rs);
83
+ if (sr == null)
84
+ return null;
85
+ const m2 = moment(rs, 2);
86
+ if (m2 === 0)
87
+ return null;
88
+ const skew = moment(rs, 3) / m2 ** 1.5;
89
+ const kurt = moment(rs, 4) / m2 ** 2; // 3 for a normal distribution
90
+ const denom = Math.sqrt(Math.max(1e-9, 1 - skew * sr + ((kurt - 1) / 4) * sr * sr));
91
+ const sr0 = Math.sqrt(2 * Math.log(Math.max(1, trials))) / Math.sqrt(n);
92
+ const z = ((sr - sr0) * Math.sqrt(n - 1)) / denom;
93
+ return normalCdf(z);
94
+ }
95
+ // Brier score: mean((p - outcome)^2). Lower = better-calibrated.
96
+ function brier(preds) {
97
+ if (preds.length === 0)
98
+ return null;
99
+ return mean(preds.map((x) => (x.p - x.outcome) ** 2));
100
+ }
101
+ // Expected calibration error over 10 equal-width probability buckets.
102
+ function ece(preds) {
103
+ if (preds.length === 0)
104
+ return null;
105
+ const buckets = 10;
106
+ let total = 0;
107
+ for (let b = 0; b < buckets; b += 1) {
108
+ const lo = b / buckets;
109
+ const hi = (b + 1) / buckets;
110
+ const inB = preds.filter((x) => (b === buckets - 1 ? x.p >= lo && x.p <= hi : x.p >= lo && x.p < hi));
111
+ if (inB.length === 0)
112
+ continue;
113
+ const avgP = mean(inB.map((x) => x.p));
114
+ const avgO = mean(inB.map((x) => x.outcome));
115
+ total += (inB.length / preds.length) * Math.abs(avgP - avgO);
116
+ }
117
+ return total;
118
+ }
119
+ export function computeScorecard(input) {
120
+ const pnls = input.realizedPnls.filter(Number.isFinite);
121
+ const n = pnls.length;
122
+ const wins = pnls.filter((x) => x > 0);
123
+ const losses = pnls.filter((x) => x < 0);
124
+ const decided = wins.length + losses.length;
125
+ const grossWin = sum(wins);
126
+ const grossLoss = Math.abs(sum(losses));
127
+ const avgWin = wins.length ? grossWin / wins.length : 0;
128
+ const avgLoss = losses.length ? grossLoss / losses.length : 0;
129
+ const pWin = decided ? wins.length / decided : null;
130
+ const rs = input.returns && input.returns.length ? input.returns.filter(Number.isFinite) : pnls;
131
+ const returnsBasis = input.returns && input.returns.length ? "returns" : "realized_pnl";
132
+ const ann = input.annualizationFactor ?? 1;
133
+ const cumulative = input.cumulative && input.cumulative.length
134
+ ? input.cumulative
135
+ : pnls.reduce((acc, x) => {
136
+ acc.push((acc.length ? acc[acc.length - 1] : 0) + x);
137
+ return acc;
138
+ }, []);
139
+ const sd = sampleStd(rs);
140
+ const dd = downsideDev(rs);
141
+ const sharpe = sd && sd !== 0 ? (mean(rs) / sd) * ann : null;
142
+ const sortino = dd && dd !== 0 ? (mean(rs) / dd) * ann : null;
143
+ const metrics = {
144
+ realized_pnl_musd: round(sum(pnls)),
145
+ trade_count: n,
146
+ decided_count: decided,
147
+ win_rate: pWin == null ? null : round(pWin),
148
+ expectancy_musd: pWin == null ? null : round(pWin * avgWin - (1 - pWin) * avgLoss),
149
+ profit_factor: grossLoss > 0 ? round(grossWin / grossLoss) : grossWin > 0 ? null : 0, // null = ∞ (no losses)
150
+ reward_to_risk: avgLoss > 0 ? round(avgWin / avgLoss) : null,
151
+ sharpe: sharpe == null ? null : round(sharpe),
152
+ sortino: sortino == null ? null : round(sortino),
153
+ deflated_sharpe: round0(deflatedSharpe(rs, input.trials ?? 1)),
154
+ max_drawdown_musd: round(maxDrawdown(cumulative)),
155
+ brier_score: round0(brier(input.predictions ?? [])),
156
+ calibration_error: round0(ece(input.predictions ?? [])),
157
+ stop_coverage: input.gates?.stopCoverage ?? null,
158
+ evidence_coverage: input.gates?.evidenceCoverage ?? null,
159
+ leakage_clean: input.gates?.leakageClean == null ? null : input.gates.leakageClean ? 1 : 0,
160
+ };
161
+ // Canonicalize (sorted keys) and hash, so the report card carries a stable,
162
+ // verifiable fingerprint — a scorecard whose hash does not reproduce is not trusted.
163
+ const canonical = JSON.stringify(Object.keys(metrics)
164
+ .sort()
165
+ .map((k) => [k, metrics[k]]));
166
+ const contentHash = createHash("sha256").update(canonical).digest("hex");
167
+ return {
168
+ schema: "coinrithm.agent.scorecard.v1",
169
+ sampleSize: n,
170
+ returnsBasis,
171
+ metrics,
172
+ contentHash,
173
+ };
174
+ }
175
+ function round0(n) {
176
+ return n == null ? null : round(n);
177
+ }
@@ -0,0 +1,12 @@
1
+ import { AgentSpec, ParsedSkill, ResolvedAgent, ResolveIssue } from "./types.js";
2
+ export declare function buildSpec(raw: Record<string, unknown>): AgentSpec;
3
+ export declare function parseSkill(text: string): ParsedSkill;
4
+ export declare function loadSkill(path: string): ParsedSkill;
5
+ export interface LoadedAgent {
6
+ resolved: ResolvedAgent;
7
+ spec: AgentSpec;
8
+ body: string;
9
+ raw: Record<string, unknown>;
10
+ lint: ResolveIssue[];
11
+ }
12
+ export declare function loadAgent(inputPath: string, mode?: "self-host" | "hosted"): LoadedAgent;
@@ -3,6 +3,7 @@ import { parseFrontmatter } from "./frontmatter.js";
3
3
  import { VENUES, PROVIDERS, ALLOWED_CAPABILITIES, } from "./types.js";
4
4
  import { resolveAgent, ResolveError } from "./resolve.js";
5
5
  import { strictLint } from "./strictLint.js";
6
+ import { checkCapabilityDrift } from "./capabilityGuard.js";
6
7
  // Safe defaults for the OPTIONAL policy blocks. A minimal self-host skill
7
8
  // (name/description/spec/trigger/model/venues/risk) runs under these. Hosted
8
9
  // mode requires them to be explicit (see skillValidator).
@@ -83,6 +84,7 @@ export function buildSpec(raw) {
83
84
  maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
84
85
  requireStopLoss: bool(risk.requireStopLoss, true),
85
86
  watchlist: strArr(risk.watchlist),
87
+ blocklist: strArr(risk.blocklist),
86
88
  },
87
89
  limits: {
88
90
  maxTradesPerDay: num(limits.maxTradesPerDay, DEFAULT_LIMITS.maxTradesPerDay),
@@ -124,9 +126,11 @@ export function loadSkill(path) {
124
126
  export function loadAgent(inputPath, mode = "self-host") {
125
127
  const resolved = resolveAgent(inputPath);
126
128
  const raw = resolved.rawFrontmatter;
127
- const lint = strictLint(raw);
129
+ const spec = buildSpec(raw);
130
+ // strictLint = frontmatter keys/enums; capability drift = prose references to
131
+ // venues/actions/caps the runner (or this agent's venues) does not support.
132
+ const lint = [...strictLint(raw), ...checkCapabilityDrift(resolved, spec)];
128
133
  if (mode === "hosted" && lint.length)
129
134
  throw new ResolveError(lint);
130
- const spec = buildSpec(raw);
131
135
  return { resolved, spec, body: resolved.mergedProse, raw, lint };
132
136
  }
@@ -0,0 +1,7 @@
1
+ import { ParsedSkill, ValidationResult } from "./types.js";
2
+ export type SkillValidationMode = "self-host" | "hosted";
3
+ export interface SkillValidation {
4
+ valid: boolean;
5
+ issues: ValidationResult[];
6
+ }
7
+ export declare function validateSkill(parsed: ParsedSkill, mode?: SkillValidationMode): SkillValidation;
@@ -0,0 +1,7 @@
1
+ import { RunState, AgentSpec } from "./types.js";
2
+ export declare function newState(runId: string): RunState;
3
+ export declare function loadState(file: string | undefined, runId: string): RunState;
4
+ export declare function saveState(file: string | undefined, state: RunState): void;
5
+ export declare function rollDay(state: RunState): RunState;
6
+ export declare function accrueRealized(state: RunState, closedTrades: Record<string, unknown>[]): void;
7
+ export declare function checkKillSwitch(spec: AgentSpec, state: RunState): string | null;
@@ -42,7 +42,9 @@ export function loadState(file, runId) {
42
42
  ...base,
43
43
  ...parsed,
44
44
  seen: Array.isArray(parsed.seen) ? parsed.seen : [],
45
- intentSeq: parsed.intentSeq && typeof parsed.intentSeq === "object" ? parsed.intentSeq : {},
45
+ intentSeq: parsed.intentSeq && typeof parsed.intentSeq === "object" && !Array.isArray(parsed.intentSeq)
46
+ ? parsed.intentSeq
47
+ : {},
46
48
  });
47
49
  }
48
50
  return newState(runId);
@@ -0,0 +1,3 @@
1
+ import { ResolveIssue } from "./types.js";
2
+ export declare function levenshtein(a: string, b: string): number;
3
+ export declare function strictLint(raw: Record<string, unknown>): ResolveIssue[];
@@ -34,6 +34,7 @@ const ALLOWED_KEYS = {
34
34
  "maxConcurrentPositions",
35
35
  "requireStopLoss",
36
36
  "watchlist",
37
+ "blocklist",
37
38
  ],
38
39
  sizing: null,
39
40
  limits: [
@@ -58,7 +59,7 @@ const ALLOWED_KEYS = {
58
59
  ],
59
60
  objective: ["primary", "secondary", "horizon"],
60
61
  };
61
- function levenshtein(a, b) {
62
+ export function levenshtein(a, b) {
62
63
  const m = a.length;
63
64
  const n = b.length;
64
65
  const d = Array.from({ length: n + 1 }, (_, i) => i);
@@ -0,0 +1,14 @@
1
+ export type PresetName = "conservative" | "balanced" | "bold";
2
+ export declare const PRESET_NAMES: PresetName[];
3
+ export interface AgentTemplate {
4
+ frontmatter: Record<string, unknown>;
5
+ body: string;
6
+ }
7
+ export declare function buildAgentObject(name: string, preset: PresetName): AgentTemplate;
8
+ export declare function renderFolderOfOne(name: string, preset: PresetName): string;
9
+ export declare function renderCoinrithmPin(): string;
10
+ export declare function renderRuntime(model: unknown, trigger: unknown): string;
11
+ export interface EjectResult {
12
+ files: Record<string, string>;
13
+ }
14
+ export declare function ejectFiles(fm: Record<string, unknown>, body: string): EjectResult;