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/README.md +5 -1
- package/dist/{chunk-JSR4MDEK.js → chunk-RKIJTDS3.js} +68 -2
- package/dist/cli.cjs +301 -3
- package/dist/cli.js +234 -2
- package/dist/index.cjs +68 -2
- package/dist/index.d.cts +234 -3
- package/dist/index.d.ts +234 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,10 +44,14 @@ if (event.type === "task.completed" && event.data.result?.agreement === 1) {
|
|
|
44
44
|
|
|
45
45
|
## API
|
|
46
46
|
|
|
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.
|
|
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.
|
|
47
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).
|
|
48
52
|
- `plures.tasks.retrieve(id)`, `plures.tasks.list({ status?, limit?, starting_after?, app? })`, `plures.tasks.cancel(id)`
|
|
49
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.
|
|
50
|
-
- `plures.maps.guide()`, `.retrieve(app)`, `.upload(app, map)
|
|
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.
|
|
51
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 (first map free, then Claude's cost plus 30%, at most $5).
|
|
52
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.
|
|
53
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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
var VERSION = "1.
|
|
2
|
+
var VERSION = "1.18.0";
|
|
3
3
|
var PluresError = class extends Error {
|
|
4
4
|
constructor(message, status, type) {
|
|
5
5
|
super(message);
|
|
@@ -100,7 +100,8 @@ var Plures = class {
|
|
|
100
100
|
},
|
|
101
101
|
retrieve: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map`),
|
|
102
102
|
/** Uploads a new version. Checks you already added or dismissed keep that. */
|
|
103
|
-
|
|
103
|
+
/** A branch other than the main one gets its own map, compared with main, and noted on its pull request. */
|
|
104
|
+
upload: (app, map, opts = {}) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app)}/map`, map, opts.branch ? { "plures-branch": opts.branch } : {}),
|
|
104
105
|
setCheckpoint: (app, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app)}/map/checkpoints/${encodeURIComponent(id)}`, { status }),
|
|
105
106
|
/** A key that can only read and upload this app's map, for CI. The key is in the response once. */
|
|
106
107
|
createKey: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`, opts),
|
|
@@ -111,6 +112,8 @@ var Plures = class {
|
|
|
111
112
|
runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
|
|
112
113
|
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
113
114
|
progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
|
|
115
|
+
/** 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. */
|
|
116
|
+
checkPr: (app, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/checks/${encodeURIComponent(id)}/pr`, p),
|
|
114
117
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
115
118
|
workflow: async (app, branch = "main") => {
|
|
116
119
|
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app)}&branch=${encodeURIComponent(branch)}`);
|
|
@@ -166,8 +169,71 @@ var Plures = class {
|
|
|
166
169
|
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
167
170
|
}
|
|
168
171
|
};
|
|
172
|
+
/**
|
|
173
|
+
* Answer reviews: send each turn your assistant answers, with where it routed and what it
|
|
174
|
+
* retrieved. Plures picks the ones worth a look (rerouted, refused, empty, no sources, asked
|
|
175
|
+
* again, plus a few at random) and your team grades them. Free, and nothing waits on it: call
|
|
176
|
+
* it after the reply and don't await it in the user's path.
|
|
177
|
+
*/
|
|
178
|
+
turns = {
|
|
179
|
+
log: (t) => this.req("POST", "/api/v1/turns", t),
|
|
180
|
+
/** Grade a turn from your own tools. */
|
|
181
|
+
grade: (id, g) => this.req("POST", `/api/v1/turns/${encodeURIComponent(id)}/grade`, g),
|
|
182
|
+
/** What the grades say: per intent, misroutes, retrieval misses and missed asks. */
|
|
183
|
+
insights: (app, opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/turns/insights${opts.days ? `?days=${opts.days}` : ""}`),
|
|
184
|
+
/** Graded turns as eval data: routes (routing examples), retrieval ({ query, doc, must_contain }), qa ({ question, fact } or { question, unanswerable }), missed. */
|
|
185
|
+
export: async (app, kind, opts = {}) => {
|
|
186
|
+
const q = new URLSearchParams({ kind, ...opts.since ? { since: opts.since } : {} });
|
|
187
|
+
const r = await fetch(`${this.base}/api/v1/apps/${encodeURIComponent(app)}/turns/export?${q}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
|
|
188
|
+
if (!r.ok) {
|
|
189
|
+
const d = await r.json().catch(() => ({}));
|
|
190
|
+
throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
|
|
191
|
+
}
|
|
192
|
+
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Drift: when a rubric's grades or an answer intent's grades fall, Plures opens a case, the repo's
|
|
197
|
+
* runner reads the code with the examples that got worse and opens a fix, and Plures checks the
|
|
198
|
+
* fix brings quality back. npx plures drift fix does the reading in your repo.
|
|
199
|
+
*/
|
|
200
|
+
drift = {
|
|
201
|
+
list: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift`),
|
|
202
|
+
/** Runs the fix on GitHub through the repo's Plures workflow. */
|
|
203
|
+
fix: (app, id) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/fix`, {}),
|
|
204
|
+
/** What got worse: graded examples, the change that started it, and the steps on the map. */
|
|
205
|
+
packet: (app, id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/packet`),
|
|
206
|
+
/** After reading the code: the diagnosis, and the branch with the fix if there is one. Plures opens the pull request. */
|
|
207
|
+
report: (app, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/drift/${encodeURIComponent(id)}/report`, p)
|
|
208
|
+
};
|
|
169
209
|
/** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
|
|
170
210
|
balance = () => this.req("GET", "/api/v1/balance");
|
|
211
|
+
/**
|
|
212
|
+
* Runs a check from your app's map and waits for the decision. Send the data by name; the check
|
|
213
|
+
* saved on Plures supplies the question, options, guidance, which fields people see (the rest
|
|
214
|
+
* never leave Plures), who decides, how often and what happens on a timeout. Any of those can be
|
|
215
|
+
* overridden for this call. `answer` is the option to act on: what people chose, the first option
|
|
216
|
+
* when the check wasn't needed (a sample or a condition) or timed out set to go ahead, and null
|
|
217
|
+
* when it timed out set to hold.
|
|
218
|
+
*/
|
|
219
|
+
async check(id, data = {}, overrides = {}, opts = {}) {
|
|
220
|
+
const t = await this.req("POST", `/api/v1/checks/${encodeURIComponent(id)}`, { data, ...overrides }, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {});
|
|
221
|
+
const final = t.status === "pending" ? await this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
|
|
222
|
+
const opt = (k) => final.options.find((o) => o.id === k)?.label ?? k ?? null;
|
|
223
|
+
const first = final.options[0]?.label ?? null;
|
|
224
|
+
const timedOut = final.status === "expired" || final.status === "pending";
|
|
225
|
+
const answer = final.status === "completed" ? opt(final.result?.answer) : final.status === "skipped" ? first : timedOut && t.on_timeout === "proceed" ? first : null;
|
|
226
|
+
return {
|
|
227
|
+
...final,
|
|
228
|
+
check: t.check,
|
|
229
|
+
on_timeout: t.on_timeout,
|
|
230
|
+
kept_private: t.kept_private ?? [],
|
|
231
|
+
skipped_because: t.skipped_because ?? null,
|
|
232
|
+
answer,
|
|
233
|
+
checked: final.status !== "skipped",
|
|
234
|
+
timed_out: timedOut
|
|
235
|
+
};
|
|
236
|
+
}
|
|
171
237
|
/** Creates a task and waits for people to answer it. */
|
|
172
238
|
async verify(params, opts = {}) {
|
|
173
239
|
const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
|
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.
|
|
33
|
+
var VERSION = "1.18.0";
|
|
34
34
|
var PluresError = class extends Error {
|
|
35
35
|
constructor(message, status, type) {
|
|
36
36
|
super(message);
|
|
@@ -131,7 +131,8 @@ var Plures = class {
|
|
|
131
131
|
},
|
|
132
132
|
retrieve: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map`),
|
|
133
133
|
/** Uploads a new version. Checks you already added or dismissed keep that. */
|
|
134
|
-
|
|
134
|
+
/** A branch other than the main one gets its own map, compared with main, and noted on its pull request. */
|
|
135
|
+
upload: (app2, map2, opts = {}) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app2)}/map`, map2, opts.branch ? { "plures-branch": opts.branch } : {}),
|
|
135
136
|
setCheckpoint: (app2, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app2)}/map/checkpoints/${encodeURIComponent(id)}`, { status }),
|
|
136
137
|
/** A key that can only read and upload this app's map, for CI. The key is in the response once. */
|
|
137
138
|
createKey: (app2, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys`, opts),
|
|
@@ -142,6 +143,8 @@ var Plures = class {
|
|
|
142
143
|
runs: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`),
|
|
143
144
|
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
144
145
|
progress: (app2, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map/progress`, p),
|
|
146
|
+
/** 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. */
|
|
147
|
+
checkPr: (app2, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/checks/${encodeURIComponent(id)}/pr`, p),
|
|
145
148
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
146
149
|
workflow: async (app2, branch2 = "main") => {
|
|
147
150
|
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app2)}&branch=${encodeURIComponent(branch2)}`);
|
|
@@ -197,8 +200,71 @@ var Plures = class {
|
|
|
197
200
|
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
198
201
|
}
|
|
199
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: (app2, opts = {}) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/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 (app2, kind, opts = {}) => {
|
|
217
|
+
const q = new URLSearchParams({ kind, ...opts.since ? { since: opts.since } : {} });
|
|
218
|
+
const r = await fetch(`${this.base}/api/v1/apps/${encodeURIComponent(app2)}/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: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/drift`),
|
|
233
|
+
/** Runs the fix on GitHub through the repo's Plures workflow. */
|
|
234
|
+
fix: (app2, id) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/drift/${encodeURIComponent(id)}/fix`, {}),
|
|
235
|
+
/** What got worse: graded examples, the change that started it, and the steps on the map. */
|
|
236
|
+
packet: (app2, id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/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: (app2, id, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/drift/${encodeURIComponent(id)}/report`, p)
|
|
239
|
+
};
|
|
200
240
|
/** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
|
|
201
241
|
balance = () => this.req("GET", "/api/v1/balance");
|
|
242
|
+
/**
|
|
243
|
+
* Runs a check from your app's map and waits for the decision. Send the data by name; the check
|
|
244
|
+
* saved on Plures supplies the question, options, guidance, which fields people see (the rest
|
|
245
|
+
* never leave Plures), who decides, how often and what happens on a timeout. Any of those can be
|
|
246
|
+
* overridden for this call. `answer` is the option to act on: what people chose, the first option
|
|
247
|
+
* when the check wasn't needed (a sample or a condition) or timed out set to go ahead, and null
|
|
248
|
+
* when it timed out set to hold.
|
|
249
|
+
*/
|
|
250
|
+
async check(id, data = {}, overrides = {}, opts = {}) {
|
|
251
|
+
const t = await this.req("POST", `/api/v1/checks/${encodeURIComponent(id)}`, { data, ...overrides }, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {});
|
|
252
|
+
const final = t.status === "pending" ? await this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
|
|
253
|
+
const opt = (k) => final.options.find((o) => o.id === k)?.label ?? k ?? null;
|
|
254
|
+
const first = final.options[0]?.label ?? null;
|
|
255
|
+
const timedOut = final.status === "expired" || final.status === "pending";
|
|
256
|
+
const answer = final.status === "completed" ? opt(final.result?.answer) : final.status === "skipped" ? first : timedOut && t.on_timeout === "proceed" ? first : null;
|
|
257
|
+
return {
|
|
258
|
+
...final,
|
|
259
|
+
check: t.check,
|
|
260
|
+
on_timeout: t.on_timeout,
|
|
261
|
+
kept_private: t.kept_private ?? [],
|
|
262
|
+
skipped_because: t.skipped_because ?? null,
|
|
263
|
+
answer,
|
|
264
|
+
checked: final.status !== "skipped",
|
|
265
|
+
timed_out: timedOut
|
|
266
|
+
};
|
|
267
|
+
}
|
|
202
268
|
/** Creates a task and waits for people to answer it. */
|
|
203
269
|
async verify(params, opts = {}) {
|
|
204
270
|
const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
|
|
@@ -446,11 +512,19 @@ async function upload(file) {
|
|
|
446
512
|
}
|
|
447
513
|
if (!fancy) console.log(dim(`Sending the map to Plures. No source code is sent.`));
|
|
448
514
|
let m;
|
|
515
|
+
const gitBranch = (0, import_node_child_process.spawnSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" }).stdout?.trim();
|
|
516
|
+
const branchName = process.env.PLURES_BRANCH || process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || (gitBranch && gitBranch !== "HEAD" ? gitBranch : "");
|
|
449
517
|
try {
|
|
450
|
-
m = await client().maps.upload(app, map2);
|
|
518
|
+
m = await client().maps.upload(app, map2, branchName ? { branch: branchName } : {});
|
|
451
519
|
} catch (e) {
|
|
452
520
|
die(e instanceof PluresError ? `Plures said: ${e.message}` : String(e));
|
|
453
521
|
}
|
|
522
|
+
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}` : ""}`);
|
|
523
|
+
if (m.changes && (m.changes.added.length || m.changes.removed.length || m.changes.renamed.length || m.changes.risky.length)) {
|
|
524
|
+
const c2 = m.changes;
|
|
525
|
+
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"}`);
|
|
526
|
+
for (const r of c2.risky) console.log(` ${coral("no check")} ${r.label} ${dim(`score ${r.score}: ${r.why.join("; ")}`)}`);
|
|
527
|
+
}
|
|
454
528
|
if (fancy) drawMap(m, m.url ?? `${base}/dashboard/apps/${m.app}/map`);
|
|
455
529
|
else report(m);
|
|
456
530
|
}
|
|
@@ -669,8 +743,230 @@ It adds one workflow file. Review it, then turn mapping on with ${cyan("npx plur
|
|
|
669
743
|
throw e;
|
|
670
744
|
}
|
|
671
745
|
}
|
|
746
|
+
async function checkAdd(id) {
|
|
747
|
+
if (!id) die("npx plures check add <check id> [--app <app>]");
|
|
748
|
+
const cl = client();
|
|
749
|
+
const report2 = async (p) => {
|
|
750
|
+
if (process.env.GITHUB_ACTIONS) await cl.maps.checkPr(app, id, p).catch(() => {
|
|
751
|
+
});
|
|
752
|
+
};
|
|
753
|
+
const fail = async (msg) => {
|
|
754
|
+
await report2({ error: msg });
|
|
755
|
+
die(msg);
|
|
756
|
+
};
|
|
757
|
+
let m;
|
|
758
|
+
try {
|
|
759
|
+
m = await cl.maps.retrieve(app);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
return fail(e instanceof PluresError ? e.message : String(e));
|
|
762
|
+
}
|
|
763
|
+
const cp0 = m.checkpoints.find((x) => x.id === id || x.category === id);
|
|
764
|
+
const cp = cp0;
|
|
765
|
+
if (!cp0) return fail(`No check called ${id} on ${app}'s map.`);
|
|
766
|
+
const action = m.nodes.find((n) => n.id === cp.at);
|
|
767
|
+
const cid = cp.id ?? id;
|
|
768
|
+
if (!cp.code) return fail(`The check ${cid} has no drafted question yet. Edit it on Plures first.`);
|
|
769
|
+
if (!hasClaude()) return fail("Claude Code isn't installed here, so nothing could add the line. Copy it from the check's page instead.");
|
|
770
|
+
const git = (...a) => (0, import_node_child_process.spawnSync)("git", a, { encoding: "utf8" });
|
|
771
|
+
if (git("status", "--porcelain").stdout.trim()) return fail("This repo has changes that aren't committed. Commit or stash them first.");
|
|
772
|
+
const childEnv = { ...process.env };
|
|
773
|
+
const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
|
|
774
|
+
if (!ownClaude && process.env.CI) {
|
|
775
|
+
try {
|
|
776
|
+
const run = await cl.maps.startRun(app);
|
|
777
|
+
childEnv.ANTHROPIC_BASE_URL = run.base_url;
|
|
778
|
+
childEnv.ANTHROPIC_API_KEY = run.token;
|
|
779
|
+
} catch (e) {
|
|
780
|
+
return fail(e instanceof PluresError ? e.message : String(e));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const where = action?.file ? `${action.file}${action.line ? ` near line ${action.line}` : ""}` : "the code";
|
|
784
|
+
console.log(`Adding the check ${bold(cid)} before ${bold(action?.label ?? cp.at)}, in ${where}.`);
|
|
785
|
+
const prompt = `Add one Plures human check to this codebase. Change as little as possible.
|
|
786
|
+
|
|
787
|
+
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.
|
|
788
|
+
|
|
789
|
+
What to add, adapted to the file's language and style (for Python: \`check = plures.check("${cid}", fields)\` and \`check.answer\`):
|
|
790
|
+
|
|
791
|
+
${cp.code}
|
|
792
|
+
|
|
793
|
+
- 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.
|
|
794
|
+
- 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.
|
|
795
|
+
- 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.
|
|
796
|
+
- 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.
|
|
797
|
+
- Don't change anything else, don't reformat, don't add tests, and don't run commands.`;
|
|
798
|
+
const code = await new Promise((resolve) => {
|
|
799
|
+
const p = (0, import_node_child_process.spawn)("claude", ["-p", prompt, "--allowedTools", "Read,Grep,Glob,Edit,Write"], { stdio: ["ignore", "inherit", "inherit"], env: childEnv });
|
|
800
|
+
p.on("close", (n) => resolve(n ?? 1));
|
|
801
|
+
p.on("error", () => resolve(1));
|
|
802
|
+
});
|
|
803
|
+
const changed = git("status", "--porcelain").stdout.trim();
|
|
804
|
+
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.");
|
|
805
|
+
const br = `plures/check-${cid.replace(/[^a-z0-9_-]+/gi, "-")}-${Date.now().toString(36)}`;
|
|
806
|
+
for (const a of [["checkout", "-b", br], ["add", "-A"], ["commit", "-m", `Add the Plures check ${cid} before ${(action?.label ?? cp.at).toLowerCase()}`]]) {
|
|
807
|
+
const r = git(...a);
|
|
808
|
+
if (r.status !== 0) return fail(`git ${a[0]} failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`);
|
|
809
|
+
}
|
|
810
|
+
if (!process.env.GITHUB_ACTIONS) {
|
|
811
|
+
console.log(`
|
|
812
|
+
${cyan("\u2713")} The check is on the branch ${bold(br)}. Review it with ${cyan("git show")}, then push it and open a pull request.`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const push = git("push", "-u", "origin", br);
|
|
816
|
+
if (push.status !== 0) return fail(`Pushing the branch failed: ${(push.stderr || push.stdout).trim().slice(0, 200)}`);
|
|
817
|
+
try {
|
|
818
|
+
const r = await cl.maps.checkPr(app, cid, { branch: br });
|
|
819
|
+
console.log(`${cyan("\u2713")} Pull request: ${r.pull_request ?? "opened"}`);
|
|
820
|
+
} catch (e) {
|
|
821
|
+
die(e instanceof PluresError ? e.message : String(e));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function driftFix(id) {
|
|
825
|
+
if (!id) die("npx plures drift fix <case id> [--app <app>]");
|
|
826
|
+
const cl = client();
|
|
827
|
+
const report2 = async (p) => cl.drift.report(app, id, p);
|
|
828
|
+
const fail = async (msg) => {
|
|
829
|
+
await report2({ error: msg }).catch(() => {
|
|
830
|
+
});
|
|
831
|
+
die(msg);
|
|
832
|
+
};
|
|
833
|
+
let packet;
|
|
834
|
+
try {
|
|
835
|
+
packet = await cl.drift.packet(app, id);
|
|
836
|
+
} catch (e) {
|
|
837
|
+
return fail(e instanceof PluresError ? e.message : String(e));
|
|
838
|
+
}
|
|
839
|
+
if (!hasClaude()) return fail("Claude Code isn't installed here, so nothing could read the code.");
|
|
840
|
+
const git = (...a) => (0, import_node_child_process.spawnSync)("git", a, { encoding: "utf8" });
|
|
841
|
+
if (git("status", "--porcelain").stdout.trim()) return fail("This repo has changes that aren't committed. Commit or stash them first.");
|
|
842
|
+
const childEnv = { ...process.env };
|
|
843
|
+
const ownClaude = Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN);
|
|
844
|
+
if (!ownClaude && process.env.CI) {
|
|
845
|
+
try {
|
|
846
|
+
const run = await cl.maps.startRun(app);
|
|
847
|
+
childEnv.ANTHROPIC_BASE_URL = run.base_url;
|
|
848
|
+
childEnv.ANTHROPIC_API_KEY = run.token;
|
|
849
|
+
} catch (e) {
|
|
850
|
+
return fail(e instanceof PluresError ? e.message : String(e));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
const session = packet.session || `drift_${(0, import_node_crypto.randomBytes)(9).toString("base64url").replace(/[-_]/g, "")}`;
|
|
854
|
+
const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
|
|
855
|
+
let queue = [], phase = "reading", sent = "";
|
|
856
|
+
const flush = async () => {
|
|
857
|
+
const events = queue;
|
|
858
|
+
queue = [];
|
|
859
|
+
if (!events.length && sent === phase) return;
|
|
860
|
+
sent = phase;
|
|
861
|
+
await cl.maps.progress(app, { session, phase, source, events }).catch(() => {
|
|
862
|
+
});
|
|
863
|
+
};
|
|
864
|
+
const tick = setInterval(() => {
|
|
865
|
+
void flush();
|
|
866
|
+
}, 900);
|
|
867
|
+
void flush();
|
|
868
|
+
const DIAG = ".plures-drift.json";
|
|
869
|
+
console.log(`${bold("Plures drift")} ${packet.label}
|
|
870
|
+
${dim("Claude Code reads this repo with the graded examples that got worse. Only file names and the diagnosis go to Plures.")}
|
|
871
|
+
`);
|
|
872
|
+
const prompt = `Quality fell in this codebase's AI assistant, and people graded the examples. Find out why in the code and fix it.
|
|
873
|
+
|
|
874
|
+
What Plures measured (JSON):
|
|
875
|
+
${JSON.stringify({ ...packet, session: void 0 }, null, 2).slice(0, 6e4)}
|
|
876
|
+
|
|
877
|
+
Do this:
|
|
878
|
+
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.
|
|
879
|
+
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.
|
|
880
|
+
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.
|
|
881
|
+
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.
|
|
882
|
+
Don't run the app or tests, and never include secrets or customer data in ${DIAG}.`;
|
|
883
|
+
const rel = (p) => typeof p === "string" && p ? (0, import_node_path.isAbsolute)(p) ? (0, import_node_path.relative)(process.cwd(), p) : p : null;
|
|
884
|
+
const code = await new Promise((resolve) => {
|
|
885
|
+
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 });
|
|
886
|
+
let buf = "";
|
|
887
|
+
p.stdout.on("data", (chunk) => {
|
|
888
|
+
buf += chunk.toString();
|
|
889
|
+
let i;
|
|
890
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
891
|
+
const line = buf.slice(0, i);
|
|
892
|
+
buf = buf.slice(i + 1);
|
|
893
|
+
let ev;
|
|
894
|
+
try {
|
|
895
|
+
ev = JSON.parse(line);
|
|
896
|
+
} catch {
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
|
|
900
|
+
if (b.type !== "tool_use") continue;
|
|
901
|
+
const f = rel(b.input?.file_path ?? b.input?.path);
|
|
902
|
+
if (!f || f.startsWith("..") || f.endsWith(DIAG)) continue;
|
|
903
|
+
if (b.name === "Read") {
|
|
904
|
+
queue.push({ path: f, tool: "Read" });
|
|
905
|
+
console.log(` ${dim("reading")} ${f}`);
|
|
906
|
+
} else if (b.name === "Edit" || b.name === "Write") {
|
|
907
|
+
phase = "drawing";
|
|
908
|
+
queue.push({ path: f, tool: "Edit" });
|
|
909
|
+
console.log(` ${violet("fixing")} ${f}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
p.on("close", (n) => resolve(n ?? 1));
|
|
915
|
+
p.on("error", () => resolve(1));
|
|
916
|
+
});
|
|
917
|
+
clearInterval(tick);
|
|
918
|
+
let diagnosis = null;
|
|
919
|
+
try {
|
|
920
|
+
diagnosis = JSON.parse((0, import_node_fs.readFileSync)(DIAG, "utf8"));
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
(0, import_node_child_process.spawnSync)("rm", ["-f", DIAG]);
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
if (code !== 0 || !diagnosis) {
|
|
928
|
+
phase = "failed";
|
|
929
|
+
await flush();
|
|
930
|
+
return fail("Claude Code didn't write a diagnosis. Nothing was changed on Plures.");
|
|
931
|
+
}
|
|
932
|
+
const changed = git("status", "--porcelain").stdout.trim();
|
|
933
|
+
let branch2 = null;
|
|
934
|
+
if (changed) {
|
|
935
|
+
branch2 = `plures/drift-${id.replace(/[^a-z0-9_-]+/gi, "-")}-${Date.now().toString(36)}`;
|
|
936
|
+
for (const a of [["checkout", "-b", branch2], ["add", "-A"], ["commit", "-m", `Fix drift: ${String(packet.label).slice(0, 60)}`]]) {
|
|
937
|
+
const r = git(...a);
|
|
938
|
+
if (r.status !== 0) {
|
|
939
|
+
phase = "failed";
|
|
940
|
+
await flush();
|
|
941
|
+
return fail(`git ${a[0]} failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
945
|
+
const push = git("push", "-u", "origin", branch2);
|
|
946
|
+
if (push.status !== 0) {
|
|
947
|
+
phase = "failed";
|
|
948
|
+
await flush();
|
|
949
|
+
return fail(`Pushing the branch failed: ${(push.stderr || push.stdout).trim().slice(0, 200)}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
phase = "done";
|
|
954
|
+
await flush();
|
|
955
|
+
try {
|
|
956
|
+
const r = await report2({ diagnosis, branch: process.env.GITHUB_ACTIONS ? branch2 : null });
|
|
957
|
+
const d = diagnosis;
|
|
958
|
+
console.log(`
|
|
959
|
+
${cyan("\u2713")} ${d.summary ?? "Diagnosed."}`);
|
|
960
|
+
if (r.pull_request) console.log(`${cyan("\u2713")} Pull request: ${r.pull_request}`);
|
|
961
|
+
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.`);
|
|
962
|
+
} catch (e) {
|
|
963
|
+
die(e instanceof PluresError ? e.message : String(e));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
672
966
|
async function main() {
|
|
673
967
|
const [cmd, sub, arg] = argv;
|
|
968
|
+
if (cmd === "check" && sub === "add") return checkAdd(arg);
|
|
969
|
+
if (cmd === "drift" && sub === "fix") return driftFix(arg);
|
|
674
970
|
if (cmd !== "map") {
|
|
675
971
|
console.log(`Plures
|
|
676
972
|
|
|
@@ -681,6 +977,8 @@ async function main() {
|
|
|
681
977
|
npx plures map github connect let Plures add that Action by pull request
|
|
682
978
|
npx plures map github on merge it, which turns mapping on
|
|
683
979
|
npx plures map github run map now on GitHub, and watch it build
|
|
980
|
+
npx plures check add <id> put a check into your code, on a new branch
|
|
981
|
+
npx plures drift fix <case> find why quality fell and fix it, on a new branch
|
|
684
982
|
|
|
685
983
|
Docs: https://plures.si/docs`);
|
|
686
984
|
return;
|