bareloop 0.40.0 → 0.42.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/CHANGELOG.md +96 -1
- package/README.md +13 -21
- package/bareloop.context.md +141 -128
- package/package.json +5 -4
- package/src/authorflow.js +346 -38
- package/src/authoring.js +92 -7
- package/src/authorjob.js +82 -20
- package/src/authorreadout.js +89 -5
- package/src/authorrun.js +39 -30
- package/src/authorscout.js +112 -4
- package/src/bundle.js +7 -4
- package/src/bundlerun.js +4 -1
- package/src/cli.js +1 -1
- package/src/config.js +129 -29
- package/src/declaredclose.js +23 -12
- package/src/draftspend.js +4 -4
- package/src/facts.js +317 -0
- package/src/factwords.js +129 -0
- package/src/index.js +2 -4
- package/src/inputlook.js +301 -0
- package/src/interviewrun.js +17 -25
- package/src/job.js +15 -8
- package/src/jobblock.js +7 -2
- package/src/judged.js +11 -43
- package/src/keysfile.js +15 -7
- package/src/kinds.js +86 -5
- package/src/labelscan.js +170 -0
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/pagefetch.js +125 -0
- package/src/panel/authorroutes.js +34 -16
- package/src/panel/authorsession.js +161 -80
- package/src/panel/index.html +553 -152
- package/src/panel/jobplan.js +34 -11
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +54 -18
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +5 -4
- package/src/providerrows.js +226 -141
- package/src/providers.js +58 -332
- package/src/providershapes.js +18 -0
- package/src/replay.js +1 -1
- package/src/rubricstep.js +804 -0
- package/src/source.js +166 -1
- package/src/thinking.js +130 -0
- package/src/u-readout.js +1 -1
- package/src/userrun.js +64 -75
- package/types/authorflow.d.ts +272 -18
- package/types/authoring.d.ts +29 -1
- package/types/authorjob.d.ts +9 -7
- package/types/authorreadout.d.ts +27 -2
- package/types/authorscout.d.ts +72 -2
- package/types/config.d.ts +23 -6
- package/types/declaredclose.d.ts +4 -2
- package/types/draftspend.d.ts +8 -8
- package/types/facts.d.ts +71 -0
- package/types/factwords.d.ts +42 -0
- package/types/index.d.ts +1 -1
- package/types/inputlook.d.ts +238 -0
- package/types/job.d.ts +5 -5
- package/types/jobblock.d.ts +8 -1
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/labelscan.d.ts +38 -0
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/pagefetch.d.ts +71 -0
- package/types/panel/authorsession.d.ts +20 -11
- package/types/panel/jobplan.d.ts +12 -4
- package/types/panel/server.d.ts +18 -6
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +34 -159
- package/types/providershapes.d.ts +10 -0
- package/types/rubricstep.d.ts +286 -0
- package/types/source.d.ts +94 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +17 -1
package/src/source.js
CHANGED
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
// on rather than catch.
|
|
24
24
|
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
|
+
import { tmpdir } from 'node:os';
|
|
26
27
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
27
|
-
import { mkdir, readdir, readFile, writeFile, lstat, stat, chmod, access, copyFile, cp, open, rm, realpath, readlink, symlink, constants as fsConstants } from 'node:fs/promises';
|
|
28
|
+
import { mkdir, mkdtemp, readdir, readFile, writeFile, lstat, stat, chmod, access, copyFile, cp, open, rm, realpath, readlink, symlink, constants as fsConstants } from 'node:fs/promises';
|
|
28
29
|
import { dirname, join, relative, resolve, sep, basename, isAbsolute } from 'node:path';
|
|
29
30
|
import { git } from './kinds.js';
|
|
30
31
|
import { addWorktree, hideBareloopDir } from './worktree.js';
|
|
31
32
|
import { MAX_BUFFER } from './kinds.js';
|
|
32
33
|
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
33
34
|
import { scanSecrets, SECRET_PATTERNS, SECRET_PATTERN_NAMES } from './validate.js';
|
|
35
|
+
import { fetchPageText, isWebUrl } from './pagefetch.js';
|
|
34
36
|
|
|
35
37
|
/** @typedef {{stop: string, code: string}} SourceRefusal a named, non-throwing stop */
|
|
36
38
|
|
|
@@ -1223,3 +1225,166 @@ export async function proveInputFiles(inputs) {
|
|
|
1223
1225
|
}
|
|
1224
1226
|
return { ok: true, repo, root };
|
|
1225
1227
|
}
|
|
1228
|
+
|
|
1229
|
+
/** the per-file ceiling for a rubric input: bareguard's `cited` check searches a quote in a source only up to this size
|
|
1230
|
+
* (its `quoteSourceMaxBytes`); a bigger file could never be cited from, so the door refuses it at $0 */
|
|
1231
|
+
export const RUBRIC_INPUT_MAX_BYTES = 5 * 1024 * 1024;
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Does this source manifest belong to a rubric job on local files (`proveRubricInputs` wrote it)? One spelling for both
|
|
1235
|
+
* doors (the panel session and the CLI author run): a non-repo source that froze numbered, labelled input files.
|
|
1236
|
+
* @param {Record<string, any>|null|undefined} manifest
|
|
1237
|
+
* @returns {boolean}
|
|
1238
|
+
*/
|
|
1239
|
+
export function isRubricFilesManifest(manifest) {
|
|
1240
|
+
return !!manifest && manifest.kind !== 'repo' && Array.isArray(manifest.inputs) && manifest.inputs.length > 0;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* PART 1a (docs/product/ITEM33-BUILD.md, "Addendum 2026-10-12"): the front door for a RUBRIC job whose inputs are the
|
|
1245
|
+
* person's own LOCAL FILES, not a repo. Each numbered `label: /absolute/file` line is proven at $0 and frozen into the
|
|
1246
|
+
* same hidden-git tree a plain folder gets (`prepareSource`, which also refuses a secret shape, an environment file
|
|
1247
|
+
* and a symlink), under `input/`; the manifest then also records `inputs: [{label, rel, sha256}]` — the bytes the
|
|
1248
|
+
* rubric's `cited` check will search a quote in, hashed once, here. A line that is a web address is fetched ONCE with
|
|
1249
|
+
* barebrowse (PART 1c, read only) and its text frozen under `input/` like a file (the manifest row also records its `url`,
|
|
1250
|
+
* so run start can fetch it again); a `search:` line is a named refusal (a later build); so is a relative path, a missing file, a folder, a file over
|
|
1251
|
+
* {@link RUBRIC_INPUT_MAX_BYTES}, and text that is not valid UTF-8. Every refusal is plain words naming the line and
|
|
1252
|
+
* ends "Nothing spent." — nothing here touches a model.
|
|
1253
|
+
* @param {{inputs: {n: number, label: string, value: string}[], into: string, destination?: string, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>}} o `fetchUrl` - how a web address is read (default barebrowse via `fetchPageText`; a test injects a stub, never the network)
|
|
1254
|
+
* @returns {Promise<{ok: true, prep: {stop: null, into: string, tree: string, manifestPath: string, manifest: Record<string, any>}}|{ok: false, error: string, code: string}>}
|
|
1255
|
+
*/
|
|
1256
|
+
export async function proveRubricInputs({ inputs, into, destination, fetchUrl = (u) => fetchPageText(u) }) {
|
|
1257
|
+
/** @param {string} code @param {number} n @param {string} msg @returns {{ok: false, error: string, code: string}} */
|
|
1258
|
+
const bad = (code, n, msg) => ({ ok: false, code, error: `line ${n}: ${msg} Nothing spent.` });
|
|
1259
|
+
if (!Array.isArray(inputs) || inputs.length === 0) return { ok: false, code: 'inputs-missing', error: 'Inputs is required. Nothing spent.' };
|
|
1260
|
+
/** @type {Set<string>} */
|
|
1261
|
+
const labels = new Set();
|
|
1262
|
+
/** @type {{label: string, name: string, buf: Buffer, url?: string}[]} */
|
|
1263
|
+
const staged = [];
|
|
1264
|
+
/** @type {Set<string>} */
|
|
1265
|
+
const names = new Set();
|
|
1266
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true });
|
|
1267
|
+
for (const inp of inputs) {
|
|
1268
|
+
const { n, label, value } = inp;
|
|
1269
|
+
if (/^search$/i.test(label) || (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) && !isWebUrl(value))) {
|
|
1270
|
+
return bad('web-input', n, 'a search, or an address that is not http or https, is not an input yet.');
|
|
1271
|
+
}
|
|
1272
|
+
if (labels.has(label.toLowerCase())) return bad('label-repeat', n, `the label "${label}" is already used by an earlier line.`);
|
|
1273
|
+
labels.add(label.toLowerCase());
|
|
1274
|
+
if (isWebUrl(value)) {
|
|
1275
|
+
const got = await fetchUrl(value.trim());
|
|
1276
|
+
if (!got.ok) return bad('input-url-unreadable', n, `${got.error.replace(/[.\s]+$/, '')}.`);
|
|
1277
|
+
const pageBuf = Buffer.from(got.text, 'utf8');
|
|
1278
|
+
const leak = secretPatternNames(got.text);
|
|
1279
|
+
if (leak.length) return bad('input-url-secret', n, `the page at ${got.url} carries a known secret shape (${leak.join(', ')}) - refused, secrets never enter a job.`);
|
|
1280
|
+
const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'page';
|
|
1281
|
+
const pageName = names.has(`${stem}.txt`) ? `${n}-${stem}.txt` : `${stem}.txt`;
|
|
1282
|
+
names.add(pageName);
|
|
1283
|
+
staged.push({ label, name: pageName, buf: pageBuf, url: got.url });
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (!isAbsolute(value)) return bad('input-not-absolute', n, `${value} must be the full path of a file, like /home/me/notes.md.`);
|
|
1287
|
+
const abs = resolve(value);
|
|
1288
|
+
let ls;
|
|
1289
|
+
try { ls = await lstat(abs); } catch { return bad('input-missing', n, `${abs} does not exist.`); }
|
|
1290
|
+
if (ls.isSymbolicLink()) return bad('input-symlink', n, `${abs} is a symlink — refused, never followed.`);
|
|
1291
|
+
if (!ls.isFile()) return bad('input-not-file', n, `${abs} is not a file — each input line is one file.`);
|
|
1292
|
+
const base = basename(abs);
|
|
1293
|
+
if (ENV_FILE.test(base)) return bad('input-env-file', n, `${abs} is an environment file — refused by name, whatever it contains.`);
|
|
1294
|
+
if (ls.size > RUBRIC_INPUT_MAX_BYTES) return bad('input-oversize', n, `${abs} is over the ${RUBRIC_INPUT_MAX_BYTES / (1024 * 1024)} MiB limit for a quoted input.`);
|
|
1295
|
+
const buf = await readFile(abs);
|
|
1296
|
+
try { utf8.decode(buf); } catch { return bad('input-not-utf8', n, `${abs} is not UTF-8 text — text files only for now.`); }
|
|
1297
|
+
if (hasNulByte(buf)) return bad('input-not-text', n, `${abs} is not a text file — text files only for now.`);
|
|
1298
|
+
// two files with one name keep the line number in front, so neither overwrites the other under input/
|
|
1299
|
+
const name = names.has(base) ? `${n}-${base}` : base;
|
|
1300
|
+
names.add(name);
|
|
1301
|
+
staged.push({ label, name, buf });
|
|
1302
|
+
}
|
|
1303
|
+
const stage = await mkdtemp(join(tmpdir(), 'bareloop-rubric-inputs-'));
|
|
1304
|
+
try {
|
|
1305
|
+
for (const f of staged) await writeFile(join(stage, f.name), f.buf);
|
|
1306
|
+
const prep = await prepareSource({ source: stage, into, ...(destination === undefined ? {} : { destination }) });
|
|
1307
|
+
if (prep.stop !== null) return { ok: false, code: prep.code, error: prep.stop };
|
|
1308
|
+
// `source` names the person's own first file, never the scratch staging folder that was already removed
|
|
1309
|
+
const manifest = {
|
|
1310
|
+
...prep.manifest,
|
|
1311
|
+
source: isWebUrl(inputs[0].value) ? inputs[0].value.trim() : resolve(inputs[0].value),
|
|
1312
|
+
inputs: staged.map((f) => ({ label: f.label, rel: `input/${f.name}`, sha256: sha256Hex(f.buf), ...(f.url === undefined ? {} : { url: f.url }) })),
|
|
1313
|
+
};
|
|
1314
|
+
await writeFile(prep.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1315
|
+
return { ok: true, prep: { ...prep, manifest } };
|
|
1316
|
+
} finally {
|
|
1317
|
+
await rm(stage, { recursive: true, force: true });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Is this Inputs value a path to an existing regular FILE on this machine (not a repo folder, not a web address, not a
|
|
1323
|
+
* missing path)? The $0 peek that tells a rubric job on local files from a rubric job on a repo before a session exists;
|
|
1324
|
+
* `proveRubricInputs` makes the authoritative, line-by-line proof.
|
|
1325
|
+
* @param {string} value
|
|
1326
|
+
* @returns {boolean}
|
|
1327
|
+
*/
|
|
1328
|
+
export function looksLikeLocalFile(value) {
|
|
1329
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || !isAbsolute(value)) return false;
|
|
1330
|
+
try { return lstatSync(resolve(value)).isFile(); } catch { return false; }
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Is this Inputs value one a rubric job can work on without a repo: a regular FILE on this machine, or a web address (PART 1c)?
|
|
1335
|
+
* @param {string} value
|
|
1336
|
+
* @returns {boolean}
|
|
1337
|
+
*/
|
|
1338
|
+
export function looksLikeRubricInput(value) {
|
|
1339
|
+
return isWebUrl(value) || looksLikeLocalFile(value);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* RUN START for a rubric job with web inputs (PART 1c, RF22): fetch every web input AGAIN and make that text the run's frozen input
|
|
1344
|
+
* BEFORE the worker starts. The drafting copy was only for the look (prices change); the close grades, and the worker reads, THIS
|
|
1345
|
+
* copy. It is written into the prepared tree, committed as the new seed (so the page is never read as a worker write), and
|
|
1346
|
+
* `source.json` is rewritten (new sha256 per input, new seed, `draftSha256` keeps what the look saw). Nothing but the text of a
|
|
1347
|
+
* recorded web address is ever refetched; a job with only file inputs returns at once and changes nothing.
|
|
1348
|
+
* @param {{into: string, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>}} o `into` - the prepared copy (source.json + tree/)
|
|
1349
|
+
* @returns {Promise<{ok: true, refreshed: string[], seed: string|null}|{ok: false, error: string}>}
|
|
1350
|
+
*/
|
|
1351
|
+
export async function refreezeUrlInputs({ into, fetchUrl = (u) => fetchPageText(u) }) {
|
|
1352
|
+
const read = await readSourceManifest(into);
|
|
1353
|
+
if (read.stop !== null) return { ok: false, error: read.stop };
|
|
1354
|
+
if (!read.present || !read.manifest) return { ok: true, refreshed: [], seed: null };
|
|
1355
|
+
const manifest = read.manifest;
|
|
1356
|
+
const rows = Array.isArray(manifest.inputs) ? manifest.inputs.filter((/** @type {any} */ r) => r && typeof r.url === 'string' && r.url !== '') : [];
|
|
1357
|
+
if (rows.length === 0) return { ok: true, refreshed: [], seed: null };
|
|
1358
|
+
const tree = join(resolve(into), 'tree');
|
|
1359
|
+
/** @type {{row: any, buf: Buffer}[]} */
|
|
1360
|
+
const fresh = [];
|
|
1361
|
+
for (const row of rows) {
|
|
1362
|
+
const got = await fetchUrl(row.url);
|
|
1363
|
+
if (!got.ok) return { ok: false, error: `Input "${row.label}": ${got.error}. The run did not start. Nothing spent.` };
|
|
1364
|
+
const leak = secretPatternNames(got.text);
|
|
1365
|
+
if (leak.length) return { ok: false, error: `Input "${row.label}": the page at ${got.url} carries a known secret shape (${leak.join(', ')}) - refused, secrets never enter a job. The run did not start. Nothing spent.` };
|
|
1366
|
+
fresh.push({ row, buf: Buffer.from(got.text, 'utf8') });
|
|
1367
|
+
}
|
|
1368
|
+
for (const f of fresh) await writeFile(join(tree, f.row.rel), f.buf);
|
|
1369
|
+
const add = await git(tree, [...NO_HOOKS, 'add', '-A', '-f']);
|
|
1370
|
+
if (!add.ok) return { ok: false, error: `the fresh page copy could not be staged: ${add.err}. The run did not start. Nothing spent.` };
|
|
1371
|
+
const commit = await git(tree, [...GIT_IDENTITY, ...NO_HOOKS, 'commit', '-q', '--allow-empty', '-m', 'bareloop: run start - fresh copy of the web inputs']);
|
|
1372
|
+
if (!commit.ok) return { ok: false, error: `the fresh page copy could not be committed: ${commit.err}. The run did not start. Nothing spent.` };
|
|
1373
|
+
const head = await git(tree, ['rev-parse', 'HEAD']);
|
|
1374
|
+
if (!head.ok) return { ok: false, error: `git rev-parse HEAD failed in ${tree}: ${head.err}. The run did not start. Nothing spent.` };
|
|
1375
|
+
const seed = head.out.trim();
|
|
1376
|
+
const byLabel = new Map(fresh.map((f) => [f.row.label, f.buf]));
|
|
1377
|
+
const inputs = manifest.inputs.map((/** @type {any} */ r) => {
|
|
1378
|
+
const buf = byLabel.get(r.label);
|
|
1379
|
+
return buf === undefined || typeof r.url !== 'string' ? r : { ...r, draftSha256: r.draftSha256 ?? r.sha256, sha256: sha256Hex(buf) };
|
|
1380
|
+
});
|
|
1381
|
+
const files = Array.isArray(manifest.files) ? manifest.files.map((/** @type {any} */ m) => {
|
|
1382
|
+
const hit = inputs.find((/** @type {any} */ r) => typeof r.url === 'string' && r.rel === `input/${m.path}`);
|
|
1383
|
+
if (!hit) return m;
|
|
1384
|
+
const buf = byLabel.get(hit.label);
|
|
1385
|
+
return buf === undefined ? m : { ...m, bytes: buf.length, sha256: sha256Hex(buf) };
|
|
1386
|
+
}) : manifest.files;
|
|
1387
|
+
const next = { ...manifest, inputs, files, seed, refrozenAt: new Date().toISOString() };
|
|
1388
|
+
await writeFile(join(resolve(into), 'source.json'), `${JSON.stringify(next, null, 2)}\n`);
|
|
1389
|
+
return { ok: true, refreshed: rows.map((/** @type {any} */ r) => r.label), seed };
|
|
1390
|
+
}
|
package/src/thinking.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// rewritten-from: fwdloop src/thinking.js@f9449e8
|
|
2
|
+
// PANEL-BUILD.md P8 batch 5 — THE one owner of what a drafting or judging model call sends for thinking. bareloop keeps NO per-model
|
|
3
|
+
// thinking list and NO vendor regex: bare-agent (>=0.52) owns which request shape switches thinking for which model, and
|
|
4
|
+
// `thinkingSupport(kind, model, {baseUrl})` is its own pure lookup (no network): `true` (measured switchable), `false` (measured
|
|
5
|
+
// NOT switchable), `'unverified'` (not measured — the shape ladder is tried at runtime; if every shape fails with a client
|
|
6
|
+
// validation error ONE plain request decides, and the call still runs, reported `thinkingStatus: 'unsupported'`).
|
|
7
|
+
//
|
|
8
|
+
// The tick is the person's, per job ("Thinking on", the signed optional `thinking` field; absent = off). It covers DRAFTING and the
|
|
9
|
+
// rubric JUDGE; the WORKER is never handed a thinking option. PER-CALL options, never constructor options (the drafter's provider is
|
|
10
|
+
// the worker's instance, deliberately left as it was). The raw `thinking` option is never sent (bare-agent throws if both are set).
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { thinkingSupport: baseSupport, AnthropicProvider, OpenAIProvider, GeminiProvider } = require('bare-agent/providers');
|
|
15
|
+
|
|
16
|
+
/** The words under a greyed "Thinking on" box (the server's `thinking.canChange` decides; the page decides nothing). */
|
|
17
|
+
export const THINKING_FIXED_SAY = "can't change this for this model";
|
|
18
|
+
|
|
19
|
+
/** API shape id -> bare-agent's thinking-ladder kind. A missing shape (clipipe, a test double) has no kind. */
|
|
20
|
+
const KIND_OF_SHAPE = Object.freeze({ 'anthropic-api': 'anthropic', 'openai-api': 'openai', 'gemini-api': 'gemini' });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What bare-agent knows about switching this model's thinking. A missing kind, a non-function `impl`, or a throw all read
|
|
24
|
+
* `'unverified'` — never a guess in either direction.
|
|
25
|
+
* @param {string} shape an API shape id (src/providershapes.js)
|
|
26
|
+
* @param {string} model
|
|
27
|
+
* @param {{ baseUrl?: string|null, impl?: Function }} [o] `impl` is a test seam; production uses bare-agent's own
|
|
28
|
+
* @returns {true|false|'unverified'}
|
|
29
|
+
*/
|
|
30
|
+
export function thinkingSupport(shape, model, { baseUrl, impl = baseSupport } = {}) {
|
|
31
|
+
const kind = /** @type {Record<string, string>} */ (KIND_OF_SHAPE)[shape];
|
|
32
|
+
if (kind === undefined || typeof impl !== 'function') return 'unverified';
|
|
33
|
+
try {
|
|
34
|
+
const r = impl(kind, model, baseUrl ? { baseUrl } : undefined);
|
|
35
|
+
return r?.switchable === true ? true : (r?.switchable === false ? false : 'unverified');
|
|
36
|
+
} catch { return 'unverified'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The per-call options for one support answer and one tick (`true` = "Thinking on"). The table (PANEL-BUILD.md P8, ruling Q1):
|
|
41
|
+
* tick on : true -> on | false -> {} (the box is greyed, cannot be on) | 'unverified' -> on
|
|
42
|
+
* tick off: true -> off | false -> {} (the model thinks as it would) | 'unverified' -> off (bare-agent decides; plain request on failure)
|
|
43
|
+
* @param {true|false|'unverified'} support
|
|
44
|
+
* @param {boolean} tick
|
|
45
|
+
* @returns {{ thinkingMode: 'on'|'off' } | {}}
|
|
46
|
+
*/
|
|
47
|
+
export function thinkingOptions(support, tick) {
|
|
48
|
+
if (support === false) return {};
|
|
49
|
+
return { thinkingMode: tick ? 'on' : 'off' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The enumerated operator knob a drafting call is built with: 'on' | 'off' (the tick, spelled as a word). */
|
|
53
|
+
export const THINKING_MODES = Object.freeze(['on', 'off']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Throw unless `mode` is one of {@link THINKING_MODES} — an operator-param guard, so a typo never silently means a mode.
|
|
57
|
+
* @param {unknown} mode
|
|
58
|
+
* @returns {'on'|'off'}
|
|
59
|
+
*/
|
|
60
|
+
export function assertThinkingMode(mode) {
|
|
61
|
+
if (mode === 'on' || mode === 'off') return mode;
|
|
62
|
+
throw new Error(`[thinking] draftThinking must be one of ${THINKING_MODES.join(' | ')}, got ${JSON.stringify(mode)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The API shape of a constructed bare-agent provider (the three client classes only); `null` for a test double or a clipipe.
|
|
67
|
+
* @param {any} provider
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
export function shapeOfProvider(provider) {
|
|
71
|
+
if (provider instanceof AnthropicProvider) return 'anthropic-api';
|
|
72
|
+
if (provider instanceof OpenAIProvider) return 'openai-api';
|
|
73
|
+
if (provider instanceof GeminiProvider) return 'gemini-api';
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The per-call options every DRAFTING and JUDGING model call on this provider instance carries for this mode. A provider that is
|
|
79
|
+
* not one of the three client classes (a test double, a clipipe) is never sent the switch.
|
|
80
|
+
* @param {any} provider a constructed bare-agent provider
|
|
81
|
+
* @param {unknown} [mode] 'on' | 'off' (default 'off')
|
|
82
|
+
* @returns {{ thinkingMode: 'on'|'off' } | {}}
|
|
83
|
+
*/
|
|
84
|
+
export function thinkingCallOptions(provider, mode = 'off') {
|
|
85
|
+
const m = assertThinkingMode(mode);
|
|
86
|
+
const shape = shapeOfProvider(provider);
|
|
87
|
+
if (shape === null) return {};
|
|
88
|
+
return thinkingOptions(thinkingSupport(shape, String(provider.model), { baseUrl: provider.baseUrl }), m === 'on');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* What the drafting calls will carry for a model the caller has only NAMED (shape, model id, Base URL) — no key, no network. The
|
|
93
|
+
* first drafting row is shown from this, before the key is read.
|
|
94
|
+
* @param {{providerName: string, model: string, baseUrl?: string|null}} o
|
|
95
|
+
* @param {unknown} [mode] 'on' | 'off'
|
|
96
|
+
* @returns {{ support: true|false|'unverified', mode: 'on'|'off', options: {thinkingMode?: 'on'|'off'} }}
|
|
97
|
+
*/
|
|
98
|
+
export function thinkingPlanFor({ providerName, model, baseUrl }, mode = 'off') {
|
|
99
|
+
const m = assertThinkingMode(mode);
|
|
100
|
+
const support = thinkingSupport(providerName, model, { baseUrl });
|
|
101
|
+
return { support, mode: m, options: thinkingOptions(support, m === 'on') };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The server's answer for one Model-menu line: whether the "Thinking on" box can change and the words under it. `false` greys and
|
|
106
|
+
* unticks the box (the note reads {@link THINKING_FIXED_SAY}); `'unverified'` leaves it enabled with no note.
|
|
107
|
+
* @param {string} shape @param {string} model @param {string|null|undefined} baseUrl
|
|
108
|
+
* @returns {{ support: true|false|'unverified', canChange: boolean, say: string }}
|
|
109
|
+
*/
|
|
110
|
+
export function thinkingForLine(shape, model, baseUrl) {
|
|
111
|
+
const support = thinkingSupport(shape, model, { baseUrl });
|
|
112
|
+
return { support, canChange: support !== false, say: support === false ? THINKING_FIXED_SAY : '' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The first drafting row's words — code-owned, ALWAYS starting "thinking on" or "thinking off". `status` is bare-agent's own
|
|
117
|
+
* `thinkingStatus` for a call that has run ('applied' | 'unsupported' | 'ignored' | 'unknown'); undefined = before any call, read from
|
|
118
|
+
* the plan (`support`, `mode`).
|
|
119
|
+
* @param {{ mode: 'on'|'off', support?: true|false|'unverified', status?: string }} s
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function thinkingRowText({ mode, support, status }) {
|
|
123
|
+
if (status === 'applied') return `thinking ${mode}`;
|
|
124
|
+
if (status === 'unsupported') return "thinking on (this model can't change it)";
|
|
125
|
+
if (status === 'ignored') return 'thinking on (the model thought anyway)';
|
|
126
|
+
if (status === 'unknown') return `thinking ${mode} (not confirmed)`;
|
|
127
|
+
if (support === false) return "thinking on (this model can't change it)";
|
|
128
|
+
if (support === 'unverified') return `thinking ${mode} (not verified for this model)`;
|
|
129
|
+
return `thinking ${mode}`;
|
|
130
|
+
}
|
package/src/u-readout.js
CHANGED
|
@@ -202,7 +202,7 @@ export function reviewDoorPackage({ door, diff }) {
|
|
|
202
202
|
const out = [];
|
|
203
203
|
out.push(`REVIEW DOOR — this run's close minted ${String(d.outcome ?? 'a verdict')} and it STANDS. Nothing you do here changes it.`);
|
|
204
204
|
if (held) {
|
|
205
|
-
out.push(' held this
|
|
205
|
+
out.push(' held this is a rubric (soft-green) job, so its green earns NO reuse and NO learning credit until you accept it, whether or not a judge ran');
|
|
206
206
|
}
|
|
207
207
|
// the body: what the machine judged, what moved, and the lines themselves —
|
|
208
208
|
// rendered by the SAME code the pause screen uses, off the same record shape.
|