codiedev 0.10.0-beta.5 → 0.10.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +4 -4
  2. package/dist/captureSignals.js +2 -0
  3. package/dist/cli.js +38 -50
  4. package/dist/codexHookTrust.js +67 -4
  5. package/dist/codie.js +23 -74
  6. package/dist/codieLauncher.js +18 -22
  7. package/dist/commands/agentTemplates.generated.js +136 -0
  8. package/dist/commands/agents.js +1068 -0
  9. package/dist/commands/agentsSdk.generated.js +7 -0
  10. package/dist/commands/doctor.js +12 -38
  11. package/dist/commands/evals.js +187 -0
  12. package/dist/commands/pack.js +1 -1
  13. package/dist/commands/traces.js +79 -15
  14. package/dist/commands/tracesLabel.js +348 -0
  15. package/dist/connect.js +11 -52
  16. package/dist/copilotHooks.js +0 -4
  17. package/dist/cursorHooks.js +0 -15
  18. package/dist/harnessDirs.js +4 -0
  19. package/dist/hook.js +33 -197
  20. package/dist/limitEventReporter.js +1 -1
  21. package/dist/managedPi.js +240 -0
  22. package/dist/mcp.js +0 -307
  23. package/dist/packs/harnesses.js +40 -3
  24. package/dist/packs/install.js +2 -1
  25. package/dist/piHooks.js +216 -0
  26. package/dist/piRuntime.js +120 -0
  27. package/dist/selfUpdate.js +47 -2
  28. package/dist/utils.js +232 -115
  29. package/package.json +5 -7
  30. package/dist/codieStartup.js +0 -27
  31. package/dist/commands/promote.js +0 -53
  32. package/dist/commands/react.js +0 -38
  33. package/dist/commands/reverseTicket.js +0 -444
  34. package/dist/commands/security.js +0 -156
  35. package/dist/prUrl.js +0 -28
  36. package/dist/security.js +0 -417
  37. package/dist/securityDeepScan.js +0 -235
  38. package/dist/securityDoctor.js +0 -61
  39. package/dist/securityFeature.js +0 -6
  40. package/dist/securityInstall.js +0 -232
  41. package/dist/securityInventory.js +0 -566
  42. package/dist/securityPilot.js +0 -59
  43. package/dist/securityPolicy.js +0 -181
  44. package/dist/securitySession.js +0 -72
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsPackage = exports.jsTypes = exports.jsIndex = exports.python = void 0;
4
+ exports.python = "\"\"\"Codiedev agents SDK — the run primitive, from Python.\n\nInside a deployed agent (everything is already configured):\n\n import codiedev_agents as agents\n\n task = agents.input() # this run's input (JSON)\n llm = agents.openai() # OpenAI client on the Codiedev gateway, traced\n child = agents.run(\"summarize\", {\"text\": ...}) # start another agent as a child run\n agents.output({\"summary\": child[\"output\"]}) # this run's result\n agents.request() # webhook runs: method, headers, raw body\n agents.respond({\"text\": \"on it\"}) # what a webhook caller waiting (?wait=) gets back\n\n agents.state.get(\"cursor\") # per-agent storage that outlives runs\n agents.state.set(\"cursor\", 42)\n agents.step(\"fetch\", lambda: fetch()) # done once per run, even across retries\n agents.start(\"triage\", {...}, delay=3600) # start a run later (up to 7 days)\n agents.request_approval(\"Refund $120?\", context={...}) # a person decides; a run acts on it\n agents.attempt() # 1, or 2+ when this run is a retry\n\nOutside a run (a script, a notebook, another service), set CODIEDEV_TOKEN\n(your CLI token) and CODIEDEV_AGENTS_URL, then call agents.run() the same way.\nStandard library only, so it drops into any project.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom typing import Any\n\n__all__ = [\n \"input\", \"output\", \"request\", \"respond\", \"run\", \"start\", \"get\", \"openai\", \"attempt\",\n \"state\", \"step\", \"request_approval\", \"get_approval\", \"AgentRunError\", \"StateConflict\",\n]\n\n_INPUT_PATH = os.environ.get(\"CODIEDEV_INPUT_PATH\", \"/agent/.codiedev/input.json\")\n_OUTPUT_PATH = os.environ.get(\"CODIEDEV_OUTPUT_PATH\", \"/agent/.codiedev/output.json\")\n_REQUEST_PATH = os.environ.get(\"CODIEDEV_REQUEST_PATH\", \"/agent/.codiedev/request.json\")\n_RESPONSE_PATH = os.environ.get(\"CODIEDEV_RESPONSE_PATH\", \"/agent/.codiedev/response.json\")\n# Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\n_STATE_PATH = os.environ.get(\"CODIEDEV_STATE_PATH\")\n_DONE = {\"succeeded\", \"failed\", \"cancelled\"}\n\n\nclass StateConflict(RuntimeError):\n \"\"\"state.set(..., if_version=n) found the key at another version.\"\"\"\n\n def __init__(self, key: str, version: int):\n super().__init__(f\"state {key!r} changed (now version {version})\")\n self.key = key\n self.version = version\n\n\nclass AgentRunError(RuntimeError):\n def __init__(self, run: dict):\n super().__init__(f\"{run.get('agent')} run {run.get('id')} {run.get('status')}: {run.get('error') or 'no error message'}\")\n self.run = run\n\n\ndef input(default: Any = None) -> Any: # noqa: A001 - mirrors output()\n \"\"\"This run's input, parsed from JSON (None when started without one).\"\"\"\n try:\n with open(_INPUT_PATH) as f:\n value = json.load(f)\n except FileNotFoundError:\n return default\n return default if value is None else value\n\n\ndef output(value: Any) -> None:\n \"\"\"Set this run's result. Must be JSON-serializable; the last call wins.\"\"\"\n os.makedirs(os.path.dirname(_OUTPUT_PATH), exist_ok=True)\n with open(_OUTPUT_PATH, \"w\") as f:\n json.dump(value, f)\n\n\ndef respond(body: Any = None, status: int = 200, headers: \"dict | None\" = None) -> None:\n \"\"\"What a webhook caller gets back when it waits for the run (?wait=N):\n a string is sent as text, anything else as JSON. Without respond(), a\n waiting caller gets the run's output.\"\"\"\n os.makedirs(os.path.dirname(_RESPONSE_PATH), exist_ok=True)\n with open(_RESPONSE_PATH, \"w\") as f:\n json.dump({\"status\": status, \"headers\": headers or {}, \"body\": body}, f)\n\n\ndef attempt() -> int:\n \"\"\"This run's attempt: 1, or 2+ when it's a retry of a failed attempt.\"\"\"\n return int(os.environ.get(\"CODIEDEV_ATTEMPT\", \"1\"))\n\n\ndef request() -> \"dict | None\":\n \"\"\"For a webhook run: {\"method\", \"query\", \"headers\", \"body\"} with the raw\n body, to verify the sender's signature. None for other runs.\"\"\"\n try:\n with open(_REQUEST_PATH) as f:\n return json.load(f)\n except FileNotFoundError:\n return None\n\n\ndef _base() -> str:\n url = os.environ.get(\"CODIEDEV_AGENTS_URL\", \"\").rstrip(\"/\")\n if not url:\n raise RuntimeError(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\")\n return url\n\n\ndef _token() -> str:\n token = os.environ.get(\"CODIEDEV_RUN_TOKEN\") or os.environ.get(\"CODIEDEV_TOKEN\")\n if not token:\n raise RuntimeError(\"Set CODIEDEV_TOKEN to your Codiedev CLI token (inside a run it's set for you)\")\n return token\n\n\ndef _request(method: str, path: str, body: dict | None = None, ok: tuple[int, ...] = ()) -> dict:\n req = urllib.request.Request(\n f\"{_base()}{path}\",\n method=method,\n data=json.dumps(body).encode() if body is not None else None,\n headers={\"Authorization\": f\"Bearer {_token()}\", \"Content-Type\": \"application/json\"},\n )\n try:\n with urllib.request.urlopen(req, timeout=30) as res:\n return json.loads(res.read() or b\"{}\")\n except urllib.error.HTTPError as err:\n detail = err.read().decode(errors=\"replace\")\n if err.code in ok:\n return {\"_status\": err.code, **json.loads(detail or \"{}\")}\n try:\n detail = json.loads(detail).get(\"error\", detail)\n except ValueError:\n pass\n raise RuntimeError(f\"Codiedev agents API {err.code}: {detail}\") from None\n\n\ndef start(\n agent: str,\n input: Any = None, # noqa: A002\n idempotency_key: str | None = None,\n delay: float | None = None,\n retries: int | None = None,\n) -> str:\n \"\"\"Start a run and return its id without waiting. With an idempotency key,\n a second start within a day returns the first run's id. delay (seconds,\n up to 7 days) starts it later; retries overrides the agent's retries.\"\"\"\n body: dict = {\"agent\": agent, \"input\": input}\n if idempotency_key:\n body[\"idempotencyKey\"] = idempotency_key\n if delay:\n body[\"delaySec\"] = int(delay)\n if retries is not None:\n body[\"retries\"] = retries\n return _request(\"POST\", \"/api/agents/v1/run\", body)[\"runId\"]\n\n\ndef get(run_id: str) -> dict:\n \"\"\"A run's status, input, output (parsed), error, log tail and children.\"\"\"\n run = _request(\"GET\", f\"/api/agents/v1/run?id={run_id}\")\n if isinstance(run.get(\"output\"), str):\n try:\n run[\"output\"] = json.loads(run[\"output\"])\n except ValueError:\n pass\n return run\n\n\ndef run(\n agent: str,\n input: Any = None, # noqa: A002\n timeout: float = 900,\n poll: float = 2.0,\n check: bool = True,\n idempotency_key: str | None = None,\n) -> dict:\n \"\"\"Run an agent and wait for it. Returns the run; its result is run[\"output\"].\n\n Inside a run this starts a child run, so the chain shows as one tree.\n Raises AgentRunError if it fails (pass check=False to get the run anyway).\n \"\"\"\n run_id = start(agent, input, idempotency_key)\n deadline = time.monotonic() + timeout\n while True:\n result = get(run_id)\n if result.get(\"status\") in _DONE:\n if check and result[\"status\"] != \"succeeded\":\n raise AgentRunError(result)\n return result\n if time.monotonic() > deadline:\n raise TimeoutError(f\"{agent} run {run_id} still {result.get('status')} after {timeout}s\")\n time.sleep(poll)\n\n\nclass _State:\n \"\"\"Per-agent key-value storage that outlives runs (values: any JSON, up\n to 64 KB; 1,000 keys). Every write bumps the key's version; pass\n if_version to write only if nobody else did since you read it.\"\"\"\n\n def get(self, key: str, default: Any = None) -> Any:\n value, _ = self.get_versioned(key)\n return default if value is None else value\n\n def get_versioned(self, key: str) -> \"tuple[Any, int]\":\n \"\"\"(value, version); (None, 0) when the key isn't set.\"\"\"\n if _STATE_PATH:\n row = _local_state().get(key)\n return (row[\"value\"], row[\"version\"]) if row else (None, 0)\n res = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode({'key': key, 'agent': _agent()})}\", ok=(404,))\n if res.get(\"_status\") == 404:\n return None, 0\n return json.loads(res[\"value\"]), int(res[\"version\"])\n\n def set(self, key: str, value: Any, if_version: \"int | None\" = None, ttl: \"float | None\" = None) -> int:\n \"\"\"Store value; returns the new version. Raises StateConflict when\n if_version is given and the key is at another version (0 = unset).\"\"\"\n return self._write(key, value, if_version, ttl)\n\n def delete(self, key: str, if_version: \"int | None\" = None) -> None:\n self._write(key, None, if_version, None)\n\n def list(self, prefix: str = \"\", values: bool = False) -> \"list[dict]\": # noqa: A003\n \"\"\"Keys (and with values=True, values) starting with prefix.\"\"\"\n if _STATE_PATH:\n rows = sorted((k, r) for k, r in _local_state().items() if k.startswith(prefix))\n return [{\"key\": k, \"version\": r[\"version\"], **({\"value\": r[\"value\"]} if values else {})} for k, r in rows]\n keys: list = []\n after = None\n while True:\n params = {\"prefix\": prefix, \"values\": \"1\" if values else \"0\", \"limit\": 200, \"agent\": _agent()}\n if after:\n params[\"after\"] = after\n page = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode(params)}\")\n for row in page[\"keys\"]:\n if \"value\" in row:\n row[\"value\"] = json.loads(row[\"value\"])\n keys.extend(page[\"keys\"])\n after = page.get(\"next\")\n if not after:\n return keys\n\n def update(self, key: str, fn, default: Any = None, retries: int = 10) -> Any:\n \"\"\"Read-modify-write that's safe with runs in parallel: fn(old) -> new,\n retried if another run wrote first. Returns the new value.\"\"\"\n for _ in range(retries):\n value, version = self.get_versioned(key)\n new = fn(default if value is None else value)\n try:\n self.set(key, new, if_version=version)\n return new\n except StateConflict:\n time.sleep(0.05)\n raise StateConflict(key, -1)\n\n def _write(self, key: str, value: Any, if_version: \"int | None\", ttl: \"float | None\") -> int:\n if _STATE_PATH:\n data = _local_state()\n current = data.get(key, {}).get(\"version\", 0)\n if if_version is not None and if_version != current:\n raise StateConflict(key, current)\n if value is None:\n data.pop(key, None)\n else:\n data[key] = {\"value\": value, \"version\": current + 1}\n with open(_STATE_PATH, \"w\") as f:\n json.dump(data, f, indent=2)\n return 0 if value is None else current + 1\n body: dict = {\"key\": key, \"value\": value, \"agent\": _agent()}\n if if_version is not None:\n body[\"ifVersion\"] = if_version\n if ttl:\n body[\"ttlSec\"] = int(ttl)\n res = _request(\"POST\", \"/api/agents/v1/state\", body, ok=(409,))\n if res.get(\"_status\") == 409:\n raise StateConflict(key, int(res.get(\"version\", 0)))\n return int(res.get(\"version\", 0))\n\n\ndef _agent() -> str:\n # Inside a run the run token picks the agent; a CLI token needs it named.\n return os.environ.get(\"CODIEDEV_AGENT\", \"\")\n\n\ndef _local_state() -> dict:\n try:\n with open(_STATE_PATH or \"\") as f:\n return json.load(f)\n except (FileNotFoundError, ValueError):\n return {}\n\n\nstate = _State()\n\n\ndef step(name: str, fn, ttl: float = 7 * 86400) -> Any:\n \"\"\"Run fn() once for this run and remember its (JSON) result: if the run\n is retried after a failure, finished steps return their saved result\n instead of running again. Use it around side effects (posting a reply,\n charging a card) so a retry doesn't repeat them.\"\"\"\n key = f\"step/{os.environ.get('CODIEDEV_RUN_ID', 'local')}/{name}\"\n value, version = state.get_versioned(key)\n if version:\n return value[\"result\"]\n result = fn()\n state.set(key, {\"result\": result}, ttl=ttl)\n return result\n\n\n_SELF = object()\n\n\ndef request_approval(\n title: str,\n details: Any = None,\n options: \"list[str] | None\" = None,\n approvers: \"list[str] | None\" = None,\n teams: \"list[str] | None\" = None,\n fields: \"list[dict] | None\" = None,\n then: Any = _SELF,\n context: Any = None,\n expires_in: \"float | None\" = None,\n) -> dict:\n \"\"\"Ask a person to decide. It shows in Codiedev (Agents → Approvals) and\n at the returned [\"url\"], which you can post anywhere (Slack, email).\n\n When it's decided, a run of `then` (this agent by default; None for no\n run) starts with input {\"approval\": {\"decision\", \"comment\", \"decidedBy\",\n ...}, \"context\": context}. If nobody decides within expires_in seconds\n (default 7 days), it starts with decision \"expired\". Usually the run\n ends after asking, so nothing runs (or costs) while it waits.\n options default to [\"approve\", \"reject\"]; approvers (emails) and teams\n (team names, e.g. [\"Finance\"]) limit who can decide (admins always can).\n Apps your company connects (Agents → Approvals → Apps) are told about it\n by webhook and can decide on a person's behalf.\n\n fields asks the person to fill something in as they decide, e.g.\n [{\"name\": \"rating\", \"type\": \"rating\"}, {\"name\": \"why\", \"type\": \"textarea\"}]\n (types: text, textarea, number, rating, select, multiselect, boolean;\n required, label, help, options, min, max, default). What they entered\n arrives as input[\"approval\"][\"values\"]. With fields and no options, the\n only choice is \"submit\".\"\"\"\n body: dict = {\"title\": title, \"details\": details, \"options\": options, \"approvers\": approvers, \"teams\": teams, \"fields\": fields, \"context\": context}\n if then is not _SELF:\n body[\"onDecision\"] = then\n if expires_in:\n body[\"expiresSec\"] = int(expires_in)\n if os.environ.get(\"CODIEDEV_LOCAL\") == \"1\":\n print(f\"[local] approval requested: {title} ({', '.join(options or (['submit'] if fields else ['approve', 'reject']))}); not sent on a local run\", flush=True)\n return {\"id\": f\"local-{int(time.time())}\", \"status\": \"pending\", \"url\": None, \"title\": title}\n return _request(\"POST\", \"/api/agents/v1/approvals\", {k: v for k, v in body.items() if v is not None or k == \"onDecision\"})\n\n\ndef get_approval(approval_id: str) -> dict:\n \"\"\"An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment.\"\"\"\n return _request(\"GET\", f\"/api/agents/v1/approvals?id={urllib.parse.quote(approval_id)}\")\n\n\ndef openai(**kwargs):\n \"\"\"An OpenAI client on the Codiedev gateway, with trace headers for this run.\"\"\"\n from openai import OpenAI\n\n headers = {\n \"X-Codiedev-Workload\": os.environ.get(\"CODIEDEV_AGENT\", \"local-agent\"),\n \"X-Codiedev-Run-Id\": os.environ.get(\"CODIEDEV_RUN_ID\", f\"local-{int(time.time())}\"),\n \"X-Codiedev-Environment\": \"production\" if os.environ.get(\"CODIEDEV_RUN_ID\") else \"development\",\n }\n headers.update(kwargs.pop(\"default_headers\", {}) or {})\n return OpenAI(\n base_url=os.environ.get(\"CODIEDEV_GATEWAY_URL\", \"https://gateway.codiedev.com/api/v1\"),\n api_key=os.environ.get(\"CODIEDEV_API_KEY\") or os.environ.get(\"CODIEDEV_TOKEN\"),\n default_headers=headers,\n **kwargs,\n )\n";
5
+ exports.jsIndex = "// Codiedev agents SDK for JavaScript and TypeScript, preinstalled in every\n// run. Same primitive as the Python SDK (codiedev_agents.py):\n//\n// import * as agents from \"codiedev-agents\";\n// const task = agents.input(); // this run's input (JSON)\n// const llm = await agents.openai(); // OpenAI client on the Codiedev gateway, traced\n// const child = await agents.run(\"summarize\", { text }); // another agent, as a child run\n// agents.output({ summary: child.output }); // this run's result\n// agents.respond({ text: \"on it\" }); // what a webhook caller waiting (?wait=) gets back\n// await agents.state.get(\"cursor\"); // per-agent storage that outlives runs\n// await agents.step(\"post\", () => post()); // done once per run, even across retries\n// await agents.start(\"triage\", input, { delaySec: 3600 }); // start a run later\n// await agents.requestApproval(\"Refund $120?\", { context }); // a person decides; a run acts on it\n//\n// Outside a run, set CODIEDEV_TOKEN and CODIEDEV_AGENTS_URL and call run() the same way.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst INPUT_PATH = process.env.CODIEDEV_INPUT_PATH || \"/agent/.codiedev/input.json\";\nconst OUTPUT_PATH = process.env.CODIEDEV_OUTPUT_PATH || \"/agent/.codiedev/output.json\";\nconst REQUEST_PATH = process.env.CODIEDEV_REQUEST_PATH || \"/agent/.codiedev/request.json\";\nconst RESPONSE_PATH = process.env.CODIEDEV_RESPONSE_PATH || \"/agent/.codiedev/response.json\";\n// Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\nconst STATE_PATH = process.env.CODIEDEV_STATE_PATH;\n// Inside a run the run token picks the agent; a CLI token needs it named.\nconst AGENT = process.env.CODIEDEV_AGENT || \"\";\nconst DONE = new Set([\"succeeded\", \"failed\", \"cancelled\"]);\n\n/** state.set(..., { ifVersion }) found the key at another version. */\nexport class StateConflict extends Error {\n constructor(key, version) {\n super(`state ${JSON.stringify(key)} changed (now version ${version})`);\n this.key = key;\n this.version = version;\n }\n}\n\nexport class AgentRunError extends Error {\n constructor(run) {\n super(`${run.agent} run ${run.id} ${run.status}: ${run.error || \"no error message\"}`);\n this.run = run;\n }\n}\n\nfunction readJson(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch (err) {\n if (err.code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/** This run's input, parsed from JSON (`fallback` when started without one). */\nexport function input(fallback = null) {\n const value = readJson(INPUT_PATH);\n return value ?? fallback;\n}\n\n/** Set this run's result. Must be JSON-serializable; the last call wins. */\nexport function output(value) {\n fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });\n fs.writeFileSync(OUTPUT_PATH, JSON.stringify(value));\n}\n\n/** What a webhook caller gets back when it waits for the run (?wait=N): a\n * string is sent as text, anything else as JSON. Without it, a waiting\n * caller gets the run's output. */\nexport function respond(body = null, { status = 200, headers = {} } = {}) {\n fs.mkdirSync(path.dirname(RESPONSE_PATH), { recursive: true });\n fs.writeFileSync(RESPONSE_PATH, JSON.stringify({ status, headers, body }));\n}\n\n/** This run's attempt: 1, or 2+ when it's a retry of a failed attempt. */\nexport function attempt() {\n return Number(process.env.CODIEDEV_ATTEMPT || \"1\");\n}\n\n/** For a webhook run: { method, query, headers, body } with the raw body, to verify a signature. */\nexport function request() {\n return readJson(REQUEST_PATH);\n}\n\nfunction base() {\n const url = (process.env.CODIEDEV_AGENTS_URL || \"\").replace(/\\/$/, \"\");\n if (!url) throw new Error(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\");\n return url;\n}\n\nfunction token() {\n const t = process.env.CODIEDEV_RUN_TOKEN || process.env.CODIEDEV_TOKEN;\n if (!t) throw new Error(\"Set CODIEDEV_TOKEN to your Codiedev CLI token (inside a run it's set for you)\");\n return t;\n}\n\nasync function call(method, route, body, ok = []) {\n const res = await fetch(`${base()}${route}`, {\n method,\n headers: { Authorization: `Bearer ${token()}`, \"Content-Type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await res.text();\n let data = {};\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = { error: text };\n }\n if (ok.includes(res.status)) return { _status: res.status, ...data };\n if (!res.ok) throw new Error(`Codiedev agents API ${res.status}: ${data.error || text}`);\n return data;\n}\n\n/** Start a run and return its id without waiting. delaySec (up to 7 days)\n * starts it later; retries overrides the agent's retries. */\nexport async function start(agent, input = null, { idempotencyKey, delaySec, retries } = {}) {\n return (await call(\"POST\", \"/api/agents/v1/run\", {\n agent,\n input,\n ...(idempotencyKey ? { idempotencyKey } : {}),\n ...(delaySec ? { delaySec } : {}),\n ...(retries !== undefined ? { retries } : {}),\n })).runId;\n}\n\nfunction localState() {\n try {\n return JSON.parse(fs.readFileSync(STATE_PATH, \"utf8\"));\n } catch {\n return {};\n }\n}\n\nasync function writeState(key, value, { ifVersion, ttlSec } = {}) {\n if (STATE_PATH) {\n const data = localState();\n const current = data[key]?.version ?? 0;\n if (ifVersion !== undefined && ifVersion !== current) throw new StateConflict(key, current);\n if (value === null) delete data[key];\n else data[key] = { value, version: current + 1 };\n fs.writeFileSync(STATE_PATH, JSON.stringify(data, null, 2));\n return value === null ? 0 : current + 1;\n }\n const res = await call(\"POST\", \"/api/agents/v1/state\", { key, value, agent: AGENT, ...(ifVersion !== undefined ? { ifVersion } : {}), ...(ttlSec ? { ttlSec } : {}) }, [409]);\n if (res._status === 409) throw new StateConflict(key, res.version ?? 0);\n return res.version ?? 0;\n}\n\n/** Per-agent key-value storage that outlives runs (values: any JSON, up to\n * 64 KB; 1,000 keys). Every write bumps the key's version; pass ifVersion\n * to write only if nobody else did since you read it. */\nexport const state = {\n async get(key, fallback = null) {\n const { value } = await state.getVersioned(key);\n return value ?? fallback;\n },\n /** { value, version }; { value: null, version: 0 } when the key isn't set. */\n async getVersioned(key) {\n if (STATE_PATH) {\n const row = localState()[key];\n return row ? { value: row.value, version: row.version } : { value: null, version: 0 };\n }\n const res = await call(\"GET\", `/api/agents/v1/state?${new URLSearchParams({ key, agent: AGENT })}`, undefined, [404]);\n if (res._status === 404) return { value: null, version: 0 };\n return { value: JSON.parse(res.value), version: res.version };\n },\n /** Store value; returns the new version. Throws StateConflict on an ifVersion mismatch (0 = unset). */\n set(key, value, { ifVersion, ttlSec } = {}) {\n return writeState(key, value, { ifVersion, ttlSec });\n },\n async delete(key, { ifVersion } = {}) {\n await writeState(key, null, { ifVersion });\n },\n /** Keys (and with values: true, values) starting with prefix. */\n async list(prefix = \"\", { values = false } = {}) {\n if (STATE_PATH) {\n return Object.entries(localState())\n .filter(([k]) => k.startsWith(prefix))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, row]) => ({ key, version: row.version, ...(values ? { value: row.value } : {}) }));\n }\n const out = [];\n let after = null;\n do {\n const q = new URLSearchParams({ prefix, values: values ? \"1\" : \"0\", limit: \"200\", agent: AGENT, ...(after ? { after } : {}) });\n const page = await call(\"GET\", `/api/agents/v1/state?${q}`);\n out.push(...page.keys.map((row) => (\"value\" in row ? { ...row, value: JSON.parse(row.value) } : row)));\n after = page.next;\n } while (after);\n return out;\n },\n /** Read-modify-write that's safe with runs in parallel: fn(old) => new, retried if another run wrote first. */\n async update(key, fn, fallback = null, retries = 10) {\n for (let i = 0; i < retries; i++) {\n const { value, version } = await state.getVersioned(key);\n const next = await fn(value ?? fallback);\n try {\n await state.set(key, next, { ifVersion: version });\n return next;\n } catch (err) {\n if (!(err instanceof StateConflict)) throw err;\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n throw new StateConflict(key, -1);\n },\n};\n\n/** Run fn() once for this run and remember its (JSON) result: if the run is\n * retried after a failure, finished steps return their saved result instead\n * of running again. Use it around side effects so a retry doesn't repeat them. */\nexport async function step(name, fn, { ttlSec = 7 * 86400 } = {}) {\n const key = `step/${process.env.CODIEDEV_RUN_ID || \"local\"}/${name}`;\n const { value, version } = await state.getVersioned(key);\n if (version) return value.result;\n const result = await fn();\n await state.set(key, { result: result ?? null }, { ttlSec });\n return result;\n}\n\n/** Ask a person to decide. It shows in Codiedev (Agents → Approvals) and at\n * the returned .url, which you can post anywhere. When it's decided, a run of\n * `then` (this agent by default; null for none) starts with input\n * { approval: { decision, comment, decidedBy, ... }, context }. Undecided\n * after expiresSec (default 7 days): decision \"expired\". */\nexport async function requestApproval(title, { details, options, approvers, teams, fields, then, context, expiresSec } = {}) {\n if (process.env.CODIEDEV_LOCAL === \"1\") {\n console.log(`[local] approval requested: ${title} (${(options || (fields ? [\"submit\"] : [\"approve\", \"reject\"])).join(\", \")}); not sent on a local run`);\n return { id: `local-${Date.now()}`, status: \"pending\", url: null, title };\n }\n return call(\"POST\", \"/api/agents/v1/approvals\", {\n title,\n ...(details !== undefined ? { details } : {}),\n ...(options ? { options } : {}),\n ...(approvers ? { approvers } : {}),\n ...(teams ? { teams } : {}),\n ...(fields ? { fields } : {}),\n ...(then !== undefined ? { onDecision: then } : {}),\n ...(context !== undefined ? { context } : {}),\n ...(expiresSec ? { expiresSec } : {}),\n });\n}\n\n/** An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment. */\nexport async function getApproval(id) {\n return call(\"GET\", `/api/agents/v1/approvals?id=${encodeURIComponent(id)}`);\n}\n\n/** A run's status, input, output (parsed), error, log and children. */\nexport async function get(runId) {\n const run = await call(\"GET\", `/api/agents/v1/run?id=${encodeURIComponent(runId)}`);\n if (typeof run.output === \"string\") {\n try {\n run.output = JSON.parse(run.output);\n } catch {\n // not JSON: keep the string\n }\n }\n return run;\n}\n\n/** Run an agent and wait for it; its result is `.output`. Inside a run this\n * starts a child run. Throws AgentRunError if it fails unless check is false. */\nexport async function run(agent, input = null, { timeoutMs = 900_000, pollMs = 2000, check = true, idempotencyKey } = {}) {\n const id = await start(agent, input, { idempotencyKey });\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const r = await get(id);\n if (DONE.has(r.status)) {\n if (check && r.status !== \"succeeded\") throw new AgentRunError(r);\n return r;\n }\n if (Date.now() > deadline) throw new Error(`${agent} run ${id} still ${r.status} after ${timeoutMs / 1000}s`);\n await new Promise((resolve) => setTimeout(resolve, pollMs));\n }\n}\n\n/** Options for `new OpenAI(...)` on the Codiedev gateway, with this run's trace headers. */\nexport function openaiOptions(extra = {}) {\n const inRun = !!process.env.CODIEDEV_RUN_ID;\n return {\n baseURL: process.env.CODIEDEV_GATEWAY_URL || \"https://gateway.codiedev.com/api/v1\",\n apiKey: process.env.CODIEDEV_API_KEY || process.env.CODIEDEV_TOKEN,\n ...extra,\n defaultHeaders: {\n \"X-Codiedev-Workload\": process.env.CODIEDEV_AGENT || \"local-agent\",\n \"X-Codiedev-Run-Id\": process.env.CODIEDEV_RUN_ID || `local-${Date.now()}`,\n \"X-Codiedev-Environment\": inRun ? \"production\" : \"development\",\n ...(extra.defaultHeaders || {}),\n },\n };\n}\n\n/** An OpenAI client on the Codiedev gateway (the `openai` package is preinstalled). */\nexport async function openai(extra = {}) {\n const { default: OpenAI } = await import(\"openai\");\n return new OpenAI(openaiOptions(extra));\n}\n";
6
+ exports.jsTypes = "/* eslint-disable @typescript-eslint/no-explicit-any -- defaults are `any` on purpose: agent code reads its own JSON input/state without declaring types first. */\nexport type RunStatus = \"queued\" | \"running\" | \"succeeded\" | \"failed\" | \"cancelled\";\nexport interface Run<O = unknown> {\n id: string;\n agent: string;\n version: number;\n status: RunStatus;\n trigger: \"cli\" | \"api\" | \"portal\" | \"schedule\" | \"webhook\" | \"child\" | \"approval\";\n parentRunId: string | null;\n rootRunId: string | null;\n depth: number;\n attempt: number;\n maxAttempts: number;\n scheduledFor: number | null;\n input: string | null;\n output: O;\n error: string | null;\n logTail: string | null;\n durationMs: number | null;\n children: Run[];\n}\nexport interface WebhookRequest { method: string; query: Record<string, string>; headers: Record<string, string>; body: string }\nexport interface Approval {\n id: string;\n agent: string;\n title: string;\n details: string | null;\n options: string[];\n status: \"pending\" | \"decided\" | \"expired\" | \"cancelled\";\n decision: string | null;\n comment: string | null;\n decidedBy: string | null;\n /** The approval app that sent the decision on decidedBy's behalf; null when decided in Codiedev. */\n decidedVia: string | null;\n approvers: string[];\n approverTeams: Array<{ id: string; name: string }>;\n /** The form the person fills in, if the approval asked for one. */\n fields: ApprovalField[];\n /** What they entered, by field name (null until decided). */\n values: Record<string, any> | null;\n url: string | null;\n resumeRunId: string | null;\n}\n/** One thing a person fills in when they decide. */\nexport interface ApprovalField {\n name: string;\n type: \"text\" | \"textarea\" | \"number\" | \"rating\" | \"select\" | \"multiselect\" | \"boolean\";\n label?: string;\n help?: string;\n required?: boolean;\n /** select / multiselect choices */\n options?: string[];\n /** number bounds; rating scale 1–max (default 5) */\n min?: number;\n max?: number;\n /** prefilled, e.g. a proposed amount for the person to correct */\n default?: unknown;\n}\n/** The input a run started by a decision gets. */\nexport interface ApprovalInput<C = any, V = Record<string, any>> {\n approval: { id: string; title: string; decision: string; comment: string | null; values: V; decidedBy: string | null; decidedVia: string | null; decidedAt: number; requestedByRun: string | null };\n context: C;\n}\nexport class AgentRunError extends Error { run: Run }\nexport class StateConflict extends Error { key: string; version: number }\nexport function input<T = any>(fallback?: T): T;\nexport function output(value: unknown): void;\nexport function request(): WebhookRequest | null;\nexport function respond(body?: unknown, opts?: { status?: number; headers?: Record<string, string> }): void;\nexport function attempt(): number;\nexport function start(agent: string, input?: unknown, opts?: { idempotencyKey?: string; delaySec?: number; retries?: number }): Promise<string>;\nexport function get<O = any>(runId: string): Promise<Run<O>>;\nexport function run<O = any>(agent: string, input?: unknown, opts?: { timeoutMs?: number; pollMs?: number; check?: boolean; idempotencyKey?: string }): Promise<Run<O>>;\nexport const state: {\n get<T = any>(key: string, fallback?: T): Promise<T>;\n getVersioned<T = any>(key: string): Promise<{ value: T | null; version: number }>;\n set(key: string, value: unknown, opts?: { ifVersion?: number; ttlSec?: number }): Promise<number>;\n delete(key: string, opts?: { ifVersion?: number }): Promise<void>;\n list(prefix?: string, opts?: { values?: boolean }): Promise<Array<{ key: string; version: number; value?: any }>>;\n update<T = any>(key: string, fn: (old: T) => T | Promise<T>, fallback?: T, retries?: number): Promise<T>;\n};\nexport function step<T>(name: string, fn: () => T | Promise<T>, opts?: { ttlSec?: number }): Promise<T>;\nexport function requestApproval<C = any>(title: string, opts?: { details?: unknown; options?: string[]; approvers?: string[]; teams?: string[]; fields?: ApprovalField[]; then?: string | null; context?: C; expiresSec?: number }): Promise<Approval>;\nexport function getApproval(id: string): Promise<Approval>;\nexport function openaiOptions(extra?: Record<string, unknown>): { baseURL: string; apiKey: string | undefined; defaultHeaders: Record<string, string> };\nexport function openai(extra?: Record<string, unknown>): Promise<import(\"openai\").default>;";
7
+ exports.jsPackage = "{\n \"name\": \"codiedev-agents\",\n \"version\": \"0.1.0\",\n \"description\": \"Codiedev agents SDK: this run's input and output, chaining, and a traced OpenAI client.\",\n \"type\": \"module\",\n \"main\": \"./index.mjs\",\n \"types\": \"./index.d.ts\",\n \"exports\": { \".\": { \"types\": \"./index.d.ts\", \"default\": \"./index.mjs\" } }\n}\n";
@@ -46,9 +46,8 @@ const version_1 = require("../version");
46
46
  const upgradeNudge_1 = require("../upgradeNudge");
47
47
  const captureReadiness_1 = require("../captureReadiness");
48
48
  const captureLog_1 = require("../captureLog");
49
- const securityDoctor_1 = require("../securityDoctor");
50
- const securityFeature_1 = require("../securityFeature");
51
49
  const copilotInstallation_1 = require("../copilotInstallation");
50
+ const piHooks_1 = require("../piHooks");
52
51
  const harnessDirs_1 = require("../harnessDirs");
53
52
  const CLAUDE_DIR = (0, harnessDirs_1.claudeConfigDir)();
54
53
  const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
@@ -128,24 +127,6 @@ function hasCodiedevInstructions(instructionsPath) {
128
127
  return false;
129
128
  }
130
129
  }
131
- function hasGhCli() {
132
- try {
133
- (0, child_process_1.execSync)("gh --version", { stdio: ["pipe", "pipe", "pipe"] });
134
- return true;
135
- }
136
- catch {
137
- return false;
138
- }
139
- }
140
- function hasGlabCli() {
141
- try {
142
- (0, child_process_1.execSync)("glab --version", { stdio: ["pipe", "pipe", "pipe"] });
143
- return true;
144
- }
145
- catch {
146
- return false;
147
- }
148
- }
149
130
  async function runDoctor(_args) {
150
131
  const checks = [];
151
132
  console.log("\nCodiedev doctor — checking your setup…\n");
@@ -272,10 +253,10 @@ async function runDoctor(_args) {
272
253
  const workspaceMode = workspaceScan.repoCount > 0;
273
254
  checks.push({
274
255
  name: "git on PATH",
275
- status: readiness.gitOnPath ? "pass" : "fail",
256
+ status: readiness.gitOnPath ? "pass" : "warn",
276
257
  detail: readiness.gitOnPath
277
258
  ? undefined
278
- : "not found — capture can't tag sessions to a repo (install git, e.g. `apk add git`)",
259
+ : "not found — sessions still capture but can't be tagged to a repo (install git if you work in repos)",
279
260
  });
280
261
  checks.push({
281
262
  name: "Inside a git work tree (current dir)",
@@ -448,23 +429,16 @@ async function runDoctor(_args) {
448
429
  else {
449
430
  checks.push({ name: "GitHub Copilot", status: "warn", detail: "not installed on this machine" });
450
431
  }
451
- if ((0, securityFeature_1.isAgentSecurityEnabled)(config)) {
452
- checks.push(...(0, securityDoctor_1.securityDoctorChecks)((0, securityDoctor_1.collectSecurityDoctorReport)(config.companyId)));
432
+ if ((0, utils_1.piInstalled)()) {
433
+ const extension = (0, piHooks_1.inspectPiExtension)();
434
+ checks.push({ name: "pi capture extension", status: extension.status === "pass" ? "pass" : "fail", detail: extension.detail });
435
+ checks.push({ name: "pi agent instructions",
436
+ status: hasCodiedevInstructions((0, utils_1.piInstructionsPath)()) ? "pass" : "fail",
437
+ detail: hasCodiedevInstructions((0, utils_1.piInstructionsPath)()) ? (0, harnessDirs_1.displayPath)((0, utils_1.piInstructionsPath)()) : "missing — re-run `codiedev connect`" });
438
+ }
439
+ else {
440
+ checks.push({ name: "pi", status: "warn", detail: "not installed on this machine" });
453
441
  }
454
- checks.push({
455
- name: "GitHub CLI (`gh`) for reverse-ticket from a PR url",
456
- status: hasGhCli() ? "pass" : "warn",
457
- detail: hasGhCli()
458
- ? "installed"
459
- : "not installed — only needed for `codiedev reverse-ticket <github-pr-url>`",
460
- });
461
- checks.push({
462
- name: "GitLab CLI (`glab`) for reverse-ticket from an MR url",
463
- status: hasGlabCli() ? "pass" : "warn",
464
- detail: hasGlabCli()
465
- ? "installed"
466
- : "not installed — only needed for `codiedev reverse-ticket <gitlab-mr-url>`",
467
- });
468
442
  report(checks);
469
443
  const failures = checks.filter((c) => c.status === "fail");
470
444
  if (failures.length > 0) {
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EVALS_HELP = void 0;
37
+ exports.runEvals = runEvals;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const shared_1 = require("./shared");
41
+ exports.EVALS_HELP = `
42
+ codiedev evals — experiments and their runs, shared with the team
43
+
44
+ codiedev evals report <run.json> [--harness name]
45
+ File one variant's scores under an
46
+ experiment (created on first use)
47
+ codiedev evals last <subject> [--json] The latest experiment on a subject,
48
+ its runs, and the current champion
49
+
50
+ run.json:
51
+ {
52
+ "subject": "session-summaries", // agent name or workload
53
+ "question": "Is Terra medium as good as GLM high?",
54
+ "experimentId": "…", // optional, instead of subject + question
55
+ "dataset": "30 Signal & Code sessions", // optional
56
+ "run": {
57
+ "label": "terra-med-v2", "model": "openai/gpt-6.1-terra",
58
+ "provider": "openai", "reasoningEffort": "medium", "prompt": "summary_extract_v2",
59
+ "gitSha": "…", "isControl": false, "judgeModel": "anthropic/claude-sonnet-5.5",
60
+ "casesTotal": 30, "casesScored": 30, "errors": 0,
61
+ "scoringRevision": 1, // required once experiment scoring rules are defined
62
+ "score": 3.85, "metrics": { "fidelity": 3.6 },
63
+ "costPerCaseUsd": 0.026, "latencyMsAvg": 21000,
64
+ "wins": 20, "losses": 8, "ties": 2, "note": "…",
65
+ "cases": [{ "key": "case-1", "score": 4, "note": "…" }]
66
+ }
67
+ }
68
+
69
+ Read scoring rules and their current revision with: codiedev evals last <subject> --json
70
+
71
+ Scores and notes only: never put prompts, transcripts or outputs in it.
72
+ `.trim();
73
+ function flag(args, i, name) {
74
+ const a = args[i];
75
+ if (a === name && i + 1 < args.length)
76
+ return { value: args[i + 1], skip: 1 };
77
+ if (a.startsWith(`${name}=`))
78
+ return { value: a.slice(name.length + 1), skip: 0 };
79
+ return { skip: -1 };
80
+ }
81
+ function variant(run) {
82
+ return [run.model, run.reasoningEffort, run.prompt].filter(Boolean).join(" · ");
83
+ }
84
+ function fmtScore(n) {
85
+ return n === null ? "–" : n.toFixed(2);
86
+ }
87
+ function fmtCost(n) {
88
+ return n === null ? "–" : `$${n.toFixed(4)}`;
89
+ }
90
+ async function runReport(args, config) {
91
+ let file;
92
+ let harness;
93
+ for (let i = 0; i < args.length; i++) {
94
+ const f = flag(args, i, "--harness");
95
+ if (f.skip >= 0) {
96
+ harness = f.value;
97
+ i += f.skip;
98
+ continue;
99
+ }
100
+ if (!args[i].startsWith("-") && !file)
101
+ file = args[i];
102
+ }
103
+ if (!file) {
104
+ console.error("Usage: codiedev evals report <run.json> [--harness name]");
105
+ process.exit(1);
106
+ }
107
+ let body;
108
+ try {
109
+ body = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), file), "utf8"));
110
+ }
111
+ catch (err) {
112
+ console.error(`Couldn't read ${file}: ${err.message}`);
113
+ process.exit(1);
114
+ }
115
+ if (!body.subject || !body.question || !body.run?.label || !body.run?.model || typeof body.run?.casesTotal !== "number") {
116
+ console.error(`${file} needs subject, question, and run.label, run.model, run.casesTotal. See: codiedev evals help`);
117
+ process.exit(1);
118
+ }
119
+ const start = Date.now();
120
+ try {
121
+ const res = await (0, shared_1.apiRequest)("POST", "/api/cli/evals/runs", {
122
+ config,
123
+ body: { ...body, run: { ...body.run, harness: harness ?? body.run.harness ?? undefined } },
124
+ });
125
+ (0, shared_1.recordAgentEvent)(config, { tool: "codiedev_evals_report", ok: true, latencyMs: Date.now() - start });
126
+ console.log(`✓ Reported ${body.run.label} on ${body.subject}${res.created ? " (new experiment)" : ""}: "${body.question}"`);
127
+ console.log(` experiment ${res.experimentId}. See it in Codiedev → Traces → Evals.`);
128
+ }
129
+ catch (err) {
130
+ const e = err;
131
+ (0, shared_1.recordAgentEvent)(config, { tool: "codiedev_evals_report", ok: false, latencyMs: Date.now() - start, error: e.message });
132
+ console.error(`Couldn't report: ${e.message}`);
133
+ process.exit(1);
134
+ }
135
+ }
136
+ async function runLast(args, config) {
137
+ const json = args.includes("--json");
138
+ const subject = args.find((a) => !a.startsWith("-"));
139
+ if (!subject) {
140
+ console.error("Usage: codiedev evals last <subject> [--json]");
141
+ process.exit(1);
142
+ }
143
+ let res;
144
+ try {
145
+ res = await (0, shared_1.apiRequest)("GET", `/api/cli/evals/last?subject=${encodeURIComponent(subject)}`, { config });
146
+ }
147
+ catch (err) {
148
+ const e = err;
149
+ console.error(e.status === 404 ? `No evals on ${subject} yet.` : `Couldn't load evals: ${e.message}`);
150
+ process.exit(1);
151
+ }
152
+ if (json) {
153
+ console.log(JSON.stringify(res, null, 2));
154
+ return;
155
+ }
156
+ const { experiment, runs, champion } = res;
157
+ if (champion) {
158
+ console.log(`Champion: ${champion.run.label} (${variant(champion.run)}), score ${fmtScore(champion.run.score)}, ${fmtCost(champion.run.costPerCaseUsd)}/case`);
159
+ if (champion.decisionNote)
160
+ console.log(` Why: ${champion.decisionNote}`);
161
+ }
162
+ else {
163
+ console.log("Champion: none picked yet");
164
+ }
165
+ console.log("");
166
+ console.log(`Latest experiment (${res.experiments} on ${experiment.subject}): ${experiment.question}`);
167
+ if (experiment.dataset)
168
+ console.log(` Dataset: ${experiment.dataset}`);
169
+ console.log(` id ${experiment.id}, by ${experiment.createdBy}, updated ${new Date(experiment.updatedAt).toISOString().slice(0, 16).replace("T", " ")}`);
170
+ console.log("");
171
+ for (const run of [...runs].sort((a, b) => a.ranAt - b.ranAt)) {
172
+ const h2h = run.wins !== null ? ` · ${run.wins}W/${run.losses ?? 0}L/${run.ties ?? 0}T` : "";
173
+ console.log(` ${run.isControl ? "[control] " : ""}${run.label.padEnd(24)} ${fmtScore(run.score).padStart(5)} ${fmtCost(run.costPerCaseUsd).padStart(8)}/case${h2h} ${variant(run)}`);
174
+ }
175
+ console.log("");
176
+ console.log(`Next run on this experiment: put "experimentId": "${experiment.id}" in run.json, then codiedev evals report run.json`);
177
+ }
178
+ async function runEvals(args, configOverride) {
179
+ const [sub, ...rest] = args;
180
+ if (sub === "report")
181
+ return runReport(rest, configOverride ?? (0, shared_1.requireConfig)());
182
+ if (sub === "last")
183
+ return runLast(rest, configOverride ?? (0, shared_1.requireConfig)());
184
+ console.log(exports.EVALS_HELP);
185
+ if (sub && sub !== "help")
186
+ process.exit(1);
187
+ }
@@ -88,7 +88,7 @@ function ciHarnesses(args, env, home) {
88
88
  }
89
89
  const USAGE = `Usage:
90
90
  codiedev pack list Packs assigned to your team
91
- codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie] [--dry-run] [--json] [--quiet]
91
+ codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie,pi] [--dry-run] [--json] [--quiet]
92
92
  Install one pack, or every pack that applies
93
93
  codiedev pack status [--json] Installed packs and whether they're up to date
94
94
  codiedev pack export <slug> --out <dir> [--git] [--manifest]
@@ -41,14 +41,15 @@ exports.runTraces = runTraces;
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const shared_1 = require("./shared");
44
+ const tracesLabel_1 = require("./tracesLabel");
44
45
  exports.TRACES_HELP = `
45
- codiedev traces — test cases saved from Codiedev → Traces
46
+ codiedev traces — test cases, scores, review queues and automatic scoring from Codiedev → Traces
46
47
 
47
48
  codiedev traces agents Agents you maintain, with saved case counts
48
- codiedev traces cases <agent> [--suite name] [--case id[,id]] [--model id] [--out dir] [--json]
49
- Write an agent's cases (one suite, or just
49
+ codiedev traces cases <agent> [--dataset name [--version n]] [--case id[,id]] [--model id] [--out dir] [--json]
50
+ Write an agent's cases (one dataset, or just
50
51
  the cases named) into this repo, default
51
- .codiedev/traces/<agent>[/<suite>]/, then
52
+ .codiedev/traces/<agent>[/<dataset>[/v<n>]]/, then
52
53
  ask your coding agent to run them.
53
54
  --model runs them on another model, e.g.
54
55
  the replacement for a retiring one
@@ -56,6 +57,36 @@ codiedev traces — test cases saved from Codiedev → Traces
56
57
  Send the filled-in results back, so
57
58
  Traces shows the pass rate for that model
58
59
 
60
+ codiedev traces overview [--days n] [--json]
61
+ Every agent you can see: runs, failures,
62
+ spend, and what's going wrong most
63
+ codiedev traces runs <agent> [--failed] [--flag name] [--model id] [--verdict pass|fail] [--days n] [--limit n] [--json]
64
+ An agent's runs, newest first
65
+ codiedev traces run <run> [--agent name] [--json]
66
+ One run: steps, what went wrong and the fix,
67
+ scores, prompts and replies (maintainers)
68
+ codiedev traces score-kinds [add <name> --type number|categories|boolean [--min n --max n] [--categories a,b]]
69
+ List or add the kinds of score your team uses
70
+ codiedev traces queue create <name> [--scores a,b] [--description text]
71
+ codiedev traces score <run> <score> <value> [--agent name] [--comment text] [--step n]
72
+ Score a run (a portal run id agent~run, or
73
+ the agent's own run id with --agent)
74
+ codiedev traces scores <agent> [--days n] [--json]
75
+ Each score's average or mix, and the score kinds
76
+ codiedev traces queue list Review queues and how many are left
77
+ codiedev traces queue add <queue> --agent <name> [--failed] [--days n] [--run id ...]
78
+ File runs into a queue for people to review
79
+ codiedev traces judge <agent> [--on | --off] [--scores a,b] [--instructions text]
80
+ [--model id] [--sample percent] [--daily n] [--budget usd | --key cdvm_…]
81
+ Show or set automatic scoring (a judge model
82
+ scores a sample of runs, billed to your key)
83
+ codiedev traces import <agent> <file.csv|file.jsonl> --dataset <name>
84
+ Add examples from a file (input, expected, name)
85
+ codiedev traces add-example <agent> --dataset <name> --input <text> [--expected <text>] [--name <text>]
86
+ codiedev traces versions <dataset> A dataset's saved versions and what changed
87
+ codiedev traces save-version <dataset> [--note text]
88
+ Save the dataset as its next version
89
+
59
90
  <agent> is the agent's name (its API key name) or id. Only an agent's
60
91
  maintainers can pull its cases or report on them.
61
92
  `.trim();
@@ -68,7 +99,7 @@ const FLAG_LABEL = {
68
99
  };
69
100
  const TRIES = 3;
70
101
  const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
71
- const recordedModel = (res) => res.cases.flatMap((c) => c.steps.map((s) => s.model))[0] ?? "the recorded model";
102
+ const recordedModel = (res) => res.cases.flatMap((c) => c.steps.map((s) => s.model)).filter(Boolean)[0] ?? "the agent's current model";
72
103
  function slugify(name) {
73
104
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
74
105
  }
@@ -79,7 +110,7 @@ function casesBrief(res, casesFile, opts = {}) {
79
110
  const target = opts.model;
80
111
  const results = opts.resultsFile ? path.basename(opts.resultsFile) : "results.json";
81
112
  const lines = [
82
- `# Test cases for ${res.agent.name}${res.suite ? ` · ${res.suite}` : ""}${target ? ` on ${target}` : ""}`,
113
+ `# Test cases for ${res.agent.name}${res.suite ? ` · ${res.suite}` : ""}${res.version ? ` v${res.version}` : ""}${target ? ` on ${target}` : ""}`,
83
114
  "",
84
115
  res.agent.description ? `${res.agent.description}\n` : "",
85
116
  `Saved from Codiedev → Traces. ${plural(res.cases.length, "case")} in \`${path.basename(casesFile)}\` (one JSON object per line):`,
@@ -90,7 +121,7 @@ function casesBrief(res, casesFile, opts = {}) {
90
121
  if (target) {
91
122
  lines.push("## This is a model check", "", `Run every case on **${target}** instead of ${recordedModel(res)} (the model the cases were recorded on). Change only the model the agent asks the gateway for; keep its prompts and tools as they are, so the result says how ${target} does on the same work.`, "");
92
123
  }
93
- lines.push("## How to run them", "", "1. Find where this agent builds its model requests and calls the Codiedev gateway.", `2. For each case, replay \`steps[0].request\` (the first prompt the agent sent) through the agent's own code path, so tools and prompts are the current version, not the recorded one${target ? `, with the model set to ${target}` : ""}.`, "3. Judge each try against the case:", " - A case's `expected`, when present, is what a passing answer looks like (a reference reply or criteria). It wins over everything below.", " - `fix` cases: the run must not reproduce its `flags` (listed below), and must finish (no failed call, no `finish_reason: \"length\"`, no repeated identical tool call).", " - `keep` cases: the final reply must still do what the recorded final reply did. Compare meaning, not wording.", " - A case's `note`, when present, is the engineer's extra context.");
124
+ lines.push("## How to run them", "", "1. Find where this agent builds its model requests and calls the Codiedev gateway.", `2. For each case, replay \`steps[0].request\` (the first prompt the agent sent) through the agent's own code path, so tools and prompts are the current version, not the recorded one${target ? `, with the model set to ${target}` : ""}.`, "3. Judge each try against the case:", " - A case's `expected`, when present, is what a passing answer looks like (a reference reply or criteria). It wins over everything below.", " - `fix` cases: the run must not reproduce its `flags` (listed below), and must finish (no failed call, no `finish_reason: \"length\"`, no repeated identical tool call).", " - `keep` cases: the final reply must still do what the recorded final reply did. Compare meaning, not wording.", " - A case's `note`, when present, is the engineer's extra context.", " - Cases whose `runId` starts with `manual:` or `import:` were written by hand, not recorded: `steps[0].request` is the input and there's no recorded reply, so judge them on `expected`.");
94
125
  if (res.judge?.model || res.judge?.rubric) {
95
126
  lines.push(` - Where passing needs judgement, judge with **${res.judge.model ?? "a strong model"}** through the gateway${res.judge.rubric ? `, using this rubric: "${res.judge.rubric}"` : ""}. Use the same judge for every model so results compare.`);
96
127
  }
@@ -109,6 +140,7 @@ function resultsTemplate(res, model) {
109
140
  return {
110
141
  agent: res.agent.name,
111
142
  suite: res.suite,
143
+ ...(res.version ? { version: res.version } : {}),
112
144
  model: model ?? recordedModel(res),
113
145
  judgeModel: res.judge?.model ?? null,
114
146
  harness: null,
@@ -128,7 +160,7 @@ function parseCasesArgs(args) {
128
160
  for (let i = 0; i < args.length; i++) {
129
161
  const a = args[i];
130
162
  let matched = false;
131
- for (const [name, key] of [["--out", "out"], ["-o", "out"], ["--suite", "suite"], ["--model", "model"], ["--case", "caseIds"]]) {
163
+ for (const [name, key] of [["--out", "out"], ["-o", "out"], ["--dataset", "suite"], ["--suite", "suite"], ["--model", "model"], ["--case", "caseIds"], ["--version", "version"]]) {
132
164
  const f = flag(args, i, name);
133
165
  if (f.skip >= 0) {
134
166
  parsed[key] = f.value;
@@ -170,20 +202,26 @@ async function runAgents(config) {
170
202
  console.log("\nPull one: codiedev traces cases <agent>");
171
203
  }
172
204
  async function runCases(args, config) {
173
- const { agent, suite, model, caseIds, out, json } = parseCasesArgs(args);
205
+ const { agent, suite, model, caseIds, version, out, json } = parseCasesArgs(args);
174
206
  if (!agent) {
175
- console.error("Usage: codiedev traces cases <agent> [--suite name] [--model id] [--out dir] [--json]");
207
+ console.error("Usage: codiedev traces cases <agent> [--dataset name [--version n]] [--model id] [--out dir] [--json]");
208
+ process.exit(1);
209
+ }
210
+ if (version !== undefined && (!suite || !/^[1-9]\d*$/.test(version))) {
211
+ console.error("--version takes a whole number and needs --dataset: codiedev traces cases <agent> --dataset <name> --version <n>");
176
212
  process.exit(1);
177
213
  }
178
214
  const start = Date.now();
179
215
  let res;
180
216
  try {
181
- res = await (0, shared_1.apiRequest)("GET", "/api/cli/traces/cases", { config, query: { agent, ...(suite ? { suite } : {}), ...(caseIds ? { case: caseIds } : {}) } });
217
+ res = await (0, shared_1.apiRequest)("GET", "/api/cli/traces/cases", { config, query: { agent, ...(suite ? { suite } : {}), ...(version ? { version } : {}), ...(caseIds ? { case: caseIds } : {}) } });
182
218
  }
183
219
  catch (err) {
184
220
  const e = err;
185
221
  (0, shared_1.recordAgentEvent)(config, { tool: "codiedev_traces_cases", ok: false, latencyMs: Date.now() - start, error: e.message });
186
- if (e.status === 404)
222
+ if (e.status === 404 && version && /No version/.test(e.message))
223
+ console.error(`No version ${version} of "${suite}". See its versions in Codiedev → Traces → Test cases.`);
224
+ else if (e.status === 404)
187
225
  console.error(`No agent called "${agent}". See your agents with: codiedev traces agents`);
188
226
  else if (e.status === 403)
189
227
  console.error(`Only ${agent}'s maintainers can pull its test cases.`);
@@ -198,11 +236,11 @@ async function runCases(args, config) {
198
236
  }
199
237
  if (!res.cases.length) {
200
238
  console.log(suite
201
- ? `${res.agent.name} has no test cases in "${suite}". See your suites in Codiedev → Traces → Test cases.`
239
+ ? `${res.agent.name} has no test cases in "${suite}". See your datasets in Codiedev → Traces → Test cases.`
202
240
  : `${res.agent.name} has no saved test cases. Open a run in Codiedev → Traces and click "Save as test case".`);
203
241
  return;
204
242
  }
205
- const dir = path.resolve(process.cwd(), out ?? path.join(".codiedev", "traces", slugify(res.agent.name), ...(res.suite ? [slugify(res.suite)] : []), ...(caseIds && res.cases.length === 1 ? [slugify(res.cases[0].name)] : []), ...(model ? [slugify(model)] : [])));
243
+ const dir = path.resolve(process.cwd(), out ?? path.join(".codiedev", "traces", slugify(res.agent.name), ...(res.suite ? [slugify(res.suite)] : []), ...(res.version ? [`v${res.version}`] : []), ...(caseIds && res.cases.length === 1 ? [slugify(res.cases[0].name)] : []), ...(model ? [slugify(model)] : [])));
206
244
  fs.mkdirSync(dir, { recursive: true });
207
245
  const casesFile = path.join(dir, "cases.jsonl");
208
246
  fs.writeFileSync(casesFile, res.cases.map((c) => JSON.stringify(c)).join("\n") + "\n", "utf8");
@@ -212,7 +250,9 @@ async function runCases(args, config) {
212
250
  fs.writeFileSync(briefFile, casesBrief(res, casesFile, { model, resultsFile }), "utf8");
213
251
  const fix = res.cases.filter((c) => c.kind === "fix").length;
214
252
  const rel = (file) => path.relative(process.cwd(), file) || file;
215
- console.log(`✓ ${plural(res.cases.length, "case")} for ${res.agent.name}${res.suite ? ` · ${res.suite}` : ""}${model ? ` on ${model}` : ""} (${fix} to fix, ${res.cases.length - fix} to keep passing)`);
253
+ console.log(`✓ ${plural(res.cases.length, "case")} for ${res.agent.name}${res.suite ? ` · ${res.suite}` : ""}${res.version ? ` v${res.version}` : ""}${model ? ` on ${model}` : ""} (${fix} to fix, ${res.cases.length - fix} to keep passing)`);
254
+ if (res.missing)
255
+ console.log(` ${plural(res.missing, "case")} in v${res.version} were deleted since and can't be pulled.`);
216
256
  console.log(` ${rel(casesFile)}`);
217
257
  console.log(` ${rel(briefFile)}`);
218
258
  console.log(` ${rel(resultsFile)} (filled in by your coding agent, then: codiedev traces report ${rel(resultsFile)})`);
@@ -274,6 +314,30 @@ async function runTraces(args, configOverride) {
274
314
  return runCases(rest, configOverride ?? (0, shared_1.requireConfig)());
275
315
  if (sub === "report")
276
316
  return runReport(rest, configOverride ?? (0, shared_1.requireConfig)());
317
+ if (sub === "overview")
318
+ return (0, tracesLabel_1.runOverview)(rest, configOverride ?? (0, shared_1.requireConfig)());
319
+ if (sub === "runs")
320
+ return (0, tracesLabel_1.runRuns)(rest, configOverride ?? (0, shared_1.requireConfig)());
321
+ if (sub === "run")
322
+ return (0, tracesLabel_1.runRun)(rest, configOverride ?? (0, shared_1.requireConfig)());
323
+ if (sub === "score-kinds")
324
+ return (0, tracesLabel_1.runScoreKinds)(rest, configOverride ?? (0, shared_1.requireConfig)());
325
+ if (sub === "score")
326
+ return (0, tracesLabel_1.runScore)(rest, configOverride ?? (0, shared_1.requireConfig)());
327
+ if (sub === "scores")
328
+ return (0, tracesLabel_1.runScores)(rest, configOverride ?? (0, shared_1.requireConfig)());
329
+ if (sub === "queue")
330
+ return (0, tracesLabel_1.runQueue)(rest, configOverride ?? (0, shared_1.requireConfig)());
331
+ if (sub === "judge")
332
+ return (0, tracesLabel_1.runJudge)(rest, configOverride ?? (0, shared_1.requireConfig)());
333
+ if (sub === "import")
334
+ return (0, tracesLabel_1.runImport)(rest, configOverride ?? (0, shared_1.requireConfig)());
335
+ if (sub === "add-example")
336
+ return (0, tracesLabel_1.runAddExample)(rest, configOverride ?? (0, shared_1.requireConfig)());
337
+ if (sub === "versions")
338
+ return (0, tracesLabel_1.runVersions)(rest, configOverride ?? (0, shared_1.requireConfig)());
339
+ if (sub === "save-version")
340
+ return (0, tracesLabel_1.runSaveVersion)(rest, configOverride ?? (0, shared_1.requireConfig)());
277
341
  console.log(exports.TRACES_HELP);
278
342
  if (sub && sub !== "help")
279
343
  process.exit(1);