diff --git a/.agents/cursor-codex-cheatsheet.md b/.agents/cursor-codex-cheatsheet.md new file mode 100644 index 0000000..ee373b0 --- /dev/null +++ b/.agents/cursor-codex-cheatsheet.md @@ -0,0 +1,126 @@ +# Chuleta: Cursor + Codex en QuantLab + +Guía rápida para el flujo de trabajo en este repo. No sustituye a `AGENTS.md`, `.agents/implementation-rules.md` ni `.agents/prompts/codex-master-prompt.md`. + +## Regla base + +- Cursor analiza alcance y revisa diffs. +- Codex propone plan mínimo, ejecuta y valida. +- No uses ambos editando el mismo archivo a la vez. +- En broker, execution, submit o tooling de seguridad: plan primero, scope mínimo. + +## Prompt canónico + +Para trabajo directo en el repo, usa: + +- `.agents/prompts/codex-master-prompt.md` + +Ese prompt define la versión larga del flujo de dos fases. + +## 1. Cursor: análisis + +```text +Read AGENTS.md and the relevant .agents files first: +- .agents/project-brief.md +- .agents/implementation-rules.md +- .agents/current-state.md +- .agents/cursor-codex-cheatsheet.md +- .agents/prompts/codex-master-prompt.md +- .cursor/rules/ and .cursor/mcp.json if relevant + +Task: +Explain the exact scope of this change in QuantLab. + +Output only: +- files involved +- architectural boundaries / what must not change +- risks +- smallest safe next step + +Do not edit files. +``` + +## 2. Codex: plan + +```text +Read these files first: +- AGENTS.md +- .agents/project-brief.md +- .agents/implementation-rules.md +- .agents/current-state.md +- .agents/cursor-codex-cheatsheet.md +- .agents/prompts/codex-master-prompt.md +- + +Task: +Propose the smallest safe change for this request. + +Constraints: +- no edits yet +- no unrelated changes +- preserve backward compatibility unless explicitly noted +- keep scope narrow + +Output only: +- goal +- exact files to change +- what must not change +- minimal plan +- validation commands +- suggested PR title + 4-line body +``` + +## 3. Codex: ejecución + +```text +Execute the approved plan. + +Constraints: +- change only the listed files +- no unrelated cleanup +- preserve compatibility +- run the validation commands +- show exact files changed +- show a compact diff summary +``` + +## 4. Cursor: revisión final + +```text +Review the exact diff for this change. + +Tell me: +1. whether scope stayed narrow +2. whether compatibility was preserved +3. any risk or caveat +4. whether this is ready to commit + +Do not make edits. +``` + +## Validation matrix + +- `docs/` or `.agents/` markdown only: + - `git diff --check` + - human read-through +- `desktop/` or `*.mjs` MCP work: + - `node --check ` + - `git diff --check` +- `src/quantlab/` or tests: + - focused `pytest` + - `python main.py --check` when CLI/runtime behavior changes + - `git diff --check` + +## Uso recomendado + +- Cambio pequeño en `desktop/` o MCP: Cursor → Codex plan → Codex execute → Cursor +- Cambio CLI o contrato visible: Cursor → Codex plan → Codex execute → Cursor +- Broker / execution sensible: Cursor primero; Codex solo en subtareas muy acotadas + +## Reglas de branch y PR + +- Usa ramas `codex/…` +- 1 issue = 1 branch = 1 PR +- PR breve: Summary | Scope | Validation | Notes +- Evita `git add .` + diff --git a/.agents/prompts/codex-master-prompt.md b/.agents/prompts/codex-master-prompt.md index 45f2f5e..6f15231 100644 --- a/.agents/prompts/codex-master-prompt.md +++ b/.agents/prompts/codex-master-prompt.md @@ -2,7 +2,7 @@ You are Codex working directly inside the QuantLab repository. Treat `.agents/` as repository context and operating guidance, not as product runtime code. -Before making changes, read and treat these files as the source of truth for repo context: +Before making changes, read and treat these files as source of truth for the task: - `.agents/project-brief.md` - `.agents/architecture.md` @@ -12,51 +12,112 @@ Before making changes, read and treat these files as the source of truth for rep - `.agents/implementation-rules.md` - `.agents/workflow.md` - `.agents/session-log.md` +- `.agents/cursor-codex-cheatsheet.md` If the task is stage-specific or issue-specific, also read the relevant file in: - `.agents/tasks/` -Your job in QuantLab is to be a disciplined implementation agent. +## Operating constraints -Core responsibilities: +- Keep changes small, reversible, and reviewable. +- Preserve repository architecture and artifact contracts unless the task explicitly changes them. +- Keep `main.py` thin and CLI/bootstrap-only. +- Prefer read-only inspection and validation for tooling tasks. +- Do not widen scope into adjacent broker, execution, or UI surfaces unless the task explicitly requires it. +- Use a dedicated `codex/` branch from up-to-date `main` when implementation is needed. -1. inspect the current repository state before acting -2. preserve the layered architecture -3. keep `main.py` thin and CLI-only -4. prefer minimal, reviewable changes -5. preserve artifact and CLI contracts unless the task explicitly changes them -6. keep outputs reproducible and deterministic -7. add or update focused tests when behavior changes -8. avoid hidden refactors and unrelated edits +## Current QuantLab priorities to respect -Execution rules: +- Hyperliquid-first supervised safety is the active execution focus. +- Read-only artifact/output visibility is preferred over new execution surface. +- Desktop and MCP work should inspect and summarize, not add trading logic. +- Paper/session visibility matters as a bridge, not as the current bottleneck. -- If the user asks for plan-only, do not implement. -- If the user has already approved implementation and scope is clear, execute after reading the required context. -- If scope, target files, or consequences are ambiguous, stop and surface the ambiguity. -- Do not infer extra tasks beyond the approved issue or request. -- Do not create duplicate paths or alternate implementations when an existing seam already exists. +## Two-phase workflow -QuantLab architecture rules: +### Phase 1 - Plan -- CLI orchestration belongs in `src/quantlab/cli/` -- data logic belongs in `src/quantlab/data/` -- indicators belong in `src/quantlab/features/` -- strategy logic belongs in `src/quantlab/strategies/` -- simulation belongs in `src/quantlab/backtest/` -- forward or paper execution belongs in `src/quantlab/execution/` -- reporting belongs in `src/quantlab/reporting/` -- run lifecycle and registry logic belongs in `src/quantlab/runs/` +Before editing, produce: -Artifact and safety rules: +1. Goal +2. Exact files to change +3. What must not change +4. Minimal plan +5. Validation commands to run after edits +6. Suggested PR title and a short 4-line body template -- write artifacts under `outputs/` -- preserve stable reporting and artifact semantics -- default to paper-mode-safe behavior -- never introduce live trading behavior unless explicitly requested +Rules: -When responding before implementation, prefer this structure: +- no edits in the plan response +- no unrelated files +- no scope expansion +- preserve backward compatibility unless explicitly requested otherwise +- if scope, files, or behavior are ambiguous, stop and report the ambiguity + +### Phase 2 - Execute + +After approval, or when the user has already explicitly approved implementation and scope is clear: + +1. Change only the approved files +2. Keep the change narrowly scoped +3. Add or update focused tests when behavior changes +4. Run the validation commands from the approved plan +5. Stage only the exact files for the task +6. Verify the staged diff before committing +7. Commit with a clear, scoped message +8. Push the branch if the task is complete +9. Report the result briefly and clearly + +Rules: + +- no unrelated cleanup +- no hidden refactors +- no duplicate implementation paths +- no ad hoc scope creep + +## Validation matrix + +Choose validation based on the touched area: + +- `docs/` or `.agents/` markdown only: + - `git diff --check` + - human read-through +- `desktop/` or `*.mjs` MCP work: + - `node --check ` + - `git diff --check` +- `src/quantlab/` or tests: + - focused `pytest` + - `python main.py --check` when CLI/runtime behavior is touched + - `git diff --check` + +Prefer the repository MCP tools for routine validation when they apply: + +- `quantlab_check` +- `quantlab_version` +- `quantlab_runs_list` +- `quantlab_paper_sessions_health` +- `quantlab_desktop_smoke` + +## PR shape + +Keep PRs short and structured: + +- Summary +- Scope +- Validation +- Notes + +The body should state: + +- what changed +- which files changed +- how it was validated +- the compatibility or risk note + +## Response shape + +When planning: 1. current understanding 2. task in scope @@ -66,7 +127,7 @@ When responding before implementation, prefer this structure: 6. risks or things to avoid 7. next logical step only -When responding after implementation, prefer this structure: +When finishing implementation: 1. files changed 2. what changed diff --git a/desktop/README.md b/desktop/README.md index eaf500e..6c941b5 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -43,6 +43,8 @@ Available tools: - `quantlab_paper_sessions_health` - `quantlab_desktop_smoke` - `quantlab_read_file` +- `quantlab_outputs_list` +- `quantlab_artifact_read` The server entrypoint is `mcp-server.mjs`, and the `mcp` npm script runs it directly. diff --git a/desktop/mcp-server.mjs b/desktop/mcp-server.mjs index acee598..814e8fe 100644 --- a/desktop/mcp-server.mjs +++ b/desktop/mcp-server.mjs @@ -10,14 +10,53 @@ import { z } from "zod"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DESKTOP_ROOT = __dirname; const PROJECT_ROOT = path.resolve(DESKTOP_ROOT, ".."); +const OUTPUTS_ROOT = path.resolve(PROJECT_ROOT, "outputs"); const PYTHON_EXECUTABLE = process.env.QUANTLAB_PYTHON || "python"; const MAX_OUTPUT_CHARS = 12000; +const BINARY_ARTIFACT_EXTENSIONS = new Set([ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".bmp", + ".ico", + ".pdf", +]); function truncateText(text) { if (text.length <= MAX_OUTPUT_CHARS) return text; return `${text.slice(0, MAX_OUTPUT_CHARS)}\n...[truncated]`; } +function resolveOutputsPath(relativePath) { + const requested = relativePath || ""; + const resolvedPath = path.resolve(OUTPUTS_ROOT, requested); + const relative = path.relative(OUTPUTS_ROOT, resolvedPath); + if (relative.startsWith("..") || path.isAbsolute(relative)) { + throw new Error(`Refusing to access outside outputs/: ${relativePath}`); + } + return resolvedPath; +} + +function formatBytes(size) { + if (!Number.isFinite(size)) return "unknown"; + if (size < 1024) return `${size} B`; + const units = ["KB", "MB", "GB"]; + let value = size / 1024; + for (const unit of units) { + if (value < 1024 || unit === units[units.length - 1]) { + return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`; + } + value /= 1024; + } + return `${size} B`; +} + +function toIsoString(date) { + return date instanceof Date ? date.toISOString() : null; +} + function runProcess(command, args, options = {}) { return new Promise((resolve) => { const child = spawn(command, args, { @@ -74,6 +113,66 @@ async function runPythonCli(args, timeoutMs = 120000) { }); } +async function listOutputs(relativePath = "") { + const targetPath = resolveOutputsPath(relativePath); + const stat = await fs.stat(targetPath); + if (!stat.isDirectory()) { + throw new Error(`Not a directory under outputs/: ${relativePath || "."}`); + } + + const entries = await fs.readdir(targetPath, { withFileTypes: true }); + const detailed = []; + for (const entry of entries) { + const entryPath = path.join(targetPath, entry.name); + const entryStat = await fs.stat(entryPath); + detailed.push({ + name: entry.name, + kind: entry.isDirectory() ? "directory" : "file", + relative_path: path.relative(PROJECT_ROOT, entryPath).replaceAll("\\", "/"), + size_bytes: entry.isDirectory() ? null : entryStat.size, + size_human: entry.isDirectory() ? null : formatBytes(entryStat.size), + modified_at: toIsoString(entryStat.mtime), + }); + } + + detailed.sort((left, right) => { + if (left.kind !== right.kind) { + return left.kind === "directory" ? -1 : 1; + } + return left.name.localeCompare(right.name); + }); + + return { + root: "outputs", + requested_path: relativePath || ".", + absolute_path: targetPath, + entry_count: detailed.length, + entries: detailed, + }; +} + +async function readOutputsArtifact(relativePath) { + const targetPath = resolveOutputsPath(relativePath); + const stat = await fs.stat(targetPath); + if (stat.isDirectory()) { + throw new Error(`Expected a file under outputs/, got directory: ${relativePath}`); + } + if (BINARY_ARTIFACT_EXTENSIONS.has(path.extname(targetPath).toLowerCase())) { + throw new Error(`Binary artifact reading is not supported for ${relativePath}`); + } + + const data = await fs.readFile(targetPath, "utf8"); + return { + root: "outputs", + requested_path: relativePath, + absolute_path: targetPath, + bytes: stat.size, + modified_at: toIsoString(stat.mtime), + truncated: data.length > MAX_OUTPUT_CHARS, + content: truncateText(data), + }; +} + async function formatProcessResult(label, result, commandLine) { const sections = [ `Command: ${commandLine}`, @@ -186,6 +285,72 @@ async function main() { } }); + server.registerTool("quantlab_outputs_list", { + description: "List artifacts and directories under outputs/.", + inputSchema: { + relative_path: z.string().optional().default("").describe("Path relative to outputs/"), + }, + }, async ({ relative_path }) => { + try { + const payload = await listOutputs(relative_path); + return { + content: [ + { + type: "text", + text: JSON.stringify(payload, null, 2), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Failed to list outputs/${relative_path || ""}: ${error.message || String(error)}`, + }, + ], + isError: true, + }; + } + }); + + server.registerTool("quantlab_artifact_read", { + description: "Read a text artifact within outputs/.", + inputSchema: { + relative_path: z.string().describe("Path relative to outputs/"), + }, + }, async ({ relative_path }) => { + try { + const payload = await readOutputsArtifact(relative_path); + return { + content: [ + { + type: "text", + text: [ + `Path: outputs/${payload.requested_path}`, + `Bytes: ${payload.bytes}`, + `Size: ${formatBytes(payload.bytes)}`, + `Modified at: ${payload.modified_at || "unknown"}`, + `Truncated: ${payload.truncated ? "yes" : "no"}`, + "", + payload.content, + ].join("\n"), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Failed to read outputs/${relative_path}: ${error.message || String(error)}`, + }, + ], + isError: true, + }; + } + }); + const transport = new StdioServerTransport(); await server.connect(transport); }