plures 1.15.0 → 1.18.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/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  Plures,
4
4
  PluresError,
5
5
  VERSION
6
- } from "./chunk-JSR4MDEK.js";
6
+ } from "./chunk-RKIJTDS3.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { spawnSync, spawn } from "child_process";
@@ -228,11 +228,19 @@ 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
233
  try {
232
- m = await client().maps.upload(app, map2);
234
+ m = await client().maps.upload(app, map2, branchName ? { branch: branchName } : {});
233
235
  } catch (e) {
234
236
  die(e instanceof PluresError ? `Plures said: ${e.message}` : String(e));
235
237
  }
238
+ if (m.branch) console.log(`${bold(`Branch ${m.branch}`)}: compared with the main map${m.main_version ? ` (version ${m.main_version})` : ""}, not replacing it.${m.pull_request ? ` Noted on ${m.pull_request}` : ""}`);
239
+ if (m.changes && (m.changes.added.length || m.changes.removed.length || m.changes.renamed.length || m.changes.risky.length)) {
240
+ const c2 = m.changes;
241
+ console.log(`${dim("Changes:")} ${[c2.added.length ? `${c2.added.length} new` : "", c2.renamed.length ? `${c2.renamed.length} renamed, checks kept` : "", c2.removed.length ? `${c2.removed.length} removed` : ""].filter(Boolean).join(", ") || "no new actions"}`);
242
+ for (const r of c2.risky) console.log(` ${coral("no check")} ${r.label} ${dim(`score ${r.score}: ${r.why.join("; ")}`)}`);
243
+ }
236
244
  if (fancy) drawMap(m, m.url ?? `${base}/dashboard/apps/${m.app}/map`);
237
245
  else report(m);
238
246
  }
@@ -451,8 +459,230 @@ It adds one workflow file. Review it, then turn mapping on with ${cyan("npx plur
451
459
  throw e;
452
460
  }
453
461
  }
462
+ async function checkAdd(id) {
463
+ if (!id) die("npx plures check add <check id> [--app <app>]");
464
+ const cl = client();
465
+ const report2 = async (p) => {
466
+ if (process.env.GITHUB_ACTIONS) await cl.maps.checkPr(app, id, p).catch(() => {
467
+ });
468
+ };
469
+ const fail = async (msg) => {
470
+ await report2({ error: msg });
471
+ die(msg);
472
+ };
473
+ let m;
474
+ try {
475
+ m = await cl.maps.retrieve(app);
476
+ } catch (e) {
477
+ return fail(e instanceof PluresError ? e.message : String(e));
478
+ }
479
+ const cp0 = m.checkpoints.find((x) => x.id === id || x.category === id);
480
+ const cp = cp0;
481
+ if (!cp0) return fail(`No check called ${id} on ${app}'s map.`);
482
+ const action = m.nodes.find((n) => n.id === cp.at);
483
+ const cid = cp.id ?? id;
484
+ if (!cp.code) return fail(`The check ${cid} has no drafted question yet. Edit it on Plures first.`);
485
+ if (!hasClaude()) return fail("Claude Code isn't installed here, so nothing could add the line. Copy it from the check's page instead.");
486
+ const git = (...a) => spawnSync("git", a, { encoding: "utf8" });
487
+ if (git("status", "--porcelain").stdout.trim()) return fail("This repo has changes that aren't committed. Commit or stash them first.");
488
+ const childEnv = { ...process.env };
489
+ const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
490
+ if (!ownClaude && process.env.CI) {
491
+ try {
492
+ const run = await cl.maps.startRun(app);
493
+ childEnv.ANTHROPIC_BASE_URL = run.base_url;
494
+ childEnv.ANTHROPIC_API_KEY = run.token;
495
+ } catch (e) {
496
+ return fail(e instanceof PluresError ? e.message : String(e));
497
+ }
498
+ }
499
+ const where = action?.file ? `${action.file}${action.line ? ` near line ${action.line}` : ""}` : "the code";
500
+ console.log(`Adding the check ${bold(cid)} before ${bold(action?.label ?? cp.at)}, in ${where}.`);
501
+ const prompt = `Add one Plures human check to this codebase. Change as little as possible.
502
+
503
+ Where: just before the step "${action?.label ?? cp.at}"${action?.via ? ` (it goes through ${action.via})` : ""}, in ${where}. Find the exact place; the line may have moved.
504
+
505
+ What to add, adapted to the file's language and style (for Python: \`check = plures.check("${cid}", fields)\` and \`check.answer\`):
506
+
507
+ ${cp.code}
508
+
509
+ - fields: an object with the values a person needs to decide, taken from variables already in scope at that point${cp.draft?.show?.length ? `. Use these field names: ${cp.draft.show.join(", ")}` : ""}. Never pass secrets, card numbers or passwords.
510
+ - If the answer isn't the first option, the agent must not do the step: return, skip, or raise, whichever fits the code around it.
511
+ - If the file has no Plures client yet, add one: JavaScript or TypeScript \`import { Plures } from "plures"\` and \`const plures = new Plures(process.env.PLURES_API_KEY${app !== "default" ? `, { app: "${app}" }` : ""})\`; Python \`from plures import Plures\` and \`plures = Plures(os.environ["PLURES_API_KEY"]${app !== "default" ? `, app="${app}"` : ""})\`. Keep to how the project sets up other clients.
512
+ - If the package isn't a dependency yet, add "plures" to package.json dependencies, or plures to requirements.txt or pyproject.toml, whichever the project uses. Don't run installers.
513
+ - Don't change anything else, don't reformat, don't add tests, and don't run commands.`;
514
+ const code = await new Promise((resolve) => {
515
+ const p = spawn("claude", ["-p", prompt, "--allowedTools", "Read,Grep,Glob,Edit,Write"], { stdio: ["ignore", "inherit", "inherit"], env: childEnv });
516
+ p.on("close", (n) => resolve(n ?? 1));
517
+ p.on("error", () => resolve(1));
518
+ });
519
+ const changed = git("status", "--porcelain").stdout.trim();
520
+ if (code !== 0 || !changed) return fail("Claude Code didn't change any files, so there's nothing to review. Copy the line from the check's page instead.");
521
+ const br = `plures/check-${cid.replace(/[^a-z0-9_-]+/gi, "-")}-${Date.now().toString(36)}`;
522
+ for (const a of [["checkout", "-b", br], ["add", "-A"], ["commit", "-m", `Add the Plures check ${cid} before ${(action?.label ?? cp.at).toLowerCase()}`]]) {
523
+ const r = git(...a);
524
+ if (r.status !== 0) return fail(`git ${a[0]} failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`);
525
+ }
526
+ if (!process.env.GITHUB_ACTIONS) {
527
+ console.log(`
528
+ ${cyan("\u2713")} The check is on the branch ${bold(br)}. Review it with ${cyan("git show")}, then push it and open a pull request.`);
529
+ return;
530
+ }
531
+ const push = git("push", "-u", "origin", br);
532
+ if (push.status !== 0) return fail(`Pushing the branch failed: ${(push.stderr || push.stdout).trim().slice(0, 200)}`);
533
+ try {
534
+ const r = await cl.maps.checkPr(app, cid, { branch: br });
535
+ console.log(`${cyan("\u2713")} Pull request: ${r.pull_request ?? "opened"}`);
536
+ } catch (e) {
537
+ die(e instanceof PluresError ? e.message : String(e));
538
+ }
539
+ }
540
+ async function driftFix(id) {
541
+ if (!id) die("npx plures drift fix <case id> [--app <app>]");
542
+ const cl = client();
543
+ const report2 = async (p) => cl.drift.report(app, id, p);
544
+ const fail = async (msg) => {
545
+ await report2({ error: msg }).catch(() => {
546
+ });
547
+ die(msg);
548
+ };
549
+ let packet;
550
+ try {
551
+ packet = await cl.drift.packet(app, id);
552
+ } catch (e) {
553
+ return fail(e instanceof PluresError ? e.message : String(e));
554
+ }
555
+ if (!hasClaude()) return fail("Claude Code isn't installed here, so nothing could read the code.");
556
+ const git = (...a) => spawnSync("git", a, { encoding: "utf8" });
557
+ if (git("status", "--porcelain").stdout.trim()) return fail("This repo has changes that aren't committed. Commit or stash them first.");
558
+ const childEnv = { ...process.env };
559
+ const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
560
+ if (!ownClaude && process.env.CI) {
561
+ try {
562
+ const run = await cl.maps.startRun(app);
563
+ childEnv.ANTHROPIC_BASE_URL = run.base_url;
564
+ childEnv.ANTHROPIC_API_KEY = run.token;
565
+ } catch (e) {
566
+ return fail(e instanceof PluresError ? e.message : String(e));
567
+ }
568
+ }
569
+ const session = packet.session || `drift_${randomBytes(9).toString("base64url").replace(/[-_]/g, "")}`;
570
+ const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
571
+ let queue = [], phase = "reading", sent = "";
572
+ const flush = async () => {
573
+ const events = queue;
574
+ queue = [];
575
+ if (!events.length && sent === phase) return;
576
+ sent = phase;
577
+ await cl.maps.progress(app, { session, phase, source, events }).catch(() => {
578
+ });
579
+ };
580
+ const tick = setInterval(() => {
581
+ void flush();
582
+ }, 900);
583
+ void flush();
584
+ const DIAG = ".plures-drift.json";
585
+ console.log(`${bold("Plures drift")} ${packet.label}
586
+ ${dim("Claude Code reads this repo with the graded examples that got worse. Only file names and the diagnosis go to Plures.")}
587
+ `);
588
+ const prompt = `Quality fell in this codebase's AI assistant, and people graded the examples. Find out why in the code and fix it.
589
+
590
+ What Plures measured (JSON):
591
+ ${JSON.stringify({ ...packet, session: void 0 }, null, 2).slice(0, 6e4)}
592
+
593
+ Do this:
594
+ 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.
595
+ 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.
596
+ 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.
597
+ 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.
598
+ Don't run the app or tests, and never include secrets or customer data in ${DIAG}.`;
599
+ const rel = (p) => typeof p === "string" && p ? isAbsolute(p) ? relative(process.cwd(), p) : p : null;
600
+ const code = await new Promise((resolve) => {
601
+ 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 });
602
+ let buf = "";
603
+ p.stdout.on("data", (chunk) => {
604
+ buf += chunk.toString();
605
+ let i;
606
+ while ((i = buf.indexOf("\n")) >= 0) {
607
+ const line = buf.slice(0, i);
608
+ buf = buf.slice(i + 1);
609
+ let ev;
610
+ try {
611
+ ev = JSON.parse(line);
612
+ } catch {
613
+ continue;
614
+ }
615
+ for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
616
+ if (b.type !== "tool_use") continue;
617
+ const f = rel(b.input?.file_path ?? b.input?.path);
618
+ if (!f || f.startsWith("..") || f.endsWith(DIAG)) continue;
619
+ if (b.name === "Read") {
620
+ queue.push({ path: f, tool: "Read" });
621
+ console.log(` ${dim("reading")} ${f}`);
622
+ } else if (b.name === "Edit" || b.name === "Write") {
623
+ phase = "drawing";
624
+ queue.push({ path: f, tool: "Edit" });
625
+ console.log(` ${violet("fixing")} ${f}`);
626
+ }
627
+ }
628
+ }
629
+ });
630
+ p.on("close", (n) => resolve(n ?? 1));
631
+ p.on("error", () => resolve(1));
632
+ });
633
+ clearInterval(tick);
634
+ let diagnosis = null;
635
+ try {
636
+ diagnosis = JSON.parse(readFileSync(DIAG, "utf8"));
637
+ } catch {
638
+ }
639
+ try {
640
+ spawnSync("rm", ["-f", DIAG]);
641
+ } catch {
642
+ }
643
+ if (code !== 0 || !diagnosis) {
644
+ phase = "failed";
645
+ await flush();
646
+ return fail("Claude Code didn't write a diagnosis. Nothing was changed on Plures.");
647
+ }
648
+ const changed = git("status", "--porcelain").stdout.trim();
649
+ let branch2 = null;
650
+ if (changed) {
651
+ branch2 = `plures/drift-${id.replace(/[^a-z0-9_-]+/gi, "-")}-${Date.now().toString(36)}`;
652
+ for (const a of [["checkout", "-b", branch2], ["add", "-A"], ["commit", "-m", `Fix drift: ${String(packet.label).slice(0, 60)}`]]) {
653
+ const r = git(...a);
654
+ if (r.status !== 0) {
655
+ phase = "failed";
656
+ await flush();
657
+ return fail(`git ${a[0]} failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`);
658
+ }
659
+ }
660
+ if (process.env.GITHUB_ACTIONS) {
661
+ const push = git("push", "-u", "origin", branch2);
662
+ if (push.status !== 0) {
663
+ phase = "failed";
664
+ await flush();
665
+ return fail(`Pushing the branch failed: ${(push.stderr || push.stdout).trim().slice(0, 200)}`);
666
+ }
667
+ }
668
+ }
669
+ phase = "done";
670
+ await flush();
671
+ try {
672
+ const r = await report2({ diagnosis, branch: process.env.GITHUB_ACTIONS ? branch2 : null });
673
+ const d = diagnosis;
674
+ console.log(`
675
+ ${cyan("\u2713")} ${d.summary ?? "Diagnosed."}`);
676
+ if (r.pull_request) console.log(`${cyan("\u2713")} Pull request: ${r.pull_request}`);
677
+ 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.`);
678
+ } catch (e) {
679
+ die(e instanceof PluresError ? e.message : String(e));
680
+ }
681
+ }
454
682
  async function main() {
455
683
  const [cmd, sub, arg] = argv;
684
+ if (cmd === "check" && sub === "add") return checkAdd(arg);
685
+ if (cmd === "drift" && sub === "fix") return driftFix(arg);
456
686
  if (cmd !== "map") {
457
687
  console.log(`Plures
458
688
 
@@ -463,6 +693,8 @@ async function main() {
463
693
  npx plures map github connect let Plures add that Action by pull request
464
694
  npx plures map github on merge it, which turns mapping on
465
695
  npx plures map github run map now on GitHub, and watch it build
696
+ npx plures check add <id> put a check into your code, on a new branch
697
+ npx plures drift fix <case> find why quality fell and fix it, on a new branch
466
698
 
467
699
  Docs: https://plures.si/docs`);
468
700
  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.15.0";
29
+ var VERSION = "1.18.0";
30
30
  var PluresError = class extends Error {
31
31
  constructor(message, status, type) {
32
32
  super(message);
@@ -127,7 +127,8 @@ var Plures = class {
127
127
  },
128
128
  retrieve: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map`),
129
129
  /** Uploads a new version. Checks you already added or dismissed keep that. */
130
- upload: (app, map) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app)}/map`, map),
130
+ /** A branch other than the main one gets its own map, compared with main, and noted on its pull request. */
131
+ upload: (app, map, opts = {}) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app)}/map`, map, opts.branch ? { "plures-branch": opts.branch } : {}),
131
132
  setCheckpoint: (app, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app)}/map/checkpoints/${encodeURIComponent(id)}`, { status }),
132
133
  /** A key that can only read and upload this app's map, for CI. The key is in the response once. */
133
134
  createKey: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`, opts),
@@ -138,6 +139,8 @@ var Plures = class {
138
139
  runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
139
140
  /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
140
141
  progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
142
+ /** After npx plures check add pushed a branch with the check in it: Plures opens the pull request. Pass error instead if adding it failed. */
143
+ checkPr: (app, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/checks/${encodeURIComponent(id)}/pr`, p),
141
144
  /** The GitHub Actions workflow that maps a repo on every push to its main branch. */
142
145
  workflow: async (app, branch = "main") => {
143
146
  const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app)}&branch=${encodeURIComponent(branch)}`);
@@ -193,8 +196,71 @@ var Plures = class {
193
196
  return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
194
197
  }
195
198
  };
199
+ /**
200
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
201
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
202
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
203
+ * it after the reply and don't await it in the user's path.
204
+ */
205
+ turns = {
206
+ log: (t) => this.req("POST", "/api/v1/turns", t),
207
+ /** Grade a turn from your own tools. */
208
+ grade: (id, g) => this.req("POST", `/api/v1/turns/${encodeURIComponent(id)}/grade`, g),
209
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
210
+ insights: (app, opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/turns/insights${opts.days ? `?days=${opts.days}` : ""}`),
211
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
212
+ export: async (app, kind, opts = {}) => {
213
+ const q = new URLSearchParams({ kind, ...opts.since ? { since: opts.since } : {} });
214
+ const r = await fetch(`${this.base}/api/v1/apps/${encodeURIComponent(app)}/turns/export?${q}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
215
+ if (!r.ok) {
216
+ const d = await r.json().catch(() => ({}));
217
+ throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
218
+ }
219
+ return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
220
+ }
221
+ };
222
+ /**
223
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
224
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
225
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
226
+ */
227
+ drift = {
228
+ list: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift`),
229
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
230
+ fix: (app, id) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/fix`, {}),
231
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
232
+ packet: (app, id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/packet`),
233
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
234
+ report: (app, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/report`, p)
235
+ };
196
236
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
197
237
  balance = () => this.req("GET", "/api/v1/balance");
238
+ /**
239
+ * Runs a check from your app's map and waits for the decision. Send the data by name; the check
240
+ * saved on Plures supplies the question, options, guidance, which fields people see (the rest
241
+ * never leave Plures), who decides, how often and what happens on a timeout. Any of those can be
242
+ * overridden for this call. `answer` is the option to act on: what people chose, the first option
243
+ * when the check wasn't needed (a sample or a condition) or timed out set to go ahead, and null
244
+ * when it timed out set to hold.
245
+ */
246
+ async check(id, data = {}, overrides = {}, opts = {}) {
247
+ const t = await this.req("POST", `/api/v1/checks/${encodeURIComponent(id)}`, { data, ...overrides }, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {});
248
+ const final = t.status === "pending" ? await this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
249
+ const opt = (k) => final.options.find((o) => o.id === k)?.label ?? k ?? null;
250
+ const first = final.options[0]?.label ?? null;
251
+ const timedOut = final.status === "expired" || final.status === "pending";
252
+ const answer = final.status === "completed" ? opt(final.result?.answer) : final.status === "skipped" ? first : timedOut && t.on_timeout === "proceed" ? first : null;
253
+ return {
254
+ ...final,
255
+ check: t.check,
256
+ on_timeout: t.on_timeout,
257
+ kept_private: t.kept_private ?? [],
258
+ skipped_because: t.skipped_because ?? null,
259
+ answer,
260
+ checked: final.status !== "skipped",
261
+ timed_out: timedOut
262
+ };
263
+ }
198
264
  /** Creates a task and waits for people to answer it. */
199
265
  async verify(params, opts = {}) {
200
266
  const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
package/dist/index.d.cts CHANGED
@@ -4,6 +4,93 @@ type Option = {
4
4
  };
5
5
  /** "verifiers" asks ID verified people first; "team" goes straight to your own people ($0.05 per internal vote live). */
6
6
  type Route = "verifiers" | "team";
7
+ type DriftDiagnosis = {
8
+ summary: string;
9
+ confidence: "high" | "medium" | "low";
10
+ causes: {
11
+ title: string;
12
+ detail: string;
13
+ files: {
14
+ file: string;
15
+ line?: number | null;
16
+ }[];
17
+ }[];
18
+ changes: {
19
+ file: string;
20
+ why: string;
21
+ }[];
22
+ evals_added: number;
23
+ };
24
+ type DriftCase = {
25
+ object: "drift_case";
26
+ id: string;
27
+ app: string;
28
+ mode: "live" | "test";
29
+ source: "rubric" | "answers";
30
+ key: string;
31
+ label: string;
32
+ status: "open" | "diagnosing" | "diagnosed" | "fix_open" | "verifying" | "recovered" | "dismissed" | "failed";
33
+ baseline: number | null;
34
+ at_open: number | null;
35
+ current: number | null;
36
+ points: number;
37
+ version_before: string | null;
38
+ version_after: string | null;
39
+ diagnosis: DriftDiagnosis | null;
40
+ pr_url: string | null;
41
+ merged_at: string | null;
42
+ post_n: number;
43
+ recovered_at: string | null;
44
+ opened_at: string;
45
+ };
46
+ type TurnParams = {
47
+ /** Which version of your assistant answered, like "prompt v15", so a drop can be traced to a change. */ version?: string;
48
+ /** What the user said or typed. */ question: string;
49
+ /** What the assistant replied ("" if it said nothing). */ answer: string;
50
+ /** Where your router sent it. */ intent?: string;
51
+ /** If the model corrected the router. */ rerouted_to?: string | null;
52
+ /** How it retrieved, like "full", "rerank" or "agentic". */ retrieval?: string;
53
+ /** What it used. [] means it answered with none. */ sources?: {
54
+ id?: string;
55
+ title?: string;
56
+ snippet?: string;
57
+ score?: number;
58
+ }[];
59
+ /** It said it didn't know. Plures guesses from the answer if you leave it out. */ refused?: boolean;
60
+ /** A conversation or call id, so Plures can tell when the user asks again. */ session?: string;
61
+ channel?: "voice" | "text";
62
+ /** A recording people can play, for voice. */ audio_url?: string;
63
+ /** Every intent your router can pick, so graders can choose the right one. */ intents?: string[];
64
+ /** Send this one for review whatever the sample says. */ review?: boolean;
65
+ app?: string;
66
+ metadata?: Record<string, unknown>;
67
+ };
68
+ type CheckOverrides = {
69
+ question?: string;
70
+ options?: string[];
71
+ instructions?: string;
72
+ verifiers?: number;
73
+ team?: string;
74
+ route?: "verifiers" | "team";
75
+ /** Check this one even if the sample or a condition would skip it. */ always?: boolean;
76
+ /** Share of calls people check, 0 to 1, for this call. */ sample?: number;
77
+ /** Seconds people have to answer. */ timeout?: number;
78
+ metadata?: Record<string, unknown>;
79
+ webhook_url?: string;
80
+ };
81
+ type CheckExtra = {
82
+ check: string;
83
+ on_timeout: "proceed" | "hold";
84
+ kept_private?: string[];
85
+ skipped_because?: string;
86
+ };
87
+ type CheckResult = Task & Omit<CheckExtra, "kept_private" | "skipped_because"> & {
88
+ /** The option to act on, by label. */ answer: string | null;
89
+ /** false when the check wasn't needed this time (a sample or a condition). */ checked: boolean;
90
+ timed_out: boolean;
91
+ kept_private: string[];
92
+ skipped_because: string | null;
93
+ };
7
94
  type TaskStatus = "pending" | "completed" | "expired" | "canceled" | "skipped";
8
95
  type Result = {
9
96
  answer: string | null;
@@ -253,6 +340,30 @@ type MapKey = {
253
340
  created_at: string;
254
341
  last_used_at: string | null;
255
342
  };
343
+ type MapChanges = {
344
+ since: number | null;
345
+ added: {
346
+ id: string;
347
+ label: string;
348
+ score: number | null;
349
+ checked: boolean;
350
+ }[];
351
+ removed: {
352
+ id: string;
353
+ label: string;
354
+ }[];
355
+ renamed: {
356
+ from: string;
357
+ to: string;
358
+ label: string;
359
+ }[];
360
+ risky: {
361
+ id: string;
362
+ label: string;
363
+ score: number;
364
+ why: string[];
365
+ }[];
366
+ };
256
367
  type StoredAgentMap = AgentMap & {
257
368
  object: "agent_map";
258
369
  app: string;
@@ -260,6 +371,23 @@ type StoredAgentMap = AgentMap & {
260
371
  created_at?: string;
261
372
  via?: string;
262
373
  url?: string;
374
+ /** Set for a branch map: the branch, the main map version it's compared with, and its pull request. */
375
+ branch?: string;
376
+ main_version?: number | null;
377
+ pull_request?: string | null;
378
+ changes?: MapChanges;
379
+ engine?: {
380
+ version: string;
381
+ actions: Record<string, {
382
+ score: number;
383
+ factors: string[];
384
+ oversight: {
385
+ mode: string;
386
+ rate: number | null;
387
+ verifiers: number;
388
+ };
389
+ }>;
390
+ };
263
391
  };
264
392
  type Rubric = {
265
393
  object: "rubric";
@@ -352,7 +480,7 @@ type WebhookEvent = {
352
480
  created_at: string;
353
481
  data: Task;
354
482
  };
355
- declare const VERSION = "1.15.0";
483
+ declare const VERSION = "1.18.0";
356
484
  type GitHubLink = {
357
485
  object: "github_link";
358
486
  app: string;
@@ -469,7 +597,10 @@ declare class Plures {
469
597
  guide: () => Promise<string>;
470
598
  retrieve: (app: string) => Promise<StoredAgentMap>;
471
599
  /** Uploads a new version. Checks you already added or dismissed keep that. */
472
- upload: (app: string, map: AgentMap) => Promise<StoredAgentMap>;
600
+ /** A branch other than the main one gets its own map, compared with main, and noted on its pull request. */
601
+ upload: (app: string, map: AgentMap, opts?: {
602
+ branch?: string;
603
+ }) => Promise<StoredAgentMap>;
473
604
  setCheckpoint: (app: string, id: string, status: "added" | "dismissed" | "suggested") => Promise<StoredAgentMap>;
474
605
  /** A key that can only read and upload this app's map, for CI. The key is in the response once. */
475
606
  createKey: (app: string, opts?: {
@@ -525,6 +656,17 @@ declare class Plures {
525
656
  phase: string;
526
657
  received: number;
527
658
  }>;
659
+ /** After npx plures check add pushed a branch with the check in it: Plures opens the pull request. Pass error instead if adding it failed. */
660
+ checkPr: (app: string, id: string, p: {
661
+ branch: string;
662
+ } | {
663
+ error: string;
664
+ }) => Promise<{
665
+ object: "check_pr";
666
+ check: string;
667
+ opened: boolean;
668
+ pull_request?: string;
669
+ }>;
528
670
  /** The GitHub Actions workflow that maps a repo on every push to its main branch. */
529
671
  workflow: (app: string, branch?: string) => Promise<string>;
530
672
  };
@@ -652,8 +794,97 @@ declare class Plures {
652
794
  metadata: Record<string, unknown>;
653
795
  }[]>;
654
796
  };
797
+ /**
798
+ * Answer reviews: send each turn your assistant answers, with where it routed and what it
799
+ * retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
800
+ * again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
801
+ * it after the reply and don't await it in the user's path.
802
+ */
803
+ turns: {
804
+ log: (t: TurnParams) => Promise<{
805
+ object: "turn";
806
+ id: string;
807
+ app: string;
808
+ picked: boolean;
809
+ reason: string | null;
810
+ signals: string[];
811
+ asked_again_of?: string;
812
+ }>;
813
+ /** Grade a turn from your own tools. */
814
+ grade: (id: string, g: {
815
+ intent_ok?: boolean;
816
+ right_intent?: string;
817
+ grounded?: "yes" | "partly" | "no" | "should_refuse" | "should_answer";
818
+ complete?: boolean;
819
+ missed?: string;
820
+ fact?: string;
821
+ doc?: string;
822
+ note?: string;
823
+ email?: string;
824
+ }) => Promise<{
825
+ object: "turn_grade";
826
+ turn: string;
827
+ }>;
828
+ /** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
829
+ insights: (app: string, opts?: {
830
+ days?: number;
831
+ }) => Promise<Record<string, unknown>>;
832
+ /** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
833
+ export: (app: string, kind: "routes" | "retrieval" | "qa" | "missed", opts?: {
834
+ since?: string;
835
+ }) => Promise<Record<string, unknown>[]>;
836
+ };
837
+ /**
838
+ * Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
839
+ * runner reads the code with the examples that got worse and opens a fix, and Plures checks the
840
+ * fix brings quality back. npx plures drift fix does the reading in your repo.
841
+ */
842
+ drift: {
843
+ list: (app: string) => Promise<{
844
+ object: "list";
845
+ data: DriftCase[];
846
+ }>;
847
+ /** Runs the fix on GitHub through the repo's Plures workflow. */
848
+ fix: (app: string, id: string) => Promise<{
849
+ object: "drift_fix";
850
+ case: string;
851
+ status: string;
852
+ session: string;
853
+ }>;
854
+ /** What got worse: graded examples, the change that started it, and the steps on the map. */
855
+ packet: (app: string, id: string) => Promise<Record<string, unknown> & {
856
+ case: string;
857
+ label: string;
858
+ session: string | null;
859
+ kind: "rubric" | "answers";
860
+ }>;
861
+ /** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
862
+ report: (app: string, id: string, p: {
863
+ diagnosis: DriftDiagnosis;
864
+ branch?: string | null;
865
+ } | {
866
+ error: string;
867
+ }) => Promise<{
868
+ object: "drift_fix";
869
+ case: string;
870
+ status: string;
871
+ pull_request?: string;
872
+ }>;
873
+ };
655
874
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
656
875
  balance: () => Promise<Balance>;
876
+ /**
877
+ * Runs a check from your app's map and waits for the decision. Send the data by name; the check
878
+ * saved on Plures supplies the question, options, guidance, which fields people see (the rest
879
+ * never leave Plures), who decides, how often and what happens on a timeout. Any of those can be
880
+ * overridden for this call. `answer` is the option to act on: what people chose, the first option
881
+ * when the check wasn't needed (a sample or a condition) or timed out set to go ahead, and null
882
+ * when it timed out set to hold.
883
+ */
884
+ check(id: string, data?: Record<string, unknown>, overrides?: CheckOverrides, opts?: {
885
+ timeoutMs?: number;
886
+ idempotencyKey?: string;
887
+ }): Promise<CheckResult>;
657
888
  /** Creates a task and waits for people to answer it. */
658
889
  verify(params: CreateTask, opts?: {
659
890
  timeoutMs?: number;
@@ -667,4 +898,4 @@ declare class Plures {
667
898
  static verifyWebhook(rawBody: string, signatureHeader: string | null, secret: string, toleranceSec?: number): Promise<WebhookEvent>;
668
899
  }
669
900
 
670
- export { type AgentMap, type App, type Balance, type Consensus, type CreateTask, type Escalation, type GitHubLink, 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 };
901
+ 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 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 };