plures 1.16.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,12 +45,14 @@ if (event.type === "task.completed" && event.data.result?.agreement === 1) {
45
45
  ## API
46
46
 
47
47
  - `plures.check(id, data, overrides?)`: run a check from your app's map, one line before the action it guards: `const check = await plures.check("refunds", { order_total, reason }); if (check.answer !== "Approve") return`. The check saved on Plures supplies the question, options, which fields people see (the rest come back in `kept_private`), who decides, how often, its conditions and what happens on a timeout, so you change them on Plures, not in code. `checked` says whether a person looked; `skipped_because` says why not. Overrides: `question`, `options`, `verifiers`, `team`, `route`, `sample`, `timeout`, `always`.
48
+ - `plures.turns.log({ question, answer, intent, rerouted_to, retrieval, sources, session, channel })`: answer reviews for assistants. Send each turn after it's answered (don't await it in the user's path); Plures picks the ones that went wrong (rerouted, refused, empty, no sources, asked again) plus a few at random, and your team grades them. `plures.turns.insights(app)` and `plures.turns.export(app, "routes" | "retrieval" | "qa" | "missed")` turn the grades into routing examples and eval sets.
49
+ - `plures.drift.list(app)`, `.fix(app, id)`: drift cases, when a rubric or an answer intent's grades fall. `npx plures drift fix <case> --app <app>` has Claude Code read the repo with the graded examples that got worse, find the cause, fix it and add eval cases; on GitHub, Plures opens the pull request and then checks the grades come back.
48
50
  - `npx plures check add <id> --app <app>`: Claude Code puts that line before the action on a new branch. On GitHub (the dashboard's **Add it to my code**) Plures then opens a pull request.
49
51
  - `plures.tasks.create(params, { idempotencyKey? })`: send a task. Params: `question`, `options` (2 to 8, strings or `{ id, label }`), `context`, `image_url`, `audio_url` or `video_url` with `media_seconds` (a clip up to 180 seconds that people play through before answering), `sensitive`, `instructions`, `verifiers` (1, 3, 5, 7 or 9), `max_verifiers`, `presence_check` (default true), `webhook_url`, `metadata`, `expires_in`, `route` (`"team"` sends it straight to your own people, $0.05 per internal vote live), `category` (matches your routing rules) and `team` (one of your named teams), `team_votes` and `team_unanimous` (internal consensus: how many of your team vote, and whether they must all agree; a split goes to the team's lead).
50
52
  - `plures.tasks.retrieve(id)`, `plures.tasks.list({ status?, limit?, starting_after?, app? })`, `plures.tasks.cancel(id)`
51
53
  - `new Plures(key, { app: "support-agent" })` or `PLURES_APP`: send everything to one of your apps. `plures.apps.list()`, `.create({ name, id? })`, `.retrieve(id)`, `.update(id, { name?, id?, default? })`, `.del(id)`. Each app keeps its own tasks, ledger view, map and quality.
52
54
  - `plures.maps.guide()`, `.retrieve(app)`, `.upload(app, map, { branch? })` (a branch other than main gets its own map, compared with main and noted on its pull request), `.setCheckpoint(app, id, status)`: the agent map, where your agent needs a person. Or run `npx plures map` in your repo: your own coding agent reads the code and only the map is sent.
53
- - `plures.maps.createKey(app, { label? })`, `.keys(app)`, `.revokeKey(app, id)`: map keys for CI, which can only read and upload that app's map. `.workflow(app, branch?)` returns the GitHub Action that maps on every push; `npx plures map workflow` writes it to `.github/workflows/plures-map.yml`. Or use **Map with GitHub** on the app's map page and Plures opens the pull request for you, then **Turn on mapping** merges it. `plures.maps.startRun(app)` and `.runs(app)`: in CI with no Claude key, `npx plures map` starts a run where Plures runs Claude (first map free, then Claude's cost plus 30%, at most $5).
55
+ - `plures.maps.createKey(app, { label? })`, `.keys(app)`, `.revokeKey(app, id)`: map keys for CI, which can only read and upload that app's map. `.workflow(app, branch?)` returns the GitHub Action that maps on every push; `npx plures map workflow` writes it to `.github/workflows/plures-map.yml`. Or use **Map with GitHub** on the app's map page and Plures opens the pull request for you, then **Turn on mapping** merges it. `plures.maps.startRun(app)` and `.runs(app)`: in CI with no Claude key, `npx plures map` starts a run where Plures runs Claude (Claude at cost, at most $5 a run). `plures.maps.estimate(app, "map" | "drift")` shows what the next map or drift fix costs before it starts: the Plures engine fee, 5¢ a step of the app's map (the first map of each app is free; branch maps are free), plus Claude at cost when Plures runs it. `npx plures map` and `npx plures drift fix` check it first and stop if your credit can't cover it. A drift fix is paid by outcome: each quality point it wins back, once people have graded the new outputs, costs 20¢ a step of the map ($3 to $15 a point), at most $500 a fix, with Claude included; no better or worse is free.
54
56
  - In your terminal, `npx plures map` opens your map page so you can watch it build (`--no-open` to skip), keeps one live line of what it's reading, and ends by drawing the map's flows with the checks Plures suggests. On GitHub and other CI it prints a plain log.
55
57
  - `npx plures map` shows each file as Claude Code reads it and sends the file names (never their contents) to Plures, so the map page shows the reading live and the map builds itself when it lands. `plures.maps.progress(app, { session, phase, events })` is the call behind it.
56
58
  - `plures.github.repos()`, `.connect(app, "owner/repo")`, `.turnOn(app)`, `.run(app)` (map now), `.status(app)`, `.disconnect(app)`: Map with GitHub from code. Plures sets the repo's map key secret and opens a pull request adding the workflow; `turnOn` merges it. Installing the Plures GitHub App needs a browser once (`install_url`). From a terminal: `npx plures map github connect`, then `npx plures map github on`; `npx plures map github run` maps again now.
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var VERSION = "1.16.0";
2
+ var VERSION = "1.21.0";
3
3
  var PluresError = class extends Error {
4
4
  constructor(message, status, type) {
5
5
  super(message);
@@ -107,8 +107,12 @@ var Plures = class {
107
107
  createKey: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`, opts),
108
108
  keys: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`),
109
109
  revokeKey: (app, id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/map_keys/${encodeURIComponent(id)}`),
110
+ /** What the next map or drift fix will cost before it starts. A map: the Plures engine fee, per step of the app's map
111
+ * (the first map of each app is free), and Claude at cost if Plures runs it. A drift fix: paid by outcome,
112
+ * drift_fix.per_point_cents for each quality point it wins back, at most drift_fix.max_cents, Claude included. blocked says why it can't start. */
113
+ estimate: (app, kind = "map", opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_estimate?for=${kind}${opts.case ? `&case=${encodeURIComponent(opts.case)}` : ""}`),
110
114
  /** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
111
- startRun: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, {}),
115
+ startRun: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, opts),
112
116
  runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
113
117
  /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
114
118
  progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
@@ -169,6 +173,43 @@ var Plures = class {
169
173
  return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
170
174
  }
171
175
  };
176
+ /**
177
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
178
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
179
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
180
+ * it after the reply and don't await it in the user's path.
181
+ */
182
+ turns = {
183
+ log: (t) => this.req("POST", "/api/v1/turns", t),
184
+ /** Grade a turn from your own tools. */
185
+ grade: (id, g) => this.req("POST", `/api/v1/turns/${encodeURIComponent(id)}/grade`, g),
186
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
187
+ insights: (app, opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/turns/insights${opts.days ? `?days=${opts.days}` : ""}`),
188
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
189
+ export: async (app, kind, opts = {}) => {
190
+ const q = new URLSearchParams({ kind, ...opts.since ? { since: opts.since } : {} });
191
+ const r = await fetch(`${this.base}/api/v1/apps/${encodeURIComponent(app)}/turns/export?${q}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
192
+ if (!r.ok) {
193
+ const d = await r.json().catch(() => ({}));
194
+ throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
195
+ }
196
+ return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
197
+ }
198
+ };
199
+ /**
200
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
201
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
202
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
203
+ */
204
+ drift = {
205
+ list: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift`),
206
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
207
+ fix: (app, id) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/fix`, {}),
208
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
209
+ packet: (app, id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/packet`),
210
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
211
+ report: (app, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/report`, p)
212
+ };
172
213
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
173
214
  balance = () => this.req("GET", "/api/v1/balance");
174
215
  /**
package/dist/cli.cjs CHANGED
@@ -30,7 +30,7 @@ var import_node_path = require("path");
30
30
  var import_node_fs = require("fs");
31
31
 
32
32
  // src/index.ts
33
- var VERSION = "1.16.0";
33
+ var VERSION = "1.21.0";
34
34
  var PluresError = class extends Error {
35
35
  constructor(message, status, type) {
36
36
  super(message);
@@ -138,8 +138,12 @@ var Plures = class {
138
138
  createKey: (app2, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys`, opts),
139
139
  keys: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys`),
140
140
  revokeKey: (app2, id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys/${encodeURIComponent(id)}`),
141
+ /** What the next map or drift fix will cost before it starts. A map: the Plures engine fee, per step of the app's map
142
+ * (the first map of each app is free), and Claude at cost if Plures runs it. A drift fix: paid by outcome,
143
+ * drift_fix.per_point_cents for each quality point it wins back, at most drift_fix.max_cents, Claude included. blocked says why it can't start. */
144
+ estimate: (app2, kind = "map", opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_estimate?for=${kind}${opts.case ? `&case=${encodeURIComponent(opts.case)}` : ""}`),
141
145
  /** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
142
- startRun: (app2) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`, {}),
146
+ startRun: (app2, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`, opts),
143
147
  runs: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`),
144
148
  /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
145
149
  progress: (app2, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map/progress`, p),
@@ -200,6 +204,43 @@ var Plures = class {
200
204
  return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
201
205
  }
202
206
  };
207
+ /**
208
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
209
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
210
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
211
+ * it after the reply and don't await it in the user's path.
212
+ */
213
+ turns = {
214
+ log: (t) => this.req("POST", "/api/v1/turns", t),
215
+ /** Grade a turn from your own tools. */
216
+ grade: (id, g) => this.req("POST", `/api/v1/turns/${encodeURIComponent(id)}/grade`, g),
217
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
218
+ insights: (app2, opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/turns/insights${opts.days ? `?days=${opts.days}` : ""}`),
219
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
220
+ export: async (app2, kind, opts = {}) => {
221
+ const q = new URLSearchParams({ kind, ...opts.since ? { since: opts.since } : {} });
222
+ const r = await fetch(`${this.base}/api/v1/apps/${encodeURIComponent(app2)}/turns/export?${q}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
223
+ if (!r.ok) {
224
+ const d = await r.json().catch(() => ({}));
225
+ throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
226
+ }
227
+ return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
228
+ }
229
+ };
230
+ /**
231
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
232
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
233
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
234
+ */
235
+ drift = {
236
+ list: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/drift`),
237
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
238
+ fix: (app2, id) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/drift/${encodeURIComponent(id)}/fix`, {}),
239
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
240
+ packet: (app2, id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/drift/${encodeURIComponent(id)}/packet`),
241
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
242
+ report: (app2, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/drift/${encodeURIComponent(id)}/report`, p)
243
+ };
203
244
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
204
245
  balance = () => this.req("GET", "/api/v1/balance");
205
246
  /**
@@ -475,8 +516,7 @@ async function upload(file) {
475
516
  }
476
517
  if (!fancy) console.log(dim(`Sending the map to Plures. No source code is sent.`));
477
518
  let m;
478
- const gitBranch = (0, import_node_child_process.spawnSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" }).stdout?.trim();
479
- const branchName = process.env.PLURES_BRANCH || process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || (gitBranch && gitBranch !== "HEAD" ? gitBranch : "");
519
+ const branchName = currentBranch();
480
520
  try {
481
521
  m = await client().maps.upload(app, map2, branchName ? { branch: branchName } : {});
482
522
  } catch (e) {
@@ -526,6 +566,14 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
526
566
  return;
527
567
  }
528
568
  const before = (0, import_node_fs.existsSync)(FILE) ? (0, import_node_fs.statSync)(FILE).mtimeMs : 0;
569
+ const est = await client().maps.estimate(app).catch(() => null);
570
+ if (est) {
571
+ const br = currentBranch();
572
+ const main2 = !br || br === "main" || br === "master";
573
+ if (est.blocked && main2) die(est.blocked);
574
+ if (main2 && est.engine_fee.free) console.log(dim("Plures engine: this app's first map is free."));
575
+ else if (main2 && est.engine_fee.cents) console.log(dim(`Plures engine: about $${(est.engine_fee.cents / 100).toFixed(2)} (${est.engine_fee.steps} steps at ${est.engine_fee.per_step_cents}\xA2), from your credit when the map uploads.`));
576
+ }
529
577
  const childEnv = { ...process.env };
530
578
  const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
531
579
  if (!ownClaude && (process.env.CI || argv.includes("--plures-claude"))) {
@@ -784,9 +832,158 @@ ${cyan("\u2713")} The check is on the branch ${bold(br)}. Review it with ${cyan(
784
832
  die(e instanceof PluresError ? e.message : String(e));
785
833
  }
786
834
  }
835
+ function currentBranch() {
836
+ const gitBranch = (0, import_node_child_process.spawnSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" }).stdout?.trim();
837
+ return process.env.PLURES_BRANCH || process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || (gitBranch && gitBranch !== "HEAD" ? gitBranch : "");
838
+ }
839
+ async function driftFix(id) {
840
+ if (!id) die("npx plures drift fix <case id> [--app <app>]");
841
+ const cl = client();
842
+ const report2 = async (p) => cl.drift.report(app, id, p);
843
+ const fail = async (msg) => {
844
+ await report2({ error: msg }).catch(() => {
845
+ });
846
+ die(msg);
847
+ };
848
+ let packet;
849
+ try {
850
+ packet = await cl.drift.packet(app, id);
851
+ } catch (e) {
852
+ return fail(e instanceof PluresError ? e.message : String(e));
853
+ }
854
+ const est = await cl.maps.estimate(app, "drift", { case: id }).catch(() => null);
855
+ if (est?.blocked) return fail(est.blocked);
856
+ if (!hasClaude()) return fail("Claude Code isn't installed here, so nothing could read the code.");
857
+ const git = (...a) => (0, import_node_child_process.spawnSync)("git", a, { encoding: "utf8" });
858
+ if (git("status", "--porcelain").stdout.trim()) return fail("This repo has changes that aren't committed. Commit or stash them first.");
859
+ const childEnv = { ...process.env };
860
+ const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
861
+ if (!ownClaude && process.env.CI) {
862
+ try {
863
+ const run = await cl.maps.startRun(app, { purpose: "drift", case: id });
864
+ childEnv.ANTHROPIC_BASE_URL = run.base_url;
865
+ childEnv.ANTHROPIC_API_KEY = run.token;
866
+ } catch (e) {
867
+ return fail(e instanceof PluresError ? e.message : String(e));
868
+ }
869
+ }
870
+ const session = packet.session || `drift_${(0, import_node_crypto.randomBytes)(9).toString("base64url").replace(/[-_]/g, "")}`;
871
+ const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
872
+ let queue = [], phase = "reading", sent = "";
873
+ const flush = async () => {
874
+ const events = queue;
875
+ queue = [];
876
+ if (!events.length && sent === phase) return;
877
+ sent = phase;
878
+ await cl.maps.progress(app, { session, phase, source, events }).catch(() => {
879
+ });
880
+ };
881
+ const tick = setInterval(() => {
882
+ void flush();
883
+ }, 900);
884
+ void flush();
885
+ const DIAG = ".plures-drift.json";
886
+ console.log(`${bold("Plures drift")} ${packet.label}
887
+ ${dim("Claude Code reads this repo with the graded examples that got worse. Only file names and the diagnosis go to Plures.")}
888
+ `);
889
+ const prompt = `Quality fell in this codebase's AI assistant, and people graded the examples. Find out why in the code and fix it.
890
+
891
+ What Plures measured (JSON):
892
+ ${JSON.stringify({ ...packet, session: void 0 }, null, 2).slice(0, 6e4)}
893
+
894
+ Do this:
895
+ 1. Read the code behind the steps listed (prompts, the intent router, retrieval, the answer step). Compare the examples that got worse with the ones that were fine before${packet.version_after ? `, and look for what changed in ${packet.version_after}` : ""}. Use git log or the prompt files if that helps.
896
+ 2. Fix the cause in the code with the smallest change that would make the worse examples come out right without breaking the good ones. Prefer prompt, routing and retrieval fixes over rewrites. Don't change unrelated code or formatting.
897
+ 3. If the repo has eval files (like evals/*.jsonl), add cases from the worse examples in the same format, so this can't come back unnoticed.
898
+ 4. Write ${DIAG} in the repo root, as JSON: { "summary": "the cause in one or two sentences", "confidence": "high" | "medium" | "low", "causes": [{ "title": "...", "detail": "...", "files": [{ "file": "src/...", "line": 12 }] }], "changes": [{ "file": "src/...", "why": "..." }], "evals_added": 0 }. If you change nothing, say why in summary and leave changes empty.
899
+ Don't run the app or tests, and never include secrets or customer data in ${DIAG}.`;
900
+ const rel = (p) => typeof p === "string" && p ? (0, import_node_path.isAbsolute)(p) ? (0, import_node_path.relative)(process.cwd(), p) : p : null;
901
+ const code = await new Promise((resolve) => {
902
+ const p = (0, import_node_child_process.spawn)("claude", ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash(git log:*),Bash(git show:*),Bash(git diff:*)"], { stdio: ["ignore", "pipe", "inherit"], env: childEnv });
903
+ let buf = "";
904
+ p.stdout.on("data", (chunk) => {
905
+ buf += chunk.toString();
906
+ let i;
907
+ while ((i = buf.indexOf("\n")) >= 0) {
908
+ const line = buf.slice(0, i);
909
+ buf = buf.slice(i + 1);
910
+ let ev;
911
+ try {
912
+ ev = JSON.parse(line);
913
+ } catch {
914
+ continue;
915
+ }
916
+ for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
917
+ if (b.type !== "tool_use") continue;
918
+ const f = rel(b.input?.file_path ?? b.input?.path);
919
+ if (!f || f.startsWith("..") || f.endsWith(DIAG)) continue;
920
+ if (b.name === "Read") {
921
+ queue.push({ path: f, tool: "Read" });
922
+ console.log(` ${dim("reading")} ${f}`);
923
+ } else if (b.name === "Edit" || b.name === "Write") {
924
+ phase = "drawing";
925
+ queue.push({ path: f, tool: "Edit" });
926
+ console.log(` ${violet("fixing")} ${f}`);
927
+ }
928
+ }
929
+ }
930
+ });
931
+ p.on("close", (n) => resolve(n ?? 1));
932
+ p.on("error", () => resolve(1));
933
+ });
934
+ clearInterval(tick);
935
+ let diagnosis = null;
936
+ try {
937
+ diagnosis = JSON.parse((0, import_node_fs.readFileSync)(DIAG, "utf8"));
938
+ } catch {
939
+ }
940
+ try {
941
+ (0, import_node_child_process.spawnSync)("rm", ["-f", DIAG]);
942
+ } catch {
943
+ }
944
+ if (code !== 0 || !diagnosis) {
945
+ phase = "failed";
946
+ await flush();
947
+ return fail("Claude Code didn't write a diagnosis. Nothing was changed on Plures.");
948
+ }
949
+ const changed = git("status", "--porcelain").stdout.trim();
950
+ let branch2 = null;
951
+ if (changed) {
952
+ branch2 = `plures/drift-${id.replace(/[^a-z0-9_-]+/gi, "-")}-${Date.now().toString(36)}`;
953
+ for (const a of [["checkout", "-b", branch2], ["add", "-A"], ["commit", "-m", `Fix drift: ${String(packet.label).slice(0, 60)}`]]) {
954
+ const r = git(...a);
955
+ if (r.status !== 0) {
956
+ phase = "failed";
957
+ await flush();
958
+ return fail(`git ${a[0]} failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`);
959
+ }
960
+ }
961
+ if (process.env.GITHUB_ACTIONS) {
962
+ const push = git("push", "-u", "origin", branch2);
963
+ if (push.status !== 0) {
964
+ phase = "failed";
965
+ await flush();
966
+ return fail(`Pushing the branch failed: ${(push.stderr || push.stdout).trim().slice(0, 200)}`);
967
+ }
968
+ }
969
+ }
970
+ phase = "done";
971
+ await flush();
972
+ try {
973
+ const r = await report2({ diagnosis, branch: process.env.GITHUB_ACTIONS ? branch2 : null });
974
+ const d = diagnosis;
975
+ console.log(`
976
+ ${cyan("\u2713")} ${d.summary ?? "Diagnosed."}`);
977
+ if (r.pull_request) console.log(`${cyan("\u2713")} Pull request: ${r.pull_request}`);
978
+ else if (branch2) console.log(`${cyan("\u2713")} The fix is on the branch ${bold(branch2)}. Review it with ${cyan("git show")}, then push it and open a pull request.`);
979
+ } catch (e) {
980
+ die(e instanceof PluresError ? e.message : String(e));
981
+ }
982
+ }
787
983
  async function main() {
788
984
  const [cmd, sub, arg] = argv;
789
985
  if (cmd === "check" && sub === "add") return checkAdd(arg);
986
+ if (cmd === "drift" && sub === "fix") return driftFix(arg);
790
987
  if (cmd !== "map") {
791
988
  console.log(`Plures
792
989
 
@@ -798,6 +995,7 @@ async function main() {
798
995
  npx plures map github on merge it, which turns mapping on
799
996
  npx plures map github run map now on GitHub, and watch it build
800
997
  npx plures check add <id> put a check into your code, on a new branch
998
+ npx plures drift fix <case> find why quality fell and fix it, on a new branch
801
999
 
802
1000
  Docs: https://plures.si/docs`);
803
1001
  return;
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  Plures,
4
4
  PluresError,
5
5
  VERSION
6
- } from "./chunk-Z75ZTXHZ.js";
6
+ } from "./chunk-Z3YGRA2U.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { spawnSync, spawn } from "child_process";
@@ -228,8 +228,7 @@ async function upload(file) {
228
228
  }
229
229
  if (!fancy) console.log(dim(`Sending the map to Plures. No source code is sent.`));
230
230
  let m;
231
- const gitBranch = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" }).stdout?.trim();
232
- const branchName = process.env.PLURES_BRANCH || process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || (gitBranch && gitBranch !== "HEAD" ? gitBranch : "");
231
+ const branchName = currentBranch();
233
232
  try {
234
233
  m = await client().maps.upload(app, map2, branchName ? { branch: branchName } : {});
235
234
  } catch (e) {
@@ -279,6 +278,14 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
279
278
  return;
280
279
  }
281
280
  const before = existsSync(FILE) ? statSync(FILE).mtimeMs : 0;
281
+ const est = await client().maps.estimate(app).catch(() => null);
282
+ if (est) {
283
+ const br = currentBranch();
284
+ const main2 = !br || br === "main" || br === "master";
285
+ if (est.blocked && main2) die(est.blocked);
286
+ if (main2 && est.engine_fee.free) console.log(dim("Plures engine: this app's first map is free."));
287
+ else if (main2 && est.engine_fee.cents) console.log(dim(`Plures engine: about $${(est.engine_fee.cents / 100).toFixed(2)} (${est.engine_fee.steps} steps at ${est.engine_fee.per_step_cents}\xA2), from your credit when the map uploads.`));
288
+ }
282
289
  const childEnv = { ...process.env };
283
290
  const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
284
291
  if (!ownClaude && (process.env.CI || argv.includes("--plures-claude"))) {
@@ -537,9 +544,158 @@ ${cyan("\u2713")} The check is on the branch ${bold(br)}. Review it with ${cyan(
537
544
  die(e instanceof PluresError ? e.message : String(e));
538
545
  }
539
546
  }
547
+ function currentBranch() {
548
+ const gitBranch = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" }).stdout?.trim();
549
+ return process.env.PLURES_BRANCH || process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || (gitBranch && gitBranch !== "HEAD" ? gitBranch : "");
550
+ }
551
+ async function driftFix(id) {
552
+ if (!id) die("npx plures drift fix <case id> [--app <app>]");
553
+ const cl = client();
554
+ const report2 = async (p) => cl.drift.report(app, id, p);
555
+ const fail = async (msg) => {
556
+ await report2({ error: msg }).catch(() => {
557
+ });
558
+ die(msg);
559
+ };
560
+ let packet;
561
+ try {
562
+ packet = await cl.drift.packet(app, id);
563
+ } catch (e) {
564
+ return fail(e instanceof PluresError ? e.message : String(e));
565
+ }
566
+ const est = await cl.maps.estimate(app, "drift", { case: id }).catch(() => null);
567
+ if (est?.blocked) return fail(est.blocked);
568
+ if (!hasClaude()) return fail("Claude Code isn't installed here, so nothing could read the code.");
569
+ const git = (...a) => spawnSync("git", a, { encoding: "utf8" });
570
+ if (git("status", "--porcelain").stdout.trim()) return fail("This repo has changes that aren't committed. Commit or stash them first.");
571
+ const childEnv = { ...process.env };
572
+ const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
573
+ if (!ownClaude && process.env.CI) {
574
+ try {
575
+ const run = await cl.maps.startRun(app, { purpose: "drift", case: id });
576
+ childEnv.ANTHROPIC_BASE_URL = run.base_url;
577
+ childEnv.ANTHROPIC_API_KEY = run.token;
578
+ } catch (e) {
579
+ return fail(e instanceof PluresError ? e.message : String(e));
580
+ }
581
+ }
582
+ const session = packet.session || `drift_${randomBytes(9).toString("base64url").replace(/[-_]/g, "")}`;
583
+ const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
584
+ let queue = [], phase = "reading", sent = "";
585
+ const flush = async () => {
586
+ const events = queue;
587
+ queue = [];
588
+ if (!events.length && sent === phase) return;
589
+ sent = phase;
590
+ await cl.maps.progress(app, { session, phase, source, events }).catch(() => {
591
+ });
592
+ };
593
+ const tick = setInterval(() => {
594
+ void flush();
595
+ }, 900);
596
+ void flush();
597
+ const DIAG = ".plures-drift.json";
598
+ console.log(`${bold("Plures drift")} ${packet.label}
599
+ ${dim("Claude Code reads this repo with the graded examples that got worse. Only file names and the diagnosis go to Plures.")}
600
+ `);
601
+ const prompt = `Quality fell in this codebase's AI assistant, and people graded the examples. Find out why in the code and fix it.
602
+
603
+ What Plures measured (JSON):
604
+ ${JSON.stringify({ ...packet, session: void 0 }, null, 2).slice(0, 6e4)}
605
+
606
+ Do this:
607
+ 1. Read the code behind the steps listed (prompts, the intent router, retrieval, the answer step). Compare the examples that got worse with the ones that were fine before${packet.version_after ? `, and look for what changed in ${packet.version_after}` : ""}. Use git log or the prompt files if that helps.
608
+ 2. Fix the cause in the code with the smallest change that would make the worse examples come out right without breaking the good ones. Prefer prompt, routing and retrieval fixes over rewrites. Don't change unrelated code or formatting.
609
+ 3. If the repo has eval files (like evals/*.jsonl), add cases from the worse examples in the same format, so this can't come back unnoticed.
610
+ 4. Write ${DIAG} in the repo root, as JSON: { "summary": "the cause in one or two sentences", "confidence": "high" | "medium" | "low", "causes": [{ "title": "...", "detail": "...", "files": [{ "file": "src/...", "line": 12 }] }], "changes": [{ "file": "src/...", "why": "..." }], "evals_added": 0 }. If you change nothing, say why in summary and leave changes empty.
611
+ Don't run the app or tests, and never include secrets or customer data in ${DIAG}.`;
612
+ const rel = (p) => typeof p === "string" && p ? isAbsolute(p) ? relative(process.cwd(), p) : p : null;
613
+ const code = await new Promise((resolve) => {
614
+ const p = spawn("claude", ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash(git log:*),Bash(git show:*),Bash(git diff:*)"], { stdio: ["ignore", "pipe", "inherit"], env: childEnv });
615
+ let buf = "";
616
+ p.stdout.on("data", (chunk) => {
617
+ buf += chunk.toString();
618
+ let i;
619
+ while ((i = buf.indexOf("\n")) >= 0) {
620
+ const line = buf.slice(0, i);
621
+ buf = buf.slice(i + 1);
622
+ let ev;
623
+ try {
624
+ ev = JSON.parse(line);
625
+ } catch {
626
+ continue;
627
+ }
628
+ for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
629
+ if (b.type !== "tool_use") continue;
630
+ const f = rel(b.input?.file_path ?? b.input?.path);
631
+ if (!f || f.startsWith("..") || f.endsWith(DIAG)) continue;
632
+ if (b.name === "Read") {
633
+ queue.push({ path: f, tool: "Read" });
634
+ console.log(` ${dim("reading")} ${f}`);
635
+ } else if (b.name === "Edit" || b.name === "Write") {
636
+ phase = "drawing";
637
+ queue.push({ path: f, tool: "Edit" });
638
+ console.log(` ${violet("fixing")} ${f}`);
639
+ }
640
+ }
641
+ }
642
+ });
643
+ p.on("close", (n) => resolve(n ?? 1));
644
+ p.on("error", () => resolve(1));
645
+ });
646
+ clearInterval(tick);
647
+ let diagnosis = null;
648
+ try {
649
+ diagnosis = JSON.parse(readFileSync(DIAG, "utf8"));
650
+ } catch {
651
+ }
652
+ try {
653
+ spawnSync("rm", ["-f", DIAG]);
654
+ } catch {
655
+ }
656
+ if (code !== 0 || !diagnosis) {
657
+ phase = "failed";
658
+ await flush();
659
+ return fail("Claude Code didn't write a diagnosis. Nothing was changed on Plures.");
660
+ }
661
+ const changed = git("status", "--porcelain").stdout.trim();
662
+ let branch2 = null;
663
+ if (changed) {
664
+ branch2 = `plures/drift-${id.replace(/[^a-z0-9_-]+/gi, "-")}-${Date.now().toString(36)}`;
665
+ for (const a of [["checkout", "-b", branch2], ["add", "-A"], ["commit", "-m", `Fix drift: ${String(packet.label).slice(0, 60)}`]]) {
666
+ const r = git(...a);
667
+ if (r.status !== 0) {
668
+ phase = "failed";
669
+ await flush();
670
+ return fail(`git ${a[0]} failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`);
671
+ }
672
+ }
673
+ if (process.env.GITHUB_ACTIONS) {
674
+ const push = git("push", "-u", "origin", branch2);
675
+ if (push.status !== 0) {
676
+ phase = "failed";
677
+ await flush();
678
+ return fail(`Pushing the branch failed: ${(push.stderr || push.stdout).trim().slice(0, 200)}`);
679
+ }
680
+ }
681
+ }
682
+ phase = "done";
683
+ await flush();
684
+ try {
685
+ const r = await report2({ diagnosis, branch: process.env.GITHUB_ACTIONS ? branch2 : null });
686
+ const d = diagnosis;
687
+ console.log(`
688
+ ${cyan("\u2713")} ${d.summary ?? "Diagnosed."}`);
689
+ if (r.pull_request) console.log(`${cyan("\u2713")} Pull request: ${r.pull_request}`);
690
+ else if (branch2) console.log(`${cyan("\u2713")} The fix is on the branch ${bold(branch2)}. Review it with ${cyan("git show")}, then push it and open a pull request.`);
691
+ } catch (e) {
692
+ die(e instanceof PluresError ? e.message : String(e));
693
+ }
694
+ }
540
695
  async function main() {
541
696
  const [cmd, sub, arg] = argv;
542
697
  if (cmd === "check" && sub === "add") return checkAdd(arg);
698
+ if (cmd === "drift" && sub === "fix") return driftFix(arg);
543
699
  if (cmd !== "map") {
544
700
  console.log(`Plures
545
701
 
@@ -551,6 +707,7 @@ async function main() {
551
707
  npx plures map github on merge it, which turns mapping on
552
708
  npx plures map github run map now on GitHub, and watch it build
553
709
  npx plures check add <id> put a check into your code, on a new branch
710
+ npx plures drift fix <case> find why quality fell and fix it, on a new branch
554
711
 
555
712
  Docs: https://plures.si/docs`);
556
713
  return;
package/dist/index.cjs CHANGED
@@ -26,7 +26,7 @@ __export(index_exports, {
26
26
  default: () => index_default
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
- var VERSION = "1.16.0";
29
+ var VERSION = "1.21.0";
30
30
  var PluresError = class extends Error {
31
31
  constructor(message, status, type) {
32
32
  super(message);
@@ -134,8 +134,12 @@ var Plures = class {
134
134
  createKey: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`, opts),
135
135
  keys: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`),
136
136
  revokeKey: (app, id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/map_keys/${encodeURIComponent(id)}`),
137
+ /** What the next map or drift fix will cost before it starts. A map: the Plures engine fee, per step of the app's map
138
+ * (the first map of each app is free), and Claude at cost if Plures runs it. A drift fix: paid by outcome,
139
+ * drift_fix.per_point_cents for each quality point it wins back, at most drift_fix.max_cents, Claude included. blocked says why it can't start. */
140
+ estimate: (app, kind = "map", opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_estimate?for=${kind}${opts.case ? `&case=${encodeURIComponent(opts.case)}` : ""}`),
137
141
  /** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
138
- startRun: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, {}),
142
+ startRun: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, opts),
139
143
  runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
140
144
  /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
141
145
  progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
@@ -196,6 +200,43 @@ var Plures = class {
196
200
  return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
197
201
  }
198
202
  };
203
+ /**
204
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
205
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
206
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
207
+ * it after the reply and don't await it in the user's path.
208
+ */
209
+ turns = {
210
+ log: (t) => this.req("POST", "/api/v1/turns", t),
211
+ /** Grade a turn from your own tools. */
212
+ grade: (id, g) => this.req("POST", `/api/v1/turns/${encodeURIComponent(id)}/grade`, g),
213
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
214
+ insights: (app, opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/turns/insights${opts.days ? `?days=${opts.days}` : ""}`),
215
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
216
+ export: async (app, kind, opts = {}) => {
217
+ const q = new URLSearchParams({ kind, ...opts.since ? { since: opts.since } : {} });
218
+ const r = await fetch(`${this.base}/api/v1/apps/${encodeURIComponent(app)}/turns/export?${q}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
219
+ if (!r.ok) {
220
+ const d = await r.json().catch(() => ({}));
221
+ throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
222
+ }
223
+ return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
224
+ }
225
+ };
226
+ /**
227
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
228
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
229
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
230
+ */
231
+ drift = {
232
+ list: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift`),
233
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
234
+ fix: (app, id) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/fix`, {}),
235
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
236
+ packet: (app, id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/packet`),
237
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
238
+ report: (app, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/report`, p)
239
+ };
199
240
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
200
241
  balance = () => this.req("GET", "/api/v1/balance");
201
242
  /**
package/dist/index.d.cts CHANGED
@@ -1,9 +1,96 @@
1
+ type MapEstimate = {
2
+ object: "map_estimate";
3
+ app: string;
4
+ for: "map" | "drift";
5
+ engine_fee: {
6
+ steps: number | null;
7
+ cents: number | null;
8
+ free: boolean;
9
+ per_step_cents: number;
10
+ basis: string;
11
+ };
12
+ claude_cents_if_plures_runs: number | null;
13
+ /** For a drift fix: paid by outcome, per_point_cents for each quality point won back, at most max_cents. Claude included. */
14
+ drift_fix?: {
15
+ steps: number | null;
16
+ per_point_cents: number;
17
+ lost_points: number | null;
18
+ max_cents: number;
19
+ min_point_cents: number;
20
+ max_point_cents: number;
21
+ cap_cents: number;
22
+ outcome: true;
23
+ free_under_guarantee: boolean;
24
+ };
25
+ blocked: string | null;
26
+ };
1
27
  type Option = {
2
28
  id: string;
3
29
  label: string;
4
30
  };
5
31
  /** "verifiers" asks ID verified people first; "team" goes straight to your own people ($0.05 per internal vote live). */
6
32
  type Route = "verifiers" | "team";
33
+ type DriftDiagnosis = {
34
+ summary: string;
35
+ confidence: "high" | "medium" | "low";
36
+ causes: {
37
+ title: string;
38
+ detail: string;
39
+ files: {
40
+ file: string;
41
+ line?: number | null;
42
+ }[];
43
+ }[];
44
+ changes: {
45
+ file: string;
46
+ why: string;
47
+ }[];
48
+ evals_added: number;
49
+ };
50
+ type DriftCase = {
51
+ object: "drift_case";
52
+ id: string;
53
+ app: string;
54
+ mode: "live" | "test";
55
+ source: "rubric" | "answers";
56
+ key: string;
57
+ label: string;
58
+ status: "open" | "diagnosing" | "diagnosed" | "fix_open" | "verifying" | "recovered" | "dismissed" | "failed";
59
+ baseline: number | null;
60
+ at_open: number | null;
61
+ current: number | null;
62
+ points: number;
63
+ version_before: string | null;
64
+ version_after: string | null;
65
+ diagnosis: DriftDiagnosis | null;
66
+ pr_url: string | null;
67
+ merged_at: string | null;
68
+ post_n: number;
69
+ recovered_at: string | null;
70
+ opened_at: string;
71
+ };
72
+ type TurnParams = {
73
+ /** Which version of your assistant answered, like "prompt v15", so a drop can be traced to a change. */ version?: string;
74
+ /** What the user said or typed. */ question: string;
75
+ /** What the assistant replied ("" if it said nothing). */ answer: string;
76
+ /** Where your router sent it. */ intent?: string;
77
+ /** If the model corrected the router. */ rerouted_to?: string | null;
78
+ /** How it retrieved, like "full", "rerank" or "agentic". */ retrieval?: string;
79
+ /** What it used. [] means it answered with none. */ sources?: {
80
+ id?: string;
81
+ title?: string;
82
+ snippet?: string;
83
+ score?: number;
84
+ }[];
85
+ /** It said it didn't know. Plures guesses from the answer if you leave it out. */ refused?: boolean;
86
+ /** A conversation or call id, so Plures can tell when the user asks again. */ session?: string;
87
+ channel?: "voice" | "text";
88
+ /** A recording people can play, for voice. */ audio_url?: string;
89
+ /** Every intent your router can pick, so graders can choose the right one. */ intents?: string[];
90
+ /** Send this one for review whatever the sample says. */ review?: boolean;
91
+ app?: string;
92
+ metadata?: Record<string, unknown>;
93
+ };
7
94
  type CheckOverrides = {
8
95
  question?: string;
9
96
  options?: string[];
@@ -419,7 +506,7 @@ type WebhookEvent = {
419
506
  created_at: string;
420
507
  data: Task;
421
508
  };
422
- declare const VERSION = "1.16.0";
509
+ declare const VERSION = "1.21.0";
423
510
  type GitHubLink = {
424
511
  object: "github_link";
425
512
  app: string;
@@ -556,12 +643,22 @@ declare class Plures {
556
643
  id: string;
557
644
  deleted: true;
558
645
  }>;
646
+ /** What the next map or drift fix will cost before it starts. A map: the Plures engine fee, per step of the app's map
647
+ * (the first map of each app is free), and Claude at cost if Plures runs it. A drift fix: paid by outcome,
648
+ * drift_fix.per_point_cents for each quality point it wins back, at most drift_fix.max_cents, Claude included. blocked says why it can't start. */
649
+ estimate: (app: string, kind?: "map" | "drift", opts?: {
650
+ case?: string;
651
+ }) => Promise<MapEstimate>;
559
652
  /** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
560
- startRun: (app: string) => Promise<{
653
+ startRun: (app: string, opts?: {
654
+ purpose?: "map" | "drift";
655
+ case?: string;
656
+ }) => Promise<{
561
657
  object: "map_run";
562
658
  id: string;
563
659
  app: string;
564
660
  free: boolean;
661
+ purpose: "map" | "drift";
565
662
  max_cents: number;
566
663
  token: string;
567
664
  base_url: string;
@@ -733,6 +830,83 @@ declare class Plures {
733
830
  metadata: Record<string, unknown>;
734
831
  }[]>;
735
832
  };
833
+ /**
834
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
835
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
836
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
837
+ * it after the reply and don't await it in the user's path.
838
+ */
839
+ turns: {
840
+ log: (t: TurnParams) => Promise<{
841
+ object: "turn";
842
+ id: string;
843
+ app: string;
844
+ picked: boolean;
845
+ reason: string | null;
846
+ signals: string[];
847
+ asked_again_of?: string;
848
+ }>;
849
+ /** Grade a turn from your own tools. */
850
+ grade: (id: string, g: {
851
+ intent_ok?: boolean;
852
+ right_intent?: string;
853
+ grounded?: "yes" | "partly" | "no" | "should_refuse" | "should_answer";
854
+ complete?: boolean;
855
+ missed?: string;
856
+ fact?: string;
857
+ doc?: string;
858
+ note?: string;
859
+ email?: string;
860
+ }) => Promise<{
861
+ object: "turn_grade";
862
+ turn: string;
863
+ }>;
864
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
865
+ insights: (app: string, opts?: {
866
+ days?: number;
867
+ }) => Promise<Record<string, unknown>>;
868
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
869
+ export: (app: string, kind: "routes" | "retrieval" | "qa" | "missed", opts?: {
870
+ since?: string;
871
+ }) => Promise<Record<string, unknown>[]>;
872
+ };
873
+ /**
874
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
875
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
876
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
877
+ */
878
+ drift: {
879
+ list: (app: string) => Promise<{
880
+ object: "list";
881
+ data: DriftCase[];
882
+ }>;
883
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
884
+ fix: (app: string, id: string) => Promise<{
885
+ object: "drift_fix";
886
+ case: string;
887
+ status: string;
888
+ session: string;
889
+ }>;
890
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
891
+ packet: (app: string, id: string) => Promise<Record<string, unknown> & {
892
+ case: string;
893
+ label: string;
894
+ session: string | null;
895
+ kind: "rubric" | "answers";
896
+ }>;
897
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
898
+ report: (app: string, id: string, p: {
899
+ diagnosis: DriftDiagnosis;
900
+ branch?: string | null;
901
+ } | {
902
+ error: string;
903
+ }) => Promise<{
904
+ object: "drift_fix";
905
+ case: string;
906
+ status: string;
907
+ pull_request?: string;
908
+ }>;
909
+ };
736
910
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
737
911
  balance: () => Promise<Balance>;
738
912
  /**
@@ -760,4 +934,4 @@ declare class Plures {
760
934
  static verifyWebhook(rawBody: string, signatureHeader: string | null, secret: string, toleranceSec?: number): Promise<WebhookEvent>;
761
935
  }
762
936
 
763
- export { type AgentMap, type App, type Balance, type CheckOverrides, type CheckResult, type Consensus, type CreateTask, type Escalation, type GitHubLink, type MapChanges, type MapCheckpoint, type MapDraft, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };
937
+ export { type AgentMap, type App, type Balance, type CheckOverrides, type CheckResult, type Consensus, type CreateTask, type DriftCase, type DriftDiagnosis, type Escalation, type GitHubLink, type MapChanges, type MapCheckpoint, type MapDraft, type MapEstimate, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, type TurnParams, VERSION, type WebhookEvent, Plures as default };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,96 @@
1
+ type MapEstimate = {
2
+ object: "map_estimate";
3
+ app: string;
4
+ for: "map" | "drift";
5
+ engine_fee: {
6
+ steps: number | null;
7
+ cents: number | null;
8
+ free: boolean;
9
+ per_step_cents: number;
10
+ basis: string;
11
+ };
12
+ claude_cents_if_plures_runs: number | null;
13
+ /** For a drift fix: paid by outcome, per_point_cents for each quality point won back, at most max_cents. Claude included. */
14
+ drift_fix?: {
15
+ steps: number | null;
16
+ per_point_cents: number;
17
+ lost_points: number | null;
18
+ max_cents: number;
19
+ min_point_cents: number;
20
+ max_point_cents: number;
21
+ cap_cents: number;
22
+ outcome: true;
23
+ free_under_guarantee: boolean;
24
+ };
25
+ blocked: string | null;
26
+ };
1
27
  type Option = {
2
28
  id: string;
3
29
  label: string;
4
30
  };
5
31
  /** "verifiers" asks ID verified people first; "team" goes straight to your own people ($0.05 per internal vote live). */
6
32
  type Route = "verifiers" | "team";
33
+ type DriftDiagnosis = {
34
+ summary: string;
35
+ confidence: "high" | "medium" | "low";
36
+ causes: {
37
+ title: string;
38
+ detail: string;
39
+ files: {
40
+ file: string;
41
+ line?: number | null;
42
+ }[];
43
+ }[];
44
+ changes: {
45
+ file: string;
46
+ why: string;
47
+ }[];
48
+ evals_added: number;
49
+ };
50
+ type DriftCase = {
51
+ object: "drift_case";
52
+ id: string;
53
+ app: string;
54
+ mode: "live" | "test";
55
+ source: "rubric" | "answers";
56
+ key: string;
57
+ label: string;
58
+ status: "open" | "diagnosing" | "diagnosed" | "fix_open" | "verifying" | "recovered" | "dismissed" | "failed";
59
+ baseline: number | null;
60
+ at_open: number | null;
61
+ current: number | null;
62
+ points: number;
63
+ version_before: string | null;
64
+ version_after: string | null;
65
+ diagnosis: DriftDiagnosis | null;
66
+ pr_url: string | null;
67
+ merged_at: string | null;
68
+ post_n: number;
69
+ recovered_at: string | null;
70
+ opened_at: string;
71
+ };
72
+ type TurnParams = {
73
+ /** Which version of your assistant answered, like "prompt v15", so a drop can be traced to a change. */ version?: string;
74
+ /** What the user said or typed. */ question: string;
75
+ /** What the assistant replied ("" if it said nothing). */ answer: string;
76
+ /** Where your router sent it. */ intent?: string;
77
+ /** If the model corrected the router. */ rerouted_to?: string | null;
78
+ /** How it retrieved, like "full", "rerank" or "agentic". */ retrieval?: string;
79
+ /** What it used. [] means it answered with none. */ sources?: {
80
+ id?: string;
81
+ title?: string;
82
+ snippet?: string;
83
+ score?: number;
84
+ }[];
85
+ /** It said it didn't know. Plures guesses from the answer if you leave it out. */ refused?: boolean;
86
+ /** A conversation or call id, so Plures can tell when the user asks again. */ session?: string;
87
+ channel?: "voice" | "text";
88
+ /** A recording people can play, for voice. */ audio_url?: string;
89
+ /** Every intent your router can pick, so graders can choose the right one. */ intents?: string[];
90
+ /** Send this one for review whatever the sample says. */ review?: boolean;
91
+ app?: string;
92
+ metadata?: Record<string, unknown>;
93
+ };
7
94
  type CheckOverrides = {
8
95
  question?: string;
9
96
  options?: string[];
@@ -419,7 +506,7 @@ type WebhookEvent = {
419
506
  created_at: string;
420
507
  data: Task;
421
508
  };
422
- declare const VERSION = "1.16.0";
509
+ declare const VERSION = "1.21.0";
423
510
  type GitHubLink = {
424
511
  object: "github_link";
425
512
  app: string;
@@ -556,12 +643,22 @@ declare class Plures {
556
643
  id: string;
557
644
  deleted: true;
558
645
  }>;
646
+ /** What the next map or drift fix will cost before it starts. A map: the Plures engine fee, per step of the app's map
647
+ * (the first map of each app is free), and Claude at cost if Plures runs it. A drift fix: paid by outcome,
648
+ * drift_fix.per_point_cents for each quality point it wins back, at most drift_fix.max_cents, Claude included. blocked says why it can't start. */
649
+ estimate: (app: string, kind?: "map" | "drift", opts?: {
650
+ case?: string;
651
+ }) => Promise<MapEstimate>;
559
652
  /** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
560
- startRun: (app: string) => Promise<{
653
+ startRun: (app: string, opts?: {
654
+ purpose?: "map" | "drift";
655
+ case?: string;
656
+ }) => Promise<{
561
657
  object: "map_run";
562
658
  id: string;
563
659
  app: string;
564
660
  free: boolean;
661
+ purpose: "map" | "drift";
565
662
  max_cents: number;
566
663
  token: string;
567
664
  base_url: string;
@@ -733,6 +830,83 @@ declare class Plures {
733
830
  metadata: Record<string, unknown>;
734
831
  }[]>;
735
832
  };
833
+ /**
834
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
835
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
836
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
837
+ * it after the reply and don't await it in the user's path.
838
+ */
839
+ turns: {
840
+ log: (t: TurnParams) => Promise<{
841
+ object: "turn";
842
+ id: string;
843
+ app: string;
844
+ picked: boolean;
845
+ reason: string | null;
846
+ signals: string[];
847
+ asked_again_of?: string;
848
+ }>;
849
+ /** Grade a turn from your own tools. */
850
+ grade: (id: string, g: {
851
+ intent_ok?: boolean;
852
+ right_intent?: string;
853
+ grounded?: "yes" | "partly" | "no" | "should_refuse" | "should_answer";
854
+ complete?: boolean;
855
+ missed?: string;
856
+ fact?: string;
857
+ doc?: string;
858
+ note?: string;
859
+ email?: string;
860
+ }) => Promise<{
861
+ object: "turn_grade";
862
+ turn: string;
863
+ }>;
864
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
865
+ insights: (app: string, opts?: {
866
+ days?: number;
867
+ }) => Promise<Record<string, unknown>>;
868
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
869
+ export: (app: string, kind: "routes" | "retrieval" | "qa" | "missed", opts?: {
870
+ since?: string;
871
+ }) => Promise<Record<string, unknown>[]>;
872
+ };
873
+ /**
874
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
875
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
876
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
877
+ */
878
+ drift: {
879
+ list: (app: string) => Promise<{
880
+ object: "list";
881
+ data: DriftCase[];
882
+ }>;
883
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
884
+ fix: (app: string, id: string) => Promise<{
885
+ object: "drift_fix";
886
+ case: string;
887
+ status: string;
888
+ session: string;
889
+ }>;
890
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
891
+ packet: (app: string, id: string) => Promise<Record<string, unknown> & {
892
+ case: string;
893
+ label: string;
894
+ session: string | null;
895
+ kind: "rubric" | "answers";
896
+ }>;
897
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
898
+ report: (app: string, id: string, p: {
899
+ diagnosis: DriftDiagnosis;
900
+ branch?: string | null;
901
+ } | {
902
+ error: string;
903
+ }) => Promise<{
904
+ object: "drift_fix";
905
+ case: string;
906
+ status: string;
907
+ pull_request?: string;
908
+ }>;
909
+ };
736
910
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
737
911
  balance: () => Promise<Balance>;
738
912
  /**
@@ -760,4 +934,4 @@ declare class Plures {
760
934
  static verifyWebhook(rawBody: string, signatureHeader: string | null, secret: string, toleranceSec?: number): Promise<WebhookEvent>;
761
935
  }
762
936
 
763
- export { type AgentMap, type App, type Balance, type CheckOverrides, type CheckResult, type Consensus, type CreateTask, type Escalation, type GitHubLink, type MapChanges, type MapCheckpoint, type MapDraft, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };
937
+ export { type AgentMap, type App, type Balance, type CheckOverrides, type CheckResult, type Consensus, type CreateTask, type DriftCase, type DriftDiagnosis, type Escalation, type GitHubLink, type MapChanges, type MapCheckpoint, type MapDraft, type MapEstimate, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, type TurnParams, VERSION, type WebhookEvent, Plures as default };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PluresError,
4
4
  VERSION,
5
5
  index_default
6
- } from "./chunk-Z75ZTXHZ.js";
6
+ } from "./chunk-Z3YGRA2U.js";
7
7
  export {
8
8
  Plures,
9
9
  PluresError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plures",
3
- "version": "1.16.0",
3
+ "version": "1.21.0",
4
4
  "description": "Human judgment as an API for AI agents. Send a decision to ID verified people and get their consensus back.",
5
5
  "keywords": [
6
6
  "human-in-the-loop",