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/inputlook.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// THE LOOK (PART 1c, docs/product/ITEM33-BUILD.md "3. The look", PRD item 33 RF19/RF20): before the drafter writes a single check, the AI
|
|
2
|
+
// LOOKS at the person's input - read only, bounded, with tools that can do nothing but search and read the input's own text - and
|
|
3
|
+
// reports, per job line, the facts it can check (name, shape, a label PICKED from the code-made list, one example value it saw) and
|
|
4
|
+
// what it could not find. This module holds the pure parts: the two read-only tools, the strict answer form, the prompt, and the
|
|
5
|
+
// code that turns the AI's report into a verified one. The paid loop around them is `runInputLook` in src/authorscout.js.
|
|
6
|
+
//
|
|
7
|
+
// LABELS ARE PICKED, NEVER TYPED: the AI names a label by its id on the list (src/labelscan.js) or by its exact words; a label that is
|
|
8
|
+
// not on the list is NOT carried into the report - it is moved to "could not check", named. Code never decides two words mean the
|
|
9
|
+
// same: the AI proposes, the list and the source prove.
|
|
10
|
+
|
|
11
|
+
import { scanLabels, labelOnList, renderLabelList, normLabel, LABEL_SHAPES } from './labelscan.js';
|
|
12
|
+
import { FACT_SHAPES, FACT_OPS } from './facts.js';
|
|
13
|
+
import { lineRules } from './rubricstep.js';
|
|
14
|
+
|
|
15
|
+
/** the name of the answer tool - the one place the AI's report enters */
|
|
16
|
+
export const LOOK_REPORT_TOOL = 'report_look';
|
|
17
|
+
/** how many hits one search returns at most, and how long a snippet or a read line is */
|
|
18
|
+
export const LOOK_MAX_HITS = 40;
|
|
19
|
+
const SNIPPET = 220;
|
|
20
|
+
const READ_LINE = 320;
|
|
21
|
+
const READ_LINES = 40;
|
|
22
|
+
const TAG = /\[[A-Za-z]+=[^\]\s]*\]/g;
|
|
23
|
+
const WS = /[\s ]+/g;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The input as the AI sees it: lines with any run of white space (U+202F included) made one space and the page reader's bracketed tags
|
|
27
|
+
* (`[ref=549]`) removed - an id that is only the page reader's, never part of a label.
|
|
28
|
+
* @param {string} text @returns {string[]}
|
|
29
|
+
*/
|
|
30
|
+
export function lookLines(text) {
|
|
31
|
+
return String(text ?? '').split('\n').map((l) => l.replace(TAG, ' ').replace(WS, ' ').trim());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The two read-only tools: search the input text, read a slice around a hit. They are closures over in-memory text and touch nothing
|
|
36
|
+
* else - read-only BY CONSTRUCTION, not by a fence. Several inputs: the `input` argument names which (its label).
|
|
37
|
+
* @param {Record<string, string>} texts label -> frozen text
|
|
38
|
+
* @returns {{name: string, description: string, parameters: any, execute: (a: any) => Promise<any>}[]}
|
|
39
|
+
*/
|
|
40
|
+
export function lookReadTools(texts) {
|
|
41
|
+
const labels = Object.keys(texts);
|
|
42
|
+
/** @type {Record<string, string[]>} */
|
|
43
|
+
const lines = Object.fromEntries(labels.map((l) => [l, lookLines(texts[l])]));
|
|
44
|
+
const which = (/** @type {any} */ a) => (typeof a?.input === 'string' && lines[a.input] ? a.input : (labels.length === 1 ? labels[0] : null));
|
|
45
|
+
const inputProp = { type: 'string', description: `the input's label - one of: ${labels.join(', ')}` };
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
name: 'search_input',
|
|
49
|
+
description: 'Case-insensitive search of one input for a word or phrase. Returns the line numbers and the start of each line that holds it. Search the person\'s own words AND other words that mean the same (nonstop / direct, San Francisco / SFO).',
|
|
50
|
+
parameters: { type: 'object', properties: { input: inputProp, text: { type: 'string', minLength: 1 }, max: { type: 'integer', minimum: 1, maximum: LOOK_MAX_HITS } }, required: ['text'] },
|
|
51
|
+
execute: async (a) => {
|
|
52
|
+
const name = which(a);
|
|
53
|
+
if (name === null) return { error: `name the input: ${labels.join(', ')}` };
|
|
54
|
+
const q = String(a?.text ?? '').toLowerCase().replace(WS, ' ').trim();
|
|
55
|
+
if (q === '') return { error: 'give the words to search for' };
|
|
56
|
+
const max = Math.min(Number.isInteger(a?.max) ? a.max : 20, LOOK_MAX_HITS);
|
|
57
|
+
/** @type {{line: number, text: string}[]} */
|
|
58
|
+
const hits = [];
|
|
59
|
+
let total = 0;
|
|
60
|
+
lines[name].forEach((l, i) => { if (l.toLowerCase().includes(q)) { total += 1; if (hits.length < max) hits.push({ line: i + 1, text: l.slice(0, SNIPPET) }); } });
|
|
61
|
+
return { input: name, total, hits };
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'read_input',
|
|
66
|
+
description: 'Read some lines of one input, starting at a line number (from a search).',
|
|
67
|
+
parameters: { type: 'object', properties: { input: inputProp, line: { type: 'integer', minimum: 1 }, count: { type: 'integer', minimum: 1, maximum: READ_LINES } }, required: ['line'] },
|
|
68
|
+
execute: async (a) => {
|
|
69
|
+
const name = which(a);
|
|
70
|
+
if (name === null) return { error: `name the input: ${labels.join(', ')}` };
|
|
71
|
+
const from = Math.max(1, Number.isInteger(a?.line) ? a.line : 1);
|
|
72
|
+
const n = Math.min(Number.isInteger(a?.count) ? a.count : 12, READ_LINES);
|
|
73
|
+
return { input: name, lines: lines[name].slice(from - 1, from - 1 + n).map((l, i) => ({ line: from + i, text: l.slice(0, READ_LINE) })) };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** the strict answer form (a JSON schema - never free prose with JSON inside) */
|
|
80
|
+
export function lookReportSchema() {
|
|
81
|
+
return {
|
|
82
|
+
type: 'object',
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
required: ['facts', 'prose', 'notFound'],
|
|
85
|
+
properties: {
|
|
86
|
+
facts: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
items: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
required: ['line', 'name', 'shape', 'source', 'example'],
|
|
92
|
+
properties: {
|
|
93
|
+
line: { type: 'integer', minimum: 1, description: 'the job line this fact belongs to' },
|
|
94
|
+
name: { type: 'string', minLength: 1, description: 'a short name, letters only' },
|
|
95
|
+
shape: { enum: [...FACT_SHAPES] },
|
|
96
|
+
source: { type: 'string', minLength: 1, description: 'the label of the input it is read from' },
|
|
97
|
+
label: { type: 'string', description: 'the words right before the value: the id (like t1) or the exact words of a label on the LIST for this input and shape - never a word you typed that is not on the list' },
|
|
98
|
+
value: { type: 'string', description: 'a phrase fact only: the fixed words, copied from the input exactly as it writes them (a place, a name, a word)' },
|
|
99
|
+
example: { type: 'string', description: 'ONE value you really saw in the input for this fact' },
|
|
100
|
+
rule: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
required: ['op'],
|
|
104
|
+
properties: { op: { enum: [...FACT_OPS] }, value: {}, lo: {}, hi: {} },
|
|
105
|
+
description: 'only where the person asked for a limit (or you made one out of their words and set aiProposal)',
|
|
106
|
+
},
|
|
107
|
+
aiProposal: { type: 'boolean', description: 'true when YOU made this number out of the person\'s words (for example a night window for "not a red-eye")' },
|
|
108
|
+
forRule: { type: 'integer', minimum: 1, description: 'the number of the ~ rule under that line this fact carries out (1 = the first), when it carries one - a rule you turned into a number or range yourself ("not a red-eye") is carried by a fact with aiProposal, never reported as not found' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
prose: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
description: 'job lines (and their ~ limits) that ask for written text - a summary, a report. They are not looked up.',
|
|
115
|
+
items: { type: 'object', additionalProperties: false, required: ['line'], properties: { line: { type: 'integer', minimum: 1 }, text: { type: 'string' } } },
|
|
116
|
+
},
|
|
117
|
+
notFound: {
|
|
118
|
+
type: 'array',
|
|
119
|
+
description: 'what you could not find in the input, and, when it is one ~ rule, which one',
|
|
120
|
+
items: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
required: ['line', 'why'],
|
|
124
|
+
properties: { line: { type: 'integer', minimum: 1 }, rule: { type: 'integer', minimum: 1, description: 'the number of the ~ rule under that line (1 = the first), when the thing not found is one rule' }, words: { type: 'string', description: 'the person\'s own words that you could not find' }, why: { type: 'string', minLength: 1 } },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What the AI is told: the numbered job, the inputs and their label lists, and what to do.
|
|
133
|
+
* @param {{jobLines: {n: number, text: string, rule?: string, rules?: string[]}[], labels: string[], lists: Record<string, string>}} o
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
export function lookPrompt({ jobLines, labels, lists }) {
|
|
137
|
+
const job = jobLines.map((l) => `${l.n}. ${l.text}${(l.rules ?? (l.rule ? l.rule.split('; ') : [])).map((r, i) => `\n ~ ${i + 1}. ${r}`).join('')}`).join('\n');
|
|
138
|
+
return `The person's job (line numbers start at 1; "~" lines are the person's rules for the line above, numbered under it):\n${job}\n\n`
|
|
139
|
+
+ `The inputs you can look at: ${labels.map((l) => JSON.stringify(l)).join(', ')}. You can only use search_input and read_input, then you MUST finish by calling ${LOOK_REPORT_TOOL} once.\n\n`
|
|
140
|
+
+ `${labels.map((l) => lists[l]).join('\n\n')}\n\n`
|
|
141
|
+
+ `Shapes a value can have: ${FACT_SHAPES.join(', ')} (phrase = fixed words the quote must contain; text = the words after a label).\n`
|
|
142
|
+
+ 'For each job line, find the facts the person asked for and report ONLY what you can check in the input:\n'
|
|
143
|
+
+ ' - Search the input with the person\'s words AND other words that mean the same, read the spots you find, and use what you know (for example what a red-eye is: propose a night time window and set aiProposal true).\n'
|
|
144
|
+
+ ' - A ~ rule whose meaning you can turn into a number or a range from what you know (a red-eye is a night departure; a weekend; business hours) is NOT something to ask the person about and NOT "notFound": report it as a fact on the time, date or amount the input does hold, with a between (or other) rule, aiProposal true and forRule set to that rule\'s number. Use notFound with a rule number only for intent you cannot know (for example "one per week" when the input has no weekly grouping).\n'
|
|
145
|
+
+ ' - Every place, date or name written in the job line itself is a fact too. Bind it to the label that sits right before it in the input - a direction like "from X to Y" is the label before the origin (for example Leaves) and the label before the destination (for example arrives at): report each as a phrase fact whose label is that label and whose value is the place EXACTLY as the input writes it.\n'
|
|
146
|
+
+ ' - A label is PICKED from the LIST above (its id or its exact words) - never typed. A label that is not on the list is refused. Never use the person\'s own words as a label unless the list holds them, never a one-time id like [ref=...], and never one result\'s own value.\n'
|
|
147
|
+
+ ' - Lines that ask for written prose (a summary, a short text) and the ~ limits on them (word count, paragraphs) are NOT looked up: put them in "prose" with their line number.\n'
|
|
148
|
+
+ ' - Whatever you cannot find in the input goes to "notFound" with the person\'s own words; when it is one ~ rule, give that rule\'s number.\n'
|
|
149
|
+
+ `Report only through ${LOOK_REPORT_TOOL}.`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @typedef {{line: number, name: string, shape: string, source: string, label?: string, labelId?: string, value?: string, phrase?: string, example: string, rule?: any, aiProposal?: boolean, forRule?: number}} LookFact */
|
|
153
|
+
/** @typedef {{line: number, rule?: number, words?: string, why: string}} LookNotFound */
|
|
154
|
+
/** @typedef {{facts: LookFact[], prose: {line: number, text?: string}[], notFound: LookNotFound[]}} LookReport */
|
|
155
|
+
|
|
156
|
+
/** @param {unknown} v @returns {v is Record<string, any>} */
|
|
157
|
+
const isRec = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Turn the AI's raw report into a VERIFIED one, by code, at $0. A fact is carried only when: its shape is a real shape, its source is
|
|
161
|
+
* a real input, and its label is ON THE LIST for that input and shape (an id is resolved to its exact words). Anything else is moved
|
|
162
|
+
* to `notFound` with a plain reason naming the word - never dropped silently. `prove` is the "found in the source" test of the
|
|
163
|
+
* phrase a phrase fact builds (label + value), supplied by the caller.
|
|
164
|
+
* @param {unknown} raw
|
|
165
|
+
* @param {{scans: Record<string, import('./labelscan.js').LabelScan>, jobLines: {n: number}[], prove?: (source: string, phrase: string) => number}} o
|
|
166
|
+
* @returns {LookReport}
|
|
167
|
+
*/
|
|
168
|
+
export function verifyLookReport(raw, { scans, jobLines, prove }) {
|
|
169
|
+
const rep = isRec(raw) ? raw : {};
|
|
170
|
+
const lineNos = new Set(jobLines.map((l) => l.n));
|
|
171
|
+
/** @type {LookFact[]} */
|
|
172
|
+
const facts = [];
|
|
173
|
+
/** @type {LookNotFound[]} */
|
|
174
|
+
const notFound = [];
|
|
175
|
+
const dropped = (/** @type {any} */ f, /** @type {string} */ why) => notFound.push({
|
|
176
|
+
line: Number.isInteger(f?.line) && lineNos.has(f.line) ? f.line : (jobLines[0]?.n ?? 1),
|
|
177
|
+
words: typeof f?.name === 'string' ? f.name : undefined,
|
|
178
|
+
why,
|
|
179
|
+
});
|
|
180
|
+
for (const f of Array.isArray(rep.facts) ? rep.facts : []) {
|
|
181
|
+
if (!isRec(f) || !Number.isInteger(f.line) || !lineNos.has(f.line)) { dropped(f, 'the look named a job line that does not exist'); continue; }
|
|
182
|
+
const name = typeof f.name === 'string' ? f.name.replace(/[^A-Za-z _-]/g, '').trim() : '';
|
|
183
|
+
if (name === '') { dropped(f, 'the look gave a fact no usable name'); continue; }
|
|
184
|
+
if (!FACT_SHAPES.includes(f.shape)) { dropped({ ...f, name }, `the look gave "${name}" a shape (${String(f.shape)}) that does not exist`); continue; }
|
|
185
|
+
const scan = typeof f.source === 'string' ? scans[f.source] : undefined;
|
|
186
|
+
if (!scan) { dropped({ ...f, name }, `the look read "${name}" from an input (${String(f.source)}) that is not one of yours`); continue; }
|
|
187
|
+
/** @type {LookFact} */
|
|
188
|
+
const out = { line: f.line, name, shape: f.shape, source: f.source, example: typeof f.example === 'string' ? f.example : '' };
|
|
189
|
+
if (typeof f.label === 'string' && f.label.trim() !== '') {
|
|
190
|
+
const row = labelOnList(scan, f.shape === 'phrase' ? 'text' : f.shape, f.label);
|
|
191
|
+
if (!row) { dropped({ ...f, name }, `the label "${normLabel(f.label)}" for "${name}" is not one of the labels found in "${f.source}", so it was not used`); continue; }
|
|
192
|
+
out.label = row.label;
|
|
193
|
+
out.labelId = row.id;
|
|
194
|
+
}
|
|
195
|
+
if (f.shape === 'phrase') {
|
|
196
|
+
const value = typeof f.value === 'string' ? normLabel(f.value) : '';
|
|
197
|
+
if (value === '') { dropped({ ...f, name }, `the look gave the phrase fact "${name}" no words`); continue; }
|
|
198
|
+
out.value = value;
|
|
199
|
+
out.phrase = out.label ? `${out.label} ${value}` : value;
|
|
200
|
+
if (prove && prove(f.source, out.phrase) < 1) { dropped({ ...f, name }, `"${out.phrase}" is not in the input "${f.source}"`); continue; }
|
|
201
|
+
}
|
|
202
|
+
if (isRec(f.rule)) out.rule = f.rule;
|
|
203
|
+
if (f.aiProposal === true) out.aiProposal = true;
|
|
204
|
+
if (Number.isInteger(f.forRule) && f.forRule >= 1) out.forRule = f.forRule;
|
|
205
|
+
facts.push(out);
|
|
206
|
+
}
|
|
207
|
+
for (const n of Array.isArray(rep.notFound) ? rep.notFound : []) {
|
|
208
|
+
if (!isRec(n) || !Number.isInteger(n.line) || !lineNos.has(n.line) || typeof n.why !== 'string') continue;
|
|
209
|
+
// a rule that a verified fact carries is checked, so it is not also "not found" (L3: a ~ rule is one or the other)
|
|
210
|
+
if (Number.isInteger(n.rule) && facts.some((x) => x.line === n.line && x.forRule === n.rule)) continue;
|
|
211
|
+
notFound.push({ line: n.line, ...(Number.isInteger(n.rule) ? { rule: n.rule } : {}), ...(typeof n.words === 'string' && n.words ? { words: n.words } : {}), why: n.why });
|
|
212
|
+
}
|
|
213
|
+
const prose = (Array.isArray(rep.prose) ? rep.prose : []).filter((p) => isRec(p) && Number.isInteger(p.line) && lineNos.has(p.line))
|
|
214
|
+
.map((p) => ({ line: p.line, ...(typeof p.text === 'string' ? { text: p.text.slice(0, 400) } : {}) }));
|
|
215
|
+
return { facts, prose, notFound };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The look's report as the DRAFTER reads it: one block, per job line, of what can be checked (with the exact words to copy) and what
|
|
220
|
+
* could not be found. Written by code from the verified report.
|
|
221
|
+
* @param {LookReport} report @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
export function renderLookReport(report) {
|
|
224
|
+
const byLine = new Map();
|
|
225
|
+
const add = (/** @type {number} */ n, /** @type {string} */ s) => byLine.set(n, [...(byLine.get(n) ?? []), s]);
|
|
226
|
+
for (const f of report.facts) {
|
|
227
|
+
const bits = [`${f.name} - ${f.shape}`, `from "${f.source}"`];
|
|
228
|
+
if (f.phrase !== undefined) bits.push(`the quote must say ${JSON.stringify(f.phrase)}`);
|
|
229
|
+
else if (f.label !== undefined) bits.push(`label ${JSON.stringify(f.label)} (id ${f.labelId})`);
|
|
230
|
+
else bits.push('no label (the value as the quote writes it)');
|
|
231
|
+
if (f.example) bits.push(`seen: ${JSON.stringify(f.example)}`);
|
|
232
|
+
if (f.rule) bits.push(`rule ${JSON.stringify(f.rule)}`);
|
|
233
|
+
if (f.aiProposal) bits.push("AI's proposal");
|
|
234
|
+
if (f.forRule !== undefined) bits.push(`carries ~ rule ${f.forRule}`);
|
|
235
|
+
add(f.line, `CAN CHECK: ${bits.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
for (const p of report.prose) add(p.line, 'WRITTEN TEXT (not looked up; keep its word/section counts)');
|
|
238
|
+
for (const n of report.notFound) add(n.line, `NOT FOUND${n.rule ? ` (rule ${n.rule})` : ''}: ${n.words ? `${JSON.stringify(n.words)} - ` : ''}${n.why}`);
|
|
239
|
+
const lines = [...byLine.keys()].sort((a, b) => a - b).map((n) => `line ${n}:\n${byLine.get(n).map((/** @type {string} */ s) => ` ${s}`).join('\n')}`);
|
|
240
|
+
return `WHAT THE LOOK AT THE INPUTS FOUND - write the facts from this; copy the label and phrase words exactly\n\n${lines.join('\n') || '(the look reported nothing)'}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The label scans and the rendered lists for the inputs the look will read.
|
|
245
|
+
* @param {Record<string, string>} texts
|
|
246
|
+
* @returns {{scans: Record<string, import('./labelscan.js').LabelScan>, lists: Record<string, string>}}
|
|
247
|
+
*/
|
|
248
|
+
export function labelListsOf(texts) {
|
|
249
|
+
/** @type {Record<string, import('./labelscan.js').LabelScan>} */
|
|
250
|
+
const scans = {};
|
|
251
|
+
/** @type {Record<string, string>} */
|
|
252
|
+
const lists = {};
|
|
253
|
+
for (const [label, text] of Object.entries(texts)) { scans[label] = scanLabels(text); lists[label] = renderLabelList(label, scans[label]); }
|
|
254
|
+
return { scans, lists };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export { LABEL_SHAPES };
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* L3: a reason the AI gave, cut only at a sentence end (never mid-sentence): whole sentences while they fit in `max` characters,
|
|
261
|
+
* and nothing at all if the first one alone does not fit. A trailing full stop is dropped (the caller closes the clause).
|
|
262
|
+
* @param {string} text @param {number} max @returns {string}
|
|
263
|
+
*/
|
|
264
|
+
export function wholeSentences(text, max) {
|
|
265
|
+
const sentences = text.replace(/\s+/g, ' ').trim().split(/(?<=[.!?])\s+/).filter(Boolean);
|
|
266
|
+
let out = '';
|
|
267
|
+
for (const s of sentences) {
|
|
268
|
+
const next = out === '' ? s : `${out} ${s}`;
|
|
269
|
+
if (next.length > max) break;
|
|
270
|
+
out = next;
|
|
271
|
+
}
|
|
272
|
+
return out.replace(/[.!?]+$/, '');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* I1 (hamr, 2026-10-13, "Ruled by hamr" 2): when the look shows the person's intent on one `~` rule was NOT captured - its words or
|
|
277
|
+
* meaning are not in the source - the person is asked about THAT rule at once, named by its line and rule. Only a look entry that names
|
|
278
|
+
* a rule number counts; anything else the look could not find is not asked piecemeal (it is Not checked, or the whole job is reviewed).
|
|
279
|
+
* The wording is the code's own, never the model's. The keep-or-drop and the cap are `normalizeQuestions` (src/authorflow.js).
|
|
280
|
+
* @param {LookNotFound[]} notFound @param {{n: number, text?: string, rule?: string, rules?: string[]}[]} jobLines
|
|
281
|
+
* @returns {{line: number, question: string}[]} every candidate question (the caller applies the machine's caps)
|
|
282
|
+
*/
|
|
283
|
+
export function ruleQuestions(notFound, jobLines) {
|
|
284
|
+
/** @type {{line: number, question: string}[]} */
|
|
285
|
+
const out = [];
|
|
286
|
+
/** @type {Set<string>} */
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
for (const n of Array.isArray(notFound) ? notFound : []) {
|
|
289
|
+
if (!Number.isInteger(n.rule)) continue;
|
|
290
|
+
const line = jobLines.find((l) => l.n === n.line);
|
|
291
|
+
const rule = line ? lineRules(line)[/** @type {number} */ (n.rule) - 1] : undefined;
|
|
292
|
+
if (!line || rule === undefined) continue;
|
|
293
|
+
const key = `${n.line}:${n.rule}`;
|
|
294
|
+
if (seen.has(key)) continue;
|
|
295
|
+
seen.add(key);
|
|
296
|
+
const words = n.words ? `${JSON.stringify(n.words)} (from your words)` : 'what this rule asks for';
|
|
297
|
+
const why = wholeSentences(String(n.why), 200);
|
|
298
|
+
out.push({ line: n.line, question: `Line ${n.line}, rule ${n.rule} - "${rule}": I could not find ${words} in your input${why ? ` (${why})` : ''}. What do you mean by it? Your answer is added as a new ~ rule on line ${n.line}.` });
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
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
|
package/src/job.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
17
|
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier, closeScriptCandidateToken } from './validate.js';
|
|
18
18
|
import { validateCloseDecl, DECLARED_CLOSE_CLASSES } from './declaredclose.js';
|
|
19
|
+
import { inputLabelsOf } from './rubricstep.js';
|
|
19
20
|
import { CLOSE_TIMEOUT_FLOOR_MS } from './closetimeout.js';
|
|
20
21
|
|
|
21
22
|
// The menus below ARE the close-authoring hierarchy (PRD §7) and ship frozen:
|
|
@@ -167,7 +168,7 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
|
|
|
167
168
|
// `steps` stays in the field list ONLY so a retired spec reds by name
|
|
168
169
|
// (`shape-retired`) instead of falling through to a generic unknown-field —
|
|
169
170
|
// the operator gets told what happened, not just that something is wrong.
|
|
170
|
-
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'judge', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools', 'jobLines', 'inputs'];
|
|
171
|
+
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'thinking', 'judge', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools', 'jobLines', 'inputs'];
|
|
171
172
|
/** the exact fields of a signed `judge` override — nested objects red unknown
|
|
172
173
|
* keys too, because no smuggling level exists in a signed spec */
|
|
173
174
|
const JUDGE_FIELDS = ['provider', 'model'];
|
|
@@ -329,7 +330,12 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
|
|
|
329
330
|
// on THAT model, not merely on the goal/close it runs against. The judge
|
|
330
331
|
// model stays library-pinned pending recalibration — out of scope here.
|
|
331
332
|
if (spec.model !== undefined && !isNonEmptyString(spec.model)) {
|
|
332
|
-
red('invalid-value', 'model', 'non-empty string — exact
|
|
333
|
+
red('invalid-value', 'model', 'non-empty string — the exact model id of a line ticked in Settings > Providers');
|
|
334
|
+
}
|
|
335
|
+
// THINKING (P8) — the job's "Thinking on" tick, OPTIONAL: absent means off (and an old spec keeps the hash it always had); present it is
|
|
336
|
+
// signed like `model`. It covers DRAFTING and the rubric JUDGE only — the worker is never handed a thinking option. A boolean, nothing else.
|
|
337
|
+
if (spec.thinking !== undefined && typeof spec.thinking !== 'boolean') {
|
|
338
|
+
red('invalid-value', 'thinking', 'true or false — the job\'s "Thinking on" tick (absent = off)');
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
// JUDGE — PRD item 32.1 (hamr, 2026-09-10: the judge "defaults to whatever
|
|
@@ -790,7 +796,7 @@ function validatePlanShape(spec, red, reds) {
|
|
|
790
796
|
// class the user picked (PRD v1.57 §2), so the gate cannot check D5 without
|
|
791
797
|
// it. An unknown or locked class is refused there, on its own axis, beside
|
|
792
798
|
// the counted `request-red` above.
|
|
793
|
-
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType, writeScope: spec.writeScope, jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null });
|
|
799
|
+
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType, writeScope: spec.writeScope, jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null, inputLabels: inputLabelsOf(spec.inputs) });
|
|
794
800
|
for (const r of cd.reds) reds.push(r);
|
|
795
801
|
if (demanded !== undefined && !CLASS_BY_CLOSE.declared.includes(demanded)) {
|
|
796
802
|
// the same laundering guard as the close-type hierarchy, one level up: a
|
|
@@ -984,15 +990,15 @@ export function checkApproval(job, approvals) {
|
|
|
984
990
|
* mismatched flag is a mistake worth stopping on, never a silent swap); the
|
|
985
991
|
* same id from both is not a conflict. Extracted from scripts/run-u.mjs so
|
|
986
992
|
* the decision is testable without running the script or touching a provider.
|
|
987
|
-
* @param {{ specModel?: string, flagModel?: string,
|
|
993
|
+
* @param {{ specModel?: string, flagModel?: string, suppliedModel?: string }} args
|
|
988
994
|
* `specModel` — `job.model` if the signed spec carries one, else undefined.
|
|
989
995
|
* `flagModel` — the model id an explicit `--model <tier>` resolved to, else
|
|
990
996
|
* undefined (a flag that was never passed, not merely a default tier name).
|
|
991
|
-
* `
|
|
997
|
+
* `suppliedModel` — a model the CALLER supplies when neither spec nor flag says (bareloop keeps none of its own).
|
|
992
998
|
* @returns {{ model: string, source: 'spec'|'flag'|'default' }}
|
|
993
|
-
* @throws {Error} when the spec names a model and the flag names a different one
|
|
999
|
+
* @throws {Error} when the spec names a model and the flag names a different one, or when no model is named at all
|
|
994
1000
|
*/
|
|
995
|
-
export function resolveWorkerModel({ specModel, flagModel,
|
|
1001
|
+
export function resolveWorkerModel({ specModel, flagModel, suppliedModel }) {
|
|
996
1002
|
if (specModel !== undefined) {
|
|
997
1003
|
if (flagModel !== undefined && flagModel !== specModel) {
|
|
998
1004
|
throw new Error(`spec names model "${specModel}" but --model resolved to "${flagModel}" — the signed spec wins; a mismatched --model flag is refused, never silently overridden. Drop --model or point it at the same model.`);
|
|
@@ -1000,5 +1006,6 @@ export function resolveWorkerModel({ specModel, flagModel, defaultModel }) {
|
|
|
1000
1006
|
return { model: specModel, source: 'spec' };
|
|
1001
1007
|
}
|
|
1002
1008
|
if (flagModel !== undefined) return { model: flagModel, source: 'flag' };
|
|
1003
|
-
|
|
1009
|
+
if (suppliedModel === undefined) throw new Error('no model is named — a job carries its own model, or the run is given --model <id> (one ticked in Settings > Providers with both prices)');
|
|
1010
|
+
return { model: suppliedModel, source: 'default' };
|
|
1004
1011
|
}
|
package/src/jobblock.js
CHANGED
|
@@ -113,16 +113,21 @@ export function renderInputs(inputs) {
|
|
|
113
113
|
* The two cross-checks between a parsed job block and the Check type: a RUBRIC job needs at least one `PASS:` and one
|
|
114
114
|
* `FAIL:` example (the calibration compile reads them); a DETERMINISTIC job has no judge, so an example line would be
|
|
115
115
|
* silently dropped — a red instead.
|
|
116
|
+
* PART 1a (hamr's ruling P1=B, docs/product/ITEM33-BUILD.md "Addendum 2026-10-12"): the examples are required of a rubric
|
|
117
|
+
* job ONLY when it holds a judged part. A rubric job on the person's own local files holds none in this build (its
|
|
118
|
+
* parts are counted or checked against a quote), so the caller says `judged: false` and a block without examples fits.
|
|
119
|
+
* Examples written anyway are never a red there — they simply have no judge to read them yet.
|
|
116
120
|
* @param {JobLine[]} lines
|
|
117
121
|
* @param {boolean} rubric
|
|
122
|
+
* @param {{judged?: boolean}} [o] `judged` (default true): does this job hold a judged part, so that it needs examples?
|
|
118
123
|
* @returns {string|null} the plain red, or null when the block fits the Check type
|
|
119
124
|
*/
|
|
120
|
-
export function jobBlockFitsCheck(lines, rubric) {
|
|
125
|
+
export function jobBlockFitsCheck(lines, rubric, { judged = true } = {}) {
|
|
121
126
|
const all = lines.flatMap((l) => l.examples.map((e) => ({ n: l.n, ...e })));
|
|
122
127
|
if (!rubric) {
|
|
123
128
|
return all.length ? `The job, line ${all[0].n}: PASS:/FAIL: examples are for a Rubric check — a Deterministic job has no judge to read them` : null;
|
|
124
129
|
}
|
|
125
|
-
if (!all.some((e) => e.verdict === 'PASS') || !all.some((e) => e.verdict === 'FAIL')) {
|
|
130
|
+
if (judged && (!all.some((e) => e.verdict === 'PASS') || !all.some((e) => e.verdict === 'FAIL'))) {
|
|
126
131
|
return 'The job: a Rubric check needs at least one "~ PASS:" line and one "~ FAIL:" line under a job line';
|
|
127
132
|
}
|
|
128
133
|
return null;
|
package/src/judged.js
CHANGED
|
@@ -66,43 +66,11 @@
|
|
|
66
66
|
import { quoteIn } from 'bareguard';
|
|
67
67
|
import { createRequire } from 'node:module';
|
|
68
68
|
import { extractArtifact, priceOf, scrubRaw, rateSourceFields } from './text.js';
|
|
69
|
-
import { resolveProvider
|
|
69
|
+
import { resolveProvider } from './providers.js';
|
|
70
|
+
import { thinkingCallOptions } from './thinking.js';
|
|
70
71
|
|
|
71
72
|
const require = createRequire(import.meta.url);
|
|
72
73
|
|
|
73
|
-
/**
|
|
74
|
-
* THE JUDGE'S HISTORICAL PIN. Until PRD item 32 (2026-09-10) this constant WAS
|
|
75
|
-
* the judge: every grading path imported it, so bareloop could run a worker on
|
|
76
|
-
* any provider and still judge only on Claude.
|
|
77
|
-
*
|
|
78
|
-
* It is no longer read by any grading path. The judge identity is now RESOLVED
|
|
79
|
-
* per job (`resolveJudge` below) and travels as data — `ctx.judgeModel` into
|
|
80
|
-
* `runJudgedFloor`, an explicit argument into the calibration gate, and
|
|
81
|
-
* `closeDecl.calibration.judgeModel` into the signed spec, exactly as before.
|
|
82
|
-
* Keeping the export is deliberate: it is part of the public API
|
|
83
|
-
* (`src/index.js`) and an adopter importing it should not break on a minor.
|
|
84
|
-
* What it means now is "the judge bareloop pinned before item 32", which is
|
|
85
|
-
* still the right DEFAULT for an anthropic-api job and still the only tier with
|
|
86
|
-
* established injection resistance upstream (BA-20's `INJECTION_BATTERY` is
|
|
87
|
-
* haiku-4.5 only).
|
|
88
|
-
*
|
|
89
|
-
* THE ENFORCEMENT IS UNCHANGED, and still lives in three wired places because a
|
|
90
|
-
* rule with no detector is prose (F45) — only the value being compared moved
|
|
91
|
-
* from this constant to the resolved identity:
|
|
92
|
-
* 1. the identity is STORED with the set it graded — `foldJudgedArtifacts`
|
|
93
|
-
* writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
|
|
94
|
-
* `jobSpecHash` covers it by construction. Change the judge and every
|
|
95
|
-
* signed judged spec hashes differently: the signature dies, and re-signing
|
|
96
|
-
* is what re-runs the calibration gate.
|
|
97
|
-
* 2. `validateCloseDecl` REQUIRES it on any stored set — a set nobody can
|
|
98
|
-
* attribute to a judge is a floor nobody can tell apart from a moved one.
|
|
99
|
-
* 3. `runJudgedFloor` refuses to grade when the stored identity is not the one
|
|
100
|
-
* about to grade, naming both. It does not degrade in either direction:
|
|
101
|
-
* grading on the new judge judges against a floor nobody calibrated, and
|
|
102
|
-
* the old judge is not on offer.
|
|
103
|
-
*/
|
|
104
|
-
export const JUDGE_MODEL = 'claude-haiku-4-5';
|
|
105
|
-
|
|
106
74
|
/**
|
|
107
75
|
* RESOLVE WHICH MODEL GRADES THIS JOB (PRD item 32.1).
|
|
108
76
|
*
|
|
@@ -122,7 +90,7 @@ export const JUDGE_MODEL = 'claude-haiku-4-5';
|
|
|
122
90
|
* item 31.4 a green job runs on its own key alone, and this is what lets a
|
|
123
91
|
* JUDGED one do the same.
|
|
124
92
|
*
|
|
125
|
-
* There is deliberately NO third fallback to
|
|
93
|
+
* There is deliberately NO third fallback to a library-pinned model. A caller that cannot
|
|
126
94
|
* say which model will grade has not wired a judge, and the grading path stops
|
|
127
95
|
* as a wiring gap rather than grading under an identity nobody chose — the same
|
|
128
96
|
* answer an absent `judgeLoop` already gets. A silent default here would be the
|
|
@@ -198,15 +166,13 @@ export function resolveJudge({ specJudge, workerProvider, workerModel } = {}) {
|
|
|
198
166
|
*
|
|
199
167
|
* @param {{provider?: string, model?: string, judge?: any}} source draft or spec
|
|
200
168
|
* @param {string} fallbackProviderName used only when `source.provider` is absent
|
|
201
|
-
* @param {(o: {specModel?: string, flagModel?: string,
|
|
169
|
+
* @param {(o: {specModel?: string, flagModel?: string, suppliedModel?: string}) => {model: string, source: string}} resolveWorkerModelFn `resolveWorkerModel` from `./job.js`
|
|
202
170
|
* @returns {{provider: string, model: string}}
|
|
203
171
|
*/
|
|
204
172
|
export function resolveJobJudge(source, fallbackProviderName, resolveWorkerModelFn) {
|
|
205
173
|
const providerName = source?.provider ?? fallbackProviderName;
|
|
206
|
-
|
|
207
|
-
const { model } = resolveWorkerModelFn({
|
|
208
|
-
specModel: source?.model, flagModel: undefined, defaultModel: providerEntry.tiers.sonnet,
|
|
209
|
-
});
|
|
174
|
+
resolveProvider(providerName); // an unknown shape is the named throw, never a silent default
|
|
175
|
+
const { model } = resolveWorkerModelFn({ specModel: source?.model, flagModel: undefined });
|
|
210
176
|
return resolveJudge({ specJudge: source?.judge, workerProvider: providerName, workerModel: model });
|
|
211
177
|
}
|
|
212
178
|
|
|
@@ -1089,9 +1055,11 @@ function topLevelFunctionNames(artifactText) {
|
|
|
1089
1055
|
* so the priced seam is the only legal one.
|
|
1090
1056
|
* `rates` is the customer's own price for the judge's row (USD per 1K, `ratesFor`'s
|
|
1091
1057
|
* `rates`); null/absent = bare-agent's built-in guess, exactly as before.
|
|
1092
|
-
*
|
|
1058
|
+
* `thinking` is the job's SIGNED "Thinking on" tick (false/absent = off): the judge follows the signed value — so the calibration gate
|
|
1059
|
+
* (10/10) and the later grading run with the SAME tick — and bare-agent decides what each model can switch (src/thinking.js).
|
|
1060
|
+
* @param {{provider: any, system: string, rates?: {in: number, out: number}|null, thinking?: boolean}} o
|
|
1093
1061
|
* @returns {{run: (msgs: any[], tools: any[], opts?: any) => Promise<any>}} */
|
|
1094
|
-
export const defaultJudgeLoop = ({ provider, system, rates = null }) => {
|
|
1062
|
+
export const defaultJudgeLoop = ({ provider, system, rates = null, thinking = false }) => {
|
|
1095
1063
|
const { Loop } = require('bare-agent');
|
|
1096
1064
|
// BA-21: the Loop's run RESULT carries no `rateSource` — only the per-call metering event
|
|
1097
1065
|
// does (`onLlmResult`). Capture it there and ride it on the result, so `runLocate` can
|
|
@@ -1107,7 +1075,7 @@ export const defaultJudgeLoop = ({ provider, system, rates = null }) => {
|
|
|
1107
1075
|
return {
|
|
1108
1076
|
run: async (/** @type {any[]} */ msgs, /** @type {any[]} */ tools, /** @type {any} */ opts) => {
|
|
1109
1077
|
seen = null;
|
|
1110
|
-
const res = await loop.run(msgs, tools, { ...
|
|
1078
|
+
const res = await loop.run(msgs, tools, { ...thinkingCallOptions(provider, thinking === true ? 'on' : 'off'), ...opts });
|
|
1111
1079
|
return res && typeof res === 'object' && seen ? { ...res, rateSource: /** @type {any} */ (seen).rateSource } : res;
|
|
1112
1080
|
},
|
|
1113
1081
|
};
|
package/src/keysfile.js
CHANGED
|
@@ -76,18 +76,24 @@ export function filledKeyNames(home) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
79
|
+
* The two comment lines a fresh keys file is created with — no provider name, no preset key.
|
|
80
|
+
* Each key the person types becomes a row in Settings > Providers.
|
|
81
|
+
*/
|
|
82
|
+
export const KEYS_FILE_BODY = '# bareloop keys — type NAME=value by hand; this file is never shown or written by bareloop.\n'
|
|
83
|
+
+ '# One line per provider key, e.g. MY_PROVIDER_API_KEY=... ; each key becomes a row in Settings > Providers.\n';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create the keys file with {@link KEYS_FILE_BODY} when — and only when — it does not
|
|
87
|
+
* exist (mode 600, dir 700). An existing file is never touched.
|
|
82
88
|
* @param {string} [home]
|
|
83
89
|
* @returns {boolean} true if it was created
|
|
84
90
|
*/
|
|
85
|
-
export function ensureKeysFile(
|
|
91
|
+
export function ensureKeysFile(home) {
|
|
86
92
|
const file = keysFilePath(home);
|
|
87
93
|
if (existsSync(file)) return false;
|
|
88
|
-
mkdirSync(keysHome(home), { recursive: true });
|
|
94
|
+
mkdirSync(keysHome(home), { recursive: true, mode: 0o700 });
|
|
89
95
|
try {
|
|
90
|
-
writeFileSync(file,
|
|
96
|
+
writeFileSync(file, KEYS_FILE_BODY, { mode: 0o600, flag: 'wx' });
|
|
91
97
|
} catch (/** @type {any} */ e) {
|
|
92
98
|
if (e?.code === 'EEXIST') return false; // lost a race: the file is there, untouched
|
|
93
99
|
throw e;
|
|
@@ -101,6 +107,8 @@ export function ensureKeysFile(presetNames, home) {
|
|
|
101
107
|
* @property {string[]} names the NAMES the file defines (sorted; never values)
|
|
102
108
|
* @property {string|null} warning a plain-words warning (file readable by others, or
|
|
103
109
|
* unreadable), else null — a warning, never a refusal
|
|
110
|
+
* @property {boolean} [unreadable] true only when the file is there but could not be read (the Settings row then
|
|
111
|
+
* reads "refused — see above"); absent otherwise
|
|
104
112
|
* @property {Record<string,string|undefined>} env shell env with the file filling the
|
|
105
113
|
* names the shell does not set. THE ONLY place values leave this module.
|
|
106
114
|
*/
|
|
@@ -127,7 +135,7 @@ export function loadKeysEnv(opts = {}) {
|
|
|
127
135
|
try {
|
|
128
136
|
parsed = parseKeysText(readFileSync(file, 'utf8'));
|
|
129
137
|
} catch (/** @type {any} */ e) {
|
|
130
|
-
return { exists: true, names: [], warning: `${file} could not be read (${e?.code ?? 'error'})`, env: { ...base } };
|
|
138
|
+
return { exists: true, names: [], warning: `${file} could not be read (${e?.code ?? 'error'})`, unreadable: true, env: { ...base } };
|
|
131
139
|
}
|
|
132
140
|
/** @type {Record<string,string|undefined>} */
|
|
133
141
|
const env = { ...base };
|