@tradejs/cli 2.0.14 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/lib/runtimeScorecard.ts
21
+ var runtimeScorecard_exports = {};
22
+ __export(runtimeScorecard_exports, {
23
+ buildRuntimeScorecard: () => buildRuntimeScorecard,
24
+ formatRuntimeScorecardMarkdown: () => formatRuntimeScorecardMarkdown
25
+ });
26
+ module.exports = __toCommonJS(runtimeScorecard_exports);
27
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
28
+ var asArray = (value) => Array.isArray(value) ? value : [];
29
+ var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
30
+ var finiteString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
31
+ var round = (value, decimals = 6) => Number(value.toFixed(decimals));
32
+ var unwrapRows = (value, key) => asArray(value).map((item) => asRecord(asRecord(item)?.[key]) ?? asRecord(item)).filter((item) => item != null);
33
+ var runtimePayload = (artifact) => {
34
+ const root = asRecord(artifact) ?? {};
35
+ return asRecord(root.runtime) ?? root;
36
+ };
37
+ var runtimeWindow = (artifact) => {
38
+ const root = asRecord(artifact) ?? {};
39
+ return asRecord(root.window) ?? asRecord(runtimePayload(artifact).window) ?? {};
40
+ };
41
+ var extractRuntimeRows = (artifact) => {
42
+ const runtime = runtimePayload(artifact);
43
+ return {
44
+ evaluations: unwrapRows(runtime.evaluations, "evaluation"),
45
+ signals: unwrapRows(runtime.signals, "signal"),
46
+ trades: unwrapRows(runtime.trades, "trade"),
47
+ statsBuckets: asArray(runtime.evaluationStatsBuckets).map(asRecord).filter((item) => item != null)
48
+ };
49
+ };
50
+ var countBy = (items, getter) => {
51
+ const counts = {};
52
+ for (const item of items) {
53
+ const key = getter(item);
54
+ if (!key) continue;
55
+ counts[key] = (counts[key] ?? 0) + 1;
56
+ }
57
+ return Object.fromEntries(
58
+ Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))
59
+ );
60
+ };
61
+ var buildDistributions = ({
62
+ evaluations,
63
+ signals
64
+ }) => {
65
+ const candidates = evaluations.filter(
66
+ (evaluation) => evaluation.status === "signal"
67
+ );
68
+ return {
69
+ candidatesByStrategy: countBy(
70
+ candidates,
71
+ (evaluation) => finiteString(evaluation.strategy)
72
+ ),
73
+ orderStatus: countBy(signals, (signal) => finiteString(signal.orderStatus)),
74
+ skipReasons: countBy(
75
+ signals,
76
+ (signal) => finiteString(signal.orderSkipReason)
77
+ ),
78
+ aiQuality: countBy(candidates, (evaluation) => {
79
+ const quality = finiteNumber(asRecord(evaluation.aiAnalysis)?.quality);
80
+ return quality == null ? null : String(quality);
81
+ })
82
+ };
83
+ };
84
+ var buildDistributionDelta = (current, previous) => {
85
+ const currentTotal = Object.values(current).reduce(
86
+ (total, value) => total + value,
87
+ 0
88
+ );
89
+ const previousTotal = Object.values(previous).reduce(
90
+ (total, value) => total + value,
91
+ 0
92
+ );
93
+ const keys = [
94
+ .../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(previous)])
95
+ ].sort((left, right) => left.localeCompare(right));
96
+ return Object.fromEntries(
97
+ keys.map((key) => {
98
+ const currentCount = current[key] ?? 0;
99
+ const previousCount = previous[key] ?? 0;
100
+ const currentShare = currentTotal > 0 ? currentCount / currentTotal : 0;
101
+ const previousShare = previousTotal > 0 ? previousCount / previousTotal : 0;
102
+ return [
103
+ key,
104
+ {
105
+ currentCount,
106
+ previousCount,
107
+ currentShare: round(currentShare),
108
+ previousShare: round(previousShare),
109
+ shareDelta: round(currentShare - previousShare)
110
+ }
111
+ ];
112
+ })
113
+ );
114
+ };
115
+ var sum = (values) => round(values.reduce((total, value) => total + (value ?? 0), 0));
116
+ var statsCount = (buckets, field) => sum(buckets.map((bucket) => finiteNumber(asRecord(bucket.stats)?.[field])));
117
+ var tradeTimestamp = (trade) => finiteNumber(trade.exitTimestamp) ?? finiteNumber(trade.lastSyncedAt) ?? finiteNumber(trade.entryTimestamp) ?? 0;
118
+ var dedupeTrades = (artifacts) => {
119
+ const tradesByOrderId = /* @__PURE__ */ new Map();
120
+ for (const artifact of artifacts) {
121
+ for (const trade of extractRuntimeRows(artifact).trades) {
122
+ const orderId = finiteString(trade.orderId);
123
+ if (!orderId) continue;
124
+ const existing = tradesByOrderId.get(orderId);
125
+ if (!existing || tradeTimestamp(trade) >= tradeTimestamp(existing)) {
126
+ tradesByOrderId.set(orderId, trade);
127
+ }
128
+ }
129
+ }
130
+ return [...tradesByOrderId.values()];
131
+ };
132
+ var maxDrawdown = (trades) => {
133
+ let cumulative = 0;
134
+ let peak = 0;
135
+ let drawdown = 0;
136
+ for (const trade of trades.sort(
137
+ (left, right) => tradeTimestamp(left) - tradeTimestamp(right)
138
+ )) {
139
+ cumulative += finiteNumber(trade.closedPnl) ?? 0;
140
+ peak = Math.max(peak, cumulative);
141
+ drawdown = Math.max(drawdown, peak - cumulative);
142
+ }
143
+ return round(drawdown);
144
+ };
145
+ var buildRollingPerformance = ({
146
+ trades,
147
+ anchorTime,
148
+ days
149
+ }) => {
150
+ const startTime = anchorTime - days * 24 * 60 * 60 * 1e3;
151
+ const closed = trades.filter((trade) => {
152
+ const exitTimestamp = finiteNumber(trade.exitTimestamp);
153
+ return trade.status === "closed" && exitTimestamp != null && exitTimestamp >= startTime && exitTimestamp < anchorTime;
154
+ });
155
+ const realizedPnl = sum(closed.map((trade) => finiteNumber(trade.closedPnl)));
156
+ return {
157
+ days,
158
+ closedTrades: closed.length,
159
+ realizedPnl,
160
+ expectancy: closed.length > 0 ? round(realizedPnl / closed.length) : null,
161
+ maxDrawdown: maxDrawdown(closed)
162
+ };
163
+ };
164
+ var getReplayComparison = (artifact) => {
165
+ const root = asRecord(artifact) ?? {};
166
+ return asRecord(asRecord(root.replay)?.runtimeComparison) ?? null;
167
+ };
168
+ var getCalibrationSummary = (artifact) => {
169
+ const root = asRecord(artifact) ?? {};
170
+ return asRecord(asRecord(root.summary)?.all) ?? null;
171
+ };
172
+ var buildRuntimeScorecard = ({
173
+ runtimeArtifact,
174
+ replayEvidenceArtifact,
175
+ calibrationArtifact,
176
+ historyRuntimeArtifacts = [],
177
+ thresholds = {
178
+ minimumParityRatio: 0.95,
179
+ maximumSlippageResidualBps: 3,
180
+ minimumClosedTrades: 20,
181
+ minimumExpectancy: 0
182
+ },
183
+ generatedAt = Date.now()
184
+ }) => {
185
+ const rows = extractRuntimeRows(runtimeArtifact);
186
+ const window = runtimeWindow(runtimeArtifact);
187
+ const startTime = finiteNumber(window.startTime) ?? generatedAt - 864e5;
188
+ const endTime = finiteNumber(window.endTime) ?? generatedAt;
189
+ const evaluationsFromStats = statsCount(rows.statsBuckets, "evaluated");
190
+ const candidatesFromStats = statsCount(rows.statsBuckets, "signals");
191
+ const signalEvaluations = rows.evaluations.filter(
192
+ (evaluation) => evaluation.status === "signal"
193
+ );
194
+ const gateDecisions = signalEvaluations.map((evaluation) => ({
195
+ evaluation,
196
+ decision: finiteString(asRecord(evaluation.aiAnalysis)?.gateDecision)
197
+ })).filter(
198
+ ({ decision }) => decision === "approved" || decision === "rejected"
199
+ );
200
+ const allocatorDecisions = signalEvaluations.map((evaluation) => asRecord(evaluation.allocatorDecision)).filter((item) => item != null);
201
+ const riskDecisions = signalEvaluations.map((evaluation) => asRecord(evaluation.riskDecision)).filter((item) => item != null);
202
+ const orderAttempts = rows.signals.filter(
203
+ (signal) => signal.orderStatus === "completed" || signal.orderStatus === "failed"
204
+ );
205
+ const currentClosedTrades = rows.trades.filter((trade) => {
206
+ const exitTimestamp = finiteNumber(trade.exitTimestamp);
207
+ return trade.status === "closed" && exitTimestamp != null && exitTimestamp >= startTime && exitTimestamp < endTime;
208
+ });
209
+ const replayComparison = getReplayComparison(replayEvidenceArtifact);
210
+ const replayCounts = asRecord(replayComparison?.counts);
211
+ const matched = finiteNumber(replayCounts?.matched) ?? 0;
212
+ const backtestOnly = finiteNumber(replayCounts?.backtestOnly) ?? 0;
213
+ const runtimeOnly = finiteNumber(replayCounts?.runtimeOnly) ?? 0;
214
+ const comparisonTotal = matched + backtestOnly + runtimeOnly;
215
+ const parityRatio = comparisonTotal > 0 ? matched / comparisonTotal : null;
216
+ const lineage = asRecord(replayComparison?.lineage);
217
+ const lineageReason = finiteString(lineage?.reason);
218
+ const calibration = getCalibrationSummary(calibrationArtifact);
219
+ const actualSlippageBps = finiteNumber(
220
+ asRecord(calibration?.signalToFillAdverseBps)?.avg
221
+ );
222
+ const residualVsModelBps = finiteNumber(
223
+ asRecord(calibration?.residualVsCurrentModelBps)?.avg
224
+ );
225
+ const historyTrades = dedupeTrades([
226
+ ...historyRuntimeArtifacts,
227
+ runtimeArtifact
228
+ ]);
229
+ const currentDistributions = buildDistributions(rows);
230
+ const previousArtifact = historyRuntimeArtifacts.map((artifact) => ({ artifact, window: runtimeWindow(artifact) })).filter(({ window: candidateWindow }) => {
231
+ const candidateEndTime = finiteNumber(candidateWindow.endTime);
232
+ return candidateEndTime != null && candidateEndTime <= startTime;
233
+ }).sort(
234
+ (left, right) => (finiteNumber(right.window.endTime) ?? 0) - (finiteNumber(left.window.endTime) ?? 0)
235
+ )[0];
236
+ const previousDistributions = previousArtifact ? buildDistributions(extractRuntimeRows(previousArtifact.artifact)) : null;
237
+ const rolling = [7, 30, 90].map(
238
+ (days) => buildRollingPerformance({
239
+ trades: [...historyTrades],
240
+ anchorTime: endTime,
241
+ days
242
+ })
243
+ );
244
+ const sevenDay = rolling[0];
245
+ const reactions = [];
246
+ if (lineageReason) {
247
+ reactions.push({
248
+ code: "LINEAGE_NOT_COMPARABLE",
249
+ severity: "blocking",
250
+ message: lineageReason
251
+ });
252
+ }
253
+ if (parityRatio != null && parityRatio < thresholds.minimumParityRatio) {
254
+ reactions.push({
255
+ code: "PARITY_REGRESSION",
256
+ severity: "blocking",
257
+ message: `Parity ${round(parityRatio * 100, 2)}% is below ${round(thresholds.minimumParityRatio * 100, 2)}%.`
258
+ });
259
+ }
260
+ if (residualVsModelBps != null && residualVsModelBps > thresholds.maximumSlippageResidualBps) {
261
+ reactions.push({
262
+ code: "SLIPPAGE_DRIFT",
263
+ severity: "warning",
264
+ message: `Execution residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
265
+ });
266
+ }
267
+ if (sevenDay.closedTrades >= thresholds.minimumClosedTrades && sevenDay.expectancy != null && sevenDay.expectancy < thresholds.minimumExpectancy) {
268
+ reactions.push({
269
+ code: "EXPECTANCY_DEGRADATION",
270
+ severity: "blocking",
271
+ message: `7d expectancy ${sevenDay.expectancy} is below ${thresholds.minimumExpectancy}.`
272
+ });
273
+ }
274
+ if (sevenDay.closedTrades < thresholds.minimumClosedTrades) {
275
+ reactions.push({
276
+ code: "INSUFFICIENT_RECENT_SAMPLE",
277
+ severity: "info",
278
+ message: `Only ${sevenDay.closedTrades} closed trades in 7d; ${thresholds.minimumClosedTrades} required for expectancy reaction.`
279
+ });
280
+ }
281
+ const promotionStatus = reactions.some(
282
+ (reaction) => reaction.severity === "blocking"
283
+ ) ? "PROMOTION_BLOCKED" : reactions.some((reaction) => reaction.severity === "warning") ? "REVIEW_REQUIRED" : "OBSERVE";
284
+ return {
285
+ reportType: "runtime-scorecard",
286
+ schemaVersion: 1,
287
+ generatedAt,
288
+ window: { startTime, endTime },
289
+ promotionStatus,
290
+ thresholds,
291
+ funnel: {
292
+ evaluations: evaluationsFromStats > 0 ? evaluationsFromStats : rows.evaluations.length,
293
+ coreCandidates: candidatesFromStats > 0 ? candidatesFromStats : signalEvaluations.length,
294
+ gate: {
295
+ available: gateDecisions.length > 0,
296
+ approved: gateDecisions.filter(
297
+ ({ decision }) => decision === "approved"
298
+ ).length,
299
+ rejected: gateDecisions.filter(
300
+ ({ decision }) => decision === "rejected"
301
+ ).length,
302
+ coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null
303
+ },
304
+ allocator: {
305
+ available: allocatorDecisions.length > 0,
306
+ approved: allocatorDecisions.filter(
307
+ (decision) => decision.decision === "approved"
308
+ ).length,
309
+ rejected: allocatorDecisions.filter(
310
+ (decision) => decision.decision === "rejected"
311
+ ).length
312
+ },
313
+ risk: {
314
+ available: riskDecisions.length > 0,
315
+ approved: riskDecisions.filter(
316
+ (decision) => decision.decision === "approved"
317
+ ).length,
318
+ rejected: riskDecisions.filter(
319
+ (decision) => decision.decision === "rejected"
320
+ ).length
321
+ },
322
+ orderAttempts: orderAttempts.length,
323
+ orderFailures: orderAttempts.filter(
324
+ (signal) => signal.orderStatus === "failed"
325
+ ).length,
326
+ balanceRejects: rows.signals.filter(
327
+ (signal) => /BALANCE|MARGIN/i.test(
328
+ `${finiteString(signal.orderSkipReason) ?? ""} ${finiteString(signal.orderFailureReason) ?? ""}`
329
+ )
330
+ ).length,
331
+ fills: rows.trades.length,
332
+ closedTrades: currentClosedTrades.length
333
+ },
334
+ realized: {
335
+ pnl: sum(
336
+ currentClosedTrades.map((trade) => finiteNumber(trade.closedPnl))
337
+ ),
338
+ fees: sum(
339
+ currentClosedTrades.map((trade) => finiteNumber(trade.totalFee))
340
+ ),
341
+ funding: sum(
342
+ currentClosedTrades.map((trade) => finiteNumber(trade.fundingFee))
343
+ )
344
+ },
345
+ parity: {
346
+ available: comparisonTotal > 0,
347
+ matched,
348
+ backtestOnly,
349
+ runtimeOnly,
350
+ ratio: parityRatio == null ? null : round(parityRatio),
351
+ lineageReason
352
+ },
353
+ execution: {
354
+ available: calibration != null,
355
+ actualSignalToFillSlippageBps: actualSlippageBps,
356
+ residualVsCurrentModelBps: residualVsModelBps
357
+ },
358
+ rolling,
359
+ distributions: currentDistributions,
360
+ distributionChanges: {
361
+ available: previousDistributions != null,
362
+ baselineWindow: previousArtifact ? {
363
+ startTime: finiteNumber(previousArtifact.window.startTime),
364
+ endTime: finiteNumber(previousArtifact.window.endTime)
365
+ } : null,
366
+ metrics: previousDistributions ? Object.fromEntries(
367
+ Object.entries(currentDistributions).map(([name, current]) => [
368
+ name,
369
+ buildDistributionDelta(
370
+ current,
371
+ previousDistributions[name]
372
+ )
373
+ ])
374
+ ) : {}
375
+ },
376
+ reactions
377
+ };
378
+ };
379
+ var formatRuntimeScorecardMarkdown = (scorecard) => {
380
+ const rollingRows = scorecard.rolling.map(
381
+ (row) => `| ${row.days}d | ${row.closedTrades} | ${row.realizedPnl} | ${row.expectancy ?? "n/a"} | ${row.maxDrawdown} |`
382
+ ).join("\n");
383
+ const reactions = scorecard.reactions.length ? scorecard.reactions.map(
384
+ (reaction) => `- ${reaction.severity.toUpperCase()} \`${reaction.code}\`: ${reaction.message}`
385
+ ).join("\n") : "- No automatic reactions.";
386
+ const distributionChanges = Object.entries(
387
+ scorecard.distributionChanges.metrics
388
+ ).flatMap(
389
+ ([metric, values]) => Object.entries(values).map(([value, change]) => ({
390
+ metric,
391
+ value,
392
+ shareDelta: change.shareDelta
393
+ }))
394
+ ).sort(
395
+ (left, right) => Math.abs(right.shareDelta) - Math.abs(left.shareDelta)
396
+ ).slice(0, 10).map(
397
+ (change) => `- ${change.metric}.${change.value}: ${change.shareDelta >= 0 ? "+" : ""}${round(change.shareDelta * 100, 2)} pp`
398
+ ).join("\n");
399
+ return `# Runtime scorecard
400
+
401
+ Status: **${scorecard.promotionStatus}**
402
+
403
+ Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(scorecard.window.endTime).toISOString()}
404
+
405
+ ## Funnel
406
+
407
+ - Evaluations: ${scorecard.funnel.evaluations}
408
+ - Core candidates: ${scorecard.funnel.coreCandidates}
409
+ - Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected}
410
+ - Allocator approvals/rejects: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}` : "unavailable"}
411
+ - Risk approvals/rejects: ${scorecard.funnel.risk.available ? `${scorecard.funnel.risk.approved}/${scorecard.funnel.risk.rejected}` : "unavailable"}
412
+ - Order attempts/failures: ${scorecard.funnel.orderAttempts}/${scorecard.funnel.orderFailures}
413
+ - Fills: ${scorecard.funnel.fills}
414
+ - Closed trades: ${scorecard.funnel.closedTrades}
415
+
416
+ ## Replay and execution
417
+
418
+ - Parity: ${scorecard.parity.ratio == null ? "n/a" : `${round(scorecard.parity.ratio * 100, 2)}%`}
419
+ - Runtime/replay mismatches: ${scorecard.parity.backtestOnly + scorecard.parity.runtimeOnly}
420
+ - Actual signal-to-fill slippage: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
421
+ - Residual vs current model: ${scorecard.execution.residualVsCurrentModelBps ?? "n/a"} bps
422
+
423
+ ## Rolling performance
424
+
425
+ | Window | Closed | Realized PnL | Expectancy | Max drawdown |
426
+ | --- | ---: | ---: | ---: | ---: |
427
+ ${rollingRows}
428
+
429
+ ## Distribution changes
430
+
431
+ ${scorecard.distributionChanges.available ? distributionChanges || "- No distribution changes." : "- Previous comparable artifact is unavailable."}
432
+
433
+ ## Reactions
434
+
435
+ ${reactions}
436
+ `;
437
+ };
438
+ // Annotate the CommonJS export names for ESM import in node:
439
+ 0 && (module.exports = {
440
+ buildRuntimeScorecard,
441
+ formatRuntimeScorecardMarkdown
442
+ });
@@ -281,6 +281,15 @@ var syncRuntimeTrades = async ({
281
281
  nextTrade,
282
282
  { expire: import_constants.TTL_1M }
283
283
  ),
284
+ (0, import_redis.setHashJsonField)(
285
+ import_redis.redisKeys.runtimeClosedTradeBucket(
286
+ userName,
287
+ (0, import_time.getRuntimeStorageDayKey)(nextTrade.exitTimestamp)
288
+ ),
289
+ trade.orderId,
290
+ nextTrade,
291
+ { expire: import_constants.TTL_1M }
292
+ ),
284
293
  ...isCurrentActiveTrade ? [(0, import_redis.delKey)(activeTradeKey)] : []
285
294
  ]);
286
295
  syncedTrades.push(nextTrade);
@@ -37,7 +37,7 @@ var import_args = __toESM(require("args"));
37
37
  var import_chalk = __toESM(require("chalk"));
38
38
  var import_promises = __toESM(require("fs/promises"));
39
39
  var import_node_os = __toESM(require("os"));
40
- var import_node_path2 = __toESM(require("path"));
40
+ var import_node_path = __toESM(require("path"));
41
41
  var import_node_child_process = require("child_process");
42
42
  var import_constants = require("@tradejs/core/constants");
43
43
  var import_redis = require("@tradejs/infra/redis");
@@ -113,17 +113,9 @@ ${chunk}`, {
113
113
  };
114
114
 
115
115
  // src/lib/researchAgent.ts
116
- var import_node_path = __toESM(require("path"));
117
116
  var RESEARCH_AGENT_MODEL = "openai/gpt-5.4";
118
117
  var RESEARCH_AGENT_REASONING_EFFORT = "medium";
119
118
  var RESEARCH_AGENT_BRANCH_PREFIX = "codex/research";
120
- var toUpperSnakeCase = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
121
- var RESEARCH_NOTE_FILE_OVERRIDES = {
122
- TrendLine: "AI_TRENDLINE_REPLAY_NOTES.md",
123
- ReverseTrendLine: "AI_REVERSE_TRENDLINE_REPLAY_NOTES.md",
124
- AdaptiveMomentumRibbon: "AI_ADAPTIVE_MOMENTUM_RIBBON_REPLAY_NOTES.md",
125
- VolumeDivergence: "AI_VOLUME_DIVERGENCE_REPLAY_NOTES.md"
126
- };
127
119
  var buildResearchAgentBranchName = (strategy, runId) => {
128
120
  const normalizedStrategy = strategy.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
129
121
  const normalizedRunId = runId.trim().replace(/[^a-zA-Z0-9-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
@@ -131,13 +123,8 @@ var buildResearchAgentBranchName = (strategy, runId) => {
131
123
  };
132
124
  var buildResearchAgentCommitMessage = (strategy, runId) => `Research agent: ${strategy} follow-up for ${runId}`;
133
125
  var buildResearchAgentPrTitle = (strategy, runId) => `Research agent: ${strategy} follow-up for ${runId}`;
134
- var getResearchAgentNotePath = (strategy) => import_node_path.default.join(
135
- "notes",
136
- RESEARCH_NOTE_FILE_OVERRIDES[strategy] || `AI_${toUpperSnakeCase(strategy)}_REPLAY_NOTES.md`
137
- );
138
126
  var getResearchAgentAllowedPathPrefixes = (strategy) => [
139
- `packages/strategies/src/${strategy}/`,
140
- getResearchAgentNotePath(strategy)
127
+ `packages/strategies/src/${strategy}/`
141
128
  ];
142
129
  var parseGithubRepositoryFromRemote = (value) => {
143
130
  const trimmed = value.trim();
@@ -263,7 +250,7 @@ Output ONLY one of:
263
250
 
264
251
  Rules:
265
252
  - Keep the patch small and surgical.
266
- - Prefer strategy-scoped tests and notes updates over broad refactors.
253
+ - Prefer strategy-scoped tests over broad refactors.
267
254
  - If you change strategy logic, also add or update tests in the same strategy directory.
268
255
  - Do not touch package boundaries or unrelated files.
269
256
  - Do not add markdown fences, commentary, or explanations.
@@ -332,7 +319,7 @@ var listFilesRecursive = async (targetPath) => {
332
319
  const entries = await import_promises.default.readdir(targetPath, { withFileTypes: true });
333
320
  const nested = await Promise.all(
334
321
  entries.map(async (entry) => {
335
- const absolutePath = import_node_path2.default.join(targetPath, entry.name);
322
+ const absolutePath = import_node_path.default.join(targetPath, entry.name);
336
323
  if (entry.isDirectory()) {
337
324
  return listFilesRecursive(absolutePath);
338
325
  }
@@ -345,27 +332,21 @@ var listFilesRecursive = async (targetPath) => {
345
332
  }
346
333
  };
347
334
  var collectStrategyContextFiles = async (worktreePath, strategy) => {
348
- const strategyRoot = import_node_path2.default.join(
335
+ const strategyRoot = import_node_path.default.join(
349
336
  worktreePath,
350
337
  "packages",
351
338
  "strategies",
352
339
  "src",
353
340
  strategy
354
341
  );
355
- const notePath = import_node_path2.default.join(worktreePath, getResearchAgentNotePath(strategy));
356
342
  const strategyFiles = await listFilesRecursive(strategyRoot);
357
343
  const files = strategyFiles.filter(
358
344
  (filePath) => /\.(ts|tsx|md)$/.test(filePath)
359
345
  );
360
- try {
361
- await import_promises.default.access(notePath);
362
- files.push(notePath);
363
- } catch {
364
- }
365
346
  return [...new Set(files)].sort((left, right) => left.localeCompare(right));
366
347
  };
367
348
  var renderFileContext = async (worktreePath, filePath) => {
368
- const relativePath = import_node_path2.default.relative(worktreePath, filePath).replace(/\\/g, "/");
349
+ const relativePath = import_node_path.default.relative(worktreePath, filePath).replace(/\\/g, "/");
369
350
  const content = await import_promises.default.readFile(filePath, "utf8");
370
351
  return [
371
352
  `=== FILE: ${relativePath} ===`,
@@ -390,9 +371,12 @@ var buildPrompt = async (worktreePath, run, allowedPaths) => {
390
371
  {
391
372
  status: run.status,
392
373
  finishedAt: run.finishedAt,
374
+ backtestResultKey: run.artifacts.backtestResultKey,
393
375
  backtestResult: run.artifacts.backtestResult,
376
+ aiExportFile: run.artifacts.aiExportFile,
394
377
  aiTrainLocal: run.artifacts.aiTrainLocal,
395
- strategyConfig: run.artifacts.strategyConfig
378
+ strategyConfig: run.artifacts.strategyConfig,
379
+ backtestConfig: run.artifacts.backtestConfig
396
380
  },
397
381
  null,
398
382
  2
@@ -401,8 +385,9 @@ var buildPrompt = async (worktreePath, run, allowedPaths) => {
401
385
  "Task:",
402
386
  "- Inspect the research results and current strategy files.",
403
387
  "- Produce the smallest safe follow-up patch.",
404
- "- Prefer notes and tests if the data does not justify a production logic change.",
388
+ "- Prefer strategy-scoped tests or NO_CHANGES if the data does not justify a production logic change.",
405
389
  "- If you change strategy logic, include matching regression tests.",
390
+ "- Do not create or modify notes/: local research notes are permanently Git-ignored and are not allowed in agent commits.",
406
391
  "",
407
392
  contexts.join("\n\n")
408
393
  ].join("\n");
@@ -475,8 +460,8 @@ var requestAgentDiff = async (userName, messages) => {
475
460
  return normalized;
476
461
  };
477
462
  var applyDiff = async (worktreePath, diffText) => {
478
- const tempDir = await import_promises.default.mkdtemp(import_node_path2.default.join(import_node_os.default.tmpdir(), "tradejs-agent-"));
479
- const patchPath = import_node_path2.default.join(tempDir, "agent.patch");
463
+ const tempDir = await import_promises.default.mkdtemp(import_node_path.default.join(import_node_os.default.tmpdir(), "tradejs-agent-"));
464
+ const patchPath = import_node_path.default.join(tempDir, "agent.patch");
480
465
  try {
481
466
  await import_promises.default.writeFile(patchPath, diffText, "utf8");
482
467
  await runCommandOrThrow(
@@ -701,8 +686,8 @@ var createGithubPullRequest = async (params) => {
701
686
  };
702
687
  var createWorktree = async (branchName) => {
703
688
  const worktreeToken = branchName.replace(/[/:]+/g, "--");
704
- const worktreeRoot = import_node_path2.default.join(projectRoot, ".agent-worktrees");
705
- const worktreePath = import_node_path2.default.join(worktreeRoot, worktreeToken);
689
+ const worktreeRoot = import_node_path.default.join(projectRoot, ".agent-worktrees");
690
+ const worktreePath = import_node_path.default.join(worktreeRoot, worktreeToken);
706
691
  await import_promises.default.mkdir(worktreeRoot, { recursive: true });
707
692
  await runCommand("git", ["worktree", "remove", "--force", worktreePath], {
708
693
  cwd: projectRoot