bareloop 0.39.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 +60 -0
- package/bareloop.context.md +137 -112
- package/package.json +2 -2
- package/src/authorflow.js +333 -58
- package/src/authoring.js +65 -2
- package/src/authorjob.js +57 -9
- package/src/authorreadout.js +50 -2
- package/src/authorrun.js +30 -27
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/bundle.js +7 -4
- package/src/bundlerun.js +4 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/cli.js +1 -1
- package/src/config.js +129 -29
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/index.js +2 -4
- package/src/interviewrun.js +73 -52
- package/src/job.js +50 -11
- package/src/jobblock.js +140 -0
- package/src/judged.js +12 -44
- package/src/keysfile.js +15 -7
- package/src/kinds.js +2 -2
- package/src/ledger.js +1 -1
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/panel/authorroutes.js +34 -19
- package/src/panel/authorsession.js +187 -72
- package/src/panel/index.html +699 -215
- package/src/panel/jobplan.js +107 -0
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +102 -28
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +6 -6
- package/src/promptregisters.js +1 -1
- package/src/providerrows.js +226 -141
- package/src/providers.js +58 -250
- package/src/providershapes.js +18 -0
- package/src/replay.js +1 -1
- package/src/reuse.js +1 -1
- package/src/root.js +1 -1
- package/src/run.js +3 -3
- package/src/selection.js +1 -1
- package/src/source.js +64 -1
- package/src/text.js +13 -0
- package/src/thinking.js +130 -0
- package/src/transport.js +1 -1
- package/src/userrun.js +53 -76
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +7 -1
- package/types/authorjob.d.ts +35 -3
- package/types/authorreadout.d.ts +17 -1
- package/types/authorscout.d.ts +5 -3
- package/types/config.d.ts +23 -6
- package/types/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +6 -6
- package/types/jobblock.d.ts +102 -0
- 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 +44 -15
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +34 -4
- package/types/planrun.d.ts +1 -1
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +35 -116
- package/types/providershapes.d.ts +10 -0
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +6 -1
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/declaredclose.js
CHANGED
|
@@ -102,7 +102,7 @@ export const DECLARED_GENRES = Object.freeze([TYPES_GENRE.name]);
|
|
|
102
102
|
* constrained to the names the genre OWNS, so it can never become an environment
|
|
103
103
|
* channel of its own.
|
|
104
104
|
*/
|
|
105
|
-
export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv', 'calibration']);
|
|
105
|
+
export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'refused', 'genreEnv', 'calibration']);
|
|
106
106
|
|
|
107
107
|
/** what a stored calibration set carries, ENUMERATED: the frozen cases and the judge
|
|
108
108
|
* tier that graded them. Both are inside the spec, so `jobSpecHash` covers both — which
|
|
@@ -272,13 +272,13 @@ export function declaredStages(closeDecl) {
|
|
|
272
272
|
*
|
|
273
273
|
* @param {any} closeDecl
|
|
274
274
|
* @param {{at?: string, listing?: string[]|null, deferListing?: boolean,
|
|
275
|
-
* catalogue?: Record<string, any>, verdictType?: string|null, writeScope?: string[]|null}} [opts]
|
|
275
|
+
* catalogue?: Record<string, any>, verdictType?: string|null, writeScope?: string[]|null, jobLines?: {n: number}[]|null}} [opts]
|
|
276
276
|
* @returns {{ok: boolean, reds: Red[], closeDecl: any, grounded: boolean,
|
|
277
277
|
* scoped: {scoped: boolean, via: string|null},
|
|
278
278
|
* ceiling: {class: string, kind: string, stage: string|null, locked: boolean}|null}}
|
|
279
279
|
*/
|
|
280
280
|
export function validateCloseDecl(closeDecl, opts = {}) {
|
|
281
|
-
const { at = 'closeDecl', listing = null, catalogue, verdictType = null, writeScope = null } = opts;
|
|
281
|
+
const { at = 'closeDecl', listing = null, catalogue, verdictType = null, writeScope = null, jobLines = null } = opts;
|
|
282
282
|
const deferListing = opts.deferListing === true;
|
|
283
283
|
/** @type {Red[]} */
|
|
284
284
|
const reds = [];
|
|
@@ -402,6 +402,7 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
402
402
|
guards: classGuards({ verdictType: picked, lang: closeDecl.lang }),
|
|
403
403
|
verdictType: picked,
|
|
404
404
|
writeScope,
|
|
405
|
+
jobLines,
|
|
405
406
|
envOwned: owned,
|
|
406
407
|
// POST-injection: an owned name is accepted only when its value is EXACTLY
|
|
407
408
|
// the recorded one. `{}` (nothing recorded) keeps the pre-injection rule, so
|
package/src/draftspend.js
CHANGED
|
@@ -24,7 +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
|
-
* - `
|
|
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)
|
|
28
31
|
* (no token counts: the call path does not carry them without reworking the library, so none are recorded)
|
|
29
32
|
* @typedef {{kind: string, no: number, at: string, [k: string]: any}} DraftEvent
|
|
30
33
|
*/
|
|
@@ -148,6 +151,8 @@ export function readDraftSteps(dir) {
|
|
|
148
151
|
x.status = ev.status === 'failed' ? 'failed' : 'done';
|
|
149
152
|
x.endedAt = ev.at;
|
|
150
153
|
if (typeof ev.label === 'string') x.label = ev.label;
|
|
154
|
+
} else if (ev.kind === 'step-label' && byNo.has(ev.no)) {
|
|
155
|
+
if (typeof ev.label === 'string') byNo.get(ev.no).label = ev.label;
|
|
151
156
|
} else if (ev.kind === 'step-reopen' && byNo.has(ev.no)) {
|
|
152
157
|
const x = byNo.get(ev.no);
|
|
153
158
|
x.status = 'running';
|
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
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// form (PRD item 33 M3 piece 3): Goal / Success / Guardrails, soft-green
|
|
32
32
|
// adding Judge Examples. Source and Destination are MECHANICAL fields —
|
|
33
33
|
// proven against the machine rather than typed — and their wording is
|
|
34
|
-
// ALSO the library's (`
|
|
34
|
+
// ALSO the library's (`INPUTS_FIELD`, `destinationFieldFor`,
|
|
35
35
|
// `src/authorflow.js`), so this module prints what the library hands it
|
|
36
36
|
// for all six fields, never re-worded here, never re-ordered, never stored;
|
|
37
37
|
// - the REFUSALS are the library's (`runInterview`): a locked class refuses at
|
|
@@ -87,9 +87,9 @@
|
|
|
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
|
-
writeFileSync, mkdirSync,
|
|
92
|
+
writeFileSync, mkdirSync, readFileSync, statSync,
|
|
93
93
|
} from 'node:fs';
|
|
94
94
|
import { createInterface } from 'node:readline';
|
|
95
95
|
import { spawnSync as realSpawnSync } from 'node:child_process';
|
|
@@ -101,13 +101,18 @@ import {
|
|
|
101
101
|
PLAIN_FOLDER_DEFERRED_FIELDS,
|
|
102
102
|
} from './authorjob.js';
|
|
103
103
|
import {
|
|
104
|
-
|
|
104
|
+
INPUTS_FIELD, JOB_BLOCK_HELP, destinationFieldFor, labelsFor, SOFTGREEN_JUDGE_EXAMPLES_KEY,
|
|
105
105
|
} from './authorflow.js';
|
|
106
|
+
import {
|
|
107
|
+
parseJobBlock, parseInputs, renderJobLines, renderExamples, jobBlockFitsCheck, signedJobLines,
|
|
108
|
+
} from './jobblock.js';
|
|
106
109
|
import { validateJob, validateBaseUrl, PROVIDERS } from './job.js';
|
|
107
110
|
import { resolveProvider, probeWarningLines, apiKeyProblem } from './providers.js';
|
|
108
111
|
import { scanSecrets, redactSecrets } from './validate.js';
|
|
109
112
|
import { detectLanguage } from './detectlang.js';
|
|
110
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
prepareSource, proveDestination, looksLikeRepoSource, missingDependencies, proveInputFiles,
|
|
115
|
+
} from './source.js';
|
|
111
116
|
import { parseCeiling, ceilingLine } from './authorreadout.js';
|
|
112
117
|
import { commandFor } from './invoke.js';
|
|
113
118
|
|
|
@@ -187,6 +192,9 @@ export async function main(argv, deps = {}) {
|
|
|
187
192
|
// way an empty `--verdict`/`--provider` value already does elsewhere in this
|
|
188
193
|
// file.
|
|
189
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');
|
|
190
198
|
// `--lang` IS GONE (PRD item 33 M3, ruling 3): language is a FACT of the
|
|
191
199
|
// repository, read off its own manifest, never a flag a person sets. A
|
|
192
200
|
// `--lang` on the command line now is an operator error, stopped loud rather
|
|
@@ -353,18 +361,26 @@ export async function main(argv, deps = {}) {
|
|
|
353
361
|
throw new ExitSignal(1);
|
|
354
362
|
}
|
|
355
363
|
|
|
356
|
-
// ── 1.
|
|
364
|
+
// ── 1. INPUTS — the form's first field (P7, docs/product/PANEL-BUILD.md; replaces Source) ───────────────
|
|
365
|
+
// Line 1 is the repo (the Source this door has always taken); lines 2+ are tracked files inside it, proven at $0
|
|
366
|
+
// by `proveInputFiles` — the same function the panel calls. A bad line re-asks, naming the line.
|
|
357
367
|
say('');
|
|
358
|
-
say('──
|
|
359
|
-
// LIBRARY WORDING
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
say('── INPUTS ' + '─'.repeat(58));
|
|
369
|
+
// LIBRARY WORDING — this script prints what `src/authorflow.js` hands it and writes none of its own.
|
|
370
|
+
say(INPUTS_FIELD.prompt);
|
|
371
|
+
/** @type {import('./jobblock.js').JobInput[]} */
|
|
372
|
+
let INPUTS = [];
|
|
373
|
+
/** @type {string} */
|
|
374
|
+
let SOURCE = '';
|
|
375
|
+
for (;;) {
|
|
376
|
+
const raw = await readAnswer('the inputs', 'Inputs is required — line 1 is the repo this job reads. Again:');
|
|
377
|
+
const parsed = parseInputs(raw);
|
|
378
|
+
if (!parsed.ok) { say(` ${parsed.error}`); continue; }
|
|
379
|
+
const proof = await proveInputFiles(parsed.inputs);
|
|
380
|
+
if (!proof.ok) { say(` ${proof.error}`); continue; }
|
|
381
|
+
INPUTS = parsed.inputs;
|
|
382
|
+
SOURCE = proof.repo;
|
|
383
|
+
break;
|
|
368
384
|
}
|
|
369
385
|
// the SAME rule `prepareSource` uses to route its own destination check
|
|
370
386
|
// (`looksLikeRepoSource`, `src/source.js`) — never a second, hand-typed copy
|
|
@@ -401,7 +417,7 @@ export async function main(argv, deps = {}) {
|
|
|
401
417
|
// ruling 7): a plain-folder job still gets the form; it
|
|
402
418
|
// only gets an honest "no checks yet" stop later (M4).
|
|
403
419
|
let LANG = 'none-detected';
|
|
404
|
-
if (
|
|
420
|
+
if (statSync(SOURCE).isDirectory()) {
|
|
405
421
|
const langResult = detectLanguage(SOURCE);
|
|
406
422
|
if (langResult.kind === 'ambiguous') {
|
|
407
423
|
// NO LONGER DIES (PRD item 33 M3 piece 4, step S5): `LANG` takes the
|
|
@@ -560,25 +576,38 @@ export async function main(argv, deps = {}) {
|
|
|
560
576
|
say(` asks ${REQUIRED.length} frozen question(s) for this class, then the numbers and names the job spec needs`);
|
|
561
577
|
|
|
562
578
|
// ── 3. the class's own frozen questions, one at a time ───────────────────────
|
|
579
|
+
// P7: "The job" is ONE multi-line answer — numbered lines with their `~` rules, parsed by `parseJobBlock` (the same
|
|
580
|
+
// parser the panel uses) and filed under key 1 as the numbered text the drafter reads. A rubric's second answer
|
|
581
|
+
// (the judge examples) is DERIVED from the `~ PASS:`/`~ FAIL:` lines of that block, never asked a second time.
|
|
563
582
|
/** @type {Record<string, string>} */
|
|
564
583
|
const answers = {};
|
|
584
|
+
/** @type {import('./jobblock.js').JobLine[]} */
|
|
585
|
+
let JOB_LINES = [];
|
|
586
|
+
const ASKED = REQUIRED.filter((n) => !(VERDICT === 'soft-green' && n === SOFTGREEN_JUDGE_EXAMPLES_KEY));
|
|
565
587
|
let asked = 0;
|
|
566
|
-
for (const n of
|
|
588
|
+
for (const n of ASKED) {
|
|
567
589
|
asked += 1;
|
|
568
590
|
say('');
|
|
569
|
-
say(`── ${asked} of ${
|
|
570
|
-
// the FIELD LABEL, from the signed table
|
|
571
|
-
//
|
|
572
|
-
// stays byte-identical to what the library holds, which is what the wizard
|
|
573
|
-
// test suite (`tests/run-interview.test.js`) asserts verbatim and in order.
|
|
574
|
-
// A key with no signed-table row prints no label.
|
|
591
|
+
say(`── ${asked} of ${ASKED.length} ${'─'.repeat(Math.max(0, 56 - String(asked).length))}`);
|
|
592
|
+
// the FIELD LABEL, from the signed table — printed on its own line so the literal `${n}. ${QUESTIONS[n]}` below
|
|
593
|
+
// stays byte-identical to what the library holds. A key with no signed-table row prints no label.
|
|
575
594
|
if (LABELS[n]) say(LABELS[n]);
|
|
576
|
-
// the frozen wording, printed as the library holds it. Numbered by the library's
|
|
577
|
-
// own key, so the number a person sees is the number their answer is filed under.
|
|
595
|
+
// the frozen wording, printed as the library holds it. Numbered by the library's own key.
|
|
578
596
|
say(`${n}. ${QUESTIONS[n]}`);
|
|
579
|
-
|
|
580
|
-
|
|
597
|
+
for (const h of JOB_BLOCK_HELP) say(` ${h}`);
|
|
598
|
+
for (;;) {
|
|
599
|
+
const raw = await readAnswer(`question ${n}`,
|
|
600
|
+
'that one is required — every question in this class\'s set has to be answered before a close can be authored from it. Again:');
|
|
601
|
+
const parsed = parseJobBlock(raw);
|
|
602
|
+
if (!parsed.ok) { say(` ${parsed.error}`); continue; }
|
|
603
|
+
const misfit = jobBlockFitsCheck(parsed.lines, VERDICT === 'soft-green');
|
|
604
|
+
if (misfit) { say(` ${misfit}`); continue; }
|
|
605
|
+
JOB_LINES = parsed.lines;
|
|
606
|
+
answers[n] = renderJobLines(parsed.lines);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
581
609
|
}
|
|
610
|
+
if (VERDICT === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(JOB_LINES);
|
|
582
611
|
|
|
583
612
|
// ── 4. the OPERATOR's half of the spec — the part nothing authors for you ────
|
|
584
613
|
say('');
|
|
@@ -660,6 +689,7 @@ export async function main(argv, deps = {}) {
|
|
|
660
689
|
// written as `null`/`''`; every provider constructor defaults its own
|
|
661
690
|
// endpoint on its own in that case (PRD item 28 ruling (d)).
|
|
662
691
|
...(baseUrlArg === null ? {} : { baseUrl: baseUrlArg }),
|
|
692
|
+
...(modelArg === null ? {} : { model: modelArg }),
|
|
663
693
|
cadence: { unit: 'day', every: 1 },
|
|
664
694
|
budgetUsd,
|
|
665
695
|
...(wallMin === null ? {} : { maxWallMs: Math.round(wallMin * 60_000) }),
|
|
@@ -667,6 +697,9 @@ export async function main(argv, deps = {}) {
|
|
|
667
697
|
// meaningless fence derived from an output directory.
|
|
668
698
|
...(writeScope === null ? {} : { writeScope }),
|
|
669
699
|
escalation: { mode: 'decision-ready' },
|
|
700
|
+
// P7: the person's own words, signed beside the model's goal sentence — what the Job tab and Reuse show back
|
|
701
|
+
jobLines: signedJobLines(JOB_LINES),
|
|
702
|
+
inputs: INPUTS,
|
|
670
703
|
// `tools` is deliberately OMITTED: an omitted menu hashes as the concrete current
|
|
671
704
|
// TOOL_MENU (MED-1), which pins WHICH menu was signed and makes a widening flip the
|
|
672
705
|
// hash. Naming one here would freeze today's list into the operator's own half.
|
|
@@ -753,29 +786,17 @@ export async function main(argv, deps = {}) {
|
|
|
753
786
|
// anything this script itself let through; the try/catch is only for the
|
|
754
787
|
// off-menu case `validateJob` reds but does not stop the write for (an
|
|
755
788
|
// admitted-but-uncrafted table entry would otherwise crash this print).
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
// NO KEY, NO OFFER. `run-author.mjs` refuses without one and exits 2 at its own
|
|
768
|
-
// door, before a spine exists — so with the shell unkeyed this question has
|
|
769
|
-
// exactly one possible outcome for the person, and putting it anyway spends
|
|
770
|
-
// their attention on a choice they do not have. What is actionable instead is
|
|
771
|
-
// the command above and the one line that says how to make it work.
|
|
772
|
-
// F181 — a key that IS set can still carry a shape run-author will refuse
|
|
773
|
-
// (a line break/control char/stray whitespace, e.g. a two-line secret-store
|
|
774
|
-
// entry). A malformed key must not count as KEYED: this offer must read the
|
|
775
|
-
// same "will it actually run" question run-author itself asks at its door.
|
|
776
|
-
// P4a item 4 — the key variable the person picked in Settings stands in for the built-in one
|
|
777
|
-
const keyEnv = keys.home !== undefined ? applyConfiguredKey(env, PROVIDER, draft.baseUrl, rowsForHome(keys.home)) : env;
|
|
778
|
-
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;
|
|
779
800
|
const keyProblem = rawKeyValue ? apiKeyProblem(rawKeyValue) : null;
|
|
780
801
|
const KEYED = providerEnvKey !== null && Boolean(rawKeyValue) && !keyProblem;
|
|
781
802
|
if (!KEYED) {
|
|
@@ -785,7 +806,7 @@ export async function main(argv, deps = {}) {
|
|
|
785
806
|
} else {
|
|
786
807
|
say(providerEnvKey
|
|
787
808
|
? ` (${providerEnvKey} is not set in this shell — bareloop author refuses without it; secrets load from the environment, never the tree)`
|
|
788
|
-
:
|
|
809
|
+
: ' (no key row serves this model yet — bareloop author refuses it at $0; this is not a key problem)');
|
|
789
810
|
}
|
|
790
811
|
// The repair, in plain words and WITHOUT a command: which secret store a person
|
|
791
812
|
// keeps their key in is theirs, and printing one specific incantation would be
|