bareloop 0.40.0 → 0.41.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 +33 -0
- package/bareloop.context.md +97 -124
- package/package.json +2 -2
- package/src/authorflow.js +9 -9
- package/src/authorjob.js +4 -4
- package/src/authorrun.js +25 -25
- package/src/authorscout.js +4 -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/draftspend.js +4 -4
- package/src/index.js +2 -4
- package/src/interviewrun.js +17 -25
- package/src/job.js +13 -7
- package/src/judged.js +11 -43
- package/src/keysfile.js +15 -7
- package/src/kinds.js +2 -2
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/panel/authorroutes.js +19 -14
- package/src/panel/authorsession.js +62 -58
- package/src/panel/index.html +520 -145
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +35 -15
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +2 -2
- 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/thinking.js +130 -0
- package/src/userrun.js +50 -73
- package/types/authorflow.d.ts +5 -5
- package/types/authorjob.d.ts +2 -2
- package/types/authorscout.d.ts +2 -2
- package/types/config.d.ts +23 -6
- package/types/draftspend.d.ts +8 -8
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +5 -5
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/panel/authorsession.d.ts +2 -9
- package/types/panel/server.d.ts +14 -4
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +34 -159
- package/types/providershapes.d.ts +10 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +6 -1
package/src/authorjob.js
CHANGED
|
@@ -113,7 +113,7 @@ import {
|
|
|
113
113
|
QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook,
|
|
114
114
|
runConfirmTurn, LANGUAGE_PICK_FIELD,
|
|
115
115
|
} from './authorflow.js';
|
|
116
|
-
import {
|
|
116
|
+
import { thinkingCallOptions, assertThinkingMode } from './thinking.js';
|
|
117
117
|
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
118
118
|
import {
|
|
119
119
|
DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
|
|
@@ -423,7 +423,7 @@ function composerRefusal(reds) {
|
|
|
423
423
|
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
|
|
424
424
|
* rates?: {in: number, out: number}|null, draftThinking?: 'on'|'off',
|
|
425
425
|
* onPhase?: (phase: string, data?: any) => void,
|
|
426
|
-
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number,
|
|
426
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
|
|
427
427
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
428
428
|
* authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
|
|
429
429
|
* signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
|
|
@@ -496,7 +496,7 @@ export async function authorCloseForJob({
|
|
|
496
496
|
answerQuestions = null,
|
|
497
497
|
}) {
|
|
498
498
|
// an operator-param guard: a typo never silently means "default" (throws before any work, even when the survey is supplied)
|
|
499
|
-
|
|
499
|
+
assertThinkingMode(draftThinking);
|
|
500
500
|
/** @type {any} */
|
|
501
501
|
const base = {
|
|
502
502
|
ok: false, refusal: null, verdictType: null, closeDecl: null, seedRef: null,
|
|
@@ -597,7 +597,7 @@ export async function authorCloseForJob({
|
|
|
597
597
|
// real tree. `attempts` is the ceiling the scout enforces on itself, so the
|
|
598
598
|
// reader knows how many the line could repeat for.
|
|
599
599
|
onPhase('scout', { attempts: SCOUT_ATTEMPTS });
|
|
600
|
-
const scoutCallOptions =
|
|
600
|
+
const scoutCallOptions = thinkingCallOptions(provider, draftThinking);
|
|
601
601
|
survey = await scoutFn({ workdir, provider, ceilingUsd, onCall, ...(rates ? { rates } : {}), ...(Object.keys(scoutCallOptions).length ? { callOptions: scoutCallOptions } : {}) });
|
|
602
602
|
// …and the survey's own VERDICT, said the moment it lands. `state` is what
|
|
603
603
|
// every downstream refusal keys on (F59: absent is never "no facts needed"),
|
package/src/authorrun.js
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
// in; a draft missing one, or naming one the provider factory
|
|
102
102
|
// does not know, dies here loud, listing the known table.
|
|
103
103
|
import { keysForDoor } from './keysfile.js';
|
|
104
|
-
import {
|
|
104
|
+
import { judgeRatesFor, judgeRowFor, keyNameFor, keyValueOf, ratesFor, ratesOfLine, resolveWorker, rowsForHome } from './providerrows.js';
|
|
105
105
|
import { ConfigError } from './config.js';
|
|
106
106
|
import {
|
|
107
107
|
readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, statSync,
|
|
@@ -466,13 +466,15 @@ export async function main(argv, deps = {}) {
|
|
|
466
466
|
try { return resolveProvider(draft?.provider); } catch (e) { return die(/** @type {Error} */ (e).message); }
|
|
467
467
|
})();
|
|
468
468
|
const PROVIDER_NAME = /** @type {string} */ (draft.provider);
|
|
469
|
-
// The
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
469
|
+
// The Settings LINE this authoring run works on (P8): the draft's own `model` on a ticked, priced line of a key row on the
|
|
470
|
+
// draft's API shape (and Base URL, when it names one). The row is the key, the Base URL and the old-max_tokens tick. Nothing is
|
|
471
|
+
// built in: a draft naming no model, or a model no row ticks, stops here at $0.
|
|
472
|
+
const keyCfg = rowsForHome(keys.home, env);
|
|
473
|
+
const W = resolveWorker(keyCfg, draft);
|
|
474
|
+
if (!W.ok) { err(W.say.includes('Nothing was spent') ? W.say : `${W.say} Nothing was spent.`); throw new ExitSignal(2); }
|
|
475
|
+
const MODEL = W.model;
|
|
476
|
+
const baseUrl = W.baseUrl;
|
|
477
|
+
const AUTHOR_ROW = W.row;
|
|
476
478
|
|
|
477
479
|
// THE JUDGE THIS RUN'S OWN WORKER RESOLVES TO (PRD item 32.1) — resolved ONCE,
|
|
478
480
|
// here, from the DRAFT, and reused for BOTH the compose-time stamp (the
|
|
@@ -508,12 +510,9 @@ export async function main(argv, deps = {}) {
|
|
|
508
510
|
|
|
509
511
|
// Secrets load from the environment; they never enter argv (a command line is
|
|
510
512
|
// world-readable on /proc) and they are never printed.
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
const authorEnv = applyConfiguredKey(env, PROVIDER_NAME, baseUrl, keyCfg);
|
|
515
|
-
const AUTHOR_ENV_KEY = keyNameFor(PROVIDER_NAME, baseUrl, keyCfg).name;
|
|
516
|
-
const apiKey = authorEnv[/** @type {NonNullable<typeof providerEntry>} */ (providerEntry).envKey];
|
|
513
|
+
// The key is the chosen row's own variable (P8: no built-in name). Names only.
|
|
514
|
+
const AUTHOR_ENV_KEY = AUTHOR_ROW.envName;
|
|
515
|
+
const apiKey = keyValueOf(env, AUTHOR_ENV_KEY);
|
|
517
516
|
if (!apiKey) { err(`${AUTHOR_ENV_KEY} not set (secrets load from the environment — never the tree, never argv)`); throw new ExitSignal(2); }
|
|
518
517
|
// F181 — a key that carries a line break/control char/stray whitespace (a
|
|
519
518
|
// two-line secret-store entry, e.g.) reads as "set" by the presence check
|
|
@@ -527,17 +526,17 @@ export async function main(argv, deps = {}) {
|
|
|
527
526
|
// loop, confirm turn, calibration judge). A bad price refuses here; it never falls back to the guess.
|
|
528
527
|
/** @type {ReturnType<typeof ratesFor>} */
|
|
529
528
|
let authorPrice = null;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
throw new ExitSignal(2);
|
|
529
|
+
{
|
|
530
|
+
const al = ratesOfLine(W.line);
|
|
531
|
+
authorPrice = al ? { ...al, envName: AUTHOR_ROW.envName } : null;
|
|
534
532
|
}
|
|
535
533
|
/** The judge's key follows the RESOLVED judge provider's own env var, with
|
|
536
534
|
* `JUDGE_API_KEY` as the role-named override in front (PRD item 32.3) — the same
|
|
537
535
|
* contract `scripts/run-u.mjs` keeps, so one story covers both surfaces. When the
|
|
538
536
|
* judge IS the authoring provider this is the key already read above. */
|
|
537
|
+
const judgeRowOf = (/** @type {{provider: string, model?: string|null}} */ j) => judgeRowFor(keyCfg, j, PROVIDER_NAME, AUTHOR_ROW);
|
|
539
538
|
const judgeKeyFor = (/** @type {string} */ providerName) => (
|
|
540
|
-
env.JUDGE_API_KEY ?? (providerName === PROVIDER_NAME ?
|
|
539
|
+
env.JUDGE_API_KEY ?? (providerName === PROVIDER_NAME ? apiKey : keyValueOf(env, judgeRowFor(keyCfg, { provider: providerName }, PROVIDER_NAME, AUTHOR_ROW)?.envName))
|
|
541
540
|
);
|
|
542
541
|
|
|
543
542
|
/** F6 — an unpriced call makes the TOTAL unknown, and unknown is reported as
|
|
@@ -579,7 +578,7 @@ export async function main(argv, deps = {}) {
|
|
|
579
578
|
// one) — the real judge identity is not resolved until after `validateJob`,
|
|
580
579
|
// further down, where a second call takes only its `.judgeProvider`.
|
|
581
580
|
const { provider } = buildRunnerProviders({
|
|
582
|
-
providerName: PROVIDER_NAME, apiKey, model: MODEL,
|
|
581
|
+
providerName: PROVIDER_NAME, apiKey, model: MODEL, baseUrl, legacyMaxTokens: AUTHOR_ROW.legacyMaxTokens,
|
|
583
582
|
judgeApiKey: apiKey, judgeModel: MODEL, judgeProviderName: PROVIDER_NAME, judgeBaseUrl: baseUrl,
|
|
584
583
|
});
|
|
585
584
|
emit('author-start', { runid, source: SOURCE, lang: LANG, verdictType: VERDICT, provider: PROVIDER_NAME, model: MODEL, baseUrl: baseUrl ?? null, job: draft?.job ?? null, timeoutMs: TIMEOUT_MS, ceilingUsd: CEILING_USD });
|
|
@@ -1115,7 +1114,7 @@ export async function main(argv, deps = {}) {
|
|
|
1115
1114
|
const judgeKeyValue = judgeKeyFor(judge.provider);
|
|
1116
1115
|
const judgeKeyProblem = judgeKeyValue ? apiKeyProblem(judgeKeyValue) : null;
|
|
1117
1116
|
if (judgeKeyProblem) {
|
|
1118
|
-
const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' :
|
|
1117
|
+
const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : (judgeRowOf(judge)?.envName ?? judge.provider);
|
|
1119
1118
|
err(`${judgeEnvName} ${judgeKeyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`);
|
|
1120
1119
|
// F186 — the scout above already ran and may have left its own gate
|
|
1121
1120
|
// audit in the tree. Under `process.exit()` this path used to skip
|
|
@@ -1149,9 +1148,10 @@ export async function main(argv, deps = {}) {
|
|
|
1149
1148
|
let judgeProvider = null;
|
|
1150
1149
|
if (judge) {
|
|
1151
1150
|
judgeProvider = buildRunnerProviders({
|
|
1152
|
-
providerName: PROVIDER_NAME, apiKey, model: MODEL,
|
|
1151
|
+
providerName: PROVIDER_NAME, apiKey, model: MODEL, baseUrl, legacyMaxTokens: AUTHOR_ROW.legacyMaxTokens,
|
|
1153
1152
|
judgeApiKey: judgeKeyFor(judge.provider), judgeModel: judge.model, judgeProviderName: judge.provider,
|
|
1154
|
-
judgeBaseUrl: judge.provider === PROVIDER_NAME ? baseUrl : undefined,
|
|
1153
|
+
judgeBaseUrl: judge.provider === PROVIDER_NAME ? baseUrl : (judgeRowOf(judge)?.baseUrl.trim() || undefined),
|
|
1154
|
+
judgeLegacyMaxTokens: judge.provider === PROVIDER_NAME ? AUTHOR_ROW.legacyMaxTokens : (judgeRowOf(judge)?.legacyMaxTokens ?? false),
|
|
1155
1155
|
}).judgeProvider;
|
|
1156
1156
|
}
|
|
1157
1157
|
if (judge) {
|
|
@@ -1170,7 +1170,7 @@ export async function main(argv, deps = {}) {
|
|
|
1170
1170
|
// number would silently widen it.
|
|
1171
1171
|
ceilingUsd: CEILING_USD,
|
|
1172
1172
|
priorCalls: [...metered],
|
|
1173
|
-
judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null }) : null,
|
|
1173
|
+
judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null, thinking: spec.thinking === true }) : null,
|
|
1174
1174
|
// the seam's other half (PRD item 32.1): the graded set is STAMPED with
|
|
1175
1175
|
// this, and the close later refuses to grade under any other identity.
|
|
1176
1176
|
judgeModel: judge?.model ?? null,
|
|
@@ -1297,7 +1297,7 @@ export async function main(argv, deps = {}) {
|
|
|
1297
1297
|
+ 'and run-u will enforce the remainder (Cap $ minus this) as ITS ceiling — pass --draft-spent-usd exactly as shown below, never a rounded or re-typed figure.');
|
|
1298
1298
|
}
|
|
1299
1299
|
out('\nTo run it (the same signature and gates as any other job — nothing here bypasses them):');
|
|
1300
|
-
out(` ${
|
|
1300
|
+
out(` ${AUTHOR_ENV_KEY}=... ${commandFor('run-u', deps.invokedAs)} --spec ${specFile} --approve ${hash}${draftSpentUsd > 0 ? ` --draft-spent-usd ${draftSpentUsd}${draftIncomplete ? ' --draft-spend-incomplete' : ''}` : ''}`);
|
|
1301
1301
|
emit('author-end', { outcome: 'prepared', specHash: hash });
|
|
1302
1302
|
}
|
|
1303
1303
|
}
|
package/src/authorscout.js
CHANGED
|
@@ -375,7 +375,7 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider,
|
|
|
375
375
|
* @param {{workdir: string, provider?: any,
|
|
376
376
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
377
377
|
* attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
|
|
378
|
-
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number,
|
|
378
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
|
|
379
379
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any, rates?: {in: number, out: number}}) => any,
|
|
380
380
|
* rates?: {in: number, out: number}|null, callOptions?: Record<string, any>,
|
|
381
381
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
@@ -400,7 +400,7 @@ export async function runAuthorScout({
|
|
|
400
400
|
onCall = () => {},
|
|
401
401
|
createLoop = defaultLoop,
|
|
402
402
|
rates = null,
|
|
403
|
-
// the drafter's per-call model options (`
|
|
403
|
+
// the drafter's per-call model options (`thinkingCallOptions`, src/thinking.js; e.g. thinking off) — {} = nothing extra, byte-identical
|
|
404
404
|
callOptions = {},
|
|
405
405
|
createSurveyor = defaultSurveyor,
|
|
406
406
|
}) {
|
|
@@ -436,8 +436,8 @@ export async function runAuthorScout({
|
|
|
436
436
|
// metered without the report going out: exactly the pairing invariant the
|
|
437
437
|
// raw already rides. It REPORTS and decides nothing; the ceiling still binds
|
|
438
438
|
// through `capStop` between calls, untouched.
|
|
439
|
-
// bare-agent's per-run `
|
|
440
|
-
onCall({ ...call, ...(r?.
|
|
439
|
+
// bare-agent's per-run `thinkingStatus` rides the reporting seam only, never the stored entry (see makeCostBook.add)
|
|
440
|
+
onCall({ ...call, ...(r?.thinkingStatus === undefined ? {} : { thinkingStatus: r.thinkingStatus }) });
|
|
441
441
|
return raws.push(scrubRaw({ label, attempt, text: r?.text })) - 1;
|
|
442
442
|
};
|
|
443
443
|
// TIGHTEN-ONLY: an operator may lower the ceiling, never raise it. A garbage
|
package/src/bundle.js
CHANGED
|
@@ -459,13 +459,16 @@ export function exportBundle({ spec, closeScripts, registryDir, outDir, bareloop
|
|
|
459
459
|
'',
|
|
460
460
|
'## Running this bundle',
|
|
461
461
|
'',
|
|
462
|
-
' ./node_modules/.bin/bareloop run . --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]',
|
|
462
|
+
' ./node_modules/.bin/bareloop run . --repo <path> [--model <id>] [--budget N] [--wall MIN] [--approve <bundleHash>]',
|
|
463
463
|
'',
|
|
464
464
|
'Answer these, in the order `bareloop run` asks them:',
|
|
465
465
|
'',
|
|
466
|
-
|
|
467
|
-
'
|
|
468
|
-
'
|
|
466
|
+
"1. Provider key — put your provider's API key in your keys file (the NAME your",
|
|
467
|
+
' Settings row uses), never on the command line. A bundle signed without a',
|
|
468
|
+
' model needs `--model <id>` of a line ticked in Settings > Providers (with',
|
|
469
|
+
' both prices), plus `--provider <KEY>` when two keys tick it. With neither,',
|
|
470
|
+
' `bareloop run` prints these questions and the bundleHash again, and spends',
|
|
471
|
+
' nothing.',
|
|
469
472
|
`2. Approve — the FIRST run on this machine requires --approve ${hash}`,
|
|
470
473
|
" (this bundle's own signed hash, shown above) to prove a human read and",
|
|
471
474
|
' accepted it before anything runs. Once that first run GREENS, the',
|
package/src/bundlerun.js
CHANGED
|
@@ -204,13 +204,16 @@ export async function bundleMain(args, {
|
|
|
204
204
|
workdir: worktree,
|
|
205
205
|
seed,
|
|
206
206
|
approve: approveHash,
|
|
207
|
+
// an exported bundle follows the run-u rule: a spec that names no model takes `--model <id>` (+ `--provider <KEY>` when two keys tick it)
|
|
208
|
+
...(typeof flags.model === 'string' ? { model: flags.model } : {}),
|
|
209
|
+
...(typeof flags.provider === 'string' ? { providerKey: flags.provider } : {}),
|
|
207
210
|
deps: { ...deps, env, out, err, ...(keysHome === undefined ? {} : { keysHome }) },
|
|
208
211
|
bundle: {
|
|
209
212
|
runid,
|
|
210
213
|
runDir,
|
|
211
214
|
// the bare re-invocation prefix; the leg's OWN tightened ceilings (only those the person passed) ride as
|
|
212
215
|
// DATA so the engine, which knows the halt kind and the spend, spells the printed line. Text only.
|
|
213
|
-
invoke: `bareloop run ${bundleDirArg} --repo ${repo}`,
|
|
216
|
+
invoke: `bareloop run ${bundleDirArg} --repo ${repo}${typeof flags.model === 'string' ? ` --model ${flags.model}` : ''}${typeof flags.provider === 'string' ? ` --provider ${flags.provider}` : ''}`,
|
|
214
217
|
tightened: {
|
|
215
218
|
...(envelope.budgetUsd === undefined ? {} : { budgetUsd: envelope.budgetUsd }),
|
|
216
219
|
...(flags.wall === undefined ? {} : { wallMin: Number(flags.wall) }),
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// run-u` drives (`src/userrun.js`): providers, keys (the keys file / env, any provider the
|
|
8
8
|
// bundle's spec names), judge, monthly limit and readout are that engine's. The test seam is
|
|
9
9
|
// the engine's too: `deps.provider` (and `providerFor`/`judgeProvider`) supplies a scripted
|
|
10
|
-
// provider and skips the real-key check (same as `tests/planrun
|
|
10
|
+
// provider and skips the real-key check (same as `tests/planrun-*.test.js`'s pattern).
|
|
11
11
|
//
|
|
12
12
|
// Money/arbiter rules the bundle door keeps: `--budget`/`--wall` only TIGHTEN a bundle's
|
|
13
13
|
// signed ceilings (`checkEnvelope`, M1); a bundle's `bundleHash` (the human-facing
|
package/src/config.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
//
|
|
1
|
+
// rewritten-from: fwdloop src/config.js@f9449e8 (validators `modelsProblem`, `providerFieldProblem`, `cleanBaseUrl`)
|
|
2
|
+
// PANEL-BUILD.md P4a item 2 / P8 — the ONE reader/writer of `~/.config/bareloop/config.json`.
|
|
2
3
|
//
|
|
3
|
-
// Every panel/CLI setting lives in this one file (R5): the monthly $ limit
|
|
4
|
-
//
|
|
4
|
+
// Every panel/CLI setting lives in this one file (R5): the monthly $ limit and the Providers
|
|
5
|
+
// rows' API shape / Base URL / old-max_tokens tick (`keys`) and each row's model lines (`models`).
|
|
5
6
|
// Key VALUES never live here — they live in the keys file (`src/keysfile.js`) and the
|
|
6
7
|
// shell env. The write path refuses a document that carries a secret-shaped string
|
|
7
8
|
// (the ONE inventory: `sweepSecretLiterals`, src/validate.js).
|
|
8
9
|
//
|
|
9
10
|
// Shape (unknown fields are kept, never dropped):
|
|
10
|
-
// { "monthlyLimitUsd": <number>, // absent = no limit, no check
|
|
11
|
-
// "
|
|
12
|
-
// "
|
|
11
|
+
// { "monthlyLimitUsd": <number > 0>, // absent = no limit, no check
|
|
12
|
+
// "keys": { "<ENV NAME>": { "shape": "<shape id>", "baseUrl": "http(s)://…", "legacyMaxTokens": true|false } },
|
|
13
|
+
// "models": { "<ENV NAME>": [ { "model": "<id>", "use": true|false, "inPerM": n, "outPerM": n } ] } }
|
|
14
|
+
// An older file's `keys.<ENV>.{name,priceInPerM,priceOutPerM}` is tolerated and read as ONE model
|
|
15
|
+
// line (`effectiveModels`, src/providerrows.js); the first write of that row's `models` list removes them.
|
|
16
|
+
// `modelsProblem` is the ONE rule for a row's model lines: a price is a PAIR or neither, a ticked
|
|
17
|
+
// line needs both. Nothing here sniffs a host or a model id.
|
|
13
18
|
//
|
|
14
|
-
// A missing file = defaults. An UNREADABLE file is reported (`problem`), never
|
|
19
|
+
// A missing file = defaults. An UNREADABLE or INVALID file is reported (`problem`), never
|
|
15
20
|
// papered over — the monthly check REFUSES on it rather than silently running with
|
|
16
21
|
// no limit. Written atomically (tmp + rename), mode 600.
|
|
17
22
|
import {
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
import { join, dirname } from 'node:path';
|
|
21
26
|
import { homedir } from 'node:os';
|
|
22
27
|
import { sweepSecretLiterals } from './validate.js';
|
|
28
|
+
import { SHAPE_IDS } from './providershapes.js';
|
|
23
29
|
|
|
24
30
|
/** A plain-words config failure (bad value, secret-shaped string, unreadable file). */
|
|
25
31
|
export class ConfigError extends Error {}
|
|
@@ -55,22 +61,106 @@ export function readConfig(opts = {}) {
|
|
|
55
61
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
56
62
|
return { config: {}, exists: true, problem: `${file} must hold a JSON object — fix or remove it` };
|
|
57
63
|
}
|
|
64
|
+
const bad = docProblem(parsed);
|
|
65
|
+
if (bad) return { config: {}, exists: true, problem: `${file}: ${bad} — fix or remove it` };
|
|
58
66
|
return { config: parsed, exists: true, problem: null };
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
|
|
71
|
+
/** The fields of a key row this module validates (others are kept untouched). */
|
|
72
|
+
const KEY_FIELDS = Object.freeze(['shape', 'baseUrl', 'legacyMaxTokens']);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One key-row field's value problem in plain words, or null when fine.
|
|
76
|
+
* @param {string} field `model` | `shape` | `baseUrl` | `legacyMaxTokens`
|
|
77
|
+
* @param {unknown} v
|
|
78
|
+
* @returns {string|null}
|
|
79
|
+
*/
|
|
80
|
+
export function providerFieldProblem(field, v) {
|
|
81
|
+
if (field === 'model') {
|
|
82
|
+
if (typeof v !== 'string' || v.length === 0 || v.length > 200 || /\s/.test(v)) return 'must be a model id: 1 to 200 characters, no spaces';
|
|
83
|
+
} else if (field === 'shape') {
|
|
84
|
+
if (typeof v !== 'string' || !SHAPE_IDS.includes(v)) return `must be one of ${SHAPE_IDS.join(', ')}`;
|
|
85
|
+
} else if (field === 'legacyMaxTokens') {
|
|
86
|
+
if (typeof v !== 'boolean') return 'must be true or false';
|
|
87
|
+
} else if (field === 'baseUrl') {
|
|
88
|
+
if (typeof v !== 'string' || !(v === '' || /^https?:\/\/\S+$/.test(v))) return 'must be empty or an http(s) address';
|
|
89
|
+
} else return 'is not a provider field';
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A Base URL as stored: trailing slashes dropped. @param {string} v @returns {string} */
|
|
94
|
+
export const cleanBaseUrl = (v) => v.replace(/\/+$/, '');
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* THE one rule for a row's model lines, used by config validation and the Settings routes. A price
|
|
98
|
+
* is a PAIR (`inPerM` and `outPerM`, each a finite number >= 0 — 0 / 0 typed is a chosen price) or
|
|
99
|
+
* neither (null / absent). A ticked line (`use`) needs both. Plain words, or null.
|
|
100
|
+
* @param {unknown} list
|
|
101
|
+
* @returns {string|null}
|
|
102
|
+
*/
|
|
103
|
+
export function modelsProblem(list) {
|
|
104
|
+
if (!Array.isArray(list)) return 'The model list must be a list of lines.';
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const line of list) {
|
|
107
|
+
if (line === null || typeof line !== 'object' || Array.isArray(line)) return 'Each model line must be an object.';
|
|
108
|
+
const l = /** @type {Record<string, any>} */ (line);
|
|
109
|
+
const bad = providerFieldProblem('model', l.model);
|
|
110
|
+
if (bad) return `The model name ${bad}.`;
|
|
111
|
+
if (seen.has(l.model)) return `The model "${l.model}" is listed twice.`;
|
|
112
|
+
seen.add(l.model);
|
|
113
|
+
if (typeof l.use !== 'boolean') return 'Use must be on or off.';
|
|
114
|
+
const has = (/** @type {string} */ f) => l[f] !== undefined && l[f] !== null;
|
|
115
|
+
const isNum = (/** @type {unknown} */ v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
116
|
+
if (has('inPerM') !== has('outPerM')) return 'Fill both price boxes or clear both.';
|
|
117
|
+
if (has('inPerM') && !(isNum(l.inPerM) && isNum(l.outPerM))) return 'A price must be a number, 0 or more.';
|
|
118
|
+
if (l.use && !has('inPerM')) return 'Fill both prices to tick this model.';
|
|
119
|
+
for (const k of Object.keys(l)) if (!['model', 'use', 'inPerM', 'outPerM'].includes(k)) return `A model line has no "${k}" field.`;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The first problem in a document's known fields, in plain words, or null.
|
|
126
|
+
* @param {Record<string, any>} doc
|
|
127
|
+
* @returns {string|null}
|
|
128
|
+
*/
|
|
129
|
+
function docProblem(doc) {
|
|
130
|
+
// monthlyLimitUsd is NOT judged here: `monthlyLimitOf` (src/monthly.js) owns its read-time sentence
|
|
131
|
+
if (doc.keys !== undefined) {
|
|
132
|
+
if (doc.keys === null || typeof doc.keys !== 'object' || Array.isArray(doc.keys)) return 'keys must be an object of key variable names';
|
|
133
|
+
for (const [env, row] of Object.entries(doc.keys)) {
|
|
134
|
+
if (!ENV_NAME_RE.test(env)) return `"${env}" is not a key variable name`;
|
|
135
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) return `keys.${env} must be an object`;
|
|
136
|
+
for (const f of KEY_FIELDS) {
|
|
137
|
+
if (!Object.hasOwn(row, f)) continue;
|
|
138
|
+
const bad = providerFieldProblem(f, /** @type {Record<string, any>} */ (row)[f]);
|
|
139
|
+
if (bad) return `keys.${env}.${f} ${bad}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (doc.models !== undefined) {
|
|
144
|
+
if (doc.models === null || typeof doc.models !== 'object' || Array.isArray(doc.models)) return 'models must be an object of key variable names';
|
|
145
|
+
for (const [env, list] of Object.entries(doc.models)) {
|
|
146
|
+
if (!ENV_NAME_RE.test(env)) return `models: "${env}" is not a key variable name`;
|
|
147
|
+
const bad = modelsProblem(list);
|
|
148
|
+
if (bad) return `models.${env}: ${bad}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
64
153
|
|
|
65
154
|
/**
|
|
66
155
|
* Apply a patch and write. `null` deletes a field. Top-level fields shallow-merge;
|
|
67
|
-
* `keys` merges per key variable name, then per field
|
|
68
|
-
* written; nothing is written on a refusal.
|
|
156
|
+
* `keys` merges per key variable name, then per field; `models.<ENV>` is REPLACED WHOLE (`null`
|
|
157
|
+
* clears it). Validated before anything is written; nothing is written on a refusal.
|
|
69
158
|
*
|
|
70
|
-
* Patch fields understood: `monthlyLimitUsd` (finite > 0), `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
159
|
+
* Patch fields understood: `monthlyLimitUsd` (finite > 0), `keys.<ENV>.{shape,baseUrl,legacyMaxTokens}`,
|
|
160
|
+
* `models.<ENV>` (a list judged by {@link modelsProblem}). The first write of a row's `models` list
|
|
161
|
+
* removes that row's older `name` / `priceInPerM` / `priceOutPerM` in the SAME write (never two
|
|
162
|
+
* sources); a retired top-level `anthropicBalanceNote` is dropped on any write. Any other field is
|
|
163
|
+
* kept, but the whole document is swept for secret-shaped strings first.
|
|
74
164
|
* @param {Record<string, any>} patch
|
|
75
165
|
* @param {{ home?: string }} [opts]
|
|
76
166
|
* @returns {Record<string, any>} the document as written
|
|
@@ -81,16 +171,14 @@ export function updateConfig(patch, opts = {}) {
|
|
|
81
171
|
/** @type {Record<string, any>} */
|
|
82
172
|
const next = { ...cur.config };
|
|
83
173
|
for (const [k, v] of Object.entries(patch)) {
|
|
84
|
-
if (k === 'keys') continue;
|
|
174
|
+
if (k === 'keys' || k === 'models') continue;
|
|
85
175
|
if (v === null) { delete next[k]; continue; }
|
|
86
176
|
if (k === 'monthlyLimitUsd' && !(typeof v === 'number' && Number.isFinite(v) && v > 0)) {
|
|
87
177
|
throw new ConfigError('monthlyLimitUsd must be a number above 0 (remove the limit to have none)');
|
|
88
178
|
}
|
|
89
|
-
if (k === 'anthropicBalanceNote' && !(typeof v === 'number' && Number.isFinite(v) && v >= 0)) {
|
|
90
|
-
throw new ConfigError('anthropicBalanceNote must be a number, 0 or more');
|
|
91
|
-
}
|
|
92
179
|
next[k] = v;
|
|
93
180
|
}
|
|
181
|
+
delete next.anthropicBalanceNote;
|
|
94
182
|
if (patch.keys !== undefined) {
|
|
95
183
|
if (patch.keys === null || typeof patch.keys !== 'object' || Array.isArray(patch.keys)) {
|
|
96
184
|
throw new ConfigError('keys must be an object of key variable names');
|
|
@@ -104,21 +192,33 @@ export function updateConfig(patch, opts = {}) {
|
|
|
104
192
|
const merged = { ...(keys[envName] && typeof keys[envName] === 'object' ? keys[envName] : {}) };
|
|
105
193
|
for (const [fk, fv] of Object.entries(/** @type {Record<string, any>} */ (fields))) {
|
|
106
194
|
if (fv === null) { delete merged[fk]; continue; }
|
|
107
|
-
|
|
108
|
-
throw new ConfigError(`keys.${envName}.name must be a model id (no spaces, up to 100 characters)`);
|
|
109
|
-
}
|
|
110
|
-
if (fk === 'shape' && !SHAPE_IDS.includes(fv)) {
|
|
111
|
-
throw new ConfigError(`keys.${envName}.shape must be one of ${SHAPE_IDS.join(', ')}`);
|
|
112
|
-
}
|
|
113
|
-
if (fk === 'baseUrl' && !(typeof fv === 'string' && (fv === '' || /^https?:\/\/\S+$/.test(fv)))) {
|
|
114
|
-
throw new ConfigError(`keys.${envName}.baseUrl must be blank or an http(s) URL`);
|
|
115
|
-
}
|
|
116
|
-
merged[fk] = fv;
|
|
195
|
+
merged[fk] = fk === 'baseUrl' && typeof fv === 'string' ? cleanBaseUrl(fv) : fv;
|
|
117
196
|
}
|
|
118
197
|
keys[envName] = merged;
|
|
119
198
|
}
|
|
120
199
|
next.keys = keys;
|
|
121
200
|
}
|
|
201
|
+
if (patch.models !== undefined) {
|
|
202
|
+
if (patch.models === null || typeof patch.models !== 'object' || Array.isArray(patch.models)) {
|
|
203
|
+
throw new ConfigError('models must be an object of key variable names');
|
|
204
|
+
}
|
|
205
|
+
/** @type {Record<string, any>} */
|
|
206
|
+
const models = { ...(next.models && typeof next.models === 'object' ? next.models : {}) };
|
|
207
|
+
for (const [envName, list] of Object.entries(patch.models)) {
|
|
208
|
+
if (!ENV_NAME_RE.test(envName)) throw new ConfigError(`"${envName}" is not a key variable name`);
|
|
209
|
+
if (list === null) { delete models[envName]; continue; }
|
|
210
|
+
models[envName] = list;
|
|
211
|
+
const row = next.keys?.[envName];
|
|
212
|
+
if (row && typeof row === 'object') {
|
|
213
|
+
// one source: the older single-model fields leave with the first saved list
|
|
214
|
+
const { name: _n, priceInPerM: _i, priceOutPerM: _o, ...rest } = row;
|
|
215
|
+
next.keys = { ...next.keys, [envName]: rest };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
next.models = models;
|
|
219
|
+
}
|
|
220
|
+
const problem = docProblem(next);
|
|
221
|
+
if (problem) throw new ConfigError(problem);
|
|
122
222
|
/** @type {string[]} */
|
|
123
223
|
const secretPaths = [];
|
|
124
224
|
sweepSecretLiterals(next, (_code, path) => { secretPaths.push(path); });
|
package/src/draftspend.js
CHANGED
|
@@ -24,10 +24,10 @@ export const DRAFT_LOG_FILE = 'draft-log.jsonl';
|
|
|
24
24
|
* One line of the drafting log. `no` = the step's position in the session's progress list (a step id can repeat).
|
|
25
25
|
* - `step-start` {no, id, label, at}
|
|
26
26
|
* - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
|
|
27
|
-
* - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway")
|
|
28
|
-
* - `call` {no, step, label, model, thinking,
|
|
29
|
-
* (`thinking` =
|
|
30
|
-
* call's run
|
|
27
|
+
* - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway" / "can't change it" / "not confirmed")
|
|
28
|
+
* - `call` {no, step, label, model, thinking, thinkingSupport, thinkingStatus?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
|
|
29
|
+
* (`thinking` = the tick's mode: off | on; `thinkingSupport` = bare-agent's lookup for the model: true | false | 'unverified';
|
|
30
|
+
* `thinkingStatus` = bare-agent's own verdict for the call's run: applied | unsupported | ignored | unknown, present only when thinkingMode was sent)
|
|
31
31
|
* (no token counts: the call path does not carry them without reworking the library, so none are recorded)
|
|
32
32
|
* @typedef {{kind: string, no: number, at: string, [k: string]: any}} DraftEvent
|
|
33
33
|
*/
|
package/src/index.js
CHANGED
|
@@ -148,16 +148,14 @@ export { answerReviewDoor } from './reviewdoor.js';
|
|
|
148
148
|
// SOFTGREEN module 1 — the judged floor's core. The pieces an adopter (and an
|
|
149
149
|
// integrating UI) genuinely needs: `resolveJudge` (PRD item 32.1 — WHICH model
|
|
150
150
|
// grades a job: the spec's signed `judge` override, else the job's own worker
|
|
151
|
-
// model, and no library fall-back),
|
|
152
|
-
// exported for adopters and still the right default for an anthropic-api job,
|
|
153
|
-
// but no longer read by any grading path), the rulebook a card SELECTS from,
|
|
151
|
+
// model, and no library fall-back; the old pinned-model constant is gone), the rulebook a card SELECTS from,
|
|
154
152
|
// the card gate, and the two halves themselves so a calibration harness can
|
|
155
153
|
// grade the whole pipe without running a close.
|
|
156
154
|
// `defaultJudgeLoop` (src/judged.js) is the one spelling of how this repo drives a
|
|
157
155
|
// judge; the runner reaches it through `runPlan`'s `judgeProvider`. It is internal:
|
|
158
156
|
// the package root does not export it.
|
|
159
157
|
export {
|
|
160
|
-
|
|
158
|
+
resolveJudge, JUDGE_RULES, JUDGE_RULE_IDS,
|
|
161
159
|
runLocate, decide,
|
|
162
160
|
// SOFTGREEN module 4 — what a legal SIGNED calibration set is. The SIZE is
|
|
163
161
|
// hamr's own ruling and a size change is a spec-level threshold change;
|
package/src/interviewrun.js
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
// non-repo source gets the form up to here and then an honest named stop:
|
|
88
88
|
// bareloop has no checks for that kind of job yet (ruling 7 → M4).
|
|
89
89
|
import { keysForDoor } from './keysfile.js';
|
|
90
|
-
import {
|
|
90
|
+
import { keyValueOf, namedRowsForHome, resolveWorker } from './providerrows.js';
|
|
91
91
|
import {
|
|
92
92
|
writeFileSync, mkdirSync, readFileSync, statSync,
|
|
93
93
|
} from 'node:fs';
|
|
@@ -192,6 +192,9 @@ export async function main(argv, deps = {}) {
|
|
|
192
192
|
// way an empty `--verdict`/`--provider` value already does elsewhere in this
|
|
193
193
|
// file.
|
|
194
194
|
const baseUrlArg = arg('base-url');
|
|
195
|
+
// `--model <id>`: the model id of a line ticked in Settings > Providers (with both prices). Written into the draft as `model`, so the
|
|
196
|
+
// job carries its own model; absent when never given (run-author then stops at $0 asking for one).
|
|
197
|
+
const modelArg = arg('model');
|
|
195
198
|
// `--lang` IS GONE (PRD item 33 M3, ruling 3): language is a FACT of the
|
|
196
199
|
// repository, read off its own manifest, never a flag a person sets. A
|
|
197
200
|
// `--lang` on the command line now is an operator error, stopped loud rather
|
|
@@ -686,6 +689,7 @@ export async function main(argv, deps = {}) {
|
|
|
686
689
|
// written as `null`/`''`; every provider constructor defaults its own
|
|
687
690
|
// endpoint on its own in that case (PRD item 28 ruling (d)).
|
|
688
691
|
...(baseUrlArg === null ? {} : { baseUrl: baseUrlArg }),
|
|
692
|
+
...(modelArg === null ? {} : { model: modelArg }),
|
|
689
693
|
cadence: { unit: 'day', every: 1 },
|
|
690
694
|
budgetUsd,
|
|
691
695
|
...(wallMin === null ? {} : { maxWallMs: Math.round(wallMin * 60_000) }),
|
|
@@ -782,29 +786,17 @@ export async function main(argv, deps = {}) {
|
|
|
782
786
|
// anything this script itself let through; the try/catch is only for the
|
|
783
787
|
// off-menu case `validateJob` reds but does not stop the write for (an
|
|
784
788
|
// admitted-but-uncrafted table entry would otherwise crash this print).
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// NO KEY, NO OFFER. `run-author.mjs` refuses without one and exits 2 at its own
|
|
797
|
-
// door, before a spine exists — so with the shell unkeyed this question has
|
|
798
|
-
// exactly one possible outcome for the person, and putting it anyway spends
|
|
799
|
-
// their attention on a choice they do not have. What is actionable instead is
|
|
800
|
-
// the command above and the one line that says how to make it work.
|
|
801
|
-
// F181 — a key that IS set can still carry a shape run-author will refuse
|
|
802
|
-
// (a line break/control char/stray whitespace, e.g. a two-line secret-store
|
|
803
|
-
// entry). A malformed key must not count as KEYED: this offer must read the
|
|
804
|
-
// same "will it actually run" question run-author itself asks at its door.
|
|
805
|
-
// P4a item 4 — the key variable the person picked in Settings stands in for the built-in one
|
|
806
|
-
const keyEnv = keys.home !== undefined ? applyConfiguredKey(env, PROVIDER, draft.baseUrl, rowsForHome(keys.home)) : env;
|
|
807
|
-
const rawKeyValue = providerEnvKey ? keyEnv[providerEnvKey] : undefined;
|
|
789
|
+
// The key is the Settings row's own variable (P8: no built-in name). The worker is the draft's API shape + Base URL + model line.
|
|
790
|
+
const keyRowsNow = keys.home !== undefined ? namedRowsForHome(keys.home) : [];
|
|
791
|
+
const worker = resolveWorker(keyRowsNow, { provider: PROVIDER, baseUrl: draft.baseUrl, model: modelArg });
|
|
792
|
+
const providerEnvKey = worker.ok ? worker.row.envName : null;
|
|
793
|
+
say(` ${providerEnvKey ?? 'YOUR_KEY_NAME'}=... ${commandFor('author', deps.invokedAs)} ${childArgs.join(' ')}`);
|
|
794
|
+
if (!worker.ok) say(` (${worker.say.replace(' Nothing was spent.', '')} — bareloop author stops at $0 until it is)`);
|
|
795
|
+
// NO KEY, NO OFFER. `run-author.mjs` refuses without one and exits 2 at its own door, before a spine exists — so with the shell
|
|
796
|
+
// unkeyed this question has exactly one possible outcome for the person, and putting it anyway spends their attention on a choice
|
|
797
|
+
// they do not have. F181 — a key that IS set can still carry a shape run-author will refuse (a line break/control char/stray
|
|
798
|
+
// whitespace). A malformed key must not count as KEYED: this offer reads the same "will it actually run" question run-author asks.
|
|
799
|
+
const rawKeyValue = providerEnvKey ? keyValueOf(env, providerEnvKey) : undefined;
|
|
808
800
|
const keyProblem = rawKeyValue ? apiKeyProblem(rawKeyValue) : null;
|
|
809
801
|
const KEYED = providerEnvKey !== null && Boolean(rawKeyValue) && !keyProblem;
|
|
810
802
|
if (!KEYED) {
|
|
@@ -814,7 +806,7 @@ export async function main(argv, deps = {}) {
|
|
|
814
806
|
} else {
|
|
815
807
|
say(providerEnvKey
|
|
816
808
|
? ` (${providerEnvKey} is not set in this shell — bareloop author refuses without it; secrets load from the environment, never the tree)`
|
|
817
|
-
:
|
|
809
|
+
: ' (no key row serves this model yet — bareloop author refuses it at $0; this is not a key problem)');
|
|
818
810
|
}
|
|
819
811
|
// The repair, in plain words and WITHOUT a command: which secret store a person
|
|
820
812
|
// keeps their key in is theirs, and printing one specific incantation would be
|