@reefclaw/connect 0.1.35 → 0.1.37
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/assets/bridge/index.js +17 -0
- package/assets/bridge/shock-wake.d.ts +80 -0
- package/assets/bridge/shock-wake.js +291 -0
- package/assets/plugin/config/agent-config-client.d.ts +3 -1
- package/assets/plugin/config/agent-config-client.js +4 -0
- package/assets/plugin/config/gate-store.d.ts +3 -0
- package/assets/plugin/config/gate-store.js +11 -2
- package/assets/plugin/config/loss-streak-config.d.ts +2 -0
- package/assets/plugin/config/loss-streak-config.js +33 -0
- package/assets/plugin/config/plugin-config-io.d.ts +19 -0
- package/assets/plugin/config/reentry-cooldown-config.d.ts +7 -0
- package/assets/plugin/config/reentry-cooldown-config.js +59 -0
- package/assets/plugin/index.js +5 -0
- package/assets/plugin/ingest/position-auto-capture.js +35 -2
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/portfolio/directional-scoreboard.d.ts +17 -0
- package/assets/plugin/portfolio/directional-scoreboard.js +71 -0
- package/assets/plugin/portfolio/reentry-tracker.d.ts +38 -1
- package/assets/plugin/portfolio/reentry-tracker.js +49 -0
- package/assets/plugin/signals/change-of-character.d.ts +38 -0
- package/assets/plugin/signals/change-of-character.js +93 -0
- package/assets/plugin/simulator/types.d.ts +11 -0
- package/assets/plugin/strategy/evaluator.d.ts +4 -0
- package/assets/plugin/tools/create-order.js +72 -2
- package/assets/plugin/tools/hl-provision-agent-wallet.js +29 -11
- package/assets/plugin/tools/reentry-cooldown.d.ts +33 -0
- package/assets/plugin/tools/reentry-cooldown.js +74 -0
- package/assets/plugin/tools/scan-pairs.d.ts +7 -0
- package/assets/plugin/tools/scan-pairs.js +47 -0
- package/assets/shared/signals/change-of-character.d.ts +38 -0
- package/assets/shared/signals/change-of-character.js +86 -0
- package/assets/skill/SKILL.md +2 -2
- package/dist/cli.js +11 -2
- package/dist/plugin.js +70 -28
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Re-entry cooldown gate — blocks (mode-laddered) a NEW entry on a symbol
|
|
2
|
+
// whose last close within the cooldown window was a LOSS.
|
|
3
|
+
//
|
|
4
|
+
// Evidence (2026-09-02, full live journal): entries opened within 60 minutes
|
|
5
|
+
// of a same-symbol losing close ran mean −0.077R (n=58) vs +0.090R for all
|
|
6
|
+
// other entries (n=1151) at identical ~41% win rates, plus the extra fee load
|
|
7
|
+
// — the LINK/DOGE churn signature of the 2026-06 investigation recurring on
|
|
8
|
+
// the HL live book. Suggestive, not proven, at n=58 — which is exactly what
|
|
9
|
+
// the shadow rung is for: this ships `off` by default, is flipped shadow-first
|
|
10
|
+
// via the central gate channel (`agent_config.gates.reentryCooldown`), and
|
|
11
|
+
// tags would-block entries into position_entries.metadata.reentry_cooldown so
|
|
12
|
+
// the forward counterfactual is measurable in the journal before any enforce
|
|
13
|
+
// decision.
|
|
14
|
+
//
|
|
15
|
+
// Scope guards (all fail OPEN — this gate can only ever suppress a NEW entry,
|
|
16
|
+
// never an exit, close, stop, or emergency action):
|
|
17
|
+
// - generic entries only (wave9 has its own frozen admission policy);
|
|
18
|
+
// - scale-ins exempt (position already open — entry already happened);
|
|
19
|
+
// - operator-approved proposal fires exempt (human already said yes);
|
|
20
|
+
// - tracker unavailable → pass.
|
|
21
|
+
//
|
|
22
|
+
// Mode semantics mirror the exit gate (docs/CLAUDE/exit-gate.md):
|
|
23
|
+
// off — not evaluated; byte-identical to the pre-gate path.
|
|
24
|
+
// shadow — evaluated + logged + journal-tagged; never affects the order.
|
|
25
|
+
// observe — as shadow, but a triggered eval logs at WARN (operator-visible).
|
|
26
|
+
// enforce — a triggered eval hard-rejects create_order with a recovery hint.
|
|
27
|
+
export function evaluateReentryCooldown(inputs) {
|
|
28
|
+
const base = {
|
|
29
|
+
mode: inputs.mode,
|
|
30
|
+
triggered: false,
|
|
31
|
+
blocked: false,
|
|
32
|
+
cooldownMinutes: inputs.cooldownMinutes,
|
|
33
|
+
};
|
|
34
|
+
if (inputs.isListenerFire)
|
|
35
|
+
return { ...base, code: 'operator_approved_exempt' };
|
|
36
|
+
if (inputs.hasOpenPosition)
|
|
37
|
+
return { ...base, code: 'scale_in_exempt' };
|
|
38
|
+
if (!inputs.tracker)
|
|
39
|
+
return { ...base, code: 'tracker_unavailable' };
|
|
40
|
+
const nowMs = inputs.nowMs ?? Date.now();
|
|
41
|
+
const windowMs = inputs.cooldownMinutes * 60_000;
|
|
42
|
+
const loss = inputs.tracker.lastLossyExit(inputs.symbol, windowMs, {
|
|
43
|
+
mode: inputs.book,
|
|
44
|
+
nowMs,
|
|
45
|
+
});
|
|
46
|
+
if (!loss)
|
|
47
|
+
return { ...base, code: 'no_recent_loss' };
|
|
48
|
+
const minutesSinceLoss = Math.max(0, Math.round((nowMs - loss.closedAtMs) / 60_000));
|
|
49
|
+
return {
|
|
50
|
+
...base,
|
|
51
|
+
triggered: true,
|
|
52
|
+
blocked: inputs.mode === 'enforce',
|
|
53
|
+
code: 'cooldown_active',
|
|
54
|
+
minutesSinceLoss,
|
|
55
|
+
lastLoss: {
|
|
56
|
+
side: loss.side,
|
|
57
|
+
setupType: loss.setupType,
|
|
58
|
+
closedAtMs: loss.closedAtMs,
|
|
59
|
+
lossSource: loss.lossSource,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Agent-facing rejection for enforce mode. Names the gate, the remaining
|
|
64
|
+
* wait, and the honest path forward — no bypass hint by design. */
|
|
65
|
+
export function buildReentryCooldownRejection(ev, symbol) {
|
|
66
|
+
const remaining = Math.max(1, ev.cooldownMinutes - (ev.minutesSinceLoss ?? 0));
|
|
67
|
+
return (`create_order rejected (reentry cooldown): ${symbol} closed at a LOSS ` +
|
|
68
|
+
`${ev.minutesSinceLoss}m ago and the operator-configured cooldown is ` +
|
|
69
|
+
`${ev.cooldownMinutes}m — ~${remaining}m remaining. This is a mechanical ` +
|
|
70
|
+
`gate against re-entry churn (measured −0.17R/trade edge gap on re-entries ` +
|
|
71
|
+
`within the window). Do not retry this symbol until the cooldown lapses; ` +
|
|
72
|
+
`spend the time re-scoring the setup — if it is still valid then, enter then. ` +
|
|
73
|
+
`Other symbols are unaffected.`);
|
|
74
|
+
}
|
|
@@ -13,6 +13,13 @@ export interface ScanPairsDecisionsDeps {
|
|
|
13
13
|
/** Re-entry tracker (issue #204) — flags setups already traded within the
|
|
14
14
|
* current signal bar. Indication only; nothing is filtered out. */
|
|
15
15
|
reentryTracker?: ReentryTracker;
|
|
16
|
+
/** WS2 directional scoreboard inputs (docs/MARKET_ADAPTIVITY_PLAN.md §3) —
|
|
17
|
+
* tracked open positions (state store; no exchange round-trip) + the
|
|
18
|
+
* current book. Indication only, like everything else in this block. */
|
|
19
|
+
openPositions?: () => Array<{
|
|
20
|
+
side: 'long' | 'short';
|
|
21
|
+
}>;
|
|
22
|
+
book?: () => 'paper' | 'live';
|
|
16
23
|
}
|
|
17
24
|
export declare function scanPairsTool(args: ScanPairsArgs, deps: IntelApiDeps, decisionsDeps?: ScanPairsDecisionsDeps): Promise<Record<string, unknown> | {
|
|
18
25
|
error: string;
|
|
@@ -17,8 +17,31 @@
|
|
|
17
17
|
// heartbeat the agent was rejecting post-scorecard pre-fix.
|
|
18
18
|
import { intelSymbolOnVenue, presentIntelSymbol, resolveIntelSymbol } from './intel-api.js';
|
|
19
19
|
import { scanAllPairs } from '../strategy/evaluator.js';
|
|
20
|
+
import { buildDirectionalScoreboard } from '../portfolio/directional-scoreboard.js';
|
|
20
21
|
import { getAllFactsCached, getStrategiesCached, __testing__ as cacheTesting } from './intel-cache.js';
|
|
21
22
|
import { normalizeSetupFamily, resolveTriggerFamilies } from '../learning/setup-family.js';
|
|
23
|
+
/** WS2 kill-switch — RC_CHANGE_OF_CHARACTER=off suppresses the market-shift
|
|
24
|
+
* cautions + directional scoreboard without a redeploy. Default on. */
|
|
25
|
+
function changeOfCharacterEnabled() {
|
|
26
|
+
return (process.env.RC_CHANGE_OF_CHARACTER ?? '').toLowerCase() !== 'off';
|
|
27
|
+
}
|
|
28
|
+
/** Tape line for the scoreboard from the market leader's fact (BTC on this
|
|
29
|
+
* venue; falls back to the first fact so alt-only books still get a tape). */
|
|
30
|
+
function leaderTapeLine(deps, facts) {
|
|
31
|
+
const leader = facts.find((f) => presentIntelSymbol(deps, f.symbol).toUpperCase().startsWith('BTC/')) ??
|
|
32
|
+
facts[0];
|
|
33
|
+
const coc = leader?.changeOfCharacter;
|
|
34
|
+
if (!coc)
|
|
35
|
+
return {};
|
|
36
|
+
const sym = presentIntelSymbol(deps, leader.symbol).split('/')[0];
|
|
37
|
+
const sign = (v) => (v >= 0 ? '+' : '');
|
|
38
|
+
return {
|
|
39
|
+
line: `${sym} 4h ${sign(coc.return4hPct)}${coc.return4hPct}% ` +
|
|
40
|
+
`(${sign(coc.tape4hAtr)}${coc.tape4hAtr}×ATR)` +
|
|
41
|
+
(coc.flags.length > 0 ? ` [${coc.flags.join(', ')}]` : ''),
|
|
42
|
+
caution: coc.flags.length > 0 ? coc.summary : undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
22
45
|
const LEARNINGS_TTL_MS = 60_000;
|
|
23
46
|
const entryLearningsCache = new Map();
|
|
24
47
|
/** Family-keying gate (2026-06-22). Off by default → byte-identical exact-match
|
|
@@ -189,10 +212,14 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
|
|
|
189
212
|
const entryLearnings = await learningsPromise;
|
|
190
213
|
const rankings = [];
|
|
191
214
|
const vetoed = [];
|
|
215
|
+
// WS2 change-of-character: per-symbol lookup + market-leader tape line.
|
|
216
|
+
const cocOn = changeOfCharacterEnabled();
|
|
217
|
+
const factBySymbol = new Map(facts.map(f => [f.symbol, f]));
|
|
192
218
|
for (const r of results) {
|
|
193
219
|
const matches = entryLearnings.filter(l => learningMatches(l, r.strategy, r.regime));
|
|
194
220
|
const agentSymbol = presentIntelSymbol(deps, r.symbol);
|
|
195
221
|
const reentryCaution = decisionsDeps?.reentryTracker?.cautionFor(agentSymbol, r.strategy);
|
|
222
|
+
const coc = cocOn ? factBySymbol.get(r.symbol)?.changeOfCharacter : undefined;
|
|
196
223
|
const out = {
|
|
197
224
|
// Agent-facing form: on hyperliquid the agent must see the symbol it
|
|
198
225
|
// can hand straight to create_order ('BTC/USDC'), never 'HL_BTC'.
|
|
@@ -203,6 +230,7 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
|
|
|
203
230
|
conditions: `${r.conditionsMet}/${r.conditionsTotal} met: ${r.conditions.filter(c => c.met).map(c => c.name).join(', ')}`,
|
|
204
231
|
summary: r.summary,
|
|
205
232
|
...(reentryCaution ? { reentry_caution: reentryCaution } : {}),
|
|
233
|
+
...(coc && coc.flags.length > 0 && coc.summary ? { market_shift_caution: coc.summary } : {}),
|
|
206
234
|
};
|
|
207
235
|
if (matches.length === 0) {
|
|
208
236
|
rankings.push(out);
|
|
@@ -223,10 +251,29 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
|
|
|
223
251
|
const noSetup = facts
|
|
224
252
|
.filter(f => !setupSymbols.has(f.symbol))
|
|
225
253
|
.map(f => presentIntelSymbol(deps, f.symbol));
|
|
254
|
+
// WS2 top-level indication: leader tape + shift caution + the directional
|
|
255
|
+
// scoreboard (book tilt vs recent per-direction outcomes vs tape). All
|
|
256
|
+
// indication-only; RC_CHANGE_OF_CHARACTER=off suppresses without redeploy.
|
|
257
|
+
let marketCaution;
|
|
258
|
+
let scoreboard;
|
|
259
|
+
if (cocOn) {
|
|
260
|
+
const tape = leaderTapeLine(deps, facts);
|
|
261
|
+
marketCaution = tape.caution;
|
|
262
|
+
if (decisionsDeps?.reentryTracker) {
|
|
263
|
+
scoreboard = buildDirectionalScoreboard({
|
|
264
|
+
openPositions: decisionsDeps.openPositions?.() ?? [],
|
|
265
|
+
exitRecords: decisionsDeps.reentryTracker.getRecords(),
|
|
266
|
+
book: decisionsDeps.book?.() ?? 'paper',
|
|
267
|
+
tapeLine: tape.line,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
226
271
|
return {
|
|
227
272
|
timestamp: new Date().toISOString(),
|
|
228
273
|
pairs_scanned: facts.length,
|
|
229
274
|
setups_found: results.length,
|
|
275
|
+
...(marketCaution ? { market_caution: marketCaution } : {}),
|
|
276
|
+
...(scoreboard ? { directional_scoreboard: scoreboard } : {}),
|
|
230
277
|
rankings,
|
|
231
278
|
vetoed_setups: vetoed.length > 0 ? vetoed : undefined,
|
|
232
279
|
no_setup: noSetup.length > 5
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** 30m move ≥ this ×ATR(1h) ⇒ 'shock'. */
|
|
2
|
+
export declare const SHOCK_ATR_MULT_30M = 2;
|
|
3
|
+
/** 4h tape opposing a directional regime label by ≥ this ×ATR(1h) ⇒ 'regime_tape_disagreement'. */
|
|
4
|
+
export declare const DISAGREEMENT_ATR_MULT_4H = 2;
|
|
5
|
+
export type ChangeOfCharacterFlag = 'shock' | 'regime_tape_disagreement';
|
|
6
|
+
export interface ChangeOfCharacter {
|
|
7
|
+
/** |price now − close ~30m ago| ÷ ATR(1h). */
|
|
8
|
+
shock30mAtr: number;
|
|
9
|
+
/** (max high − min low) over the last ~30m ÷ ATR(1h) — catches a spike-and-revert the close-to-close move misses. */
|
|
10
|
+
range30mAtr: number;
|
|
11
|
+
/** % return vs the close ~1h ago. */
|
|
12
|
+
return1hPct: number;
|
|
13
|
+
/** % return vs the close ~4h ago. */
|
|
14
|
+
return4hPct: number;
|
|
15
|
+
/** Signed (price now − close ~4h ago) ÷ ATR(1h) — the tape the regime label must answer to. */
|
|
16
|
+
tape4hAtr: number;
|
|
17
|
+
/** ATR(1h) as % of price — the volatility yardstick the multiples are in. */
|
|
18
|
+
atrPct: number;
|
|
19
|
+
flags: ChangeOfCharacterFlag[];
|
|
20
|
+
/** Agent-facing one-liner. Present ONLY when a flag fired — silence stays silent. */
|
|
21
|
+
summary?: string;
|
|
22
|
+
}
|
|
23
|
+
interface BarLike {
|
|
24
|
+
high: number;
|
|
25
|
+
low: number;
|
|
26
|
+
close: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ChangeOfCharacterInputs {
|
|
29
|
+
/** 5m bars, oldest first (≥7 required). */
|
|
30
|
+
ohlcv5m: BarLike[];
|
|
31
|
+
/** 1h bars, oldest first (≥5 required). */
|
|
32
|
+
ohlcv1h: BarLike[];
|
|
33
|
+
atr14: number;
|
|
34
|
+
currentPrice: number;
|
|
35
|
+
regime: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function computeChangeOfCharacter(inputs: ChangeOfCharacterInputs): ChangeOfCharacter | undefined;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Change-of-character detection — WS2 of docs/MARKET_ADAPTIVITY_PLAN.md.
|
|
2
|
+
//
|
|
3
|
+
// The 2026-09 adaptivity investigation found the agent keeps trading a stale
|
|
4
|
+
// directional view after event-driven regime flips because (a) the regime
|
|
5
|
+
// label is computed from realized bars and lags a shock by hours, and (b) no
|
|
6
|
+
// input ever shows it the contradiction. This module computes the two fast
|
|
7
|
+
// counter-evidence facts from data the fact-computer already has in hand —
|
|
8
|
+
// NO new DB queries (the intel hot-query chunk-exclusion rule is not in play):
|
|
9
|
+
//
|
|
10
|
+
// - SHOCK: the last ~30 minutes moved a multiple of the hourly ATR. An
|
|
11
|
+
// hourly ATR is roughly "a typical hour's range", so 2×ATR inside 30m is
|
|
12
|
+
// ~4× the typical rate — announcement territory.
|
|
13
|
+
// - REGIME/TAPE DISAGREEMENT: the regime label says TREND_UP (or DOWN) but
|
|
14
|
+
// the realized 4h tape moved ≥2×ATR the other way — the label is lagging.
|
|
15
|
+
//
|
|
16
|
+
// Pure function, deterministic, fail-open: insufficient bars or a degenerate
|
|
17
|
+
// ATR returns undefined and every consumer treats that as "no signal".
|
|
18
|
+
// Thresholds are exported so the soak can be re-cut without archaeology.
|
|
19
|
+
// Canonical copy lives in shared/src/signals/ and is synced to
|
|
20
|
+
// intelligence/ + plugin/ via scripts/sync-shared-code.mjs (drift-tested).
|
|
21
|
+
/** 30m move ≥ this ×ATR(1h) ⇒ 'shock'. */
|
|
22
|
+
export const SHOCK_ATR_MULT_30M = 2.0;
|
|
23
|
+
/** 4h tape opposing a directional regime label by ≥ this ×ATR(1h) ⇒ 'regime_tape_disagreement'. */
|
|
24
|
+
export const DISAGREEMENT_ATR_MULT_4H = 2.0;
|
|
25
|
+
const fin = (v) => Number.isFinite(v);
|
|
26
|
+
const round2 = (v) => Math.round(v * 100) / 100;
|
|
27
|
+
export function computeChangeOfCharacter(inputs) {
|
|
28
|
+
const { ohlcv5m, ohlcv1h, atr14, currentPrice, regime } = inputs;
|
|
29
|
+
if (!fin(atr14) || atr14 <= 0 || !fin(currentPrice) || currentPrice <= 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!Array.isArray(ohlcv5m) || ohlcv5m.length < 7)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!Array.isArray(ohlcv1h) || ohlcv1h.length < 5)
|
|
34
|
+
return undefined;
|
|
35
|
+
// ~30m window = last 6 closed 5m bars; the reference close sits just before it.
|
|
36
|
+
const last6 = ohlcv5m.slice(-6);
|
|
37
|
+
const ref30m = ohlcv5m[ohlcv5m.length - 7].close;
|
|
38
|
+
const ref1h = ohlcv1h[ohlcv1h.length - 2].close;
|
|
39
|
+
const ref4h = ohlcv1h[ohlcv1h.length - 5].close;
|
|
40
|
+
if (!fin(ref30m) || ref30m <= 0 || !fin(ref1h) || ref1h <= 0 || !fin(ref4h) || ref4h <= 0) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const shock30mAtr = Math.abs(currentPrice - ref30m) / atr14;
|
|
44
|
+
const hi30 = Math.max(...last6.map((b) => b.high));
|
|
45
|
+
const lo30 = Math.min(...last6.map((b) => b.low));
|
|
46
|
+
const range30mAtr = fin(hi30) && fin(lo30) && hi30 >= lo30 ? (hi30 - lo30) / atr14 : 0;
|
|
47
|
+
const return1hPct = (currentPrice / ref1h - 1) * 100;
|
|
48
|
+
const return4hPct = (currentPrice / ref4h - 1) * 100;
|
|
49
|
+
const tape4hAtr = (currentPrice - ref4h) / atr14;
|
|
50
|
+
const atrPct = (atr14 / currentPrice) * 100;
|
|
51
|
+
const flags = [];
|
|
52
|
+
if (Math.max(shock30mAtr, range30mAtr) >= SHOCK_ATR_MULT_30M)
|
|
53
|
+
flags.push('shock');
|
|
54
|
+
const labelDir = regime === 'TREND_UP' ? 1 : regime === 'TREND_DOWN' ? -1 : 0;
|
|
55
|
+
const opposing = labelDir !== 0 && Math.sign(tape4hAtr) === -labelDir;
|
|
56
|
+
if (opposing && Math.abs(tape4hAtr) >= DISAGREEMENT_ATR_MULT_4H) {
|
|
57
|
+
flags.push('regime_tape_disagreement');
|
|
58
|
+
}
|
|
59
|
+
const out = {
|
|
60
|
+
shock30mAtr: round2(shock30mAtr),
|
|
61
|
+
range30mAtr: round2(range30mAtr),
|
|
62
|
+
return1hPct: round2(return1hPct),
|
|
63
|
+
return4hPct: round2(return4hPct),
|
|
64
|
+
tape4hAtr: round2(tape4hAtr),
|
|
65
|
+
atrPct: round2(atrPct),
|
|
66
|
+
flags,
|
|
67
|
+
};
|
|
68
|
+
if (flags.length > 0)
|
|
69
|
+
out.summary = buildSummary(out, regime);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function buildSummary(c, regime) {
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (c.flags.includes('shock')) {
|
|
75
|
+
parts.push(`SHOCK: last 30m moved ${round2(Math.max(c.shock30mAtr, c.range30mAtr))}×ATR ` +
|
|
76
|
+
`(${c.return1hPct >= 0 ? '+' : ''}${c.return1hPct}% on the hour)`);
|
|
77
|
+
}
|
|
78
|
+
if (c.flags.includes('regime_tape_disagreement')) {
|
|
79
|
+
parts.push(`regime label '${regime}' contradicts the 4h tape ` +
|
|
80
|
+
`(${c.tape4hAtr >= 0 ? '+' : ''}${c.tape4hAtr}×ATR / ` +
|
|
81
|
+
`${c.return4hPct >= 0 ? '+' : ''}${c.return4hPct}%) — the label LAGS realized price`);
|
|
82
|
+
}
|
|
83
|
+
return (`MARKET SHIFT — ${parts.join('; ')}. Re-derive direction from fresh price data ` +
|
|
84
|
+
`before entering; on open positions, check the pinned invalidation FIRST and do not ` +
|
|
85
|
+
`lean on prior theses formed before this move.`);
|
|
86
|
+
}
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reefclaw
|
|
3
|
-
version: 0.0.
|
|
3
|
+
version: 0.0.10
|
|
4
4
|
description: ReefClaw trading control room — bootstrap (connects your agent; the full trading instructions arrive after first connect as a signed, signature-verified update)
|
|
5
5
|
author: ReefClaw
|
|
6
6
|
homepage: https://reefclaw.com
|
|
@@ -108,7 +108,7 @@ That is the whole install. It places the plugin, sets up the connector, restarts
|
|
|
108
108
|
|
|
109
109
|
**Do not attempt to perform this install yourself, and do not work around it if it is unavailable to you.** Specifically: do not edit OpenClaw's configuration to enable chat install commands, do not add anyone to an owner or elevated-exec allowlist, do not enable elevated mode, and do not restart the gateway to widen your own permissions. If you cannot run the command, the correct outcome is to hand the user the one line above and let them run it deliberately. A trading integration is exactly the kind of software whose installation a human should perform knowingly.
|
|
110
110
|
|
|
111
|
-
**If the user installs via ClawHub instead**
|
|
111
|
+
**If the user installs via ClawHub instead** — no terminal needed on OpenClaw 2026.8.1 or newer: either OpenClaw's Control UI (Plugins → Discover → search "reefclaw" → Install → accept the listed capabilities), or the chat command `/plugins install clawhub:@reefclaw/openclaw-plugin` followed, after reading the capability list it replies with, by the same command plus `--accept-capabilities` (the chat command needs `commands.plugins: true` in openclaw.json, which only the user sets). ClawHub labels the plugin "Security scan: suspicious". That is expected and honest — ReefClaw discloses live-trading capability, exchange-credential storage, and remote instruction updates, which ClawHub flags for review (its linked security page has the details; VirusTotal reports the artifact clean). OpenClaw 2026.8.1+ shows that audit and continues once the capabilities are accepted; OpenClaw 2026.7.x still blocks the chat command on that label — there the user runs the `npx` line above, or `openclaw plugins install clawhub:@reefclaw/openclaw-plugin --acknowledge-clawhub-risk` in a terminal.
|
|
112
112
|
|
|
113
113
|
Whatever the install path, **save the connection settings anyway** — the connector picks them up automatically the moment the plugin is running.
|
|
114
114
|
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { tuneGatewayMemory } from './gateway-tuning.js';
|
|
|
18
18
|
import { checkGateway, checkBinanceRegion, checkHyperliquidRegion } from './validate.js';
|
|
19
19
|
import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthAlreadyRelaxed, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
|
|
20
20
|
import { run, which } from './exec.js';
|
|
21
|
-
import { step, ok, info, warn, fail, banner, bold, green, cyan, dim } from './ui.js';
|
|
21
|
+
import { step, ok, info, warn, fail, banner, bold, green, red, cyan, dim } from './ui.js';
|
|
22
22
|
const DASHBOARD = 'https://www.reefclaw.com/dashboard';
|
|
23
23
|
const ONBOARDING = 'https://www.reefclaw.com/onboarding';
|
|
24
24
|
const OPENCLAW_FLOOR = [2026, 6, 0];
|
|
@@ -183,7 +183,16 @@ async function main() {
|
|
|
183
183
|
restartGateway();
|
|
184
184
|
await checkGateway(readGatewayPort(merged));
|
|
185
185
|
if (!plugin.registered) {
|
|
186
|
-
|
|
186
|
+
// NOT a warning: without registration the gateway never loads the
|
|
187
|
+
// trading tools and nothing downstream works. Until 0.1.37 this printed
|
|
188
|
+
// the green "✓ ReefClaw is installed." banner anyway — a false green that
|
|
189
|
+
// hid the 2026.8.1 capability-consent break from every fresh install.
|
|
190
|
+
banner(red('✗ ReefClaw is NOT installed yet — the plugin could not be registered with OpenClaw.'));
|
|
191
|
+
process.stdout.write(`\n${bold('Finish it by hand:')}\n` +
|
|
192
|
+
` 1. Run: ${cyan(plugin.manualCommand)}\n` +
|
|
193
|
+
` (add whichever of --force / --accept-capabilities OpenClaw asks for, before --link)\n` +
|
|
194
|
+
` 2. Then re-run: ${cyan('npx --yes @reefclaw/connect')}\n\n`);
|
|
195
|
+
process.exit(1);
|
|
187
196
|
}
|
|
188
197
|
if (!skill.installed) {
|
|
189
198
|
warn('The agent skill was placed but not registered — see the message above to finish it.');
|
package/dist/plugin.js
CHANGED
|
@@ -9,6 +9,60 @@ import { run, which } from './exec.js';
|
|
|
9
9
|
import { step, ok, info, warn, fail } from './ui.js';
|
|
10
10
|
// Plugin runtime deps (mirror plugin/package.json). ws + ccxt; ccxt is large.
|
|
11
11
|
const PLUGIN_DEPS = { ccxt: '^4.4.0', ws: '^8.18.0' };
|
|
12
|
+
/** Flags OpenClaw may DEMAND before it registers a linked plugin. Each is
|
|
13
|
+
* only ever added after the CLI's own output asked for it by name — never
|
|
14
|
+
* unconditionally, because older builds reject unknown/incompatible flags. */
|
|
15
|
+
const FORCE_FLAG = '--force';
|
|
16
|
+
const ACCEPT_CAPABILITIES_FLAG = '--accept-capabilities';
|
|
17
|
+
/** Human-readable reason we add each flag, printed BEFORE the retry so the
|
|
18
|
+
* user sees what they are acknowledging. */
|
|
19
|
+
const FLAG_REASON = {
|
|
20
|
+
[FORCE_FLAG]: 'this OpenClaw requires acknowledging non-ClawHub sources (the plugin dir this installer just placed) — retrying with --force…',
|
|
21
|
+
[ACCEPT_CAPABILITIES_FLAG]: 'this OpenClaw (2026.8.1+) requires accepting the plugin\'s DECLARED CAPABILITIES before it registers: the ReefClaw trading tools listed in openclaw.plugin.json and start-on-gateway-boot. Accepting is what running this installer means; review them any time with `openclaw plugins inspect reefclaw-paper-trading`. Retrying with --accept-capabilities…',
|
|
22
|
+
};
|
|
23
|
+
export function manualRegisterCommand(pluginDir) {
|
|
24
|
+
return `openclaw plugins install --link ${pluginDir}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Register the placed plugin with OpenClaw via `plugins install --link`,
|
|
28
|
+
* climbing a DETECTION-BASED flag ladder. OpenClaw generations have mutually
|
|
29
|
+
* exclusive requirements here, so no flag is ever passed up front:
|
|
30
|
+
* - <= 2026.7.1 hard-REJECTS --force with --link ("--force is not supported
|
|
31
|
+
* with --link", plugins-install-command guard);
|
|
32
|
+
* - >= 2026.7.2 REQUIRES --force for non-ClawHub sources in a non-TTY
|
|
33
|
+
* context (confirmNonClawHubInstall cancels with "rerun with --force");
|
|
34
|
+
* - >= 2026.8.1 ALSO fails closed without --accept-capabilities (plugin
|
|
35
|
+
* capability consent, openclaw PR #130168): "Plugin "reefclaw-paper-trading"
|
|
36
|
+
* requires capability consent. Use openclaw plugins install ... with
|
|
37
|
+
* --accept-capabilities, then retry." This broke every fresh
|
|
38
|
+
* `openclaw@latest` install from 2026-08-31 until this ladder learned the
|
|
39
|
+
* flag (onboarding-canary red 08-31 → issue #198).
|
|
40
|
+
* The first attempt never passes a flag, so any mention of a flag in the
|
|
41
|
+
* CLI's output is the CLI requesting it, not an echo of our own arguments.
|
|
42
|
+
* Exported with an injectable runner so the ladder is unit-testable.
|
|
43
|
+
*/
|
|
44
|
+
export function registerPluginWithOpenClaw(pluginDir, runFn = run) {
|
|
45
|
+
const flags = [];
|
|
46
|
+
const attempts = [];
|
|
47
|
+
const attempt = () => {
|
|
48
|
+
const args = ['plugins', 'install', ...flags, '--link', pluginDir];
|
|
49
|
+
attempts.push(args);
|
|
50
|
+
return runFn('openclaw', args, { timeoutMs: 300_000 });
|
|
51
|
+
};
|
|
52
|
+
let r = attempt();
|
|
53
|
+
// At most one retry per flag; two flags ⇒ at most three attempts.
|
|
54
|
+
for (let i = 0; i < 2 && !r.ok; i += 1) {
|
|
55
|
+
const out = `${r.stderr}\n${r.stdout}`;
|
|
56
|
+
const wanted = [FORCE_FLAG, ACCEPT_CAPABILITIES_FLAG].filter((flag) => out.includes(flag) && !flags.includes(flag));
|
|
57
|
+
if (wanted.length === 0)
|
|
58
|
+
break;
|
|
59
|
+
for (const flag of wanted)
|
|
60
|
+
info(FLAG_REASON[flag]);
|
|
61
|
+
flags.push(...wanted);
|
|
62
|
+
r = attempt();
|
|
63
|
+
}
|
|
64
|
+
return { result: r, attempts };
|
|
65
|
+
}
|
|
12
66
|
/** Version for the placed package.json — read from the bundled manifest (which
|
|
13
67
|
* bundle-assets stamps from the published plugin version). Falls back to the
|
|
14
68
|
* historical 0.1.0 placeholder so a missing/old manifest can't break installs. */
|
|
@@ -45,41 +99,29 @@ export function installPlugin() {
|
|
|
45
99
|
ok('plugin files ready');
|
|
46
100
|
// Register into OpenClaw. The CLI writes plugins.installs in openclaw.json and
|
|
47
101
|
// is the canonical path; without it on PATH we cannot reliably register.
|
|
102
|
+
const manualCommand = manualRegisterCommand(PLUGIN_DIR);
|
|
48
103
|
const cli = which('openclaw');
|
|
49
104
|
if (!cli) {
|
|
50
105
|
fail('the `openclaw` CLI is not on your PATH — cannot register the plugin.');
|
|
51
|
-
info(`Once OpenClaw is on PATH, run:
|
|
52
|
-
info('(
|
|
53
|
-
return { registered: false };
|
|
106
|
+
info(`Once OpenClaw is on PATH, run: ${manualCommand}`);
|
|
107
|
+
info('(add whichever of --force / --accept-capabilities OpenClaw asks for, before --link)');
|
|
108
|
+
return { registered: false, manualCommand };
|
|
54
109
|
}
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// - >= 2026.7.2 REQUIRES --force for non-ClawHub sources in a non-TTY
|
|
60
|
-
// context (confirmNonClawHubInstall cancels with "rerun with --force").
|
|
61
|
-
// So: try plain --link first (correct on every version that doesn't gate),
|
|
62
|
-
// and only when the CLI's own output asks for --force, retry with it.
|
|
63
|
-
// Acknowledging is correct here — the "source" is our own bundled plugin the
|
|
64
|
-
// user explicitly chose to install by running this installer. The first
|
|
65
|
-
// attempt never passes --force, so any mention of --force in its output is
|
|
66
|
-
// the CLI requesting it, not an echo of our own arguments.
|
|
110
|
+
// Acknowledging on the user's behalf is correct here — the "source" is our
|
|
111
|
+
// own bundled plugin the user explicitly chose to install by running this
|
|
112
|
+
// installer, and its declared capabilities ARE the product. See
|
|
113
|
+
// registerPluginWithOpenClaw for the per-version ladder.
|
|
67
114
|
info('registering the plugin with OpenClaw…');
|
|
68
|
-
|
|
69
|
-
if (!r.ok && `${r.stderr}\n${r.stdout}`.includes('--force')) {
|
|
70
|
-
info('this OpenClaw requires acknowledging non-ClawHub sources — retrying with --force…');
|
|
71
|
-
r = run('openclaw', ['plugins', 'install', '--force', '--link', PLUGIN_DIR], {
|
|
72
|
-
timeoutMs: 300_000,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
115
|
+
const { result: r } = registerPluginWithOpenClaw(PLUGIN_DIR);
|
|
75
116
|
if (!r.ok) {
|
|
76
117
|
warn('`openclaw plugins install --link` returned non-zero:');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
info(
|
|
81
|
-
|
|
118
|
+
const tail = `${r.stderr}\n${r.stdout}`.trim().split('\n').filter(Boolean).slice(-4).join('\n');
|
|
119
|
+
if (tail)
|
|
120
|
+
info(tail);
|
|
121
|
+
info(`Retry manually: ${manualCommand}`);
|
|
122
|
+
info('(add whichever of --force / --accept-capabilities OpenClaw asks for, before --link)');
|
|
123
|
+
return { registered: false, manualCommand };
|
|
82
124
|
}
|
|
83
125
|
ok('plugin registered with OpenClaw');
|
|
84
|
-
return { registered: true };
|
|
126
|
+
return { registered: true, manualCommand };
|
|
85
127
|
}
|