@coinrithm/mcp-trading 0.1.8 → 0.3.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.
- package/CHANGELOG.md +32 -0
- package/README.md +43 -19
- package/dist/agent/act.d.ts +4 -0
- package/dist/agent/act.js +114 -0
- package/dist/agent/capabilityGuard.d.ts +2 -0
- package/dist/agent/capabilityGuard.js +131 -0
- package/dist/agent/cli.d.ts +21 -0
- package/dist/agent/cli.js +382 -0
- package/dist/agent/client.d.ts +107 -0
- package/dist/agent/client.js +173 -0
- package/dist/agent/decision.d.ts +137 -0
- package/dist/agent/decision.js +118 -0
- package/dist/agent/decisionValidator.d.ts +16 -0
- package/dist/agent/decisionValidator.js +215 -0
- package/dist/agent/engine.d.ts +10 -0
- package/dist/agent/engine.js +16 -0
- package/dist/agent/extract.d.ts +4 -0
- package/dist/agent/extract.js +5 -0
- package/dist/agent/frontmatter.d.ts +5 -0
- package/dist/agent/frontmatter.js +19 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +10 -0
- package/dist/agent/indicators.d.ts +44 -0
- package/dist/agent/indicators.js +135 -0
- package/dist/agent/manifest.d.ts +15 -0
- package/dist/agent/manifest.js +40 -0
- package/dist/agent/mergeRules.d.ts +11 -0
- package/dist/agent/mergeRules.js +82 -0
- package/dist/agent/observe.d.ts +7 -0
- package/dist/agent/observe.js +244 -0
- package/dist/agent/prompt.d.ts +3 -0
- package/dist/agent/prompt.js +76 -0
- package/dist/agent/providers.d.ts +25 -0
- package/dist/agent/providers.js +143 -0
- package/dist/agent/resolve.d.ts +11 -0
- package/dist/agent/resolve.js +499 -0
- package/dist/agent/runEvidence.d.ts +6 -0
- package/dist/agent/runEvidence.js +23 -0
- package/dist/agent/runner.d.ts +19 -0
- package/dist/agent/runner.js +280 -0
- package/dist/agent/skill.d.ts +12 -0
- package/dist/agent/skill.js +136 -0
- package/dist/agent/skillValidator.d.ts +7 -0
- package/dist/agent/skillValidator.js +123 -0
- package/dist/agent/state.d.ts +7 -0
- package/dist/agent/state.js +96 -0
- package/dist/agent/strictLint.d.ts +3 -0
- package/dist/agent/strictLint.js +165 -0
- package/dist/agent/templates.d.ts +14 -0
- package/dist/agent/templates.js +192 -0
- package/dist/agent/types.d.ts +286 -0
- package/dist/agent/types.js +88 -0
- package/dist/agent/util.d.ts +13 -0
- package/dist/agent/util.js +116 -0
- package/dist/agent/version.d.ts +11 -0
- package/dist/agent/version.js +16 -0
- package/dist/client.d.ts +162 -0
- package/dist/http.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/tools.d.ts +3 -0
- package/dist/version.d.ts +1 -0
- package/package.json +78 -67
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@coinrithm/mcp-trading` are documented here. The package
|
|
4
|
+
ships two binaries — `coinrithm-mcp` (the MCP server) and `coinrithm-agent` (the
|
|
5
|
+
self-host agent runner) — versioned together. The CoinRithm **API contract** is
|
|
6
|
+
versioned separately (see `openapi.yaml` `info.version`, currently `1.4.0`).
|
|
7
|
+
|
|
8
|
+
## 0.3.0
|
|
9
|
+
|
|
10
|
+
- **Agent risk config: coin deny-list (`blocklist`).** `risk.blocklist` lets an
|
|
11
|
+
agent name symbols it must never open, even if they are on the watchlist —
|
|
12
|
+
deny wins over allow. Enforced in the runner's decision validator (rejects
|
|
13
|
+
`futures_open` / `spot_order` on a denied symbol) and surfaced in the system
|
|
14
|
+
prompt, so the model is told the boundary and the runner re-checks it.
|
|
15
|
+
- **Docs: Open Knowledge Format positioning.** Clarified that a CoinRithm agent
|
|
16
|
+
is an OKF bundle — a portable directory of markdown + frontmatter that is
|
|
17
|
+
model-agnostic (run the same definition on any model). Develop and prove it
|
|
18
|
+
free on paper, then run it anywhere.
|
|
19
|
+
- npm keywords refreshed (`open-knowledge-format`, `okf`, `model-agnostic`,
|
|
20
|
+
`gemini`) for registry discovery.
|
|
21
|
+
|
|
22
|
+
## 0.2.0
|
|
23
|
+
|
|
24
|
+
- Added the **`coinrithm-agent`** self-host runner binary alongside the MCP
|
|
25
|
+
server: a folder-as-architecture (OKF) agent you bring your own model key to,
|
|
26
|
+
with caps enforced by the runner (not the model), dry-run by default,
|
|
27
|
+
paper-only.
|
|
28
|
+
|
|
29
|
+
## 0.1.x
|
|
30
|
+
|
|
31
|
+
- Initial `coinrithm-mcp` MCP server: reads, quotes, scoped spot/futures/PM
|
|
32
|
+
writes, ledger export, and Agent Arena integration over a user-minted API key.
|
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# @coinrithm/mcp-trading
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
CoinRithm paper-trading toolkit. Two binaries:
|
|
4
|
+
|
|
5
|
+
- **`coinrithm-mcp`** — an MCP server that lets an AI agent paper-trade on
|
|
6
|
+
CoinRithm (spot, futures, prediction markets) using a personal API key.
|
|
7
|
+
- **`coinrithm-agent`** — a self-host **agent runner**: author an agent as a
|
|
8
|
+
folder and run an observe→decide→validate→act loop with your own model key
|
|
9
|
+
(paper: spot, futures, prediction markets). See [Agent runner](#agent-runner-coinrithm-agent) below.
|
|
5
10
|
|
|
6
11
|
> **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
|
|
7
12
|
|
|
@@ -12,6 +17,25 @@ npm install
|
|
|
12
17
|
npm run build
|
|
13
18
|
```
|
|
14
19
|
|
|
20
|
+
## Agent runner (`coinrithm-agent`)
|
|
21
|
+
|
|
22
|
+
This package also ships a **self-host agent runner**. You write an agent as a
|
|
23
|
+
folder (strategy + hard caps in markdown/YAML); the runner compiles it and runs
|
|
24
|
+
an `observe → decide → validate → act` loop, asking *your* model (bring-your-own
|
|
25
|
+
key) for structured decisions and executing only the ones that pass your caps —
|
|
26
|
+
**dry-run by default**, paper-only across spot, futures, and prediction markets.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
coinrithm-agent new my-agent --preset conservative
|
|
30
|
+
coinrithm-agent validate my-agent
|
|
31
|
+
COINRITHM_API_KEY=crk_live_… ANTHROPIC_API_KEY=sk-ant-… \
|
|
32
|
+
coinrithm-agent run my-agent --once --dry-run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Full guide (env vars, fail-closed guarantees, folder layout):
|
|
36
|
+
**[docs/agent-runner.md](../../docs/agent-runner.md)**. The CoinRithm hosted
|
|
37
|
+
scheduler runs this same engine for you (later).
|
|
38
|
+
|
|
15
39
|
## Two ways to run
|
|
16
40
|
|
|
17
41
|
| Mode | Entry | Auth | Who it's for |
|
|
@@ -59,11 +83,11 @@ key upstream. See [`DEPLOY.md`](./DEPLOY.md).
|
|
|
59
83
|
| `get_market_context` (coinId) | read | `GET /api/agent/market/:coinId` |
|
|
60
84
|
| `get_candles` (coinId, range) | read | `GET /api/agent/market/:coinId/candles` |
|
|
61
85
|
| `discover_pm_markets` | read | `GET /api/agent/pm/discover` |
|
|
62
|
-
| `get_performance` | read | `GET /api/agent/performance` |
|
|
63
|
-
| `get_agent_ledger` | read | `GET /api/agent/ledger` |
|
|
64
|
-
| `export_agent_ledger` | read | `GET /api/agent/ledger/export` |
|
|
65
|
-
| `export_run_evidence` | read | `GET /api/agent/ledger/export?runId=...` |
|
|
66
|
-
| `get_arena_leaderboard` | read | `GET /api/arena` |
|
|
86
|
+
| `get_performance` | read | `GET /api/agent/performance` |
|
|
87
|
+
| `get_agent_ledger` | read | `GET /api/agent/ledger` |
|
|
88
|
+
| `export_agent_ledger` | read | `GET /api/agent/ledger/export` |
|
|
89
|
+
| `export_run_evidence` | read | `GET /api/agent/ledger/export?runId=...` |
|
|
90
|
+
| `get_arena_leaderboard` | read | `GET /api/arena` |
|
|
67
91
|
| `get_arena_agent` (handle) | read | `GET /api/arena/:handle` |
|
|
68
92
|
| `list_open_orders` | read | `GET /api/agent/orders/open` |
|
|
69
93
|
| `get_positions` (venue) | read | `GET /api/agent/positions/{futures,pm}` |
|
|
@@ -125,18 +149,18 @@ account only, latest stored market/probability snapshots, no explicit
|
|
|
125
149
|
commission/slippage in v1, no futures funding/fees, and worker-driven resting
|
|
126
150
|
order / SL / TP / settlement timing. It is a reproducibility artifact for your
|
|
127
151
|
run; it is not a full point-in-time market archive and does not expose hidden
|
|
128
|
-
reasoning. Aggregate audit stats include trace coverage for `runId` and
|
|
129
|
-
`decisionId`. Run exports also include `retentionPolicy`: private ledger rows
|
|
130
|
-
use a rolling retention window and exports are capped. They include
|
|
131
|
-
`evidenceChecklist`, a derived pass/warn/fail checklist for trace completeness,
|
|
132
|
-
decision ids, quote-before-trade coverage, rejected calls, export truncation,
|
|
133
|
-
execution assumptions, and outcome attribution; it does not create additional
|
|
134
|
-
retained data. `outcomeSummary` derives best-effort realized PnL from existing
|
|
135
|
-
related trade/position ids, and spot orders can also match through their
|
|
136
|
-
idempotency keys once a terminal `ClosedOrder` exists. It reports whether
|
|
137
|
-
coverage is `none`, `partial`, or `complete`; it does not store new data. Public
|
|
138
|
-
Arena surfaces only aggregate audit stats; raw request logs and rationale
|
|
139
|
-
summaries stay private.
|
|
152
|
+
reasoning. Aggregate audit stats include trace coverage for `runId` and
|
|
153
|
+
`decisionId`. Run exports also include `retentionPolicy`: private ledger rows
|
|
154
|
+
use a rolling retention window and exports are capped. They include
|
|
155
|
+
`evidenceChecklist`, a derived pass/warn/fail checklist for trace completeness,
|
|
156
|
+
decision ids, quote-before-trade coverage, rejected calls, export truncation,
|
|
157
|
+
execution assumptions, and outcome attribution; it does not create additional
|
|
158
|
+
retained data. `outcomeSummary` derives best-effort realized PnL from existing
|
|
159
|
+
related trade/position ids, and spot orders can also match through their
|
|
160
|
+
idempotency keys once a terminal `ClosedOrder` exists. It reports whether
|
|
161
|
+
coverage is `none`, `partial`, or `complete`; it does not store new data. Public
|
|
162
|
+
Arena surfaces only aggregate audit stats; raw request logs and rationale
|
|
163
|
+
summaries stay private.
|
|
140
164
|
|
|
141
165
|
`get_my_trades`, `list_open_orders`, and `get_positions` accept an optional
|
|
142
166
|
`updatedSince` cursor and their responses carry `asOf` — pass it back to poll
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CoinRithmClient } from "./client.js";
|
|
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): Promise<ApiResult>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Act phase: fetch the quote evidence for an open (the runner does this, never
|
|
2
|
+
// the model) and execute a validated action (futures / spot / PM) with an
|
|
3
|
+
// idempotency key.
|
|
4
|
+
import { asObj, asNum, asStr } from "./extract.js";
|
|
5
|
+
function coinIdFor(observation, symbol) {
|
|
6
|
+
return observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined;
|
|
7
|
+
}
|
|
8
|
+
function freshnessOf(block) {
|
|
9
|
+
const fr = asObj(block.freshness);
|
|
10
|
+
const status = asStr(fr.status);
|
|
11
|
+
return status ? { status, ageSeconds: asNum(fr.ageSeconds) } : undefined;
|
|
12
|
+
}
|
|
13
|
+
// Read-only quote BEFORE any open. Returns ineligible (never throws) on error.
|
|
14
|
+
export async function fetchQuote(client, action, observation, trace) {
|
|
15
|
+
let r;
|
|
16
|
+
if (action.type === "futures_open") {
|
|
17
|
+
const coinId = coinIdFor(observation, action.symbol);
|
|
18
|
+
if (!coinId)
|
|
19
|
+
return { eligible: false, blockReasons: ["unresolved_symbol"] };
|
|
20
|
+
r = await client.futuresQuote({ coinId, side: action.side, leverage: action.leverage, marginMusd: action.marginMusd }, trace);
|
|
21
|
+
}
|
|
22
|
+
else if (action.type === "spot_order") {
|
|
23
|
+
const coinId = coinIdFor(observation, action.symbol);
|
|
24
|
+
if (!coinId)
|
|
25
|
+
return { eligible: false, blockReasons: ["unresolved_symbol"] };
|
|
26
|
+
r = await client.spotQuote({ coinId, side: action.side, quantity: action.quantity }, trace);
|
|
27
|
+
}
|
|
28
|
+
else if (action.type === "pm_open") {
|
|
29
|
+
r = await client.pmQuote({
|
|
30
|
+
source: action.source,
|
|
31
|
+
slug: action.slug,
|
|
32
|
+
outcomeExternalMarketId: action.outcomeExternalMarketId,
|
|
33
|
+
stakeMusd: action.stakeMusd,
|
|
34
|
+
}, trace);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return undefined; // close / set-sltp / cancel need no quote
|
|
38
|
+
}
|
|
39
|
+
if (!r.ok)
|
|
40
|
+
return { eligible: false, blockReasons: [`quote_http_${r.status}`] };
|
|
41
|
+
const d = asObj(r.data);
|
|
42
|
+
return {
|
|
43
|
+
eligible: d.eligible === true,
|
|
44
|
+
blockReasons: d.blockReasons,
|
|
45
|
+
entryPrice: asNum(d.entryPrice), // futures
|
|
46
|
+
liquidationPrice: asNum(d.liquidationPrice), // futures
|
|
47
|
+
executionPrice: asNum(d.executionPrice), // spot live fill price
|
|
48
|
+
estimatedCostMusd: asNum(d.estimatedCostMusd), // spot gross notional
|
|
49
|
+
// Freshness lives in the response's `observation` block (anti-look-ahead).
|
|
50
|
+
freshness: freshnessOf(asObj(d.observation)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function executeAction(client, action, observation, trace, idempotencyKey) {
|
|
54
|
+
if (action.type === "futures_open") {
|
|
55
|
+
const coinId = coinIdFor(observation, action.symbol);
|
|
56
|
+
if (!coinId)
|
|
57
|
+
return { ok: false, status: 0, data: { error: "unresolved_symbol" } };
|
|
58
|
+
return client.openFutures({
|
|
59
|
+
coinId,
|
|
60
|
+
side: action.side,
|
|
61
|
+
leverage: action.leverage,
|
|
62
|
+
marginMusd: action.marginMusd,
|
|
63
|
+
idempotencyKey,
|
|
64
|
+
stopLossPrice: action.stopLossPrice ?? null,
|
|
65
|
+
takeProfitPrice: action.takeProfitPrice ?? null,
|
|
66
|
+
agentTrace: trace,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (action.type === "futures_close") {
|
|
70
|
+
return client.closeFutures({
|
|
71
|
+
positionId: action.positionId,
|
|
72
|
+
fraction: action.fraction,
|
|
73
|
+
idempotencyKey,
|
|
74
|
+
agentTrace: trace,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (action.type === "futures_set_sltp") {
|
|
78
|
+
return client.setFuturesSlTp({
|
|
79
|
+
positionId: action.positionId,
|
|
80
|
+
stopLossPrice: action.stopLossPrice ?? undefined,
|
|
81
|
+
takeProfitPrice: action.takeProfitPrice ?? undefined,
|
|
82
|
+
agentTrace: trace,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (action.type === "spot_order") {
|
|
86
|
+
const coinId = coinIdFor(observation, action.symbol);
|
|
87
|
+
if (!coinId)
|
|
88
|
+
return { ok: false, status: 0, data: { error: "unresolved_symbol" } };
|
|
89
|
+
return client.placeSpotOrder({
|
|
90
|
+
coinId,
|
|
91
|
+
side: action.side,
|
|
92
|
+
orderType: action.orderType,
|
|
93
|
+
quantity: action.quantity,
|
|
94
|
+
limitPrice: action.limitPrice,
|
|
95
|
+
stopPrice: action.stopPrice,
|
|
96
|
+
idempotencyKey,
|
|
97
|
+
agentTrace: trace,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (action.type === "spot_cancel") {
|
|
101
|
+
return client.cancelSpotOrder(action.orderId, trace);
|
|
102
|
+
}
|
|
103
|
+
if (action.type === "pm_open") {
|
|
104
|
+
return client.openPmPosition({
|
|
105
|
+
source: action.source,
|
|
106
|
+
slug: action.slug,
|
|
107
|
+
outcomeExternalMarketId: action.outcomeExternalMarketId,
|
|
108
|
+
stakeMusd: action.stakeMusd,
|
|
109
|
+
idempotencyKey,
|
|
110
|
+
agentTrace: trace,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { ok: false, status: 0, data: { error: "unsupported_action" } };
|
|
114
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Capability drift guard.
|
|
2
|
+
//
|
|
3
|
+
// An agent's PROSE (thesis, persona, tactic skills) is authored separately from
|
|
4
|
+
// the runner's code, so a skill can name a venue, an action, or a cap that the
|
|
5
|
+
// runner does NOT support — and rot silently, exactly the way a stale doc drifts
|
|
6
|
+
// from a live enum. (We watched this happen in a sibling system: a knowledge file
|
|
7
|
+
// kept naming post types the live enum had dropped, and a test pinned the stale
|
|
8
|
+
// names so the suite stayed green while the model was fed a wrong taxonomy.)
|
|
9
|
+
//
|
|
10
|
+
// This guard asserts that every capability IDENTIFIER referenced in authored
|
|
11
|
+
// prose is in the runner's LIVE capability set AND — for actions/venues — in THIS
|
|
12
|
+
// agent's enabled venues. It is deliberately deterministic and low-false-positive:
|
|
13
|
+
// it inspects only identifier-shaped tokens (snake_case `<venue>_<verb>` actions
|
|
14
|
+
// scanned anywhere; cap/venue identifiers scanned only inside markdown code
|
|
15
|
+
// spans), never free English. Runtime memory (journal/notes.md) is excluded — it
|
|
16
|
+
// records history, and may legitimately recall a venue later disabled.
|
|
17
|
+
import { VENUES, ACTION_TYPES, } from "./types.js";
|
|
18
|
+
import { levenshtein } from "./strictLint.js";
|
|
19
|
+
import { RISK_CAPS, LIMIT_CAPS } from "./mergeRules.js";
|
|
20
|
+
const KNOWN_ACTIONS = new Set(ACTION_TYPES);
|
|
21
|
+
// Every enforced cap the runner knows: risk + limits + killSwitch + abstention.
|
|
22
|
+
const LIVE_CAPS = [
|
|
23
|
+
...Object.keys(RISK_CAPS),
|
|
24
|
+
...Object.keys(LIMIT_CAPS),
|
|
25
|
+
"maxDrawdownMusd",
|
|
26
|
+
"maxConsecutiveRejects",
|
|
27
|
+
"maxConsecutiveModelFailures",
|
|
28
|
+
"onRateLimitPressure",
|
|
29
|
+
"onStaleData",
|
|
30
|
+
"onWeakSignal",
|
|
31
|
+
"onMissingQuote",
|
|
32
|
+
"onInsufficientBalance",
|
|
33
|
+
"minConfidence",
|
|
34
|
+
];
|
|
35
|
+
const LIVE_CAP_SET = new Set(LIVE_CAPS);
|
|
36
|
+
// `<venue>_<verb>` action references. The verb is restricted to the runner's
|
|
37
|
+
// real verbs, so descriptive tokens (spot_price, futures_curve) never match —
|
|
38
|
+
// only action-shaped references (real OR plausibly-fake) do.
|
|
39
|
+
const ACTION_RE = /\b(spot|futures|pm)_(open|close|order|cancel|set_sltp)\b/g;
|
|
40
|
+
// camelCase cap-shaped identifiers (maxLeverage, minConfidence, requireStopLoss…).
|
|
41
|
+
const CAP_RE = /\b(?:max|min|require|on|per)[A-Z][A-Za-z0-9]+\b/g;
|
|
42
|
+
// markdown inline code spans — where authors put identifiers.
|
|
43
|
+
const CODESPAN_RE = /`([^`\n]+)`/g;
|
|
44
|
+
// Levenshtein threshold for "this is a typo/rename of a real cap" — matches the
|
|
45
|
+
// strictLint "did you mean" threshold (< 3, i.e. edit distance <= 2).
|
|
46
|
+
const CAP_NEARMISS_MAX = 3;
|
|
47
|
+
function venueOf(action) {
|
|
48
|
+
return action.split("_")[0];
|
|
49
|
+
}
|
|
50
|
+
// Authored knowledge only: keystone + character/* + skills/*. journal/notes.md is
|
|
51
|
+
// runtime memory and is intentionally NOT checked.
|
|
52
|
+
function authoredProse(resolved) {
|
|
53
|
+
return resolved.proseParts.filter((p) => p.source !== "journal/notes.md");
|
|
54
|
+
}
|
|
55
|
+
// Returns one issue per (token, source) drift found. Empty = no drift.
|
|
56
|
+
export function checkCapabilityDrift(resolved, spec) {
|
|
57
|
+
const issues = [];
|
|
58
|
+
const enabled = new Set(spec.venues);
|
|
59
|
+
const seen = new Set(); // dedupe "kind|token|source"
|
|
60
|
+
const once = (key) => {
|
|
61
|
+
if (seen.has(key))
|
|
62
|
+
return false;
|
|
63
|
+
seen.add(key);
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
for (const part of authoredProse(resolved)) {
|
|
67
|
+
const where = part.source;
|
|
68
|
+
// ── action references (anywhere in the body) ──
|
|
69
|
+
for (const m of part.text.matchAll(ACTION_RE)) {
|
|
70
|
+
const action = m[0];
|
|
71
|
+
if (!once(`a|${action}|${where}`))
|
|
72
|
+
continue;
|
|
73
|
+
if (!KNOWN_ACTIONS.has(action)) {
|
|
74
|
+
issues.push({
|
|
75
|
+
code: "drift_unknown_action",
|
|
76
|
+
path: where,
|
|
77
|
+
message: `prose references action \`${action}\`, which the runner does not support (allowed: ${ACTION_TYPES.join(", ")})`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const venue = venueOf(action);
|
|
82
|
+
if (!enabled.has(venue)) {
|
|
83
|
+
issues.push({
|
|
84
|
+
code: "drift_venue_not_enabled",
|
|
85
|
+
path: where,
|
|
86
|
+
message: `prose references \`${action}\` but venue \`${venue}\` is not in this agent's venues [${spec.venues.join(", ")}]`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ── venue + cap references inside code spans only ──
|
|
92
|
+
for (const span of part.text.matchAll(CODESPAN_RE)) {
|
|
93
|
+
const inner = span[1];
|
|
94
|
+
// An exact venue identifier in a code span (e.g. `pm`) that the agent has
|
|
95
|
+
// not enabled — a prose promise the runner cannot keep.
|
|
96
|
+
if (VENUES.includes(inner) &&
|
|
97
|
+
!enabled.has(inner) &&
|
|
98
|
+
once(`v|${inner}|${where}`)) {
|
|
99
|
+
issues.push({
|
|
100
|
+
code: "drift_venue_not_enabled",
|
|
101
|
+
path: where,
|
|
102
|
+
message: `prose references venue \`${inner}\` but it is not in this agent's venues [${spec.venues.join(", ")}]`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// cap-shaped identifiers — flag only a near-miss of a REAL cap (a typo or
|
|
106
|
+
// rename); unrelated identifiers are left alone to avoid false positives.
|
|
107
|
+
for (const cm of inner.matchAll(CAP_RE)) {
|
|
108
|
+
const token = cm[0];
|
|
109
|
+
if (LIVE_CAP_SET.has(token))
|
|
110
|
+
continue;
|
|
111
|
+
let nearest = null;
|
|
112
|
+
let best = CAP_NEARMISS_MAX;
|
|
113
|
+
for (const cap of LIVE_CAPS) {
|
|
114
|
+
const d = levenshtein(token.toLowerCase(), cap.toLowerCase());
|
|
115
|
+
if (d < best) {
|
|
116
|
+
best = d;
|
|
117
|
+
nearest = cap;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (nearest && once(`c|${token}|${where}`)) {
|
|
121
|
+
issues.push({
|
|
122
|
+
code: "drift_unknown_cap",
|
|
123
|
+
path: where,
|
|
124
|
+
message: `prose references cap \`${token}\`, which is not a runner cap — did you mean \`${nearest}\`?`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SkillValidationMode } from "./skillValidator.js";
|
|
2
|
+
export interface CmdResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
code: number;
|
|
5
|
+
lines: string[];
|
|
6
|
+
data?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function cmdNew(targetPath: string, opts?: {
|
|
9
|
+
template?: string;
|
|
10
|
+
preset?: string;
|
|
11
|
+
}): CmdResult;
|
|
12
|
+
export declare function cmdValidate(path: string, mode?: SkillValidationMode): CmdResult;
|
|
13
|
+
export declare function cmdLock(path: string): CmdResult;
|
|
14
|
+
export declare function cmdEject(path: string): CmdResult;
|
|
15
|
+
export declare function cmdInspect(path: string, json?: boolean): CmdResult;
|
|
16
|
+
export declare function cmdRun(path: string, opts?: {
|
|
17
|
+
once?: boolean;
|
|
18
|
+
live?: boolean;
|
|
19
|
+
stateFile?: string;
|
|
20
|
+
}): Promise<CmdResult>;
|
|
21
|
+
export declare function main(argv: string[]): Promise<number>;
|