{"base_url":"https://eternalsearch.net/omega-point/api/agent/v1","files":{"SKILL.md":"---\nname: omega-point-agent-relay\ndescription: Run Omega Point (Eternal Search) for a user, in either of its two versions, both executed on the Eternal Search server. Version 1.5 breaks one research goal into open questions, competing hypotheses and deciding experiments and publishes them on the site; version 2.00 returns one card per significant knowledge gap. Ask the user which version before the first run. Your agent starts the run, follows its status, keeps the user informed and hands over the published result; at present it answers no model calls (the status says run_mode server). When the server hands model calls to agents instead (run_mode agent_relay), the relay sections at the end apply and you answer every call with your own model. Use from Claude Code, Codex, Cursor, Cowork or a plain script. Also use when a user wants the results of an existing Omega Point run by its key (pipeline-...), their own or anyone's, started here or on the site: its published questions and hypotheses with their assessments as markdown files with the structure of their pages, no registration needed.\n---\n\n# Omega Point for agents\n\nOmega Point takes one research goal (\"restore youthful skin properties in middle-aged adults\")\nand produces a tree: the requirements behind the goal, the scientific fields that study each,\nthe questions those fields still leave open, competing hypotheses for each question and the\nexperiments that would decide between them. A full run is several hundred model calls over\nseveral hours. The engine lives on the Eternal Search server.\n\n**At present every run started through this skill executes on the server.** The start response and\nthe run status say `run_mode: server`: the server answers every model call itself, you pull no tasks\nand serve nothing. Your work is to start the run, follow its status, keep the user informed and hand\nover the published result; see \"A run on the server\". The sections from \"Relay mode\" to the end\ndescribe the other mode, `run_mode: agent_relay`, in which the server hands every model call to you\nas a task; they apply only when the status says so.\n\n## Two versions: ask which one before the first run\n\nOmega Point has two versions. Unless the user already named one, ask in their language which\nthey want, with this difference in plain words:\n\n- **Omega Point 1.5** (this skill): a run breaks one research goal into open questions and\n  competing hypotheses, with the experiments that would decide between them, and publishes them on\n  the site, one page per question and per hypothesis, in English and Russian. It can also run on\n  the user's own model, with no account, nothing sent to the server and nothing published: download\n  the omega-point-1-local skill from https://eternalsearch.net/api/v1/omega1/skill?include=content\n  (or the omega_get_local_skill MCP tool), save every file under ~/.claude/skills/omega-point-1-local/\n  or ~/.codex/skills/omega-point-1-local/ and follow its SKILL.md. Offer both before the first run.\n- **Omega Point 2.00**: the engine reads open full-text papers, finds significant gaps in what is\n  known, and returns one card per gap: competing variants from the obvious to the radical, each\n  with a causal chain, quoted evidence and a first experiment. Cards are published on\n  https://eternalsearch.net/omega after review by the Eternal Search team. It needs this agent\n  linked to the user's Eternal Search account, and up to three studies per person run at a time; a\n  fourth one waits for a free slot.\n\nOmega Point 2.00 can run in two places. Before the first 2.00 study offer the user both and let\nthem choose:\n\n- **On the Eternal Search server** (the loop below): the platform models do the work, the agent is\n  linked to the user's account, and cards are kept on the site.\n- **On the user's own model**, with no account and nothing sent to the server: download the\n  omega-point-2-local skill from https://eternalsearch.net/api/v1/omega2/skill?include=content (or\n  the omega2_get_skill MCP tool), save every file under ~/.claude/skills/omega-point-2-local/ or\n  ~/.codex/skills/omega-point-2-local/ keeping its path, and follow its SKILL.md. It needs an agent\n  with files, a shell and subagents (Claude Code, Codex), and the user pays for their own model calls.\n\n### The Omega Point 2.00 loop\n\nYou never pull or answer tasks for a 2.00 study; there is nothing for your model to do except talk\nto the user.\n\n1. Link the agent to the account if it is not linked: `node omega-connector.mjs login` (then\n   `login --wait`), or MCP `omega_login_start` and `omega_login_poll`. Send the user the returned\n   URL with the words \"open it and press Connect\", then wait for the answer and continue by\n   yourself; see \"Linking\". You never open the site, never sign in, never ask for a code or a\n   password. An anonymous link is refused with 403.\n2. Start: `node omega-connector.mjs study-start \"<the question in the user's own words>\" --language <their language>`\n   (MCP `omega2_start_study`; HTTP `POST https://eternalsearch.net/api/v1/omega2/studies` with\n   `{\"question\": \"...\"}` and the link token as the Bearer header). Show the user the returned\n   `public_id`: it names the study from any session.\n3. Follow: `study-status <study>` (MCP `omega2_study_status`). Tell the user its `progress_report` in\n   their language, and read `next_action`.\n4. Clarifying questions: when the status is `awaiting_user_answer`, run `study-questions <study>`\n   (MCP `omega2_list_clarifications`), ask the user each open question in their language, wait for the\n   answer, and send it with `study-answer <study> <question_id> --file answer.txt` (MCP\n   `omega2_answer_clarification` with one stable `answer_idempotency_key` per answer). Never answer for\n   the user.\n5. A study runs for hours. Before you end any turn, read the status and tell the user the progress and\n   the phrase that continues: \"show Omega Point 2 study <study>\". The study keeps running on the\n   server while no session is open; the account lists it at https://eternalsearch.net/omega/my-runs.\n6. Paused, stopped or over its budget: `study-resume <study>` (MCP `omega2_resume_study`) on the user's\n   word only.\n7. Finished (`completed_cards_ready` or `completed_research_report`): `study-cards <study>` (MCP\n   `omega2_cards` and `omega2_card`) and give the user every card in full, never shortened; save\n   `study-export <study> --markdown` as a file and tell them where it is. Ask for feedback and store it\n   with MCP `omega2_submit_feedback`.\n\nBase URL: `https://eternalsearch.net/omega-point/api/agent/v1`\nProtocol: `<base>/openapi.json`. Page: `https://eternalsearch.net/omega-point/agents`.\n\n## A run on the server (Omega Point 1.5, run_mode server)\n\n1. Register a link once (`node omega-connector.mjs register --provider codex`, or any provider: a\n   server run does not use it; MCP `omega_register`). Keep the token: `~/.omega-point/link.json`.\n2. Ask the three questions of \"Before the first run\" in one message (Russian versions, poster\n   sheets, linking the account) and wait for the answers. If the user wants the account link, run\n   `login`, give them the URL and the code, then `login --wait`.\n3. Start with the user's question in their own words as the goal (see \"The goal is the user's\n   question\"): `node omega-connector.mjs run \"<goal>\"` (`--no-russian`, `--posters`; MCP\n   `omega_start_run`; HTTP `POST /runs`). The response carries `run_mode: server`, the run key and\n   `handoff_text`. Show the user `handoff_text` verbatim at once: they paste it into a new session\n   if this one ends, and that session follows the run. Up to three runs per person at a time: when\n   the pool is full the response says `queued_position`, and the run starts by itself.\n4. Follow the run. `node omega-connector.mjs progress <run_key>` (`GET /runs/<run_key>`,\n   `omega_run_status`) carries `progress_report`, written for the user: tell it to them in their\n   language before every turn of yours ends, with what comes next. A run takes hours and keeps\n   running while no session is open. `serve --run <run_key> --detach` watches it in the background\n   and logs every report; nothing is served. `next_action` says in one sentence what to do.\n   `keep_serving` is always false for a server run. Do not stop the run.\n5. `relay_phase` is `engine` while the engine runs, `finishing` while titles, duplicate checks,\n   explanations, headings and translations are written, `done` when nothing more will come. The\n   engine status `completed_at_step` is not the end: wait for `done`.\n6. `stopped` with `resumable: true`: `node omega-connector.mjs resume <run_key>` (`omega_resume_run`)\n   continues it under the same key, on the server. When `error_message` begins with \"stopped before\n   step 3\", the engine stopped on purpose: tell the user the reason and propose a clearer wording;\n   resume only if they ask.\n7. `done`: `outcome <run_key>` gives the links to every published question and hypothesis (English\n   and Russian) and the size of the texts; `documents <run_key>` saves the folder of markdown files\n   with the poster sheets; then ask the user for feedback and store it with `feedback`. Details in\n   \"When the run is done\", \"Then save the run as files\" and \"After the run\" below; those three\n   points apply to both modes.\n\n## Fetch the results of any run by its key\n\nWhat a run published (its open questions and hypotheses) is public, so anyone can fetch it: a run\nyou started, a run started on the Eternal Search site, a run of another user. You need only the run\nkey (`pipeline-` and twelve characters). No link, no token, no registration, no run of your own.\nWhen the user says \"fetch the results of pipeline-fbbc235bbe60\" (or the same in their language):\n\n```bash\nnode omega-connector.mjs documents pipeline-fbbc235bbe60            # the folder omega-pipeline-fbbc235bbe60/\nnode omega-connector.mjs documents pipeline-fbbc235bbe60 --dir out  # or a folder you choose\nnode omega-connector.mjs outcome pipeline-fbbc235bbe60              # links and text sizes; --texts for the texts\n```\n\nThe folder holds one markdown file per published open question and per published hypothesis,\nper language (`questions/<id>.en.md`, `hypotheses/<id>.ru.md`), each with the text and structure\nof its page on the site; an index per language (`index.en.md`, `index.ru.md`) linking every file;\nand the poster sheets as PNG files where the run has them. Every hypothesis file ends with the\nhypothesis's assessment: eight criteria rated 0 to 10 (support from research, clarity of\nmechanism, few extra conditions, completeness of the answer, novelty, few new entities, decisive\nexperiment, silver-bullet potential), each with its reason, confidence and sources, then the\nfindings in the literature with quotes, open questions, limits, the search queries and the complete\nabstracts that were read. `outcome` gives every hypothesis its scores (`assessment`), and the open\nAPI `https://eternalsearch.net/api/v1/content/hypotheses/<public_id>` serves the full assessment as\nJSON and as markdown in English and Russian. Tell the user where the folder is and\nwhich index to open first. If they want another format (one file, a table, DOCX, a summary per\nhypothesis), make it from these files yourself, keeping every text whole. Without the connector:\n`GET <base>/runs/<run_key>/documents` and `GET <base>/runs/<run_key>/documents/file?name=<name>`\n(MCP: `omega_run_documents`, `omega_run_document`, `omega_run_outcome`), no Authorization header.\n\n- `409 run_not_done`: the server is still finishing that run's pages (`publication_pending` says\n  what it still owes: titles, explanations, translations, headings). Tell the user and try again\n  in a few minutes; `outcome` already shows what is published so far.\n- `409 nothing_published`: the run published no question or hypothesis. Tell the user.\n- `404 unknown_run`: no such run on this server; ask the user to check the key.\n\nOnly reading is open. Driving a run (status with the handoff text, pulling tasks, stop, resume,\nfeedback, the raw engine JSON with drafts) stays with the link that started it.\n\n## Check that this skill is current first\n\nYou may be reading a copy saved some time ago (a file attached to a chat, a downloaded zip, an old\ncheckout). The server keeps the current skill and publishes the sha256 of each file at\n`GET <base>/skill/version` (public, no token). Before the first run, before every resume and at the\nstart of every new session, check your copy:\n\n- With the connector: `node omega-connector.mjs check-skill`. If it reports outdated files, run\n  `node omega-connector.mjs update` (it downloads the current SKILL.md, connector and mcp.json over\n  your copy and verifies their hashes), then **read the new SKILL.md before doing anything else**.\n  `run`, `resume`, `serve`, `next` and `status` also check by themselves (at most every 10 minutes)\n  and print `YOUR OMEGA POINT SKILL IS OUTDATED` when needed; `next` puts the same notice in its\n  JSON as `skill_update`.\n- Without the connector: compute the sha256 of each file you hold and compare with `files_sha256`;\n  if you cannot compute hashes, download `<base>/skill/SKILL.md` again and use that copy.\n- During a run, every pull and every status carries `skill_revision`: when it differs from the one\n  you saw at the start, check again. Updating never touches a run in progress: it continues under\n  the same key.\n- Whenever you update, tell the user in one sentence that the skill was updated to the server's\n  current version.\n\n## Before the first run of Omega Point 1.5: ask three questions in one message (mandatory)\n\nAsk the user, before you start the run, in their language, in ONE message, and wait for the answers.\nDo not mention an option in passing (\"if you like, it can be linked to your account\"): the user does\nnot know what that means or how to do it. Ask each one as a question with its benefit and its effort:\n\n1. **Russian versions?** Translations of every hypothesis and explanation, Russian question headings,\n   a Russian report and a Russian poster sheet. (`translate_ru`, default yes)\n2. **Poster sheets?** One illustrated sheet per hypothesis, English and Russian, drawn by the server.\n   (`posters`, default no)\n3. **Link this run to your Eternal Search account?** Then access to it can never be lost: if this\n   session ends, the computer changes or the key file is deleted, any new session continues the run,\n   and all your runs are listed at https://eternalsearch.net/omega/my-runs. It takes a minute: I send\n   you a link, you open it and press Connect. (recommended)\n\nFor example in Russian: \u00ab\u041f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u0442\u0440\u0438 \u0432\u043e\u043f\u0440\u043e\u0441\u0430. 1. \u041d\u0443\u0436\u043d\u044b \u043b\u0438 \u0440\u0443\u0441\u0441\u043a\u0438\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432: \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u044b\n\u0433\u0438\u043f\u043e\u0442\u0435\u0437 \u0438 \u043e\u0431\u044a\u044f\u0441\u043d\u0435\u043d\u0438\u0439, \u0440\u0443\u0441\u0441\u043a\u0438\u0439 \u043e\u0442\u0447\u0451\u0442 \u0438 \u0440\u0443\u0441\u0441\u043a\u0438\u0439 \u043f\u043e\u0441\u0442\u0435\u0440? 2. \u041d\u0443\u0436\u043d\u044b \u043b\u0438 \u043f\u043e\u0441\u0442\u0435\u0440\u044b \u043a \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0430\u043c? 3. \u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c\n\u0437\u0430\u043f\u0443\u0441\u043a \u043a \u0432\u0430\u0448\u0435\u043c\u0443 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0443 Eternal Search? \u0422\u043e\u0433\u0434\u0430 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043d\u0435\u043c\u0443 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043d\u0430\u0432\u0441\u0435\u0433\u0434\u0430: \u0435\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0441\u0435\u0430\u043d\u0441\n\u0437\u0430\u043a\u043e\u043d\u0447\u0438\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b \u0441\u043c\u0435\u043d\u0438\u0442\u0435 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440, \u043b\u044e\u0431\u043e\u0439 \u043d\u043e\u0432\u044b\u0439 \u0441\u0435\u0430\u043d\u0441 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442 \u0437\u0430\u043f\u0443\u0441\u043a, \u0430 \u0432\u0441\u0435 \u0432\u0430\u0448\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0438 \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\n\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 eternalsearch.net/omega/my-runs. \u042d\u0442\u043e \u0437\u0430\u0439\u043c\u0451\u0442 \u043c\u0438\u043d\u0443\u0442\u0443: \u044f \u043f\u0440\u0438\u0448\u043b\u044e \u0441\u0441\u044b\u043b\u043a\u0443, \u0432\u044b \u043e\u0442\u043a\u0440\u043e\u0435\u0442\u0435 \u0435\u0451 \u0438\n\u043d\u0430\u0436\u043c\u0451\u0442\u0435 \u00ab\u0421\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u044c\u00bb.\u00bb\n\nSkip question 3 only when the link is already linked (`account_linked: true` in\n`GET /links/current`, or `account_linked` in `~/.omega-point/link.json`).\n\n### Linking, when the user says yes\n\nTwo calls, because your harness may show a command's output only when it ends:\n\n```bash\nnode omega-connector.mjs login            # returns at once: a URL and a code (also as JSON on stdout)\n# now write the user the URL, the code and the three steps; ask them to say when they have confirmed\nnode omega-connector.mjs login --wait     # then: waits until they confirm (up to 15 minutes), saves the token\nnode omega-connector.mjs runs --account   # later: every run of every agent linked to the account\n```\n\nTell the user exactly: \"Open <verification_url> and press Connect. It is a page of the Eternal Search\nsite in your own browser; sign in with Google there if it asks. I keep waiting and continue by myself\nas soon as you have pressed it.\" Then run `login --wait` (it polls with `wait_s` and returns when the\nuser has pressed Connect) and start the run. Rules that never bend:\n\n- **You never open the site, a sign-in page or Google yourself**, in a browser tool or otherwise, and\n  you never sign in anywhere. The page is for the user, in the user's own browser.\n- **You never ask the user for a password, and never ask them to type or compare the code.** The URL\n  already carries the code; the page shows it only so a careful user can check it.\n- **You do not ask the user to write \"done\".** `login --wait` (or `omega_login_poll` with `wait_s`\n  25, called again and again for up to 15 minutes) returns by itself. Only if your harness cannot keep\n  a command or a tool call running that long, tell the user to write one word after pressing Connect.\n- The page connects the link this agent uses now (after `register`) or creates a new one. \"Other\n  options\" on the page lets the user pick one of their links (a lost token) or decline.\n\nWhat the link gives later:\n\n- the token can never be lost for good: in a new session, on another machine, or after the token\n  file was deleted, run `login` again and ask the user to choose \"One of my links\". The agent gets a\n  NEW token for that same link (the old one stops working) and every run continues under its key;\n- the user sees all their runs, where each one is, and the phrase to send the agent at\n  https://eternalsearch.net/omega/my-runs.\n\nWithout the connector: `POST <base>/links/login` (body like `POST /links`; send your Bearer so the\nuser can bind this link), show the user `verification_url` and `user_code`, then\n`POST <base>/links/login/poll {device_code}` every `interval_s` seconds until it answers 200 (MCP:\n`omega_login_start`, `omega_login_poll`, `omega_account_runs`).\n\n### Acting for the person on the site (team work, not only Omega)\n\nWhen the user wants you to work on Eternal Search for their team (grant searches, projects, applications),\nsign in with the site permission:\n\n```bash\nnode omega-connector.mjs login --site         # the page also asks the user to allow you to act for their team\nnode omega-connector.mjs login --site --wait  # saves your own team key with the link (file mode 0600)\nnode omega-connector.mjs whoami               # link, scope, team, last four characters of the key\n```\n\nThe key is yours alone: the user can revoke it at https://eternalsearch.net/account/agents without touching\ntheir other agents or their personal key. Send it only as an `Authorization: Bearer` header to `/api/v1/*`\nand to the `eternal-search` MCP server (`/api/mcp`); never put it in a URL, a tool argument, a log or a reply.\nA `login` without `--site` for the same link withdraws the site permission and its key.\nWeb clients that cannot run commands (claude.ai, ChatGPT) never use `login --site` and never send the user a\nConnect link for site work: there is no disk between chats, so a key handed to such a chat is lost with it,\nand the user would be asked to connect again in every new chat. They add `https://eternalsearch.net/api/mcp`\nas a connector instead, once: the first team tool answers 401 with a WWW-Authenticate challenge, the client\nopens the Eternal Search sign-in and consent page (OAuth), and from then on the client itself keeps the\ntoken (renewed for 30 days). Grant searches then go through the connector's tools `start_grant_search`,\n`get_grant_search`, `list_grant_searches` and `cancel_grant_search`; `whoami` shows whom the connection acts\nfor. If the connector is not added, tell the user once how to add it; do not fall back to a device-code\nlogin or ask for a key, a code or a password.\n\n## Relay mode: when the server hands the model calls to your agent (run_mode agent_relay)\n\nEverything from here on applies only when the start response or the status says\n`run_mode: agent_relay`. Then the model calls do not go to the server's pool: every call the engine\nneeds comes to you as a task, you answer it with your model and post the answer back. Step 7 alone\nis up to 36 calls for each of at most 10 open questions (\"Step 7 is the reviewed generation\").\n\n**A relay run stops after every stage and waits for the user.** After engine steps 1, 2, 3, 4 and\n6, after every 10 hypotheses of step 7, after the complete engine output, after the titles and\nduplicate check, after every 10 rewritten hypotheses and after every 10 translated hypotheses, the\nserver holds the run's tasks. You show the user that stage's result, end your turn, and the run goes\non when the user tells you to continue. No turn of yours has to outlast a stage, so no turn dies on\na time limit in the middle of the run. See \"At every checkpoint\".\n\n## At every checkpoint (mandatory)\n\nA checkpoint is one stage result the run waits at. You see it in four places: `serve` prints\n`\u2500\u2500\u2500\u2500 STAGE RESULT: TELL THE USER NOW` and exits; `next` returns `checkpoints` with `stop_now: true`;\na pull returns a non-empty `checkpoints` with `checkpoint_instructions`; the run status carries\n`checkpoint` (not null). Then, in this order:\n\n1. Finish the answers you already hold; pull no more tasks. The server holds the rest anyway.\n2. Read the checkpoint: `node omega-connector.mjs checkpoint <run_key>` (also saves it), or\n   `GET <base>/runs/<run_key>/checkpoint`, or `omega_run_checkpoint`. It has `summary_markdown`, the\n   overview, and `body_markdown`, the complete stage result.\n3. Write the user the overview in their language as a chat message: every item with its name and\n   essence, every number exactly as given. Translate; do not embellish, do not drop items.\n4. Save the complete result as a file (the connector writes `omega-<run_key>/stages/NN-<stage>.md`\n   and `NN-<stage>.summary.md`) and tell the user where it is. Paste it whole only when they ask;\n   ten rewritten hypotheses are tens of thousands of words, never shorten them.\n5. End your turn with one plain sentence that the run is paused and the exact phrase that continues\n   it, for example in Russian: \u00ab\u041f\u0440\u043e\u0433\u043e\u043d \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043f\u043e\u0441\u043b\u0435 \u044d\u0442\u0430\u043f\u0430 \u0438 \u0436\u0434\u0451\u0442 \u0432\u0430\u0441. \u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c, \u043d\u0430\u043f\u0438\u0448\u0438\u0442\u0435:\n   \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u00bb.\n6. Never continue on your own. When the user writes the phrase (or anything that means continue):\n   `node omega-connector.mjs continue <run_key>` (`POST /runs/<run_key>/continue`,\n   `omega_continue_run`), then serve again (`serve --run <run_key>`, `--detach` if you like, or the\n   `next` / `answer` loop). When `continue` says the next checkpoint is already due, go back to step 2.\n\nWhat the stages show: steps 1 to 6 the goal, its pillars, the requirement atoms, the research domains\nwith their scientific pillars and the open questions; step 7 the new hypotheses (with their\nreader editions); the engine output every hypothesis under its question; then the titles with the\nmerged duplicates; then the rewritten hypotheses (full explanations); then the Russian translations.\nThe last stage is the published result: the outcome with links (see below).\n\nA run waiting at a checkpoint waits for the user, not for you: it is kept on the server with all its\ntasks for 7 days, and the 24-hour absence rule does not apply to it. After 7 days at one checkpoint it\nis stopped, and `resume` continues it under the same key (a resume counts as the user's continue). The user can ask for an unattended run: start it with `--no-pauses` (`pauses: false`),\nand it never stops between stages.\n\nLong stages (step 7 can run for many hours) also stop after about 2 hours of continuous work without\na stop: a checkpoint of stage `turn_time_limit`. It carries no stage result; its `summary_markdown`\nis the run's status (where it is, what is done, calls answered, how long this stretch lasted) and\nwhat the user writes next. Treat it like any checkpoint: finish the answers in hand, write the user\nthe summary in their language, including the phrase to write (\"continue\"), save the body as a file,\nand end your turn. Nothing is lost while it waits; on \"continue\" the run goes on where it paused.\n\n## Step 7 is the reviewed generation\n\nSince 2026-10-07 step 7 writes every hypothesis in stages, each a separate task in your queue: a\ncheck of the scientific foundations, the generation itself (it receives the complete archived\nabstracts the engine read), a causal map, a scientific review, a revision, a verification, then a\nclarity loop (a reader edition in Russian, an independent retelling from the reader edition alone,\nand an audit that sends meaning errors back to a bounded scientific revision). Every stage answers\none JSON object to an exact schema; when it does not match, the engine asks for a correction with the\ncontract errors listed. Up to 36 calls per open question; at most 3 questions are composed at once\n(each one against the hypotheses already written). These prompts are long, because they carry the\nwhole source corpus: answer them with your strongest model; each such task allows 1800 s.\n\n## Honest facts first\n\n- **You are the proxy.** Every prompt of the engine travels to you in clear text, and your model\n  writes every answer.\n- **A run takes hours.** Between checkpoints your agent must keep pulling tasks. If it stops, the run\n  waits (status `awaiting_user_llm`); after 24 hours of silence the run is stopped with its\n  completed steps saved, and you can resume it with the same token. A resume continues the same\n  run under the same key, as many times as needed.\n- **Hand the user the handoff text the moment the run starts, and again after every resume.**\n  Your session can end at any moment (context limit, closed terminal, crash) while the run keeps\n  waiting on the server. The server therefore returns, with every started run, a paste-ready\n  message called `handoff_text` (in the `POST /runs` response, in `omega_start_run`, in the run\n  status, printed by the connector after `run`): the user copies it and pastes it into a new session,\n  and that session finishes the run. Show it to the user **verbatim, in a code block, in your very\n  next message, before you pull the first task**. Add one sentence in the user's language saying\n  what it is for (\"keep this; if our session ends, paste it into a new one\"). Do not shorten it,\n  do not rephrase it, do not replace it with the bare run key. A resume keeps the run key and\n  returns the `handoff_text` again (now saying how often the run was resumed): show it the same\n  way. The text looks like this:\n\n  ```\n  Finish my Omega Point run pipeline-cf15f9184116 (full run). Goal: \"restore youthful skin properties in middle-aged adults\".\n  Read the skill first: https://eternalsearch.net/omega-point/api/agent/v1/skill (SKILL.md, section \"If your session ends before the run does\"). The connector script is https://eternalsearch.net/omega-point/api/agent/v1/skill/scripts/omega-connector.mjs if it is not in this directory.\n  The link token is in ~/.omega-point/link.json on this machine. Do not register a new link: the run belongs to that one.\n  Check that your copy of the skill is current first: node omega-connector.mjs check-skill, and if it says outdated, node omega-connector.mjs update and read the new SKILL.md.\n  Keep me informed the whole time: whenever a pull returns reports_for_user, tell me its progress_report in my language at once; before you end any turn, tell me the run's progress_report from the status and the exact phrase I should send to continue. Never end a turn without a message to me.\n  Your turn can end without warning, and after that you cannot write to me. So keep the run served outside your turn when you can (serve --detach below); if your harness can schedule your own next turn, schedule it before this one ends; otherwise stop each stretch before your limit and tell me plainly that your turn has ended, the run waits, and the exact phrase I should send to continue. Whatever I write next, first tell me the progress_report, then continue.\n  1. node omega-connector.mjs status pipeline-cf15f9184116 and read relay_phase.\n  2. relay_phase engine or finishing: node omega-connector.mjs serve --run pipeline-cf15f9184116 --detach (serves in the background, independent of this session, until relay_phase is done; a link registered with --provider subagent: the next / answer loop instead, in stretches as above). Do not stop the run and do not resume it.\n  3. relay_phase stopped and resumable true: node omega-connector.mjs resume pipeline-cf15f9184116. The run keeps this same key: a lost engine continues from its saved steps, a stopped finishing phase is revived; then serve --run pipeline-cf15f9184116 again. Exception: when the status says the engine stopped the run on purpose (error_message begins \"stopped before step 3\"), do not resume it; tell me the reason and propose a clearer wording of my question.\n  4. relay_phase done: node omega-connector.mjs outcome pipeline-cf15f9184116. Give me the links to every published question and hypothesis (English and Russian), say how large the texts are, offer to paste them into the chat in the format I ask (node omega-connector.mjs outcome pipeline-cf15f9184116 --texts carries the full texts), and ask me for feedback.\n  Through the MCP server instead of the connector: omega_run_status, then omega_pull_task and omega_complete_task, omega_resume_run or omega_run_outcome, the same way.\n  ```\n- **Keep the user informed the whole time. This is mandatory.** A run takes hours, and the user\n  sees only what you write in the chat: an agent that speaks at the start and at the end leaves\n  them looking at a silent chat for hours, unable to tell a working run from a hung one. The server\n  writes the report for you: every run status carries `progress_report`, a short paragraph for the\n  user (phase, step, what is still owed by stage, calls answered, time since the start, whether the\n  run waits for an agent, what comes next), and a pull carries `reports_for_user` whenever a report\n  is due (a stage changed, or 10 minutes passed). The rules:\n  1. Whenever a pull returns a non-empty `reports_for_user` (the connector prints it between\n     `\u2500\u2500\u2500\u2500 TELL THE USER NOW` rules, and `next` puts it in its JSON), send the user a chat message\n     with that `progress_report` at once, in the user's language, before you answer more tasks.\n     Keep its numbers exactly; translate, do not embellish.\n  2. Before you end any turn, for whatever reason (a turn limit, a context limit, an error, a pause),\n     read the status (`node omega-connector.mjs progress <run_key>`, `GET /runs/{key}`,\n     `omega_run_status`) and write the user its `progress_report`, then what happens next and the\n     exact phrase they should send to continue (for example: \"continue\"; or the handoff text if this\n     session cannot go on). Never end a turn with no message to the user.\n  3. How a turn may end is set by the next point, \"When your turn can end\": read it before you serve.\n  4. If something stops you (no network, a provider error, quota), tell the user what failed and how\n     to continue, the same way. A run waiting for your agent is safe on the server for 24 hours.\n     **Network blocked by your own sandbox** (the call to eternalsearch.net never leaves your\n     environment): name the exact remedy instead of stopping. Codex cloud: in the environment\n     settings turn \"Agent internet access\" on, add the domain `eternalsearch.net` to the allowlist\n     and allow all HTTP methods (the relay uses POST), then start the task again. Any web chat: add\n     the MCP server https://eternalsearch.net/api/mcp/omega-agent as a connector, then the calls\n     leave through the connector and your sandbox is not involved. Never try to reach the site\n     through a browser tool instead: the site is not an interface for agents.\n- **When your turn can end: continue by yourself, or say so plainly. This is mandatory.** Your\n  harness can stop you without warning (a turn time limit, a step limit, a context limit, a closed\n  chat), and once it has, you cannot write a single word more: the user sees a chat that simply went\n  quiet, and the run waits for an agent that is gone. Hosted agents and web chats have ended turns\n  after 3.5 and 6.5 hours of a run with no message at all. The message must therefore be written\n  BEFORE the cut, and wherever possible the run must not depend on your turn at all. In this order:\n  0. **With checkpoints (the default) your turn ends at every stage anyway**: serve until the\n     checkpoint, show it, end the turn (see \"At every checkpoint\"). The rules below matter inside a\n     long stage and for runs started with `--no-pauses`.\n  1. **Keep the run served outside your turn.** With the connector and a CLI or API provider\n     (Route A) start the serve in the background: `node omega-connector.mjs serve --run <run_key>\n     --detach`. It runs as its own process, independent of your session, logs to\n     `~/.omega-point/serve-<run_key>.log`, stops by itself when the run owes nothing more, and refuses\n     to start a second copy. Then the end of a turn costs nothing: tell the user the serve runs in\n     the background, and on every later turn read `node omega-connector.mjs progress <run_key>` and\n     tell them the report. Use the plain (foreground) `serve` only when you will watch it to the end.\n  2. **Bring yourself back.** If your harness can start your next turn by itself (a loop or wake-up\n     command, a scheduled task, a background job that re-invokes you, an automation), schedule your\n     next turn before this one ends, with the instruction \"continue serving Omega Point run\n     <run_key>: read its status, tell the user the progress_report, serve until relay_phase is\n     done\". Tell the user that you scheduled it and when it fires. This matters most in subagent\n     mode (Route B) and over MCP (Route C), where you answer the tasks yourself and nothing serves\n     while you are stopped.\n  3. **Otherwise stop before the limit, and say so.** Serve in stretches shorter than your limit:\n     about two thirds of it if you know it, at most 30 minutes if you do not. End every stretch with\n     the message of rule 2 of \"Keep the user informed\" and one plain sentence in the user's language that your turn has\n     ended, the run is waiting for you, and the exact phrase that continues it, for example in\n     Russian: \u00ab\u041c\u043e\u0439 \u0445\u043e\u0434 \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0441\u044f, \u043f\u0440\u043e\u0433\u043e\u043d \u0436\u0434\u0451\u0442. \u0427\u0442\u043e\u0431\u044b \u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u043b, \u043d\u0430\u043f\u0438\u0448\u0438\u0442\u0435: \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u00bb. If this\n     session cannot go on at all (the context is nearly full), give the handoff text instead.\n  4. **Whatever the user writes after a pause, start with the run.** Read the status, tell them its\n     `progress_report`, then continue serving (or check that the background serve is still running:\n     `serve --run <run_key> --detach` again starts one only if none is alive).\n- **The run goes as fast as your agent: the server never caps your width.** A run is a graph of\n  units of work, and the server knows which are independent (`GET <base>/parallelism` lists them:\n  the pillars of step 3, the domains of step 4b, the review batches of steps 2, 6 and 7, and after the engine\n  every title, claim, explanation, heading, gloss, audit and translation of the run). It issues\n  every ready unit at once and queues them for you in priority lanes: lane 0 is the engine, then\n  the finishing stages in the order that unlocks the most work (titles before explanations, before\n  headings, before translations). Your pull always gets the highest lane first. So answer as many\n  tasks at once as you can: one subagent per task, as many subagents as your harness and your\n  subscription allow. Every pull shows `waiting_by_lane`, the work ready for you right now.\n  Two narrower spots are deliberate and stated in `/parallelism`: step 7 composes gaps a few at a\n  time against the hypotheses already written (the ledger), and a few engine steps are a chain.\n- **Declare the width you really have.** The link's `concurrency` (1..256) is how many tasks the\n  server keeps in flight for you. A Codex login serves 4 at once well (Eternal Search runs five per\n  login in production), an API key 4 or more, a Claude Code login 1 (its credential file can race\n  on a token refresh; raise it knowingly with `--concurrency`, and the connector drops back to 1 on\n  the first auth error). Several logins on one machine add up: `--codex-home a,b,c` registers\n  three, each call goes to the least-loaded one, and the default width is 4 \u00d7 3. With subagents,\n  pull several tasks at once (`next --max N`, `omega_pull_task` with `max_tasks`) and answer them in\n  parallel. The width can change at any moment (`set-concurrency N`, `serve --concurrency N`,\n  `omega_set_concurrency`, `PATCH /links/current`): the next pull and the run's next fan-out follow.\n- **The token is the only key.** Registration is anonymous and open; the token is shown once.\n  Lose it and that link's runs stay stored but unreachable to you.\n- **Tiers, not model names.** Each task says `tier: high | mid | low`. You decide which of your\n  models answers each tier. Use your strongest model for `high`.\n- **JSON answers.** When a task carries `response_format: {\"type\": \"json_object\"}`, answer with\n  one JSON object and nothing else. The engine asks once for a correction if parsing fails.\n- **Every run is a full run.** There is no trial mode: `POST /runs` does not take a `test_mode`\n  flag, and a run started through it goes through the same steps, selection and finishing stages\n  as a run started on the public site. Expect the full several hundred calls from the first run.\n- **The run is not over when the engine is.** A run goes the same way as one started on the\n  public site: the engine reasons to the hypotheses of step 7, then the server names each\n  hypothesis, removes duplicates, writes the explanations and question headings, translates,\n  and writes the Russian report. Every one of those calls comes to you too, through the same\n  queue. The engine's own status turns to `completed_at_step` at that point: **that is not the\n  end.** Keep serving until the run's `relay_phase` is `done` (or `stopped`); `keep_serving`\n  in the status says the same in one word, `finishing_pending` counts what each stage still\n  owes, and `next_action` says in one sentence what to do. `GET /runs/{key}/result` answers 202\n  until the phase is `done`. Do not call `stop` on a run whose engine has finished: on such a\n  run `stop` ends the finishing phase, and the hypotheses stay without titles, explanations and\n  translations on both sides. The connector's `serve --run` handles all of this by itself.\n- **The goal is the user's question, in the user's own words and language.** Pass what the user\n  asked, as they wrote it: \u00ab\u0445\u043e\u0447\u0443 \u043d\u0430\u0439\u0442\u0438 \u043d\u043e\u0432\u044b\u0435 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u044b \u0432 \u0441\u0444\u0435\u0440\u0435 \u043c\u0435\u043c\u0435\u0442\u0438\u043a\u043e\u0432; \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0438 \u0441\u0430\u043c\u044b\u0435\n  \u043f\u0435\u0440\u0441\u043f\u0435\u043a\u0442\u0438\u0432\u043d\u044b\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b \u0438 \u0442\u0435\u043e\u0440\u0438\u0438\u00bb goes in as exactly that. Do not expand it into a brief.\n  Do not add how many hypotheses or families to return, ranking criteria, a template for each item,\n  methods or disciplines to use, evidence standards, or what to hand back (pages, files,\n  translations, posters): the engine decides all of that itself, and when such instructions arrive\n  inside the goal it builds its decomposition out of your checklist instead of the user's subject.\n  Russian versions and posters are set with `translate_ru` and `posters`, never in the goal. If the\n  question is unclear, ask the user and agree a wording with them before starting; then pass the\n  wording they agreed to. The engine sets aside instruction-like text it finds in a goal, but it can\n  only work from what the user actually asked if you send it.\n- **The engine stays on the subject of the question.** It produces hypotheses, rival theories and\n  experiments for every question, so \"find new hypotheses about X\" is a question about X itself:\n  the engine studies how X works, never how to rank or count hypotheses about X. Step 0 records the\n  subject; every stage receives it; at steps 2, 6 and 7 a review removes pillars, gap questions and\n  hypotheses that are about something else (the research method, a ranking of hypotheses, another\n  phenomenon). These reviews are ordinary tasks in your queue, answered like any other.\n- **A run can stop on purpose after step 2.** When no goal pillar carries the kind of answer the\n  question asks for, or none of them is about its subject, even after one retry, the engine stops\n  the run before step 3 instead of spending hours on a different question. The status is then\n  `interrupted_at_step`, `error_message` begins with \"stopped before step 3\" and states the reason,\n  and `next_action` says so. Tell the user that reason in their language and propose a clearer\n  wording of their question; start a new run with the wording they agree. Do not resume such a run\n  on your own: resuming is the user's decision (with a missing answer type it proceeds with the\n  saved pillars; with a lost subject it recomputes step 2).\n- **Ask about Russian before starting.** Russian versions (translations, Russian headings, the\n  Russian report, the Russian poster sheet) are the user's choice. Ask the\n  user, then pass `translate_ru: false` to skip them or leave the default `true`.\n- **Ask about posters before starting.** Poster sheets (one per hypothesis, English and Russian)\n  are the user's choice too. Ask the user, then pass `posters: true` to produce them; the default is\n  `false` (no posters).\n- **Ask about linking the account before starting.** In the same message as Russian and posters\n  (see \"Before the first run: ask three questions\"): a run linked to the user's account can always be\n  continued and is listed on their runs page. If they say yes, run `login` before `run`.\n- **One thing your model does not do: poster images.** Poster sheets are drawn by the server's\n  image generation; a text model cannot draw them. Everything else, text included, is yours.\n- **When the run is done, hand the user the published result as links.** A finished run is\n  published on the site: one page per open question (`/questions/open/<public_id>`, Russian at\n  `/ru/questions/open/<public_id>`) and one per hypothesis under it (`/omega/hypothesis/<public_id>`,\n  Russian at `/ru/...`). A run can publish several questions. `GET /runs/{key}/outcome`\n  (`node omega-connector.mjs outcome <run_key>`, `omega_run_outcome`) lists them all with both\n  language URLs, the heading of each question in English and Russian, the published hypotheses\n  under it, and the size of every text counted from what is stored (words and characters, English\n  and Russian separately, with run totals). Show the user the links first, one line per question\n  and its hypotheses under it; then state the total size of the texts; then offer to paste the\n  texts into the chat in the format they ask (whole, per question, per hypothesis, a summary, a\n  table). `?texts=true` (`outcome <run_key> --texts`, `texts: true`) carries the full stored texts\n  for that. Never shorten a text you paste; a whole hypothesis page is several thousand words, so\n  say so and let the user choose. The raw engine JSON (`result`) stays available, but it is not\n  what the user should be shown.\n- **Then save the run as files and tell the user where they are.** Once `relay_phase` is `done`,\n  the server hands out the run as a folder: one markdown file per published open question and per\n  published hypothesis, per language (`questions/<id>.en.md`, `hypotheses/<id>.ru.md`), each with\n  the text and structure of its page on the site; an index per language (`index.en.md`,\n  `index.ru.md`) linking every file; and the poster sheets as the original PNG files\n  (`posters/<id>.<lang>.png`), which the documents show as their images. Forty published hypotheses\n  give forty files per language. Only what the site publishes is in it: no drafts, no merged or\n  archived hypotheses. `node omega-connector.mjs documents <run_key>` writes the whole folder\n  (`--dir` to choose where); through the API or MCP, `GET /runs/{key}/documents`\n  (`omega_run_documents`) lists the files and `GET /runs/{key}/documents/file?name=<name>`\n  (`omega_run_document`) returns one markdown file per call. Save every file under its name, in\n  full; the links and images inside are relative to that layout. Before `done` the server answers\n  409 `run_not_done`.\n- **After the run, ask the user for feedback and store it.** When `relay_phase` is `done`, show\n  the user the outcome and ask what is wrong, missing or worth improving. Post their remarks with\n  `POST /runs/{key}/feedback` as `author: user, kind: feedback`, and the concrete corrections or\n  improvements you formulate from them as `author: agent, kind: proposal` (a proposal can point at\n  one hypothesis by its public id, one explanation, one question or one stage). The entries are\n  stored per run and read by the Eternal Search operator on the runs desk; nothing is applied\n  automatically. Feedback on a stopped or failed run is welcome too. Tell the user this\n  possibility exists even if they have nothing to say.\n- **A 502 or a reset connection means the server is restarting.** New code ships several\n  times a day and the relay is down for a few seconds each time; the 502 body is Cloudflare's\n  HTML page, not JSON. The connector script and the MCP server wait this out for you (retries\n  with growing pauses, about three minutes in total). If you call the API yourself (Route B),\n  do the same: wait 15 seconds and repeat the same call. Nothing is lost: a link, a run and a\n  leased task all live in storage that survives the restart, and a run whose agent stays\n  silent is parked, not failed (`awaiting_user_llm`), until you pull again.\n\n## Route A: the connector script (recommended), relay mode\n\n`scripts/omega-connector.mjs` needs Node 20+ and nothing else. Fetch it from\n`<base>/skill/scripts/omega-connector.mjs` or use this package. (For a server run the same script\nregisters, starts and follows the run: see \"A run on the server\".)\n\n1. Register a link with the provider you will answer with:\n\n   ```bash\n   node omega-connector.mjs register --provider codex        # your Codex subscription login, concurrency 4\n   node omega-connector.mjs register --provider codex --codex-home ~/.codex-a,~/.codex-b   # two logins, concurrency 8\n   node omega-connector.mjs register --provider claude       # your Claude Code login, concurrency 1\n   node omega-connector.mjs register --provider openai-key   # OPENAI_API_KEY, concurrency 4\n   node omega-connector.mjs register --provider base-url --base-url https://host/v1 --key K\n   ```\n   Options: `--high/--mid/--low <model>` to set the tier map, `--concurrency N` (1..256, how many\n   tasks you answer at once; the engine fans out that wide), `--label \"...\"`.\n   The token is saved to `~/.omega-point/link.json` (mode 600).\n\n2. Ask the three questions of \"Before the first run\" in one message (Russian versions, poster\n   sheets, linking the account) and wait for the answers. If the user wants the account link, run\n   `login`, give them the URL and the code, then `login --wait`. Then start the run with the user's\n   question in their own words as the goal (see \"The goal is the user's question\" above); add\n   `--no-russian` if they declined Russian versions and `--posters` if they requested poster sheets:\n\n   ```bash\n   node omega-connector.mjs run \"restore youthful skin properties in middle-aged adults\"\n   node omega-connector.mjs run \"<goal>\" --no-russian\n   node omega-connector.mjs run \"<goal>\" --posters\n   node omega-connector.mjs run \"<goal>\" --no-pauses               # only if the user asks for an unattended run\n   ```\n   `run` prints the handoff text between two `\u2500\u2500\u2500\u2500` rules. Show it to the user verbatim now,\n   before serving: they paste it into a new session if this one ends.\n\n3. Serve the run until its `relay_phase` is `done`. Keep this process alive the whole time,\n   including after the engine has finished (titles, dedup, explanations, translations and\n   headings come afterwards):\n\n   ```bash\n   node omega-connector.mjs serve --run <run_key> --detach          # recommended: in the background, survives your session\n   node omega-connector.mjs serve --run <run_key>\n   node omega-connector.mjs serve --run <run_key> --concurrency 4   # widen or narrow the link first\n   ```\n   With `--detach` the serve prints its pid and its log file and keeps running after your turn or\n   session ends; the progress reports go to that log, so read `progress <run_key>` on every turn\n   and tell the user (see \"When your turn can end\").\n   At a checkpoint `serve` finishes the answers in hand, saves the stage result, prints it between\n   `\u2500\u2500\u2500\u2500 STAGE RESULT: TELL THE USER NOW` rules and exits (a detached serve too: its log has it, and\n   `checkpoint <run_key>` shows it again). Follow \"At every checkpoint\"; after the user's continue:\n\n   ```bash\n   node omega-connector.mjs checkpoint <run_key>                   # the stage result the run waits at (overview + file)\n   node omega-connector.mjs continue <run_key>                     # ONLY on the user's word; then serve again\n   node omega-connector.mjs checkpoint <run_key> --seq 3           # an earlier stage result again\n   ```\n   `serve` pulls tasks (long poll, 25 s), answers up to `concurrency` of them at once with your\n   provider, validates JSON when asked, posts the answers, logs every task with its tier, size\n   and duration, and exits by itself when the run owes nothing more. Ctrl-C releases in-flight\n   tasks back to the queue so nothing is lost. `set-concurrency N` changes the width of a link\n   at any time, also while a run is executing. When a progress report is due, `serve` prints it\n   on stderr between `\u2500\u2500\u2500\u2500 TELL THE USER NOW` rules: watch its output and tell the user each one\n   in the chat while it keeps serving.\n\n4. Hand the user the outcome: `outcome` prints the links to every published question and\n   hypothesis (English and Russian), the size of each text and the totals; relay that list, then\n   offer to paste the texts into the chat in the format the user asks (`--texts` carries them, in\n   full). The raw engine JSON is still there as `result` (202 with `next_action` while the run\n   still owes calls; `--engine-only` reads it early on purpose), but the links are what the user\n   should get. Then save the documents folder with `documents <run_key>` and tell the user its\n   path (the command prints it; `index.ru.md` or `index.en.md` is the file to open first):\n\n   ```bash\n   node omega-connector.mjs status <run_key>\n   node omega-connector.mjs progress <run_key>                     # the progress report to tell the user now\n   node omega-connector.mjs outcome <run_key>                      # links + sizes, readable on stderr, JSON on stdout\n   node omega-connector.mjs outcome <run_key> --texts --out texts.json   # the full texts, for pasting in any format\n   node omega-connector.mjs documents <run_key>                    # the folder: index, one .md per question and hypothesis, poster PNGs\n   node omega-connector.mjs result <run_key> --out result.json     # raw engine result\n   node omega-connector.mjs runs | stop <run_key> | resume <run_key>\n   ```\n\n5. Ask the user for feedback and store it, with the corrections or improvements you propose:\n\n   ```bash\n   node omega-connector.mjs feedback <run_key> --author user --kind feedback --file user-note.txt\n   node omega-connector.mjs feedback <run_key> --author agent --kind proposal --target hypothesis:<public_id> --file proposal.txt\n   node omega-connector.mjs feedback-list <run_key>\n   ```\n\n## Route B: subagent mode (no CLI login to reuse), relay mode\n\nYour harness answers each task itself, with a fresh subagent per task, while the script only\nkeeps the state:\n\n```bash\nnode omega-connector.mjs register --provider subagent --kind claude_code\n# ask the three questions of \"Before the first run\"; if the user wants the account link:\nnode omega-connector.mjs login                 # returns at once: tell the user the URL and the code\nnode omega-connector.mjs login --wait          # after that: waits for their confirmation\nnode omega-connector.mjs run \"<goal>\"          # prints the handoff text: show it to the user verbatim before the first pull\nnode omega-connector.mjs next --max 4         # prints up to 4 tasks {task_id, tier, messages, response_format, timeout_s}\n# answer them IN PARALLEL, one fresh subagent per task, the strongest model you have for its tier; one file each\nnode omega-connector.mjs answer <task_id> --file answer.txt\n```\n\nWhen the JSON printed by `next` carries `reports_for_user`, tell the user each `progress_report` in\nthe chat first (see \"Keep the user informed\"), then answer the tasks. When it carries `checkpoints`\nand `stop_now: true`, answer only the tasks you already hold, then follow \"At every checkpoint\"\n(the connector has already saved the stage result and printed the overview). In this mode nothing serves\nthe run while your turn is stopped: schedule your own next turn if your harness can, or serve in\nstretches and end each one as \"When your turn can end\" says.\n`next --max N` pulls up to N tasks and widens the link to N when it is narrower, so the engine\nissues that many at once; run one subagent per task at the same time rather than one after the\nother, and post each answer as soon as it is ready. Plain `next` prints one task. Loop `next` /\n`answer` until `status` shows `relay_phase: done` (or `stopped`); `keep_serving` false means the\nsame. Answer within `timeout_s` seconds of pulling, or the engine re-issues the task. When `next` returns `task: null`, the server is doing work that needs no model (literature\nsearch, storage, the wait for the next stage); call `next` again. A status of `completed_at_step`\nwith `relay_phase: finishing` means the engine is done and the finishing stages are still sending\ntasks: keep looping. Pass `--no-russian` when the user does not want Russian versions and\n`--posters` when they request poster sheets. When the phase is `done`, run `outcome`: give the user the\nlinks and the text sizes, offer the texts in the format they ask (`outcome <run_key> --texts`), then\nsave the folder with `documents <run_key>` and tell the user where it is, then ask for feedback and\nstore it with `feedback` (see steps 4 and 5 of Route A).\n\n## Route C: remote MCP, no script at all (both modes)\n\nAdd the Eternal Search MCP server and use its tools in the same loop:\n\n```bash\nclaude mcp add --transport http omega-point https://eternalsearch.net/api/mcp/omega-agent \\\n  --header \"Authorization: Bearer <token>\"\n```\n\nTools: `omega_register` (no token needed; returns the token once), `omega_set_concurrency`,\n`omega_start_run`, `omega_pull_task` (`max_tasks` up to your concurrency; answer the tasks in\nparallel subagents), `omega_complete_task`, `omega_run_status`, `omega_run_outcome` (links,\nsizes; `texts: true` for the full texts), `omega_run_documents` and `omega_run_document` (the\nfolder of markdown files and poster PNGs, one file per call), `omega_run_result` (raw engine JSON), `omega_stop_run`,\n`omega_resume_run`, `omega_submit_feedback`, `omega_list_feedback`, `omega_run_checkpoint` and\n`omega_continue_run` (checkpoints: see \"At every checkpoint\"; `omega_start_run` takes `pauses`). `mcp.json`\nin this package is a ready configuration.\n`omega_start_run` and `omega_resume_run` return `handoff_text` next to the `run_key`: show it to the\nuser verbatim at once, before the first `omega_pull_task`.\nWhen `omega_pull_task` returns a non-empty `reports_for_user`, tell the user each `progress_report` in\nthe chat at once; before ending a turn, read `omega_run_status` and tell them its `progress_report`.\n\n## If your session ends before the run does\n\nThe run lives on the server, not in your session. The user opens a new session and pastes the\nhandoff text above; it names the run key and says what to do. Do not start a new run: finish this\none. If the user has only the run key, or nothing, `node omega-connector.mjs runs` lists the link's\nruns and `last_run` in `~/.omega-point/link.json` is the most recent one; that file also holds the\ntoken, so this works on the machine where the link was registered. On another machine, or when the\ntoken is missing or rejected (401), do not register a new link (the run belongs to the old one): run\n`node omega-connector.mjs login` and ask the user to choose \"One of my links\" with that link; the agent\nreceives a new token for it and continues the same run key. This works when the link was linked to\nthe user's account; for an anonymous link only its original token reaches it.\nThis paragraph is about Omega runs and the link token. It never applies to site work (grant searches,\nprojects): a web client does that through the `eternal-search` connector signed in once by OAuth (see\n\"Acting for the person on the site\"), so a missing site key in a new chat is never a reason to run\n`login --site` or to send a Connect link.\n\n1. **Read the status:** `node omega-connector.mjs status <run_key>` (`GET /runs/{key}`,\n   `omega_run_status`). `relay_phase` and `next_action` say what to do:\n   - `checkpoint` is not null: the run waits for the user at a stage result. Show it (\"At every\n     checkpoint\") and end the turn; continue only on the user's word.\n   - `engine` or `finishing` (`keep_serving: true`; status `executing`, `awaiting_user_llm` or\n     `completed_at_step`): the run is waiting for you. Serve it again exactly as before: `serve --run\n     <run_key> --detach` (Route A; it starts nothing if a background serve is still alive), the `next` / `answer` loop (Route B), `omega_pull_task` (Route C). Tasks\n     whose answers never came back are re-issued by the engine; nothing is lost and nothing needs\n     resuming. Do not call `resume` on such a run.\n   - `stopped` and `error_message` begins with \"stopped before step 3\": the engine stopped the run\n     on purpose (see \"A run can stop on purpose after step 2\"). Tell the user the reason and propose\n     a clearer wording; resume only if the user asks for it.\n   - `stopped` with `resumable: true`: `node omega-connector.mjs resume <run_key>`\n     (`POST /runs/{key}/resume`, `omega_resume_run`). The run continues **under the same key**.\n     A lost engine (status `stopped_awaiting_user_llm` after 24 h without an agent, or\n     `interrupted_at_step`) continues from its saved steps (the step it died in is redone from its\n     start); `resume_count` in the status counts the restarts. A run whose engine had finished but\n     whose finishing phase was stopped (status `completed_at_step`, someone called `stop` while\n     titles, explanations or translations were still owed) is revived: the phase goes back to\n     `finishing` and the same stage rows are served again, nothing is recomputed. Either way show\n     the returned `handoff_text` to the user again, then serve the same key.\n   - `done`: nothing more will come. Hand the user the outcome (links, sizes, texts on request),\n     save the documents folder (`documents <run_key>`) and ask for feedback (steps 4 and 5 of Route A).\n   - `stopped` with `resumable: false`: the run cannot continue (it failed without saved steps).\n     Tell the user, offer to store feedback, and start a new run only if they ask for one.\n2. **Serve to the end again:** `relay_phase: done`, then the outcome, then feedback.\n3. **Show the handoff text again** when the user asks for it or when you expect to run out of\n   context: it is in every status response (`handoff_text`), so you never need to recompose it.\n\n## The wire protocol in one screen\n\nAll calls are JSON; every call except registration and reading a run's published results (`outcome`, `documents`) sends `Authorization: Bearer <token>`.\n\n| Call | Purpose |\n|---|---|\n| `POST /links` | `{agent_kind, provider_label, tier_models, concurrency}` \u2192 `{link_id, token}` once |\n| `POST /links/login`, `/links/login/poll` | device-code login to the user's site account: `{device_code, user_code, verification_url, interval_s}`, then poll \u2192 202 waiting, 200 `{token, link_id, rotated}` (a token null = this link is now linked and keeps its token), 410 declined or expired |\n| `GET /account/runs` | every run of every link of the account (the link must be account-linked) and `runs_page` |\n| `PATCH /links/current` | `{concurrency}` (1..256): how many tasks you answer at once; the next pull and the run's next fan-out follow |\n| `GET /skill/version` | public: the current skill `revision`, the sha256 of every skill file and where to download each; compare your copy before a run, a resume and a new session |\n| `GET /parallelism` | public: which units of a run are independent, the barriers with their reasons, and the priority lanes |\n| `POST /runs` | `{goal, global_lens?, translate_ru?, posters?, pauses?}` \u2192 `{run_key, run_mode, handoff_text, queued_position?}`; always a full run (no trial mode); `run_mode` server: the server executes it, up to three runs per person at a time, queued when the pool is full, `pauses` ignored; `run_mode` agent_relay: `pauses` default true, a checkpoint after every stage; `goal` is the user's question in their own words, never a brief; `translate_ru` defaults to true and `posters` defaults to false; show `handoff_text` to the user verbatim |\n| `POST /tasks/pull` | `{max_tasks \u2264 concurrency, wait_s \u2264 25}` \u2192 `{tasks: [{task_id, run_key, tier, effort?, messages, response_format, temperature, max_tokens, timeout_s, attempt}], queue_depth, in_flight, waiting_by_lane, reports_for_user: [{run_key, reason, relay_phase, progress_report}], checkpoints: [{run_key, seq, stage, title}], checkpoint_instructions}`; `effort` is the optional low, medium or high reasoning hint; highest-priority lane first; the pull is your heartbeat; tell the user every report in `reports_for_user` at once |\n| `POST /tasks/{id}/complete` | `{content, model?, usage?}` or `{error: {kind, message}}` |\n| `POST /tasks/{id}/release` | give a pulled task back untouched |\n| `GET /runs/{key}` | status, step, `run_mode` (server or agent_relay), `relay_phase`, `keep_serving` (always false for a server run), `finishing_pending`, `next_action`, `progress_report` (for the user), `handoff_text`, `resumable`, `translate_ru`, `posters`, `feedback_count`, counters, `awaiting_since`, `queue_depth`, `agent_present` |\n| `GET /runs/{key}/outcome` | open to anyone, any run, no token: what the run published, for the user: per open question its English and Russian page URLs and headings, the published hypotheses under it with their URLs, the size of every text (words and characters, counted) and run totals; `?texts=true` adds the full texts; `complete: false` while finishing |\n| `GET /runs/{key}/documents` | open to anyone, any run, no token, once the run's pages are final (`publication` finished or stopped): the run as a folder; every file name (its relative path), kind (index, question, hypothesis, poster), language, page URL, and for posters the `image_url` of the PNG; the index texts; 409 `run_not_done` before |\n| `GET /runs/{key}/documents/file?name=<name>` | one markdown file of that folder in full (`content`, `chars`, `words`); for a poster, its `image_url` |\n| `GET /runs/{key}/result` | the raw engine result JSON once `relay_phase` is `done`; 202 while the engine runs or the finishing stages still serve (`?engine_only=true` reads the engine result early) |\n| `GET /runs/{key}/checkpoint` | the stage result the run waits at: `summary_markdown` (the overview for the chat), `body_markdown` (the complete result, a file); 404 `no_checkpoint` when not paused. `/checkpoints` lists all, `/checkpoints/{seq}` one |\n| `POST /runs/{key}/continue` | only on the user's word: release the checkpoint; held tasks go back; names the next checkpoint when it is already due |\n| `POST /runs/{key}/stop`, `/resume` | stop (on a run whose engine is done: ends the finishing phase); continue a lost or parked run under the same key from its saved steps, repeatable \u2192 `{run_key (same), resume_count, handoff_text}` |\n| `POST /runs/{key}/feedback` | `{entries: [{author: user\\|agent, kind: feedback\\|proposal, target_kind?, target_id?, body}]}` \u2192 stored rows; any run status |\n| `GET /runs/{key}/feedback` | the stored feedback and proposals with their disposition |\n\nRun statuses: `queued_for_slot` (server run waiting for a free slot), `executing`,\n`awaiting_user_llm` (relay: tasks waiting, nobody pulling for 60 s), `completed`,\n`completed_at_step`, `interrupted_at_step`, `failed_at_execute`, `stopped_awaiting_user_llm`\n(relay: 24 h without you; resumable under the same key). The engine status alone ends too\nearly: `relay_phase` is `engine` while the engine runs, `finishing` while titles, dedup,\nexplanations, translations and headings are still written, `done` when nothing more will come,\n`stopped` when the run ended without finishing. In relay mode, serve while `keep_serving` is true.\n\n## What stays on the server\n\nLiterature search (PubMed, OpenAlex, Semantic Scholar), embedding-based deduplication,\nvalidation, state and storage, and, for a server run, every model call. Every run is stored in\nthe Eternal Search database and published on the site when it finishes.\n","mcp.json":"{\n  \"mcpServers\": {\n    \"omega-point\": {\n      \"type\": \"http\",\n      \"url\": \"https://eternalsearch.net/api/mcp/omega-agent\",\n      \"headers\": {\n        \"Authorization\": \"Bearer <paste the token returned by omega_register or POST /links>\"\n      }\n    }\n  }\n}\n","scripts/omega-connector.mjs":"#!/usr/bin/env node\n/**\n * Omega Point agent connector.\n *\n * The Omega Point engine (Eternal Search) orchestrates a full research run on its server:\n * steps, prompts, literature search, deduplication, validation, state. At present every run\n * started here EXECUTES ON THE SERVER (the start response and the status say run_mode: server):\n * this script registers a link, starts the run, follows its status and fetches the result; it\n * serves no tasks. When the server hands model calls to agents instead (run_mode: agent_relay),\n * `serve`, `next` and `answer` below answer them with your model. Node 20+, no dependencies.\n *\n *   node omega-connector.mjs register --provider codex            # or claude | openai-key | base-url; --concurrency N\n *   node omega-connector.mjs run \"restore youthful skin in middle-aged adults\" [--no-russian] [--posters]   # always a full run\n *   node omega-connector.mjs status <run> | progress <run>        # where the run is; the progress report to tell the user\n *   node omega-connector.mjs serve --run <run_key>                 # server run: watches the status until done (one poll a minute);\n *                                                                  # relay run: pull \u2192 answer \u2192 complete until relay_phase is done\n *   node omega-connector.mjs login                                # link this agent to the user's site account: prints a URL and\n *                                                                  # a code and returns; then `login --wait` waits for the user's\n *                                                                  # confirmation. Also the way back after a lost token (same link)\n *   node omega-connector.mjs runs --account                       # every run of the account's linked agents\n *   node omega-connector.mjs checkpoint <run>                     # relay run: the stage result it waits at: overview + saved file\n *   node omega-connector.mjs continue <run>                       # relay run, ONLY on the user's word: release it, then serve again\n *   node omega-connector.mjs serve --run <run_key> --concurrency 4 # relay run: answer 4 tasks at once; the engine fans out that wide\n *   node omega-connector.mjs serve --run <run_key> --detach        # in the background, independent of this session;\n *                                                                  # prints the pid and the log; check with `progress <run>`\n *   node omega-connector.mjs outcome <run> [--texts] [--out file]   # when done: links to every published question and\n *                                                                  # hypothesis (EN + RU), counted text sizes; --texts = the full texts\n *   node omega-connector.mjs documents <run> [--dir DIR]         # when done: one markdown file per published question and\n *                                                                  # hypothesis (EN + RU) as on the site, an index, poster PNGs\n *   outcome and documents read ANY run by its key (pipeline-...), also one started on the site or by\n *   someone else, and need no link: no register first (--server picks another server).\n *   node omega-connector.mjs status | runs | result <run> | stop <run> | resume <run>   (resume keeps the run key)\n *   node omega-connector.mjs progress <run>                       # the progress report to tell the user now (also in status)\n *   node omega-connector.mjs check-skill                          # is this copy of the skill current? (also checked before run/serve/next)\n *   node omega-connector.mjs update                               # download the current skill files from the server over this copy\n *   node omega-connector.mjs feedback <run> --author user|agent --kind feedback|proposal [--target hypothesis:<public_id>] --file note.txt\n *   node omega-connector.mjs feedback-list <run>\n *\n * Omega Point 2.00 (needs `login` first; one study per person runs at a time). Ask the user which version\n * they want before the first run:\n *   node omega-connector.mjs study-start \"question\" [--language ru] [--mode strategic] [--field biology]\n *   node omega-connector.mjs study-status [<study>]                # progress_report for the user and the next action\n *   node omega-connector.mjs study-questions [<study>]             # clarifying questions waiting for the user\n *   node omega-connector.mjs study-answer <study> <question_id> --file answer.txt\n *   node omega-connector.mjs study-cards [<study>]                 # every card in full\n *   node omega-connector.mjs study-export [<study>] [--markdown] [--out file]\n *   node omega-connector.mjs study-stop | study-resume [<study>]\n *\n * Subagent mode, relay runs only (no CLI login to reuse; your harness answers each task itself):\n *   node omega-connector.mjs next                                  # prints ONE task as JSON\n *   node omega-connector.mjs next --max 4                          # prints up to 4 tasks: answer them in parallel subagents\n *   node omega-connector.mjs answer <task_id> --file answer.txt    # posts your answer\n *\n * Concurrency defaults: codex 4 (the codex CLI is stateless per call; Eternal Search runs five per\n * login in production), claude 1 (the Claude CLI reads its credential file directly and can race on\n * a token refresh; raise it with --concurrency at your own risk, and the connector drops back to 1\n * on the first auth error), API keys 4. The engine fans out inside a run as wide as the link says.\n *\n * State lives in ~/.omega-point/link.json (override with OMEGA_LINK_FILE). Protocol reference:\n * <base_url>/openapi.json. Public page: https://eternalsearch.net/omega-point/agents\n */\n\nimport { spawn } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { fileURLToPath } from 'node:url';\nimport { mkdirSync, readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync, statSync, openSync, chmodSync, realpathSync } from 'node:fs';\nimport { homedir, tmpdir } from 'node:os';\nimport path from 'node:path';\n\nconst DEFAULT_BASE = 'https://eternalsearch.net/omega-point/api/agent/v1';\nconst LINK_FILE = process.env.OMEGA_LINK_FILE || path.join(homedir(), '.omega-point', 'link.json');\nconst TASK_DIR = path.join(path.dirname(LINK_FILE), 'tasks');\n// Where this copy of the skill lives: the connector sits in <skill>/scripts/ in the package, but a\n// user may have saved it alone; then SKILL.md and mcp.json are looked for beside it.\nconst SELF_FILE = fileURLToPath(import.meta.url);\nconst SKILL_ROOT = path.basename(path.dirname(SELF_FILE)) === 'scripts' ? path.dirname(path.dirname(SELF_FILE)) : path.dirname(SELF_FILE);\nconst SKILL_CHECK_FILE = path.join(path.dirname(LINK_FILE), 'skill-check.json');\nconst SKILL_CHECK_EVERY_MS = 10 * 60 * 1000;\n\n// Tier \u2192 model defaults per provider. Override at register time: --high/--mid/--low.\nconst DEFAULT_TIERS = {\n  'openai-key': { high: 'gpt-6-astra', mid: 'gpt-5.6-sol', low: 'gpt-5.6-terra' },\n  'base-url': { high: 'gpt-6-astra', mid: 'gpt-5.6-sol', low: 'gpt-5.6-terra' },\n  codex: { high: 'gpt-6-astra', mid: 'gpt-5.6-sol', low: 'gpt-5.6-terra' },\n  claude: { high: 'opus', mid: 'sonnet', low: 'haiku' },\n  subagent: { high: 'subagent', mid: 'subagent', low: 'subagent' },\n};\nconst PROVIDERS = Object.keys(DEFAULT_TIERS);\n\n// How many tasks the connector answers at once unless --concurrency says otherwise. The engine\n// fans out inside a run exactly this wide, so 1 makes a run a chain of hundreds of serial calls.\n//   codex: the CLI is stateless per call (a fresh `codex exec` with its own thread id); Eternal\n//          Search runs five per login in production (src/lib/claude.ts, 2026-07-20).\n//   claude: the CLI reads its credential file directly and races on OAuth refresh under\n//          concurrency (401s + a corrupted file, same measurement). Raise it with --concurrency\n//          knowingly; the serve loop drops to 1 on the first auth error.\n//   subagent: the harness sets its own width with `next --max N` and `set-concurrency N`.\nconst DEFAULT_CONCURRENCY = { 'openai-key': '4', 'base-url': '4', codex: '4', claude: '1', subagent: '1' };\n\n// \u2500\u2500\u2500 tiny CLI plumbing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction log(msg) {\n  process.stderr.write(`[${new Date().toISOString()}] ${msg}\\n`);\n}\n\nfunction parseArgs(argv) {\n  const flags = {};\n  const positional = [];\n  for (let i = 0; i < argv.length; i++) {\n    const a = argv[i];\n    if (a.startsWith('--')) {\n      const key = a.slice(2);\n      const next = argv[i + 1];\n      if (next === undefined || next.startsWith('--')) flags[key] = true;\n      else { flags[key] = next; i++; }\n    } else positional.push(a);\n  }\n  return { flags, positional };\n}\n\nfunction loadLink() {\n  if (!existsSync(LINK_FILE)) {\n    throw new Error(`No link yet. Run: node omega-connector.mjs register --provider <${PROVIDERS.join('|')}>`);\n  }\n  return JSON.parse(readFileSync(LINK_FILE, 'utf8'));\n}\n\n// Reading what a run published is open to anyone: outcome and documents use the saved link when there\n// is one (its server, and its token so the owner keeps the serving hints), and otherwise go to the\n// server anonymously. No registration is needed to fetch the results of any run by its key.\nfunction readerLink(flags) {\n  if (existsSync(LINK_FILE)) {\n    const link = loadLink();\n    return flags.server ? { ...link, base_url: String(flags.server).replace(/\\/$/, ''), token: null } : link;\n  }\n  return { base_url: String(flags.server || process.env.OMEGA_AGENT_BASE || DEFAULT_BASE).replace(/\\/$/, ''), token: null };\n}\n\nfunction saveLink(link) {\n  mkdirSync(path.dirname(LINK_FILE), { recursive: true, mode: 0o700 });\n  // mode only applies to newly created files; tighten an existing credential file too.\n  if (existsSync(LINK_FILE)) chmodSync(LINK_FILE, 0o600);\n  writeFileSync(LINK_FILE, JSON.stringify(link, null, 2), { mode: 0o600 });\n  chmodSync(LINK_FILE, 0o600);\n}\n\n// The server restarts for a few seconds when new code ships, and the edge in front of it\n// answers 502 (an HTML page, not JSON) or resets the connection meanwhile. Every call below\n// waits that out instead of failing the task: a link, a run and a leased task all live in\n// storage that survives the restart, so the same call simply succeeds a moment later.\nconst TRANSIENT_STATUS = new Set([429, 502, 503, 504]);\nconst RETRY_DELAYS_MS = [2_000, 5_000, 10_000, 20_000, 30_000, 30_000, 30_000, 30_000]; // \u2248 2.6 min\nconst sleep = (ms, signal) => new Promise((resolve, reject) => {\n  const t = setTimeout(resolve, ms);\n  signal?.addEventListener('abort', () => { clearTimeout(t); reject(signal.reason || new Error('aborted')); }, { once: true });\n});\n\nasync function api(link, method, route, body, { signal, retries = RETRY_DELAYS_MS.length } = {}) {\n  const headers = { 'Content-Type': 'application/json' };\n  if (link?.token) headers.Authorization = `Bearer ${link.token}`;\n  for (let attempt = 0; ; attempt++) {\n    const canRetry = attempt < retries && !signal?.aborted;\n    const delay = RETRY_DELAYS_MS[Math.min(attempt, RETRY_DELAYS_MS.length - 1)];\n    let res, text;\n    try {\n      res = await fetch(`${link.base_url}${route}`, {\n        method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal,\n      });\n      text = await res.text();\n    } catch (e) {\n      if (!canRetry) throw new Error(`${method} ${route} \u2192 no connection after ${attempt + 1} attempt(s): ${e.message}`);\n      log(`${method} ${route}: ${e.message}; the server may be restarting, retrying in ${delay / 1000} s (${attempt + 1}/${retries})`);\n      await sleep(delay, signal);\n      continue;\n    }\n    let data;\n    try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }\n    if (!res.ok) {\n      if (TRANSIENT_STATUS.has(res.status) && canRetry) {\n        log(`${method} ${route} \u2192 ${res.status}; the server may be restarting, retrying in ${delay / 1000} s (${attempt + 1}/${retries})`);\n        await sleep(delay, signal);\n        continue;\n      }\n      const err = new Error(`${method} ${route} \u2192 ${res.status} ${data.error || ''} ${data.message || text.slice(0, 300)}`.trim());\n      err.status = res.status;\n      err.data = data;\n      throw err;\n    }\n    return data;\n  }\n}\n\n// \u2500\u2500\u2500 is this copy of the skill current? \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Users attach the skill to a web chat once and the chat keeps that copy. The server publishes the\n// sha256 of every skill file (GET /skill/version); the connector compares its own file and the\n// SKILL.md / mcp.json beside it, and says loudly when they are outdated.\n\nfunction localSkillPath(rel) {\n  return rel === 'scripts/omega-connector.mjs' ? SELF_FILE : path.join(SKILL_ROOT, rel);\n}\n\nfunction sha256File(file) {\n  return createHash('sha256').update(readFileSync(file)).digest('hex');\n}\n\nfunction localSkillHashes(rels) {\n  const out = {};\n  for (const rel of rels) {\n    const file = localSkillPath(rel);\n    out[rel] = existsSync(file) ? sha256File(file) : null;\n  }\n  return out;\n}\n\nasync function fetchSkillVersion(baseUrl) {\n  const res = await fetch(`${baseUrl}/skill/version`, { signal: AbortSignal.timeout(15_000) });\n  if (!res.ok) throw new Error(`GET /skill/version \u2192 ${res.status}`);\n  return res.json();\n}\n\n// \u2192 { revision, stale: [rel], not_found: [rel], error? }. Cached for 10 minutes unless the local\n// files changed or `force`.\nasync function skillFreshness(baseUrl, { force = false } = {}) {\n  let cached = null;\n  try { cached = JSON.parse(readFileSync(SKILL_CHECK_FILE, 'utf8')); } catch {}\n  if (!force && cached && Date.now() - cached.at < SKILL_CHECK_EVERY_MS && cached.base_url === baseUrl) {\n    const now = localSkillHashes(Object.keys(cached.local || {}));\n    if (JSON.stringify(now) === JSON.stringify(cached.local)) return cached;\n  }\n  let version;\n  try { version = await fetchSkillVersion(baseUrl); }\n  catch (e) {\n    log(`skill check skipped: ${e.message}`);\n    return { error: e.message, stale: [], not_found: [] };\n  }\n  const remote = version.files_sha256 || {};\n  const local = localSkillHashes(Object.keys(remote));\n  const stale = Object.keys(remote).filter((rel) => local[rel] && local[rel] !== remote[rel]);\n  const notFound = Object.keys(remote).filter((rel) => !local[rel]);\n  const result = { at: Date.now(), base_url: baseUrl, revision: version.revision, local, stale, not_found: notFound,\n    skill_root: SKILL_ROOT };\n  try { mkdirSync(path.dirname(SKILL_CHECK_FILE), { recursive: true }); writeFileSync(SKILL_CHECK_FILE, JSON.stringify(result, null, 2)); } catch {}\n  log(`skill check: server revision ${version.revision}; ${stale.length ? `OUTDATED here: ${stale.join(', ')}` : 'this copy is current'}${notFound.length ? ` (not found beside the connector: ${notFound.join(', ')})` : ''}`);\n  return result;\n}\n\nfunction skillUpdateNotice(fr) {\n  if (!fr?.stale?.length) return null;\n  return `Your copy of the Omega Point skill is outdated: ${fr.stale.join(', ')} ${fr.stale.length === 1 ? 'differs' : 'differ'} from the server's revision ${fr.revision}. `\n    + 'Run: node omega-connector.mjs update, then re-read SKILL.md before going on, and tell the user the skill was updated. '\n    + 'A run in progress is not affected: it continues under the same key.';\n}\n\nasync function warnIfSkillStale(baseUrl, opts) {\n  const fr = await skillFreshness(baseUrl, opts);\n  const notice = skillUpdateNotice(fr);\n  if (notice) process.stderr.write(`\\n\u2500\u2500\u2500\u2500 YOUR OMEGA POINT SKILL IS OUTDATED \u2500\u2500\u2500\u2500\\n${notice}\\n\u2500\u2500\u2500\u2500 end \u2500\u2500\u2500\u2500\\n\\n`);\n  return fr;\n}\n\n// \u2500\u2500\u2500 providers: (task) \u2192 { content, model, usage } \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction flattenMessages(messages) {\n  return messages.map((m) => {\n    const role = (m.role || 'user').toLowerCase();\n    const content = typeof m.content === 'string' ? m.content\n      : Array.isArray(m.content) ? m.content.map((p) => p?.text || '').join('\\n') : '';\n    if (role === 'system') return `[System]\\n${content}`;\n    if (role === 'assistant') return `[Assistant]\\n${content}`;\n    return `[User]\\n${content}`;\n  }).join('\\n\\n');\n}\n\nfunction splitSystem(messages) {\n  const system = messages.filter((m) => m.role === 'system').map((m) => m.content).join('\\n\\n');\n  const rest = messages.filter((m) => m.role !== 'system');\n  return { system, prompt: flattenMessages(rest) };\n}\n\nfunction jsonInstruction(task) {\n  return task.response_format?.type === 'json_object'\n    ? '\\n\\n[Format]\\nRespond with a single valid JSON object only. No prose, no markdown code fences. Begin with { and end with }.'\n    : '';\n}\n\n// Reasoning models accept `reasoning_effort` on /chat/completions; other models answer 400 to it, which\n// would fail the task. Known families get the hint; anything else is sent without it, and a 400 that\n// names the parameter is retried once without it (both logged).\nexport function acceptsReasoningEffort(model) {\n  const name = String(model || '').toLowerCase().split('/').pop();\n  return /^(o\\d|gpt-5|gpt-oss)/.test(name);\n}\n\nexport async function callOpenAICompatible(task, model, provider, signal) {\n  let withEffort = Boolean(task.effort) && acceptsReasoningEffort(model);\n  if (task.effort && !withEffort) log(`task ${task.task_id || '?'}: effort=${task.effort} not sent; model ${model} is not known to accept reasoning_effort`);\n  const send = (effort) => fetch(`${provider.base_url.replace(/\\/$/, '')}/chat/completions`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${provider.key}` },\n    body: JSON.stringify({\n      model,\n      messages: task.messages,\n      temperature: task.temperature ?? undefined,\n      max_tokens: task.max_tokens ?? undefined,\n      response_format: task.response_format?.type === 'json_object' ? { type: 'json_object' } : undefined,\n      reasoning_effort: effort ? task.effort : undefined,\n    }),\n    signal,\n  });\n  let res = await send(withEffort);\n  let data = await res.json().catch(() => ({}));\n  if (!res.ok && res.status === 400 && withEffort && /reasoning/i.test(JSON.stringify(data?.error ?? data))) {\n    log(`task ${task.task_id || '?'}: model ${model} rejected reasoning_effort (${data?.error?.message || 'HTTP 400'}); retrying once without it`);\n    withEffort = false;\n    res = await send(false);\n    data = await res.json().catch(() => ({}));\n  }\n  if (!res.ok) {\n    const msg = data?.error?.message || `HTTP ${res.status}`;\n    const kind = res.status === 429 ? 'rate_limit' : res.status === 401 ? 'auth' : 'provider_error';\n    throw Object.assign(new Error(msg), { kind });\n  }\n  return {\n    content: data.choices?.[0]?.message?.content ?? '',\n    model: data.model || model,\n    usage: data.usage ? { prompt_tokens: data.usage.prompt_tokens, completion_tokens: data.usage.completion_tokens } : undefined,\n  };\n}\n\nfunction runChild(cmd, args, { env, stdinText, cwd, signal }) {\n  return new Promise((resolve, reject) => {\n    const child = spawn(cmd, args, { env, cwd, stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32' });\n    let stdout = '', stderr = '';\n    child.stdout.on('data', (c) => { stdout += c; });\n    child.stderr.on('data', (c) => { stderr += c; });\n    const abort = () => {\n      try { process.platform === 'win32' ? child.kill('SIGTERM') : process.kill(-child.pid, 'SIGTERM'); } catch { /* gone */ }\n      setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* gone */ } }, 3000).unref();\n    };\n    signal?.addEventListener('abort', abort, { once: true });\n    child.on('error', (err) => reject(new Error(`failed to spawn ${cmd}: ${err.message} (is it installed and on PATH?)`)));\n    child.on('close', (code) => {\n      signal?.removeEventListener('abort', abort);\n      if (signal?.aborted) return reject(Object.assign(new Error('aborted'), { kind: 'timeout' }));\n      resolve({ code, stdout, stderr });\n    });\n    if (stdinText !== undefined) child.stdin.end(stdinText);\n    else child.stdin.end();\n  });\n}\n\n// Several logins on one machine are several capacities. `--codex-home a,b,c` (or\n// `--claude-config-dir a,b`) registers them all; each call goes to the least-loaded login, so a\n// connector with three Codex logins at four calls each serves twelve at once.\nconst _loginLoad = new Map();\nfunction homesOf(provider, single, plural) {\n  const list = Array.isArray(provider[plural]) ? provider[plural].filter(Boolean) : [];\n  if (list.length) return list;\n  return provider[single] ? [provider[single]] : [];\n}\nasync function withLogin(homes, fn) {\n  if (!homes.length) return fn(undefined);\n  const home = homes.reduce((best, h) => ((_loginLoad.get(h) || 0) < (_loginLoad.get(best) || 0) ? h : best), homes[0]);\n  _loginLoad.set(home, (_loginLoad.get(home) || 0) + 1);\n  try { return await fn(home); } finally { _loginLoad.set(home, (_loginLoad.get(home) || 1) - 1); }\n}\n\nasync function callCodex(task, model, provider, signal) {\n  return withLogin(homesOf(provider, 'codex_home', 'codex_homes'), (home) => callCodexAt(task, model, home, signal));\n}\n\nasync function callCodexAt(task, model, codexHome, signal) {\n  // Mirrors workers/codex-proxy: `codex exec --json` with the prompt on stdin, the final message\n  // in a file, OPENAI_API_KEY scrubbed so the CLI uses the subscription login.\n  const env = { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0', TERM: 'dumb' };\n  delete env.OPENAI_API_KEY;\n  delete env.CODEX_API_KEY;\n  if (codexHome) env.CODEX_HOME = codexHome;\n  const dir = mkdtempSync(path.join(tmpdir(), 'omega-connector-'));\n  const outFile = path.join(dir, 'final.txt');\n  const args = ['exec', '--json', '--skip-git-repo-check', '--sandbox', 'read-only', '--model', model,\n    '--output-last-message', outFile, '-'];\n  if (task.effort) args.splice(2, 0, '-c', `model_reasoning_effort=${task.effort}`);\n  try {\n    const { code, stdout, stderr } = await runChild('codex', args, {\n      env, cwd: dir, signal, stdinText: flattenMessages(task.messages) + jsonInstruction(task),\n    });\n    let usage, turnError;\n    for (const line of stdout.split('\\n')) {\n      let evt; try { evt = JSON.parse(line.trim()); } catch { continue; }\n      if (evt?.type === 'turn.completed') usage = { prompt_tokens: evt.usage?.input_tokens || 0, completion_tokens: evt.usage?.output_tokens || 0 };\n      if (evt?.type === 'turn.failed' || evt?.type === 'error') turnError = evt.error?.message || evt.message || JSON.stringify(evt).slice(0, 300);\n    }\n    if (turnError) {\n      const lower = turnError.toLowerCase();\n      const kind = /quota|usage limit|rate limit|too many/.test(lower) ? 'quota' : /auth|unauthor|sign in/.test(lower) ? 'auth' : 'provider_error';\n      throw Object.assign(new Error(turnError), { kind });\n    }\n    if (code !== 0) throw Object.assign(new Error(`codex exec exited ${code}: ${stderr.slice(-400).trim()}`), { kind: 'provider_error' });\n    let content = '';\n    try { content = readFileSync(outFile, 'utf8'); } catch { /* handled below */ }\n    if (!content.trim()) throw Object.assign(new Error(`codex produced no final message; stderr=${stderr.slice(-300)}`), { kind: 'provider_error' });\n    return { content, model, usage };\n  } finally {\n    rmSync(dir, { recursive: true, force: true });\n  }\n}\n\nasync function callClaude(task, model, provider, signal) {\n  return withLogin(homesOf(provider, 'claude_config_dir', 'claude_config_dirs'), (dir) => callClaudeAt(task, model, dir, signal));\n}\n\nasync function callClaudeAt(task, model, configDir, signal) {\n  // Mirrors workers/claude-proxy: `claude --print --output-format json`, prompt on stdin.\n  const { system, prompt } = splitSystem(task.messages);\n  const args = ['--print', '--model', model, '--no-session-persistence', '--output-format', 'json'];\n  if (task.effort) log(`task ${task.task_id || '?'}: effort=${task.effort} is advisory; Claude CLI has no reasoning-effort flag`);\n  if (system) args.push('--system-prompt', system + jsonInstruction(task));\n  const env = { ...process.env };\n  if (configDir) env.CLAUDE_CONFIG_DIR = configDir;\n  const { code, stdout, stderr } = await runChild('claude', args, { env, signal, stdinText: prompt || '(empty)' });\n  let envelope;\n  try { envelope = JSON.parse(stdout); } catch {\n    throw Object.assign(new Error(`claude exited ${code}: ${(stderr || stdout).slice(0, 400).trim()}`), { kind: 'provider_error' });\n  }\n  if (envelope.is_error || envelope.subtype?.startsWith('error_')) {\n    const msg = typeof envelope.result === 'string' && envelope.result.trim() ? envelope.result.trim() : `claude ${envelope.subtype || 'error'}`;\n    throw Object.assign(new Error(msg), { kind: /limit|quota/i.test(msg) ? 'quota' : 'provider_error' });\n  }\n  return {\n    content: typeof envelope.result === 'string' ? envelope.result : '',\n    model: envelope.model || model,\n    usage: { prompt_tokens: envelope.usage?.input_tokens ?? 0, completion_tokens: envelope.usage?.output_tokens ?? 0 },\n  };\n}\n\nfunction stripFences(text) {\n  const t = text.trim();\n  const m = t.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n  return m ? m[1].trim() : t;\n}\n\nasync function answerTask(link, task, signal) {\n  const provider = link.provider;\n  const model = link.tier_models?.[task.tier] || DEFAULT_TIERS[provider.kind][task.tier];\n  let out;\n  if (provider.kind === 'codex') out = await callCodex(task, model, provider, signal);\n  else if (provider.kind === 'claude') out = await callClaude(task, model, provider, signal);\n  else out = await callOpenAICompatible(task, model, provider, signal);\n  if (task.response_format?.type === 'json_object') {\n    const cleaned = stripFences(out.content);\n    try { JSON.parse(cleaned); out.content = cleaned; }\n    catch { log(`task ${task.task_id}: answer is not valid JSON (${cleaned.length} chars); posting as-is, the engine asks for a correction`); }\n  }\n  return out;\n}\n\n// \u2500\u2500\u2500 commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// What a link answers with: provider, tier map, width, kind. Shared by register and login.\nfunction providerFromFlags(flags) {\n  const kind = flags.provider;\n  if (!PROVIDERS.includes(kind)) throw new Error(`--provider must be one of ${PROVIDERS.join(', ')}`);\n  const provider = { kind };\n  if (kind === 'openai-key') {\n    provider.base_url = flags['base-url'] || 'https://api.openai.com/v1';\n    provider.key = flags.key || process.env.OPENAI_API_KEY;\n    if (!provider.key) throw new Error('--key or OPENAI_API_KEY is required for --provider openai-key');\n  } else if (kind === 'base-url') {\n    if (!flags['base-url']) throw new Error('--base-url is required for --provider base-url');\n    provider.base_url = flags['base-url'];\n    provider.key = flags.key || process.env.OPENAI_API_KEY || 'none';\n  } else if (kind === 'codex') {\n    if (flags['codex-home']) provider.codex_homes = String(flags['codex-home']).split(',').map((x) => x.trim()).filter(Boolean);\n  } else if (kind === 'claude') {\n    if (flags['claude-config-dir']) provider.claude_config_dirs = String(flags['claude-config-dir']).split(',').map((x) => x.trim()).filter(Boolean);\n  }\n  const tiers = { ...DEFAULT_TIERS[kind] };\n  for (const t of ['high', 'mid', 'low']) if (flags[t]) tiers[t] = flags[t];\n  const logins = Math.max(1, (provider.codex_homes || provider.claude_config_dirs || []).length);\n  const concurrency = parseInt(flags.concurrency || String(Number(DEFAULT_CONCURRENCY[kind]) * logins), 10);\n  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 256) throw new Error('--concurrency must be an integer 1..256');\n  const agentKind = flags.kind || (kind === 'codex' ? 'codex' : kind === 'claude' ? 'claude_code' : 'script');\n  const label = flags.label || (kind === 'codex' ? 'Codex subscription' : kind === 'claude' ? 'Claude subscription' : kind);\n  return { provider, tiers, concurrency, agentKind, label };\n}\n\n// Link this agent to the user's Eternal Search account by a device code (plan\n// docs/plans/2026-10-08-omega-agent-account-link.md). The user opens the printed URL signed in and\n// chooses: a new link, the link this agent uses now (it keeps its token), or one of their links\n// (a lost token: the agent gets a NEW token for it and its runs continue under their keys).\n// The pending device-code login between `login` (start) and `login --wait` (poll). Kept apart from\n// link.json: it holds the device code, a one-time secret for 15 minutes.\nconst LOGIN_FILE = path.join(path.dirname(LINK_FILE), 'login-pending.json');\n\nasync function cmdLogin(flags) {\n  const existing = existsSync(LINK_FILE) ? loadLink() : null;\n  const base_url = (flags.server || existing?.base_url || process.env.OMEGA_AGENT_BASE || DEFAULT_BASE).replace(/\\/$/, '');\n  const spec = flags.provider ? providerFromFlags(flags)\n    : existing?.provider ? { provider: existing.provider, tiers: existing.tier_models || DEFAULT_TIERS[existing.provider.kind],\n        concurrency: existing.concurrency || 1, agentKind: existing.agent_kind || (existing.provider.kind === 'codex' ? 'codex' : existing.provider.kind === 'claude' ? 'claude_code' : 'script'),\n        label: existing.provider_label || existing.provider.kind }\n    : providerFromFlags({ ...flags, provider: 'subagent' });\n\n  // Step 1 (plain `login`): start and return AT ONCE, so a harness that shows a command's output only\n  // when it ends can give the user the URL and the code before anything waits.\n  let pending = existsSync(LOGIN_FILE) ? JSON.parse(readFileSync(LOGIN_FILE, 'utf8')) : null;\n  if (pending && Date.parse(pending.expires_at) < Date.now()) pending = null;\n  if (!flags.wait || !pending) {\n    const start = await api({ base_url, token: existing?.token }, 'POST', '/links/login', {\n      agent_kind: spec.agentKind, provider_label: spec.label, tier_models: spec.tiers, concurrency: spec.concurrency,\n      scope: flags.site ? ['omega', 'site'] : ['omega'],\n    });\n    pending = { base_url, device_code: start.device_code, user_code: start.user_code, verification_url: start.verification_url,\n      expires_at: start.expires_at, interval_s: start.interval_s, spec };\n    mkdirSync(path.dirname(LOGIN_FILE), { recursive: true, mode: 0o700 });\n    if (existsSync(LOGIN_FILE)) chmodSync(LOGIN_FILE, 0o600);\n    writeFileSync(LOGIN_FILE, JSON.stringify(pending, null, 2), { mode: 0o600 });\n    // One link, one button (plan 2026-10-10-omega-connect-one-click): the user opens the URL in their own\n    // browser and presses Connect; the agent never opens the site, never signs in, never asks for a code.\n    const steps = `Open ${start.verification_url} and press Connect` +\n      (flags.site ? ' (the page also asks you to allow the agent to act for your team; tick that box first)' : '') +\n      `. It is a page of the Eternal Search site in your own browser; sign in with Google there if it asks. ` +\n      `I keep waiting and continue by myself as soon as you have pressed it.`;\n    process.stderr.write(`\\n\u2500\u2500\u2500\u2500 TELL THE USER NOW, in their language \u2500\u2500\u2500\u2500\\n${steps}\\n` +\n      `\u2500\u2500\u2500\u2500 Do NOT open the site or sign in yourself, do NOT ask for a password or the code. ` +\n      `Then run: node omega-connector.mjs login --wait (it waits up to 15 minutes and continues by itself) \u2500\u2500\u2500\u2500\\n\\n`);\n    console.log(JSON.stringify({ verification_url: start.verification_url, user_code: start.user_code, expires_at: start.expires_at,\n      tell_the_user: steps, next: 'node omega-connector.mjs login --wait' }, null, 2));\n    if (!flags.wait) return;\n  }\n\n  // Step 2 (`login --wait`): poll until the user decides.\n  const deadline = Date.parse(pending.expires_at) + 60_000 || Date.now() + 16 * 60 * 1000;\n  log(`waiting for the confirmation of code ${pending.user_code} at ${pending.verification_url}`);\n  for (;;) {\n    let res;\n    try {\n      // wait_s: the server holds the call while the user has not pressed Connect (long poll).\n      res = await api({ base_url: pending.base_url }, 'POST', '/links/login/poll', { device_code: pending.device_code, wait_s: 25 });\n    } catch (e) {\n      if (e.status === 410 || e.status === 404) { rmSync(LOGIN_FILE, { force: true }); throw new Error(`login ended: ${e.data?.message || e.data?.error || e.message}. Start again with: node omega-connector.mjs login`); }\n      throw e;\n    }\n    if (res.status === 'awaiting_user_approval') {\n      if (Date.now() > deadline) throw new Error('nobody confirmed the login in time; start again with: node omega-connector.mjs login');\n      await sleep(Math.max(2, Number(pending.interval_s) || 5) * 1000);\n      continue;\n    }\n    log('the user pressed Connect');\n    const sp = pending.spec || spec;\n    const link = {\n      ...(existing || {}), base_url: pending.base_url, provider: sp.provider, tier_models: res.tier_models || sp.tiers,\n      concurrency: res.concurrency || sp.concurrency, agent_kind: sp.agentKind, provider_label: sp.label,\n      link_id: res.link_id, token: res.token || existing?.token, account_linked: true, runs: existing?.runs || [],\n      scope: res.scope || ['omega'], api_key: res.api_key, team: res.team,\n    };\n    if (res.link_id !== existing?.link_id) delete link.last_run;  // another link: its own runs, not this one's\n    saveLink(link);\n    rmSync(LOGIN_FILE, { force: true });\n    log(res.rotated ? `this agent is reconnected to link ${res.link_id} with a NEW token (the old one no longer works); its runs continue under their keys`\n      : res.token ? `new link ${res.link_id} created and linked to the account; token saved to ${LINK_FILE}`\n      : `link ${res.link_id} is now linked to the account; it keeps its token`);\n    log('tell the user it worked; all their runs are at https://eternalsearch.net/omega/my-runs, and `runs --account` lists them here');\n    console.log(JSON.stringify({ link_id: res.link_id, account_linked: true, rotated: Boolean(res.rotated), link_file: LINK_FILE }, null, 2));\n    return;\n  }\n}\n\nasync function cmdWhoami() {\n  const link = loadLink();\n  console.log(JSON.stringify({ link_id: link.link_id, scope: link.scope || ['omega'], team: link.team || null,\n    api_key_last4: link.api_key ? link.api_key.slice(-4) : null, account_linked: Boolean(link.account_linked) }, null, 2));\n}\n\nasync function cmdRegister(flags) {\n  const kind = flags.provider;\n  if (!PROVIDERS.includes(kind)) throw new Error(`--provider must be one of ${PROVIDERS.join(', ')}`);\n  const provider = { kind };\n  if (kind === 'openai-key') {\n    provider.base_url = flags['base-url'] || 'https://api.openai.com/v1';\n    provider.key = flags.key || process.env.OPENAI_API_KEY;\n    if (!provider.key) throw new Error('--key or OPENAI_API_KEY is required for --provider openai-key');\n  } else if (kind === 'base-url') {\n    if (!flags['base-url']) throw new Error('--base-url is required for --provider base-url');\n    provider.base_url = flags['base-url'];\n    provider.key = flags.key || process.env.OPENAI_API_KEY || 'none';\n  } else if (kind === 'codex') {\n    if (flags['codex-home']) provider.codex_homes = String(flags['codex-home']).split(',').map((x) => x.trim()).filter(Boolean);\n  } else if (kind === 'claude') {\n    if (flags['claude-config-dir']) provider.claude_config_dirs = String(flags['claude-config-dir']).split(',').map((x) => x.trim()).filter(Boolean);\n  }\n  const tiers = { ...DEFAULT_TIERS[kind] };\n  for (const t of ['high', 'mid', 'low']) if (flags[t]) tiers[t] = flags[t];\n  // Each login is its own capacity: the default width is per login times the number of logins.\n  const logins = Math.max(1, (provider.codex_homes || provider.claude_config_dirs || []).length);\n  const concurrency = parseInt(flags.concurrency || String(Number(DEFAULT_CONCURRENCY[kind]) * logins), 10);\n  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 256) throw new Error('--concurrency must be an integer 1..256');\n  const agentKind = flags.kind || (kind === 'codex' ? 'codex' : kind === 'claude' ? 'claude_code' : 'script');\n  const base_url = (flags.server || process.env.OMEGA_AGENT_BASE || DEFAULT_BASE).replace(/\\/$/, '');\n  const data = await api({ base_url }, 'POST', '/links', {\n    agent_kind: agentKind,\n    provider_label: flags.label || (kind === 'codex' ? 'Codex subscription' : kind === 'claude' ? 'Claude subscription' : kind),\n    tier_models: tiers,\n    concurrency,\n  });\n  saveLink({ base_url, token: data.token, link_id: data.link_id, concurrency, tier_models: tiers, provider, runs: [],\n    agent_kind: agentKind, provider_label: flags.label || (kind === 'codex' ? 'Codex subscription' : kind === 'claude' ? 'Claude subscription' : kind) });\n  log('before the first run, ask the user the three questions of SKILL.md \"Before the first run\" in one message: Russian versions, ' +\n    'poster sheets, and linking this agent to their Eternal Search account (node omega-connector.mjs login, then login --wait)');\n  log(`registered link ${data.link_id} (${kind}, concurrency ${concurrency}); token saved to ${LINK_FILE}`);\n  console.log(JSON.stringify({ link_id: data.link_id, concurrency, tier_models: tiers, link_file: LINK_FILE }, null, 2));\n}\n\n// The paste-ready message for the user. The server composes it (handoff_text) so every route says\n// the same thing; an older server without the field gets the connector's own version. Printed on\n// stderr so stdout stays the JSON that scripts parse.\nfunction printHandoff(text, runKey) {\n  const body = text || [\n    `Finish my Omega Point run ${runKey}.`,\n    `Read the skill first: ${DEFAULT_BASE}/skill (SKILL.md, section \"If your session ends before the run does\"). The link token is in ${LINK_FILE} on this machine; do not register a new link.`,\n    `1. node omega-connector.mjs status ${runKey} and read relay_phase.`,\n    `Your turn can end without warning, and after that you cannot write to me: keep the run served in the background (serve --detach), or schedule your own next turn if your harness can, or stop each stretch before your limit and tell me plainly that your turn has ended and the exact phrase I should send to continue.`,\n    `2. relay_phase engine or finishing: node omega-connector.mjs serve --run ${runKey} --detach (in the background, independent of this session) until relay_phase is done. Do not stop or resume the run.`,\n    `3. relay_phase stopped and resumable true: node omega-connector.mjs resume ${runKey}; the run keeps this same key (a lost engine continues from its saved steps, a stopped finishing phase is revived), then serve --run ${runKey} again. Exception: when the status says the engine stopped the run on purpose (error_message begins \"stopped before step 3\"), do not resume it; tell me the reason and propose a clearer wording of my question.`,\n    `4. relay_phase done: node omega-connector.mjs outcome ${runKey}. Give me the links to every published question and hypothesis (English and Russian), say how large the texts are, offer to paste them into the chat in the format I ask (outcome ${runKey} --texts carries the full texts), and ask me for feedback. Then node omega-connector.mjs documents ${runKey} saves one markdown file per published question and hypothesis with the poster sheets; tell me the folder.`,\n  ].join('\\n');\n  process.stderr.write(`\\n\u2500\u2500\u2500\u2500 GIVE THE USER THIS TEXT VERBATIM (they paste it into a new session if this one ends) \u2500\u2500\u2500\u2500\\n${body}\\n\u2500\u2500\u2500\u2500 end of handoff text \u2500\u2500\u2500\u2500\\n\\n`);\n}\n\n// Progress reports for the user. The server says when one is due (a stage changed, or 10 minutes\n// passed) and writes it; the agent tells it to the user in their language. A run takes hours, and an\n// agent that only speaks at the start and at the end leaves the user looking at a silent chat.\nfunction printReports(reports) {\n  for (const rep of reports || []) {\n    process.stderr.write(`\\n\u2500\u2500\u2500\u2500 TELL THE USER NOW, in their language (run ${rep.run_key}, ${rep.reason}) \u2500\u2500\u2500\u2500\\n${rep.progress_report}\\n\u2500\u2500\u2500\u2500 then carry on serving \u2500\u2500\u2500\u2500\\n\\n`);\n  }\n}\n\n// \u2500\u2500\u2500 stage stops \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// A run with pauses stops after every stage result and holds its tasks until the user continues. The\n// connector saves the complete result as a file, prints the overview for the user, and says what ends\n// the turn. Files: ./omega-<run>/stages/NN-<stage>.md (complete) and NN-<stage>.summary.md (overview).\nfunction stageFiles(runKey, stop, dir) {\n  const folder = path.resolve(String(dir || `omega-${runKey}`), 'stages');\n  const base = `${String(stop.seq).padStart(2, '0')}-${String(stop.stage || 'stage').replace(/[^a-z0-9_-]/gi, '_')}`;\n  return { folder, full: path.join(folder, `${base}.md`), summary: path.join(folder, `${base}.summary.md`) };\n}\n\nasync function showCheckpoint(link, runKey, { dir } = {}) {\n  let data;\n  try {\n    data = await api(link, 'GET', `/runs/${runKey}/checkpoint`);\n  } catch (e) {\n    if (e.status === 404) return null;\n    throw e;\n  }\n  const stop = data.checkpoint;\n  const files = stageFiles(runKey, stop, dir);\n  mkdirSync(files.folder, { recursive: true });\n  writeFileSync(files.full, stop.body_markdown);\n  writeFileSync(files.summary, stop.summary_markdown);\n  log(`run ${runKey}: checkpoint ${stop.seq} (${stop.stage}) saved: ${files.full} (${stop.body_markdown.length} characters, the complete result) ` +\n    `and ${files.summary} (the overview)`);\n  process.stderr.write(`\n\u2500\u2500\u2500\u2500 STAGE RESULT: TELL THE USER NOW, in their language (run ${runKey}, checkpoint ${stop.seq}: ${stop.title}) \u2500\u2500\u2500\u2500\n` +\n    `${stop.summary_markdown}\n` +\n    `\u2500\u2500\u2500\u2500 The complete result is in ${files.full}: tell the user where it is; paste it whole only if they ask.\n` +\n    `\u2500\u2500\u2500\u2500 The run is PAUSED and waits for the user. END YOUR TURN now with the exact phrase that continues it ` +\n    `(for example \"continue\"). When the user writes it: node omega-connector.mjs continue ${runKey}, then serve again. \u2500\u2500\u2500\u2500\n\n`);\n  return { ...stop, files, instructions: data.instructions };\n}\n\nasync function cmdCheckpoint(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: checkpoint <run_key> [--dir DIR] [--seq N]');\n  if (flags.seq) {\n    const { checkpoint } = await api(link, 'GET', `/runs/${key}/checkpoints/${parseInt(flags.seq, 10)}`);\n    const files = stageFiles(key, checkpoint, flags.dir);\n    mkdirSync(files.folder, { recursive: true });\n    writeFileSync(files.full, checkpoint.body_markdown);\n    writeFileSync(files.summary, checkpoint.summary_markdown);\n    console.log(JSON.stringify({ run_key: key, seq: checkpoint.seq, title: checkpoint.title, status: checkpoint.status, file: files.full, summary_file: files.summary }, null, 2));\n    return;\n  }\n  const stop = await showCheckpoint(link, key, { dir: flags.dir });\n  if (!stop) {\n    const { run } = await api(link, 'GET', `/runs/${key}`);\n    log(`run ${key} is not waiting at a checkpoint (${run.relay_phase ?? run.status}). ${run.next_action ?? ''}`);\n    console.log(JSON.stringify({ run_key: key, checkpoint: null, relay_phase: run.relay_phase, next_action: run.next_action }, null, 2));\n    return;\n  }\n  console.log(JSON.stringify({ run_key: key, checkpoint: { seq: stop.seq, stage: stop.stage, title: stop.title },\n    summary_markdown: stop.summary_markdown, file: stop.files.full, summary_file: stop.files.summary,\n    stop_now: true, instructions: stop.instructions }, null, 2));\n}\n\nasync function cmdContinue(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: continue <run_key>   (only when the user said to continue)');\n  let data;\n  try {\n    data = await api(link, 'POST', `/runs/${key}/continue`, {});\n  } catch (e) {\n    if (e.status === 409) {\n      log(`run ${key} was not waiting at a checkpoint; serve it as usual: node omega-connector.mjs serve --run ${key}`);\n      console.log(JSON.stringify(e.data || { error: 'nothing_to_continue' }, null, 2));\n      return;\n    }\n    throw e;\n  }\n  log(`run ${key}: checkpoint ${data.continued.seq} released, ${data.released_tasks} held task(s) back in the queue`);\n  if (data.checkpoint) {\n    log(`the next checkpoint is already due`);\n    await showCheckpoint(link, key, { dir: flags.dir });\n  } else {\n    log(`now serve the run again: node omega-connector.mjs serve --run ${key}${link.provider?.kind === 'subagent' ? '  (subagent mode: the next / answer loop)' : '  (--detach to serve in the background)'}`);\n  }\n  console.log(JSON.stringify(data, null, 2));\n}\n\nasync function cmdRun(flags, positional) {\n  const link = loadLink();\n  const goal = positional.join(' ').trim();\n  if (!goal) throw new Error('usage: run \"<research goal>\" [--lens \"<epistemic lens>\"] [--no-russian] [--posters]');\n  // Every run is a full run: the relay has no trial mode (operator decision, 2026-09-29). A stale\n  // --test flag is refused so nobody starts a short run by habit.\n  if (flags.test) throw new Error('--test is gone: every Omega Point run is a full run. Start it without --test.');\n  // Russian versions and poster sheets are the user's choice: ask before starting. Images are\n  // server-side work and default off for relay runs; --no-posters remains a compatible no-op alias.\n  const translateRu = !flags['no-russian'];\n  const posters = Boolean(flags.posters) && !flags['no-posters'];\n  // A stop after every stage result is the default: no agent turn has to outlast a stage. --no-pauses\n  // runs unattended (a background serve then goes from start to finish on its own).\n  const pauses = !flags['no-pauses'];\n  await warnIfSkillStale(link.base_url, { force: true });\n  if (!link.account_linked) {\n    // The third question before a run (SKILL.md \"Before the first run\"): asked, never just mentioned.\n    log('this link is not linked to an Eternal Search account. If you have not asked the user yet whether to link it ' +\n      '(their run then stays reachable from any session, listed at https://eternalsearch.net/omega/my-runs), ask now; ' +\n      'if they want it: node omega-connector.mjs login, then login --wait. The run below continues either way.');\n  }\n  const data = await api(link, 'POST', '/runs', { goal, global_lens: flags.lens || '', translate_ru: translateRu, posters, pauses });\n  link.runs = [...(link.runs || []), data.run_key].slice(-50);\n  link.last_run = data.run_key;\n  saveLink(link);\n  if (data.run_mode === 'server') {\n    // The server executes the run (operator, 2026-10-10): nothing to serve. Follow it and report.\n    log(`run ${data.run_key} started ON THE SERVER (full run, Russian versions ${translateRu ? 'on' : 'off'}, posters ${posters ? 'on' : 'off'}` +\n      `${data.queued_position ? `, queued at position ${data.queued_position}` : ''}). You answer no model calls and serve no tasks. ` +\n      `Follow it: node omega-connector.mjs progress ${data.run_key} (tell the user the report before every turn ends); ` +\n      `node omega-connector.mjs serve --run ${data.run_key} --detach watches it in the background; when relay_phase is done: outcome, documents, feedback.`);\n  } else {\n    log(`run ${data.run_key} started (full run, Russian versions ${translateRu ? 'on' : 'off'}, posters ${posters ? 'on' : 'off'}, ` +\n      `${pauses ? 'stops after every stage for the user' : 'no stops'}). Now: node omega-connector.mjs serve --run ${data.run_key}`);\n  }\n  printHandoff(data.handoff_text, data.run_key);\n  console.log(JSON.stringify(data, null, 2));\n}\n\nconst TERMINAL = new Set(['completed', 'completed_at_step', 'failed_at_execute', 'failed_at_persist',\n  'interrupted_at_step', 'cancelled_in_queue', 'stopped_awaiting_user_llm']);\n\nasync function setConcurrency(link, concurrency) {\n  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 256) throw new Error('concurrency must be an integer 1..256');\n  const data = await api(link, 'PATCH', '/links/current', { concurrency });\n  link.concurrency = data.link?.concurrency ?? concurrency;\n  saveLink(link);\n  log(`link concurrency is now ${link.concurrency} (applies to the next pull and the run's next fan-out)`);\n  return link.concurrency;\n}\n\nasync function cmdSetConcurrency(flags, positional) {\n  const link = loadLink();\n  const n = parseInt(positional[0] ?? flags.concurrency, 10);\n  const value = await setConcurrency(link, n);\n  console.log(JSON.stringify({ link_id: link.link_id, concurrency: value }));\n}\n\n// An agent's turn or session can end at any moment (a turn limit, a context limit, a closed\n// chat). A serve started from that session dies with it, and the run waits for an agent that is\n// gone. `serve --detach` starts the same serve as a background process of its own, with its log\n// and pid under ~/.omega-point, so the run keeps being served whatever happens to the session.\nfunction serveFiles(runKey) {\n  const dir = path.dirname(LINK_FILE);\n  const name = `serve-${runKey || 'all'}`;\n  return { dir, log: path.join(dir, `${name}.log`), pid: path.join(dir, `${name}.pid`) };\n}\n\nfunction aliveServe(runKey) {\n  const files = serveFiles(runKey);\n  if (!existsSync(files.pid)) return null;\n  const pid = parseInt(readFileSync(files.pid, 'utf8'), 10);\n  if (!pid) return null;\n  try { process.kill(pid, 0); return { ...files, pid }; } catch { return null; }\n}\n\nfunction detachServe(flags) {\n  const runKey = flags.run && flags.run !== true ? flags.run : null;\n  const running = aliveServe(runKey);\n  if (running) {\n    log(`a background serve for ${runKey || 'this link'} is already running (pid ${running.pid}, log ${running.log}); not starting a second one`);\n    console.log(JSON.stringify({ detached: true, already_running: true, pid: running.pid, log: running.log, run_key: runKey }));\n    return;\n  }\n  const files = serveFiles(runKey);\n  mkdirSync(files.dir, { recursive: true, mode: 0o700 });\n  const out = openSync(files.log, 'a');\n  const args = process.argv.slice(2).filter((a) => a !== '--detach');\n  const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...args], { detached: true, stdio: ['ignore', out, out], env: process.env });\n  child.unref();\n  writeFileSync(files.pid, String(child.pid), { mode: 0o600 });\n  log(`serve is running in the background as pid ${child.pid}, independent of this session; log ${files.log}. ` +\n    `It stops by itself when the run owes nothing more. Check the run with: node omega-connector.mjs progress ${runKey || '<run_key>'}; stop the serve with: kill ${child.pid}`);\n  console.log(JSON.stringify({ detached: true, pid: child.pid, log: files.log, pid_file: files.pid, run_key: runKey }));\n}\n\n// A run the server executes (run_mode server): nothing to pull. Watch its status until it owes nothing\n// more, print every progress report, and say what to do next. One poll a minute; exits at done/stopped.\nasync function watchServerRun(link, runKey) {\n  log(`run ${runKey} executes on the server: watching its status (one poll a minute), serving nothing; Ctrl-C stops watching, not the run`);\n  let lastReport = null;\n  for (;;) {\n    let st;\n    try {\n      st = await api(link, 'GET', `/runs/${runKey}`);\n    } catch (e) {\n      log(`status failed: ${e.message}; retrying in 60 s`);\n      await sleep(60_000);\n      continue;\n    }\n    const run = st.run;\n    const text = run.progress_report || `${run.status}, phase ${run.relay_phase ?? '-'}, step ${run.current_step ?? '-'} ${run.step_name ?? ''}. ${run.next_action ?? ''}`.trim();\n    if (text !== lastReport) {\n      printReports([{ run_key: runKey, reason: 'status changed', progress_report: text }]);\n      lastReport = text;\n    }\n    const phase = run.relay_phase ?? null;\n    const finished = phase ? (phase === 'done' || phase === 'stopped') : TERMINAL.has(run.status);\n    if (finished) {\n      log(`run ${runKey} is ${phase ?? run.status}; ${run.next_action ?? ''}`);\n      if (phase === 'done') log(`next: node omega-connector.mjs outcome ${runKey} (links, sizes; --texts for the full texts), then documents ${runKey} (the folder), then ask the user for feedback and post it with: node omega-connector.mjs feedback ${runKey} --author user --kind feedback --file note.txt`);\n      return;\n    }\n    await sleep(60_000);\n  }\n}\n\nasync function cmdServe(flags) {\n  const link = loadLink();\n  if (flags.run && !flags.detach) {\n    // A server run has no tasks: watch it instead of pulling (the status says run_mode).\n    const st = await api(link, 'GET', `/runs/${flags.run}`).catch(() => null);\n    if (st?.run?.run_mode === 'server') return watchServerRun(link, flags.run);\n  }\n  if (link.provider.kind === 'subagent') {\n    throw new Error('this link was registered for subagent mode: use `next` and `answer <task_id>` (your agent answers each task itself), or register again with --provider codex|claude|openai-key|base-url');\n  }\n  if (flags.detach) return detachServe(flags);\n  if (flags.concurrency !== undefined) await setConcurrency(link, parseInt(flags.concurrency, 10));\n  const watchRun = flags.run || (flags.once ? null : null);\n  const inFlight = new Map(); // task_id \u2192 AbortController\n  let stopping = false;\n  const shutdown = async () => {\n    if (stopping) return;\n    stopping = true;\n    log(`shutting down: releasing ${inFlight.size} in-flight task(s)`);\n    for (const [id, ctl] of inFlight) {\n      ctl.abort();\n      try { await api(link, 'POST', `/tasks/${id}/release`, undefined, { retries: 2 }); } catch (e) { log(`release ${id} failed: ${e.message}`); }\n    }\n    process.exit(0);\n  };\n  process.on('SIGINT', shutdown);\n  process.on('SIGTERM', shutdown);\n  log(`serving link ${link.link_id} with ${link.provider.kind} (concurrency ${link.concurrency}); Ctrl-C to stop`);\n  let knownRevision = (await warnIfSkillStale(link.base_url)).revision ?? null;\n  let idlePolls = 0;\n  while (!stopping) {\n    const free = link.concurrency - inFlight.size;\n    if (free <= 0) { await new Promise((r) => setTimeout(r, 500)); continue; }\n    let pulled;\n    try {\n      pulled = await api(link, 'POST', '/tasks/pull', { max_tasks: free, wait_s: 25 });\n    } catch (e) {\n      log(`pull failed: ${e.message}; retrying in 10 s`);\n      await new Promise((r) => setTimeout(r, 10_000));\n      continue;\n    }\n    printReports(pulled.reports_for_user);\n    const stops = (pulled.checkpoints || []).filter((x) => !watchRun || x.run_key === watchRun);\n    if (stops.length) {\n      // The run waits for its user at a stage stop: its tasks are held by the server. Finish the\n      // answers in hand, save and print the stage result, and exit so the agent can end its turn.\n      log(`run(s) ${stops.map((x) => x.run_key).join(', ')} paused at a checkpoint; finishing ${inFlight.size} answer(s) in hand`);\n      while (inFlight.size) await new Promise((r) => setTimeout(r, 1000));\n      for (const x of stops) await showCheckpoint(link, x.run_key, { dir: flags.dir });\n      log('serve stops here: the run waits for the user. After the user says to continue: ' +\n        `node omega-connector.mjs continue ${stops[0].run_key}, then serve again.`);\n      return;\n    }\n    if (pulled.skill_revision && knownRevision && pulled.skill_revision !== knownRevision) {\n      // The skill changed on the server during the run: say so once per revision. This process keeps\n      // serving with the code it started with; restart serve after `update` to use the new one.\n      knownRevision = pulled.skill_revision;\n      await warnIfSkillStale(link.base_url, { force: true });\n    }\n    if (!pulled.tasks.length) {\n      idlePolls++;\n      if (watchRun) {\n        const st = await api(link, 'GET', `/runs/${watchRun}`).catch(() => null);\n        if (st?.run) {\n          const phase = st.run.relay_phase ?? null;\n          log(`run ${watchRun}: ${st.run.status}${phase ? `, phase ${phase}` : ''}, step ${st.run.current_step ?? '-'} ${st.run.step_name ?? ''} (${st.run.relay_tasks_total} tasks so far)`);\n          // The engine's terminal status is not the end of the work: titles, dedup, explanations,\n          // translations and headings still send tasks afterwards. Serve until the server says the\n          // run owes nothing more (relay_phase done or stopped); older servers only have the status.\n          if (st.run.next_action && idlePolls % 12 === 1) log(`run ${watchRun}: ${st.run.next_action}`);\n          if (st.run.checkpoint && inFlight.size === 0) {\n            await showCheckpoint(link, watchRun, { dir: flags.dir });\n            log(`serve stops here: the run waits for the user. After the user says to continue: node omega-connector.mjs continue ${watchRun}, then serve again.`);\n            return;\n          }\n          const finished = phase ? (phase === 'done' || phase === 'stopped') : TERMINAL.has(st.run.status);\n          if (finished && inFlight.size === 0) {\n            log(`run ${watchRun} is ${phase ?? st.run.status}; done`);\n            if (phase === 'done') log(`next: node omega-connector.mjs outcome ${watchRun} \u2014 give the user the links to every published question and hypothesis (English and Russian), the size of the texts, and offer to paste the texts into the chat in the format they ask (outcome ${watchRun} --texts); save the documents with node omega-connector.mjs documents ${watchRun} (one markdown file per published question and hypothesis, an index, the poster PNGs) and tell the user the folder; then ask for feedback and post it with: node omega-connector.mjs feedback ${watchRun} --author user --kind feedback --file note.txt (and your proposals with --author agent --kind proposal)`);\n            return;\n          }\n        }\n      }\n      continue;\n    }\n    idlePolls = 0;\n    for (const task of pulled.tasks) {\n      const ctl = new AbortController();\n      inFlight.set(task.task_id, ctl);\n      const timer = setTimeout(() => ctl.abort(), Math.max(30, task.timeout_s) * 1000);\n      timer.unref();\n      log(`task ${task.task_id} run=${task.run_key} tier=${task.tier} attempt=${task.attempt} prompt=${JSON.stringify(task.messages).length} chars \u2192 ${link.tier_models?.[task.tier]}`);\n      const t0 = Date.now();\n      (async () => {\n        try {\n          const out = await answerTask(link, task, ctl.signal);\n          await api(link, 'POST', `/tasks/${task.task_id}/complete`, { content: out.content, model: out.model, usage: out.usage });\n          log(`task ${task.task_id} completed in ${Math.round((Date.now() - t0) / 1000)} s (${out.content.length} chars)`);\n        } catch (e) {\n          if (stopping) return;\n          const kind = e.kind || (ctl.signal.aborted ? 'timeout' : 'agent_error');\n          log(`task ${task.task_id} FAILED [${kind}]: ${e.message}`);\n          try { await api(link, 'POST', `/tasks/${task.task_id}/complete`, { error: { kind, message: e.message.slice(0, 2000) } }); }\n          catch (e2) { log(`could not report the failure for ${task.task_id}: ${e2.message}`); }\n          if (kind === 'auth' && link.concurrency > 1) {\n            // A subscription CLI that races on its credential refresh answers 401 under\n            // concurrency: narrow to one call at a time for the rest of this session and tell\n            // the server so the engine's next fan-out narrows too.\n            log(`auth error with ${inFlight.size} task(s) in flight: dropping to concurrency 1 for this session (the CLI's credential refresh may not be safe under parallel calls; re-run with --concurrency to try again)`);\n            try { await setConcurrency(link, 1); } catch (e3) { link.concurrency = 1; log(`could not tell the server: ${e3.message}`); }\n            await new Promise((r) => setTimeout(r, 30_000));\n          } else if (kind === 'quota' || kind === 'auth') {\n            log('provider quota or auth problem; pausing 5 minutes before pulling again');\n            await new Promise((r) => setTimeout(r, 300_000));\n          }\n        } finally {\n          clearTimeout(timer);\n          inFlight.delete(task.task_id);\n        }\n      })();\n    }\n  }\n}\n\nasync function cmdNext(flags) {\n  const link = loadLink();\n  const max = flags.max === undefined ? 1 : parseInt(flags.max, 10);\n  if (!Number.isInteger(max) || max < 1) throw new Error('--max must be a positive integer');\n  if (max > link.concurrency) await setConcurrency(link, Math.min(256, max));\n  const pulled = await api(link, 'POST', '/tasks/pull', { max_tasks: max, wait_s: flags.wait ? Number(flags.wait) : 25 });\n  // A due progress report rides the JSON too, so a harness that reads only stdout sees it.\n  const reports = pulled.reports_for_user?.length ? {\n    reports_for_user: pulled.reports_for_user,\n    tell_the_user_now: 'Before answering these tasks, tell the user each progress_report in reports_for_user, in their language, as a message in the chat. Then carry on.',\n  } : {};\n  printReports(pulled.reports_for_user);\n  let fr = await skillFreshness(link.base_url);\n  if (pulled.skill_revision && fr.revision && pulled.skill_revision !== fr.revision) fr = await skillFreshness(link.base_url, { force: true });\n  const notice = skillUpdateNotice(fr);\n  if (notice) {\n    reports.skill_update = notice;\n    process.stderr.write(`\\n\u2500\u2500\u2500\u2500 YOUR OMEGA POINT SKILL IS OUTDATED \u2500\u2500\u2500\u2500\\n${notice}\\n\u2500\u2500\u2500\u2500 end \u2500\u2500\u2500\u2500\\n\\n`);\n  }\n  if (pulled.checkpoints?.length) {\n    // A run waits for its user at a stage stop: say so in the JSON a harness reads, with the overview.\n    const shown = [];\n    for (const x of pulled.checkpoints) {\n      const stop = await showCheckpoint(link, x.run_key, { dir: flags.dir });\n      if (stop) shown.push({ run_key: x.run_key, seq: stop.seq, title: stop.title, summary_markdown: stop.summary_markdown, file: stop.files.full });\n    }\n    reports.checkpoints = shown;\n    reports.stop_now = true;\n    reports.tell_the_user_now = pulled.checkpoint_instructions ||\n      'The run is paused after a stage: answer the tasks you already hold, pull no more, write the user the overview, give them the file, and end your turn with the phrase that continues the run.';\n  }\n  if (!pulled.tasks.length) {\n    console.log(JSON.stringify(max > 1 ? { tasks: [], task: null, queue_depth: pulled.queue_depth, ...reports } : { task: null, queue_depth: pulled.queue_depth, ...reports }));\n    return;\n  }\n  mkdirSync(TASK_DIR, { recursive: true });\n  for (const task of pulled.tasks) writeFileSync(path.join(TASK_DIR, `${task.task_id}.json`), JSON.stringify(task, null, 2));\n  if (max === 1) {\n    const task = pulled.tasks[0];\n    console.log(JSON.stringify({ task, note: `Answer with: node omega-connector.mjs answer ${task.task_id} --file <answer.txt>. Deadline: ${task.timeout_s} s from now.`, ...reports }, null, 2));\n    return;\n  }\n  console.log(JSON.stringify({\n    tasks: pulled.tasks,\n    queue_depth: pulled.queue_depth,\n    note: `Answer these ${pulled.tasks.length} task(s) in parallel, one fresh subagent each, then post each with: node omega-connector.mjs answer <task_id> --file <answer.txt>. Each deadline is its timeout_s from now.`,\n    ...reports,\n  }, null, 2));\n}\n\nasync function cmdAnswer(flags, positional) {\n  const link = loadLink();\n  const taskId = positional[0];\n  if (!taskId) throw new Error('usage: answer <task_id> --file <path> [--model name]  (or pipe the answer on stdin)');\n  let content;\n  if (flags.file) content = readFileSync(flags.file, 'utf8');\n  else content = readFileSync(0, 'utf8');\n  if (flags.error) {\n    const data = await api(link, 'POST', `/tasks/${taskId}/complete`, { error: { kind: 'agent_error', message: content.slice(0, 2000) } });\n    console.log(JSON.stringify(data)); return;\n  }\n  const taskFile = path.join(TASK_DIR, `${taskId}.json`);\n  if (existsSync(taskFile)) {\n    const task = JSON.parse(readFileSync(taskFile, 'utf8'));\n    if (task.response_format?.type === 'json_object') {\n      const cleaned = stripFences(content);\n      try { JSON.parse(cleaned); content = cleaned; } catch { log('answer is not valid JSON; posting as-is'); }\n    }\n    rmSync(taskFile, { force: true });\n  }\n  const data = await api(link, 'POST', `/tasks/${taskId}/complete`, { content, model: flags.model || 'subagent' });\n  console.log(JSON.stringify(data));\n}\n\nasync function cmdStatus(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: status <run_key>');\n  await warnIfSkillStale(link.base_url);\n  console.log(JSON.stringify(await api(link, 'GET', `/runs/${key}`), null, 2));\n}\n\nasync function cmdCheckSkill(flags) {\n  const base = (() => { try { return loadLink().base_url; } catch { return (flags.server || process.env.OMEGA_AGENT_BASE || DEFAULT_BASE).replace(/\\/$/, ''); } })();\n  const fr = await warnIfSkillStale(base, { force: true });\n  console.log(JSON.stringify({ server_revision: fr.revision ?? null, current: !fr.error && !fr.stale.length, stale: fr.stale,\n    not_found_beside_connector: fr.not_found, skill_root: SKILL_ROOT, error: fr.error ?? null,\n    next: fr.stale.length ? 'node omega-connector.mjs update, then re-read SKILL.md and tell the user' : null }, null, 2));\n}\n\nasync function cmdUpdate(flags) {\n  const base = (() => { try { return loadLink().base_url; } catch { return (flags.server || process.env.OMEGA_AGENT_BASE || DEFAULT_BASE).replace(/\\/$/, ''); } })();\n  const version = await fetchSkillVersion(base);\n  const written = [];\n  for (const [rel, sha] of Object.entries(version.files_sha256 || {})) {\n    const url = version.download_urls?.[rel] || `${base}/skill/${rel}`;\n    const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });\n    if (!res.ok) throw new Error(`GET ${url} \u2192 ${res.status}`);\n    const bytes = Buffer.from(await res.arrayBuffer());\n    const got = createHash('sha256').update(bytes).digest('hex');\n    if (got !== sha) throw new Error(`${rel}: downloaded sha256 ${got} differs from the published ${sha}; nothing written for it, try again`);\n    const target = localSkillPath(rel);\n    const before = existsSync(target) ? sha256File(target) : null;\n    if (before === sha) { log(`${rel}: already current (${target})`); continue; }\n    mkdirSync(path.dirname(target), { recursive: true });\n    writeFileSync(target, bytes);\n    written.push({ file: rel, path: target, bytes: bytes.length });\n    log(`${rel}: updated \u2192 ${target} (${bytes.length} bytes)`);\n  }\n  await skillFreshness(base, { force: true });\n  log(written.length\n    ? `skill updated to revision ${version.revision}. Re-read ${localSkillPath('SKILL.md')} now and tell the user the skill was updated. A running serve keeps its old code until restarted; a run in progress continues under the same key.`\n    : `skill already at revision ${version.revision}`);\n  console.log(JSON.stringify({ revision: version.revision, written, skill_root: SKILL_ROOT }, null, 2));\n}\n\nasync function cmdProgress(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: progress <run_key>');\n  const { run } = await api(link, 'GET', `/runs/${key}`);\n  const text = run.progress_report || `${run.status}, phase ${run.relay_phase ?? '-'}, step ${run.current_step ?? '-'} ${run.step_name ?? ''} (${run.relay_tasks_total} tasks so far). ${run.next_action ?? ''}`.trim();\n  printReports([{ run_key: key, reason: 'asked', progress_report: text }]);\n  console.log(JSON.stringify({ run_key: key, relay_phase: run.relay_phase, keep_serving: run.keep_serving, progress_report: text }, null, 2));\n}\n\nasync function cmdRuns(flags = {}) {\n  const link = loadLink();\n  if (flags.account) {\n    // Every run of every link of the account (the link must be account-linked: `login`).\n    const data = await api(link, 'GET', '/account/runs');\n    for (const r of data.runs) console.log(`${r.run_key}  ${String(r.relay_phase ?? r.status).padEnd(10)} ${r.checkpoint ? 'waits at checkpoint ' + r.checkpoint.seq + '  ' : ''}${r.this_link ? '(this link) ' : ''}${r.started_at}  ${r.goal.slice(0, 60)}`);\n    if (!data.runs.length) console.log('(no runs on this account yet)');\n    log(`the same list for the user: ${data.runs_page}`);\n    return;\n  }\n  const data = await api(link, 'GET', '/runs');\n  for (const r of data.runs) console.log(`${r.run_key}  ${r.status.padEnd(26)} step ${r.current_step ?? '-'}  tasks ${r.relay_tasks_total}  ${r.started_at}  ${r.goal.slice(0, 60)}`);\n  if (!data.runs.length) console.log('(no runs on this link yet)');\n}\n\nasync function cmdResult(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: result <run_key> [--out file.json]');\n  const res = await fetch(`${link.base_url}/runs/${key}/result${flags['engine-only'] ? '?engine_only=true' : ''}`, { headers: { Authorization: `Bearer ${link.token}` } });\n  const text = await res.text();\n  if (res.status === 202) {\n    let why = 'the run is still running';\n    try { const b = JSON.parse(text); why = b?.run?.next_action || (b?.finishing ? 'the engine is done but the finishing stages still serve through your agent' : why); } catch {}\n    log(`run ${key}: ${why} (keep \"serve --run ${key}\" alive; --engine-only reads the engine result early)`);\n    console.log(text);\n    return;\n  }\n  if (!res.ok) throw new Error(`result \u2192 ${res.status}: ${text.slice(0, 300)}`);\n  if (flags.out) { writeFileSync(flags.out, text); log(`result written to ${flags.out} (${text.length} bytes)`); }\n  else console.log(text);\n}\n\n// One line per hypothesis: its assessment scores (the full assessment with reasons and the literature\n// is in each hypothesis document and at assessment.api_url).\nfunction assessmentLine(a) {\n  if (!a || a.status !== 'assessed') return `assessment: ${a?.status || 'not_assessed'}`;\n  const scores = Object.entries(a.scores || {}).map(([k, v]) => `${k} ${v === null ? 'n/a' : `${v}/${a.scale}`}`).join(', ');\n  return `assessment: ${scores}${a.total_score !== null && a.total_score !== undefined ? `; total ${a.total_score}` : ''}`;\n}\n\n// What the run published, for the user: links, counted sizes, texts on request. Readable list on\n// stderr (so the agent can relay it as is), full JSON on stdout (so scripts can parse it).\nasync function cmdOutcome(flags, positional) {\n  const link = readerLink(flags);\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: outcome <run_key> [--texts] [--out file.json]');\n  log(`reading what run ${key} published from ${link.base_url}${link.token ? '' : ' (no link: anonymous read)'}`);\n  const data = await api(link, 'GET', `/runs/${key}/outcome${flags.texts ? '?texts=true' : ''}`);\n  const size = (v) => `${v.words} words, ${v.chars} characters`;\n  const lines = [data.summary];\n  const langs = (urls) => Object.entries(urls || {}).map(([l, u]) => `${l.toUpperCase()} ${u}`).join('  ');\n  for (const q of data.questions || []) {\n    lines.push(`Question ${q.public_id}: ${q.heading_en || q.question}${q.heading_ru ? ` / ${q.heading_ru}` : ''}`);\n    if (Object.keys(q.urls || {}).length) lines.push(`  ${langs(q.urls)}`);\n    lines.push(`  text: EN ${size(q.volume.en)}${data.translate_ru ? `; RU ${size(q.volume.ru)}` : ''}`);\n    for (const h of q.hypotheses || []) {\n      lines.push(`  Hypothesis ${h.public_id}: ${h.title}${h.title_ru ? ` / ${h.title_ru}` : ''}`);\n      if (Object.keys(h.urls || {}).length) lines.push(`    ${langs(h.urls)}`);\n      lines.push(`    text: EN ${size(h.volume.en)}${data.translate_ru ? `; RU ${size(h.volume.ru)}` : ''}${h.poster_languages?.length ? `; poster ${h.poster_languages.join(', ')}` : ''}`);\n      lines.push(`    ${assessmentLine(h.assessment)}`);\n    }\n  }\n  for (const h of data.hypotheses_without_question || []) {\n    lines.push(`Hypothesis ${h.public_id} (its question page is not public yet): ${h.title}`);\n    if (Object.keys(h.urls || {}).length) lines.push(`  ${langs(h.urls)}`);\n  }\n  const withheld = Object.entries(data.withheld || {});\n  if (withheld.length) lines.push(`Not published: ${withheld.map(([k, n]) => `${n} ${k}`).join(', ')} (duplicates, merged or archived hypotheses stay off the site)`);\n  for (const r of data.reports || []) lines.push(`Report (${r.language}): ${size(r.volume)}`);\n  const t = data.totals || {};\n  if (t.volume) lines.push(`Total: ${t.questions} question(s), ${t.hypotheses} hypothesis(es), ${t.assessed ?? 0} assessed; EN ${size(t.volume.en)}${data.translate_ru ? `; RU ${size(t.volume.ru)}` : ''}`);\n  lines.push(data.texts_included ? 'The full texts are in the JSON (texts fields): paste them in the format the user asks, never shortened.'\n                                 : `The texts themselves: node omega-connector.mjs outcome ${key} --texts (offer the user to paste them into the chat in the format they ask).`);\n  process.stderr.write(`\\n${lines.join('\\n')}\\n\\n`);\n  const text = JSON.stringify(data, null, 2);\n  if (flags.out) { writeFileSync(flags.out, text); log(`outcome written to ${flags.out} (${text.length} bytes)`); }\n  else console.log(text);\n}\n\nasync function cmdStop(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  console.log(JSON.stringify(await api(link, 'POST', `/runs/${key}/stop`), null, 2));\n}\n\nasync function cmdFeedback(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: feedback <run_key> --author user|agent --kind feedback|proposal [--target hypothesis:<public_id>] [--file note.txt | text on stdin]');\n  const author = String(flags.author || '').toLowerCase();\n  const kind = String(flags.kind || '').toLowerCase();\n  if (!['user', 'agent'].includes(author)) throw new Error('--author must be user (what the person said) or agent (what you wrote)');\n  if (!['feedback', 'proposal'].includes(kind)) throw new Error('--kind must be feedback (a remark on the result) or proposal (a concrete correction or improvement)');\n  const body = (flags.file ? readFileSync(flags.file, 'utf8') : readFileSync(0, 'utf8')).trim();\n  if (!body) throw new Error('the note is empty: pass --file note.txt or pipe the text on stdin');\n  const entry = { author, kind, body };\n  if (flags.target) {\n    const [target_kind, ...rest] = String(flags.target).split(':');\n    entry.target_kind = target_kind;\n    if (rest.length) entry.target_id = rest.join(':');\n  }\n  const data = await api(link, 'POST', `/runs/${key}/feedback`, { entries: [entry] });\n  log(`stored ${data.stored?.length ?? 0} entr${(data.stored?.length ?? 0) === 1 ? 'y' : 'ies'} on ${key} (${author}/${kind}); the operator reads them on the runs desk`);\n  console.log(JSON.stringify(data, null, 2));\n}\n\nasync function cmdFeedbackList(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: feedback-list <run_key>');\n  console.log(JSON.stringify(await api(link, 'GET', `/runs/${key}/feedback`), null, 2));\n}\n\nasync function cmdResume(flags, positional) {\n  const link = loadLink();\n  const key = positional[0] || link.last_run;\n  await warnIfSkillStale(link.base_url, { force: true });\n  const data = await api(link, 'POST', `/runs/${key}/resume`);\n  link.last_run = data.run_key; link.runs = [...(link.runs || []), data.run_key].slice(-50); saveLink(link);\n  if (data.revived_finishing) log(`run ${data.run_key}: finishing phase revived under its own key (the stages still owed calls); then: node omega-connector.mjs serve --run ${data.run_key}`);\n  else log(`run ${data.run_key} resumed under its own key (restart #${data.resume_count ?? '?'}, inheriting steps ${(data.inherited_steps || []).join(', ') || 'none'}); then: node omega-connector.mjs serve --run ${data.run_key}`);\n  printHandoff(data.handoff_text, data.run_key);\n  console.log(JSON.stringify(data, null, 2));\n}\n\n// The finished run as files: one markdown document per published open question and hypothesis per\n// language (the text and structure of its page on the site), an index per language, the poster\n// sheets as PNG. The server only hands these out once relay_phase is done. Re-running rewrites the\n// documents and keeps poster files already downloaded.\nasync function fetchBinary(url) {\n  for (let attempt = 0; ; attempt++) {\n    const canRetry = attempt < 4;\n    try {\n      const res = await fetch(url);\n      if (res.ok) {\n        const type = res.headers.get('content-type') || '';\n        if (!type.startsWith('image/')) throw new Error(`expected an image, got ${type || 'no content type'}`);\n        return Buffer.from(await res.arrayBuffer());\n      }\n      if (!TRANSIENT_STATUS.has(res.status) || !canRetry) throw new Error(`HTTP ${res.status}`);\n      log(`GET ${url} \u2192 ${res.status}; retrying in ${RETRY_DELAYS_MS[attempt] / 1000} s`);\n    } catch (e) {\n      if (!canRetry || /expected an image|HTTP \\d/.test(e.message)) throw e;\n      log(`GET ${url}: ${e.message}; retrying in ${RETRY_DELAYS_MS[attempt] / 1000} s`);\n    }\n    await sleep(RETRY_DELAYS_MS[attempt]);\n  }\n}\n\nasync function cmdDocuments(flags, positional) {\n  const link = readerLink(flags);\n  const key = positional[0] || link.last_run;\n  if (!key) throw new Error('usage: documents <run_key> [--dir DIR] [--server URL]');\n  log(`fetching the document folder of run ${key} from ${link.base_url}${link.token ? '' : ' (no link: anonymous read)'}`);\n  let manifest;\n  try {\n    manifest = await api(link, 'GET', `/runs/${key}/documents`);\n  } catch (e) {\n    if (e.status === 409) {\n      const owed = Object.entries(e.data?.publication_pending || {}).map(([k, n]) => `${k} ${n}`).join(', ');\n      throw new Error(`no documents for ${key}${e.data?.error === 'nothing_published' ? '' : ' yet'}: ${e.data?.message || e.message}` +\n        `${owed ? `\\nstill owed: ${owed}` : ''}${e.data?.next_action ? `\\nnext: ${e.data.next_action}` : ''}`);\n    }\n    if (e.status === 404) throw new Error(`no run ${key} on ${link.base_url}: check the run key (it looks like pipeline-0123456789ab)`);\n    throw e;\n  }\n  const dir = path.resolve(String(flags.dir || manifest.folder));\n  mkdirSync(dir, { recursive: true });\n  const c = manifest.counts || {};\n  log(`run ${key}: ${manifest.files.length} files (${c.question ?? 0} question pages, ${c.hypothesis ?? 0} hypothesis pages, ${c.poster ?? 0} posters, languages ${manifest.languages.join(', ')}) \u2192 ${dir}`);\n  const written = [];\n  const failed = [];\n  const queue = [...manifest.files];\n  const width = Math.max(1, Math.min(8, Number(flags.parallel) || 4));\n  async function worker() {\n    for (let file = queue.shift(); file; file = queue.shift()) {\n      const target = path.resolve(dir, file.name);\n      if (!target.startsWith(dir + path.sep)) { failed.push({ name: file.name, error: 'name outside the folder' }); continue; }\n      mkdirSync(path.dirname(target), { recursive: true });\n      const t0 = Date.now();\n      try {\n        if (file.kind === 'poster') {\n          if (existsSync(target) && statSync(target).size > 0) {\n            log(`kept ${file.name} (already downloaded, ${statSync(target).size} bytes)`);\n            written.push({ name: file.name, bytes: statSync(target).size, kept: true });\n            continue;\n          }\n          const bytes = await fetchBinary(file.image_url);\n          writeFileSync(target, bytes);\n          written.push({ name: file.name, bytes: bytes.length });\n          log(`wrote ${file.name} (${bytes.length} bytes, ${Date.now() - t0} ms)`);\n        } else {\n          const content = file.kind === 'index'\n            ? manifest.index[file.language]\n            : (await api(link, 'GET', `/runs/${key}/documents/file?name=${encodeURIComponent(file.name)}`)).content;\n          if (typeof content !== 'string' || !content.trim()) throw new Error('the server returned no text');\n          writeFileSync(target, content);\n          written.push({ name: file.name, bytes: Buffer.byteLength(content), chars: content.length });\n          log(`wrote ${file.name} (${content.length} characters, ${Date.now() - t0} ms)`);\n        }\n      } catch (e) {\n        failed.push({ name: file.name, error: e.message });\n        log(`FAILED ${file.name}: ${e.message}`);\n      }\n    }\n  }\n  await Promise.all(Array.from({ length: width }, worker));\n  const bytes = written.reduce((sum, f) => sum + f.bytes, 0);\n  log(`${written.length} of ${manifest.files.length} files in ${dir} (${(bytes / 1048576).toFixed(1)} MB)${failed.length ? `; ${failed.length} failed, run the same command again to fetch them` : ''}`);\n  log(`open ${path.join(dir, manifest.languages.includes('ru') ? 'index.ru.md' : 'index.en.md')} first: it links every document and poster. Tell the user where the folder is.`);\n  console.log(JSON.stringify({ run_key: key, folder: dir, files: written, failed }, null, 2));\n  if (failed.length) process.exitCode = 1;\n}\n\n// \u2500\u2500\u2500 Omega Point 2.00 studies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Version 2.00 runs on the Eternal Search server: these commands only\n// start a study, pass clarifying questions and answers, and read the cards. Nothing is served. The site\n// API lives at the origin of the relay base (/api/v1/omega2) and takes the same link token, which must\n// belong to a link tied to the user's account (`login`). Plan: docs/plans/2026-10-10-omega-point-2-public-access.md\n\nfunction studyLink(link) {\n  return { ...link, base_url: `${new URL(link.base_url).origin}/api/v1/omega2` };\n}\n\nfunction studyId(link, positional, usage) {\n  const id = positional[0] || link.last_study;\n  if (!id) throw new Error(`usage: ${usage}`);\n  return encodeURIComponent(id);\n}\n\nasync function cmdStudyStart(flags, positional) {\n  const link = loadLink();\n  const question = positional.join(' ').trim() || (flags.file ? readFileSync(String(flags.file), 'utf8').trim() : '');\n  if (!question) throw new Error('usage: study-start \"question in the user\\'s own words\" [--language ru] [--mode strategic] [--field biology|clinical|physical_sciences|general]');\n  const body = { question };\n  if (flags.language) body.language = String(flags.language);\n  if (flags.mode) body.mode = String(flags.mode);\n  if (flags.field) body.domain_adapter = String(flags.field);\n  let out;\n  try {\n    out = await api(studyLink(link), 'POST', '/studies', body);\n  } catch (e) {\n    if (e.status === 403) log('Omega Point 2 needs this agent linked to the user\\'s Eternal Search account: run `node omega-connector.mjs login`, then start again.');\n    throw e;\n  }\n  saveLink({ ...link, last_study: out.public_id });\n  log(`Omega Point 2 study ${out.public_id} started (${out.status}). Tell the user this id; it names the study from any session. It runs on the server: nothing to serve. Follow it with study-status.`);\n  console.log(JSON.stringify(out, null, 2));\n}\n\nasync function cmdStudyStatus(flags, positional) {\n  const link = loadLink();\n  const id = studyId(link, positional, 'study-status <study>');\n  const out = await api(studyLink(link), 'GET', `/studies/${id}`);\n  if (out.progress_report) log(`tell the user: ${typeof out.progress_report === 'string' ? out.progress_report : JSON.stringify(out.progress_report)}`);\n  if (out.next_action) log(`next action: ${out.next_action}`);\n  console.log(JSON.stringify(out, null, 2));\n}\n\nasync function cmdStudyQuestions(flags, positional) {\n  const link = loadLink();\n  const id = studyId(link, positional, 'study-questions <study>');\n  const out = await api(studyLink(link), 'GET', `/studies/${id}/clarifications`);\n  log('Ask the user every question still awaiting_user_answer, in their language, and send each answer with study-answer.');\n  console.log(JSON.stringify(out, null, 2));\n}\n\nasync function cmdStudyAnswer(flags, positional) {\n  const link = loadLink();\n  const [study, clarification] = positional;\n  const answer = flags.file ? readFileSync(String(flags.file), 'utf8').trim() : String(flags.text ?? '').trim();\n  if (!study || !clarification || !answer) throw new Error('usage: study-answer <study> <question_id> --file answer.txt (or --text \"...\")');\n  // Same study, question and answer give the same key, so a repeated send is answered once.\n  const key = createHash('sha256').update(`${study}\\n${clarification}\\n${answer}`).digest('hex');\n  const out = await api(studyLink(link), 'POST', `/studies/${encodeURIComponent(study)}/clarifications/answer`,\n    { clarification_id: clarification, answer, answer_idempotency_key: key });\n  console.log(JSON.stringify(out, null, 2));\n}\n\nasync function cmdStudyCards(flags, positional) {\n  const link = loadLink();\n  const id = studyId(link, positional, 'study-cards <study>');\n  const target = studyLink(link);\n  const list = await api(target, 'GET', `/studies/${id}/cards`);\n  const cards = Array.isArray(list) ? list : (list.cards ?? []);\n  const full = [];\n  for (const card of cards) {\n    const cardId = card.public_id ?? card.card_public_id;\n    full.push(cardId ? await api(target, 'GET', `/studies/${id}/cards/${encodeURIComponent(cardId)}`) : card);\n  }\n  log(`${full.length} card(s). Give the user every card in full; never shorten them.`);\n  console.log(JSON.stringify({ study: decodeURIComponent(id), cards: full }, null, 2));\n}\n\nasync function cmdStudyExport(flags, positional) {\n  const link = loadLink();\n  const id = studyId(link, positional, 'study-export <study> [--markdown] [--out file]');\n  const format = flags.markdown ? 'markdown' : 'json';\n  const out = await api(studyLink(link), 'GET', `/studies/${id}/export?format=${format}`);\n  const text = typeof out === 'string' ? out : (format === 'markdown' && typeof out.content === 'string' ? out.content : JSON.stringify(out, null, 2));\n  const file = flags.out ? String(flags.out) : `omega2-${decodeURIComponent(id)}.${format === 'markdown' ? 'md' : 'json'}`;\n  writeFileSync(file, text);\n  log(`saved ${file} (${text.length} characters). Tell the user where it is.`);\n}\n\nasync function cmdStudyStop(flags, positional) {\n  const link = loadLink();\n  const id = studyId(link, positional, 'study-stop <study>');\n  console.log(JSON.stringify(await api(studyLink(link), 'POST', `/studies/${id}/stop`, {}), null, 2));\n}\n\nasync function cmdStudyResume(flags, positional) {\n  const link = loadLink();\n  const id = studyId(link, positional, 'study-resume <study>');\n  console.log(JSON.stringify(await api(studyLink(link), 'POST', `/studies/${id}/resume`, {}), null, 2));\n}\n\nconst COMMANDS = { register: cmdRegister, run: cmdRun, serve: cmdServe, next: cmdNext, answer: cmdAnswer,\n  status: cmdStatus, progress: cmdProgress, 'check-skill': cmdCheckSkill, update: cmdUpdate, runs: cmdRuns, result: cmdResult, outcome: cmdOutcome, documents: cmdDocuments, stop: cmdStop, resume: cmdResume,\n  feedback: cmdFeedback, 'feedback-list': cmdFeedbackList, 'set-concurrency': cmdSetConcurrency,\n  checkpoint: cmdCheckpoint, continue: cmdContinue, login: cmdLogin, whoami: cmdWhoami,\n  'study-start': cmdStudyStart, 'study-status': cmdStudyStatus, 'study-questions': cmdStudyQuestions,\n  'study-answer': cmdStudyAnswer, 'study-cards': cmdStudyCards, 'study-export': cmdStudyExport,\n  'study-stop': cmdStudyStop, 'study-resume': cmdStudyResume };\n\nasync function main() {\n  const [cmd, ...rest] = process.argv.slice(2);\n  const { flags, positional } = parseArgs(rest);\n  if (!cmd || !COMMANDS[cmd]) {\n    console.error(`usage: omega-connector.mjs <${Object.keys(COMMANDS).join('|')}> ...\\nSee the header of this file.`);\n    process.exit(2);\n  }\n  try { await COMMANDS[cmd](flags, positional); }\n  catch (e) { log(`error: ${e.message}`); process.exit(1); }\n}\n\n// Run as a command; imported by a test, expose the helpers above without running anything.\nconst invokedPath = (() => { try { return realpathSync(process.argv[1] || ''); } catch { return process.argv[1] || ''; } })();\nif (invokedPath === SELF_FILE) main();\n"},"files_sha256":{"SKILL.md":"4d962d752af2e0748b7494179527e9e64fd745fd019b48bd31c0cfb2454e8166","mcp.json":"09bea1210e7dc547230fc11635e404aba685f702ed9ccb77ec9043a9f84a454d","scripts/omega-connector.mjs":"0fcac6124523223afb453a3165cf2881855716dd18c7d53ef4624e409d1d9f07"},"name":"omega-point-agent-relay","openapi_url":"https://eternalsearch.net/omega-point/api/agent/v1/openapi.json","page_url":"https://eternalsearch.net/omega-point/agents","revision":"2c758ecedacd","version_url":"https://eternalsearch.net/omega-point/api/agent/v1/skill/version"}
