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
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// `src/panel/server.js`'s POST routes). Nothing here changes what a step
|
|
21
21
|
// means or what an answer does — only where the answer comes from.
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { judgePick, judgeRatesFor, judgeRowFor, keyValueOf, modelChoiceFor, ratesOfLine, rowsForHome } from '../providerrows.js';
|
|
24
24
|
import { ConfigError } from '../config.js';
|
|
25
25
|
import {
|
|
26
26
|
mkdirSync, existsSync, writeFileSync, readdirSync,
|
|
@@ -42,18 +42,23 @@ import {
|
|
|
42
42
|
import { prepareSigning } from '../authorjob.js';
|
|
43
43
|
import { checkCloseByteSignature } from '../close-integrity.js';
|
|
44
44
|
import {
|
|
45
|
-
questionsFor, requiredAnswersFor, makeLoopGenerate, CONFIRM_SYSTEM,
|
|
45
|
+
questionsFor, requiredAnswersFor, makeLoopGenerate, CONFIRM_SYSTEM, SOFTGREEN_JUDGE_EXAMPLES_KEY,
|
|
46
46
|
} from '../authorflow.js';
|
|
47
47
|
import {
|
|
48
48
|
resolveProvider, buildRunnerProviders, apiKeyProblem,
|
|
49
49
|
} from '../providers.js';
|
|
50
50
|
import { resolveJobJudge, defaultJudgeLoop } from '../judged.js';
|
|
51
|
+
import { assertThinkingMode, thinkingPlanFor, thinkingRowText } from '../thinking.js';
|
|
51
52
|
import { closeJudges } from '../kinds.js';
|
|
52
53
|
import { redactSecrets } from '../validate.js';
|
|
53
54
|
import { tallyCalls } from '../text.js';
|
|
54
55
|
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
55
|
-
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
56
|
+
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
56
57
|
import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
58
|
+
import {
|
|
59
|
+
parseJobBlock, parseInputs, jobBlockFitsCheck, renderJobLines, renderExamples, signedJobLines,
|
|
60
|
+
} from '../jobblock.js';
|
|
61
|
+
import { planFromConfirm } from './jobplan.js';
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
64
|
* THE PROGRESS LIST's one table (hamr 2026-10-04, "one place showing all"): every pipeline step the left Chat panel
|
|
@@ -62,6 +67,7 @@ import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
|
62
67
|
* @type {Readonly<Record<string, string>>}
|
|
63
68
|
*/
|
|
64
69
|
export const STEP_LABELS = Object.freeze({
|
|
70
|
+
thinking: 'thinking',
|
|
65
71
|
setup: 'checking setup',
|
|
66
72
|
copy: 'making worktree',
|
|
67
73
|
check: 'checking source',
|
|
@@ -105,6 +111,8 @@ export const PHASE_STEP = Object.freeze({
|
|
|
105
111
|
'rubric-scrubbed': 'calibrate',
|
|
106
112
|
});
|
|
107
113
|
|
|
114
|
+
export { thinkingRowText };
|
|
115
|
+
|
|
108
116
|
/** @typedef {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}} StepLine */
|
|
109
117
|
|
|
110
118
|
/** @param {StepLine[]} steps @param {string} id the LATEST line carrying this id (the list is a time-ordered log; an id can repeat) */
|
|
@@ -133,23 +141,22 @@ const SOURCE_CHECK_CODES = new Set([
|
|
|
133
141
|
'source-file-oversize', 'source-changed-after-scan', 'source-untracked-in-repo',
|
|
134
142
|
]);
|
|
135
143
|
|
|
144
|
+
/** the line under the plan bubble, in plain words (the bubble itself is drawn by the page's one `renderJobPlan`) */
|
|
145
|
+
export const PLAN_FOOTER = 'Sign & run to confirm this plan, or Revise to edit the boxes and draft again.';
|
|
146
|
+
|
|
136
147
|
/**
|
|
137
|
-
* The confirm turn's plan as
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @param {{
|
|
142
|
-
* @returns {string}
|
|
148
|
+
* The confirm turn's plan as a chat message (P7): a STRUCTURED `{role:'bot', kind:'plan', plan}` the page draws with the
|
|
149
|
+
* same renderer the Job tab uses — each numbered job line, its `~` rule, its drafted checks folded; the AI's own "not
|
|
150
|
+
* checked"; the always-on guards. `questions` (what the plan still asks) ride inside the plan. One owner of the shape.
|
|
151
|
+
* @param {{goal?: string, checks?: string[], checkItems?: {text: string, fromLine: number[]|null}[], notChecked?: string[], questions?: string[], protections?: string[]}} p the confirm turn's plan
|
|
152
|
+
* @param {{n: number, text: string, rule: string}[]|null} jobLines
|
|
153
|
+
* @returns {{role: 'bot', kind: 'plan', text: string, plan: ReturnType<typeof planFromConfirm> & {goal: string, questions: string[]}}}
|
|
143
154
|
*/
|
|
144
|
-
export function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if ((p.notChecked ?? []).length) sections.push(`#NOT CHECKED:\n${(p.notChecked ?? []).join(' · ')}`);
|
|
150
|
-
if ((p.questions ?? []).length) sections.push(`#QUESTIONS:\n${(p.questions ?? []).join(' · ')}`);
|
|
151
|
-
sections.push('Sign & run to confirm this plan, or Revise to edit the boxes and draft again.');
|
|
152
|
-
return sections.join('\n\n');
|
|
155
|
+
export function planMessage(p, jobLines) {
|
|
156
|
+
return {
|
|
157
|
+
role: 'bot', kind: 'plan', text: PLAN_FOOTER,
|
|
158
|
+
plan: { ...planFromConfirm(jobLines, p), goal: String(p.goal ?? ''), questions: [...(p.questions ?? [])] },
|
|
159
|
+
};
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/** @param {string} name @returns {boolean} kebab-case slug, same rule `job.js`'s `SLUG_RE` enforces */
|
|
@@ -176,7 +183,18 @@ export function jobNameTaken(name, opts = {}) {
|
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
/** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
|
|
179
|
-
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', '
|
|
186
|
+
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'provider', 'model', 'thinking', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The repo a card names: Inputs line 1 (P7 — the Source box is gone; the first input IS the repo). `''` when the Inputs
|
|
190
|
+
* box does not parse (the card validator refuses that before anything uses this).
|
|
191
|
+
* @param {any} card
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
export function cardSource(card) {
|
|
195
|
+
const p = parseInputs(card?.inputs);
|
|
196
|
+
return p.ok ? p.inputs[0].value : '';
|
|
197
|
+
}
|
|
180
198
|
|
|
181
199
|
/**
|
|
182
200
|
* Only the form's own fields, nothing else a request body might carry (`startFrom`, stray keys).
|
|
@@ -192,15 +210,15 @@ export function cardFields(card) {
|
|
|
192
210
|
|
|
193
211
|
/**
|
|
194
212
|
* REUSE WORKFLOW (hamr 2026-10-03, replaces P5 item 3's same-job rule; Model opened 2026-10-04): a reuse card has exactly
|
|
195
|
-
* FIVE open boxes —
|
|
213
|
+
* FIVE open boxes — Inputs, Destination, Model, $ cap, Time cap — and every other box is the signed workflow, shown greyed and never
|
|
196
214
|
* editable. The page greys them; THIS is what refuses (the server never trusts the page). Changing any locked box
|
|
197
215
|
* is a different job: Clear the card, which drafts.
|
|
198
216
|
*/
|
|
199
|
-
export const REUSE_OPEN_FIELDS = Object.freeze(['
|
|
217
|
+
export const REUSE_OPEN_FIELDS = Object.freeze(['inputs', 'destination', 'model', 'capUsd', 'maxWallMs']);
|
|
200
218
|
/** every card field that is NOT open on a reuse card, in card order */
|
|
201
219
|
export const REUSE_LOCKED_FIELDS = Object.freeze(CARD_FIELDS.filter((f) => !REUSE_OPEN_FIELDS.includes(f)));
|
|
202
220
|
const REUSE_LABELS = Object.freeze(/** @type {Record<string, string>} */ ({
|
|
203
|
-
jobName: 'Job name', checkType: 'Check type',
|
|
221
|
+
jobName: 'Job name', checkType: 'Check type', jobText: 'The job',
|
|
204
222
|
}));
|
|
205
223
|
|
|
206
224
|
/**
|
|
@@ -219,12 +237,12 @@ export function lockedFieldChanged(card, origin) {
|
|
|
219
237
|
|
|
220
238
|
/**
|
|
221
239
|
* The spec a reuse signs: a COPY of the origin's signed spec with ONLY the open fields set from the card —
|
|
222
|
-
* `writeScope` from Destination, `budgetUsd` from the $ cap, `maxWallMs` from the Time cap (absent when blank), and the
|
|
240
|
+
* `writeScope` from Destination, `inputs` from the Inputs box (P7), `budgetUsd` from the $ cap, `maxWallMs` from the Time cap (absent when blank), and the
|
|
223
241
|
* worker (`provider`/`baseUrl`/`model`) from the Model Name chosen in Settings > Providers, spelled exactly as normal
|
|
224
242
|
* authoring spells it (`baseUrl` only when the row has one; `model` only when the Name is not the provider's default
|
|
225
243
|
* tier). A RUBRIC (soft-green) origin with no explicit `judge` first has its judge pinned to the ORIGIN's resolved judge
|
|
226
244
|
* identity — the judge must not change because the worker did (`resolveJobJudge`, the one spelling); an explicit
|
|
227
|
-
* `judge` is kept; a deterministic job has no judge to move.
|
|
245
|
+
* `judge` is kept; a deterministic job has no judge to move. The repo (Inputs line 1) is in a spec only as `inputs`. The copy
|
|
228
246
|
* hashes to a NEW `jobSpecHash` and the SAME `workflowKey` — the caller checks the second after building.
|
|
229
247
|
* @param {any} originSpec
|
|
230
248
|
* @param {Record<string, any>} card
|
|
@@ -234,23 +252,25 @@ export function lockedFieldChanged(card, origin) {
|
|
|
234
252
|
export function buildReuseSpec(originSpec, card, rows) {
|
|
235
253
|
const spec = JSON.parse(JSON.stringify(originSpec));
|
|
236
254
|
spec.writeScope = String(card.destination ?? '').split(/[,\n]/).map((x) => x.trim()).filter(Boolean);
|
|
255
|
+
const ip = parseInputs(card.inputs);
|
|
256
|
+
if (ip.ok) spec.inputs = ip.inputs; else delete spec.inputs;
|
|
237
257
|
spec.budgetUsd = card.capUsd;
|
|
238
258
|
if (typeof card.maxWallMs === 'number' && Number.isFinite(card.maxWallMs) && card.maxWallMs > 0) spec.maxWallMs = card.maxWallMs;
|
|
239
259
|
else delete spec.maxWallMs;
|
|
240
|
-
const choice = rows && typeof card.model === 'string' ? modelChoiceFor(rows, card.model) : null;
|
|
260
|
+
const choice = rows && typeof card.model === 'string' ? modelChoiceFor(rows, { provider: card.provider, model: card.model }) : null;
|
|
241
261
|
if (choice) {
|
|
242
262
|
if (spec.verdictType === 'soft-green' && !spec.judge) spec.judge = resolveJobJudge(originSpec, originSpec.provider ?? choice.provider, resolveWorkerModel);
|
|
243
|
-
|
|
263
|
+
resolveProvider(choice.provider);
|
|
244
264
|
spec.provider = choice.provider;
|
|
245
265
|
if (choice.baseUrl) spec.baseUrl = choice.baseUrl; else delete spec.baseUrl;
|
|
246
|
-
|
|
266
|
+
spec.model = choice.name; // a job always carries its own model
|
|
247
267
|
}
|
|
248
268
|
return spec;
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
/**
|
|
252
272
|
* `$0` validation for a REUSE start: the signed job carries the goal, checks and guardrails, so those boxes are not
|
|
253
|
-
* required here — only what the new session itself needs:
|
|
273
|
+
* required here — only what the new session itself needs: Inputs, a Destination, a Model that is a Settings Name,
|
|
254
274
|
* and the cap. (The spec built from the open boxes is validated by the route with `validateJob`.)
|
|
255
275
|
* @param {any} card
|
|
256
276
|
* @param {{rows?: readonly import('../providerrows.js').KeyRow[]}} [opts]
|
|
@@ -258,8 +278,10 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
258
278
|
*/
|
|
259
279
|
export function validateReuseCard(card, opts = {}) {
|
|
260
280
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
261
|
-
|
|
262
|
-
if (
|
|
281
|
+
const pick = judgePick(opts.rows ?? [], { provider: card.provider, model: card.model });
|
|
282
|
+
if (!pick.ok) return { ok: false, error: pick.say };
|
|
283
|
+
const ip = parseInputs(card.inputs);
|
|
284
|
+
if (!ip.ok) return { ok: false, error: ip.error };
|
|
263
285
|
if (typeof card.destination !== 'string' || card.destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
264
286
|
if (typeof card.capUsd !== 'number' || !Number.isFinite(card.capUsd) || card.capUsd <= 0) {
|
|
265
287
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
@@ -277,7 +299,7 @@ export { nonRepoSourceMessage };
|
|
|
277
299
|
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
278
300
|
* source that does not peek as a repo) keeps today's behaviour: its Destination is an absolute output directory.
|
|
279
301
|
* @param {string} destination
|
|
280
|
-
* @param {boolean} isRepoLike `looksLikeRepoSource(card
|
|
302
|
+
* @param {boolean} isRepoLike `looksLikeRepoSource(cardSource(card))`
|
|
281
303
|
* @returns {string|null} the plain refusal, or null when the Destination is fine
|
|
282
304
|
*/
|
|
283
305
|
export function repoDestinationProblem(destination, isRepoLike) {
|
|
@@ -294,19 +316,22 @@ export function repoDestinationProblem(destination, isRepoLike) {
|
|
|
294
316
|
*/
|
|
295
317
|
export function validateJobCard(card, opts = {}) {
|
|
296
318
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
297
|
-
const { jobName, checkType, model,
|
|
319
|
+
const { jobName, checkType, model, destination, capUsd } = card;
|
|
298
320
|
if (!isSlug(jobName)) return { ok: false, error: 'Job name must be a kebab-case slug (letters, digits, dashes)' };
|
|
299
321
|
if (jobNameTaken(jobName, { jobsDir: opts.jobsDir })) {
|
|
300
322
|
return { ok: false, error: `a job named "${jobName}" already exists — P3 is new jobs only (Q4=A)` };
|
|
301
323
|
}
|
|
302
324
|
if (checkType !== 'deterministic' && checkType !== 'rubric') return { ok: false, error: 'Check type must be deterministic or rubric' };
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
}
|
|
325
|
+
const pick = judgePick(opts.rows ?? [], { provider: card.provider, model });
|
|
326
|
+
if (!pick.ok) return { ok: false, error: pick.say };
|
|
327
|
+
if (card.thinking !== undefined && typeof card.thinking !== 'boolean') return { ok: false, error: 'Thinking on must be on or off' };
|
|
328
|
+
const jb = parseJobBlock(card.jobText);
|
|
329
|
+
if (!jb.ok) return { ok: false, error: jb.error };
|
|
330
|
+
const misfit = jobBlockFitsCheck(jb.lines, checkType === 'rubric');
|
|
331
|
+
if (misfit) return { ok: false, error: misfit };
|
|
332
|
+
const ib = parseInputs(card.inputs);
|
|
333
|
+
if (!ib.ok) return { ok: false, error: ib.error };
|
|
334
|
+
if (typeof destination !== 'string' || destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
310
335
|
if (typeof capUsd !== 'number' || !Number.isFinite(capUsd) || capUsd <= 0) {
|
|
311
336
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
312
337
|
}
|
|
@@ -321,13 +346,24 @@ export function validateJobCard(card, opts = {}) {
|
|
|
321
346
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
322
347
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
323
348
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
324
|
-
* prepareSigningFn?: Function, npmCiFn?: Function, reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
349
|
+
* prepareSigningFn?: Function, npmCiFn?: Function, draftThinking?: "on"|"off", reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
325
350
|
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
326
351
|
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
327
352
|
* @returns {any} the session object
|
|
328
353
|
*/
|
|
329
354
|
export function createSession(card, deps = {}) {
|
|
330
355
|
const env = deps.env ?? process.env;
|
|
356
|
+
// The drafter's THINKING mode ('on' | 'off'): the card's "Thinking on" tick (unticked = 'off'), signed into the job only when ticked.
|
|
357
|
+
// `deps.draftThinking` is an operator override for tests/probes; a bad value throws here, before a session exists.
|
|
358
|
+
const draftThinking = assertThinkingMode(deps.draftThinking ?? (card.thinking === true ? 'on' : 'off'));
|
|
359
|
+
// P7: the person's own words, parsed ONCE by the one parser (src/jobblock.js). The card validator refused a malformed
|
|
360
|
+
// block before a session exists; a reuse card's job text is the origin's and is not drafted from.
|
|
361
|
+
const jobParsed = parseJobBlock(card.jobText);
|
|
362
|
+
const jobLines = jobParsed.ok ? jobParsed.lines : [];
|
|
363
|
+
const inputsParsed = parseInputs(card.inputs);
|
|
364
|
+
const INPUTS = inputsParsed.ok ? inputsParsed.inputs : [];
|
|
365
|
+
/** the repo — Inputs line 1 */
|
|
366
|
+
const SOURCE = INPUTS[0]?.value ?? '';
|
|
331
367
|
const sessionsRoot = deps.sessionsRoot ?? join(process.env.HOME ?? '/tmp', '.config', 'bareloop', 'panel-sessions');
|
|
332
368
|
// config.json lives beside the sessions dir (~/.config/bareloop) unless a home is injected
|
|
333
369
|
const configHome = deps.home ?? dirname(sessionsRoot);
|
|
@@ -422,6 +458,8 @@ export function createSession(card, deps = {}) {
|
|
|
422
458
|
|
|
423
459
|
/** @param {'bot'|'you'|'system'} role @param {string} text */
|
|
424
460
|
const say = (role, text) => { state.messages.push({ role, text }); };
|
|
461
|
+
/** a structured message (the plan bubble): the page draws it, the thread keeps it as data */
|
|
462
|
+
const sayPlan = (/** @type {any} */ plan) => { state.messages.push(planMessage(plan, jobLines)); };
|
|
425
463
|
|
|
426
464
|
/** @type {((value: string|null) => void)|null} */
|
|
427
465
|
let resolvePending = null;
|
|
@@ -447,8 +485,8 @@ export function createSession(card, deps = {}) {
|
|
|
447
485
|
resolvePending = resolveFn;
|
|
448
486
|
if (step.kind === 'language') say('bot', `${step.field?.prompt ?? 'Which language is this job about?'} (${(step.candidates ?? []).join(', ')})`);
|
|
449
487
|
else if (step.kind === 'menu') {
|
|
450
|
-
|
|
451
|
-
} else if (step.kind === 'answer') say('bot', `A question the plan raised (${step.index} of ${step.total}): ${step.question}`);
|
|
488
|
+
sayPlan(step.plan ?? {});
|
|
489
|
+
} else if (step.kind === 'answer') say('bot', `A question the plan raised (${step.index} of ${step.total})${Number.isInteger(step.line) ? `, about job line ${step.line}` : ''}: ${step.question}`);
|
|
452
490
|
else if (step.kind === 'goal') say('bot', 'Type the goal sentence yourself.');
|
|
453
491
|
});
|
|
454
492
|
|
|
@@ -511,8 +549,22 @@ export function createSession(card, deps = {}) {
|
|
|
511
549
|
// the call's label, the drafting model, its cost (null stays null: unpriced is never $0). One owner: this onCall.
|
|
512
550
|
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
513
551
|
const callNo = runningNo === -1 ? state.steps.length - 1 : runningNo;
|
|
552
|
+
// bare-agent's own verdict on the call's run — `thinkingStatus`: 'applied' | 'unsupported' | 'ignored' | 'unknown' — present only
|
|
553
|
+
// when thinkingMode was sent. The first drafting row then says what REALLY happened ("the model thought anyway", "can't change it",
|
|
554
|
+
// "not confirmed"); a row that moved to a worse word never moves back.
|
|
555
|
+
if (draftThinkingPlan !== null && call.thinkingStatus !== undefined && call.thinkingStatus !== thinkingStatusSeen
|
|
556
|
+
&& !(thinkingStatusSeen !== null && thinkingStatusSeen !== 'applied' && call.thinkingStatus === 'applied')) {
|
|
557
|
+
thinkingStatusSeen = call.thinkingStatus;
|
|
558
|
+
const row = latestStep(state.steps, 'thinking');
|
|
559
|
+
if (row) {
|
|
560
|
+
row.label = thinkingRowText({ mode: draftThinkingPlan.mode, support: draftThinkingPlan.support, status: call.thinkingStatus });
|
|
561
|
+
appendDraftLog(outDir, { kind: 'step-label', no: state.steps.indexOf(row), label: row.label, at: new Date().toISOString() });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
514
564
|
appendDraftLog(outDir, {
|
|
515
565
|
kind: 'call', no: callNo, step: state.steps[callNo]?.id ?? null, label: call.label, model: draftIdentity?.model ?? null,
|
|
566
|
+
thinking: draftThinkingPlan === null ? null : draftThinkingPlan.mode, thinkingSupport: draftThinkingPlan === null ? null : draftThinkingPlan.support,
|
|
567
|
+
...(call.thinkingStatus === undefined ? {} : { thinkingStatus: call.thinkingStatus }),
|
|
516
568
|
costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
|
|
517
569
|
});
|
|
518
570
|
try {
|
|
@@ -524,6 +576,11 @@ export function createSession(card, deps = {}) {
|
|
|
524
576
|
};
|
|
525
577
|
/** @type {string|null} */
|
|
526
578
|
let draftStartedAt = null;
|
|
579
|
+
/** what the drafting calls will carry for thinking — the tick's mode and bare-agent's support answer — set once the Model resolves; null before
|
|
580
|
+
* @type {ReturnType<typeof thinkingPlanFor>|null} */
|
|
581
|
+
let draftThinkingPlan = null;
|
|
582
|
+
/** the worst-so-far `thinkingStatus` a drafting call reported (null until one did) @type {string|null} */
|
|
583
|
+
let thinkingStatusSeen = null;
|
|
527
584
|
/** P6 item 1: the worktree this session made in the person's repo (null until made, and again once removed) @type {{repo: string, dir: string}|null} */
|
|
528
585
|
let madeWorktree = null;
|
|
529
586
|
/** a session that ends WITHOUT ever running (refused, errored, abandoned) takes its worktree with it; a signed one keeps it for the run */
|
|
@@ -549,34 +606,44 @@ export function createSession(card, deps = {}) {
|
|
|
549
606
|
|
|
550
607
|
async function run() {
|
|
551
608
|
// ── keys, at $0, before anything is prepared ────────────────────────
|
|
609
|
+
const keyRows = rowsForHome(configHome, env);
|
|
610
|
+
const pick = judgePick(keyRows, { provider: card.provider, model: card.model });
|
|
611
|
+
const modelChoice = pick.ok ? pick.choice : null;
|
|
612
|
+
// hamr 2026-10-11: the FIRST drafting row says whether the drafter thinks, before any other line ("checking setup" follows).
|
|
613
|
+
// A Reuse session drafts nothing, so it has no row. A provider shape that cannot be built is refused below, in words.
|
|
614
|
+
if (modelChoice && deps.reuse === undefined) {
|
|
615
|
+
try {
|
|
616
|
+
draftThinkingPlan = thinkingPlanFor({ providerName: modelChoice.provider, model: modelChoice.name, baseUrl: modelChoice.baseUrl ?? null }, draftThinking);
|
|
617
|
+
state.steps.push({ id: 'thinking', label: thinkingRowText({ mode: draftThinkingPlan.mode, support: draftThinkingPlan.support }), status: 'done', detail: '' });
|
|
618
|
+
logSteps();
|
|
619
|
+
} catch { draftThinkingPlan = null; }
|
|
620
|
+
}
|
|
552
621
|
stepStart('setup');
|
|
553
|
-
|
|
554
|
-
const modelChoice = modelChoiceFor(keyRows, card.model);
|
|
555
|
-
if (!modelChoice) { refuse(`Model "${card.model}" is not a Name in Settings > Providers. Stopped — nothing spent.`); return; }
|
|
622
|
+
if (!modelChoice) { refuse(pick.ok ? 'Pick a model. Nothing was spent.' : pick.say); return; }
|
|
556
623
|
draftIdentity = { provider: modelChoice.provider, baseUrl: modelChoice.baseUrl ?? null, model: modelChoice.name };
|
|
557
624
|
let providerEntry;
|
|
558
625
|
try { providerEntry = resolveProvider(modelChoice.provider); } catch (e) { refuse(/** @type {Error} */ (e).message); return; }
|
|
559
|
-
//
|
|
626
|
+
// the key is the chosen row's own variable (P8: no built-in name)
|
|
560
627
|
const keyCfg = keyRows;
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
const apiKey = draftEnv[providerEntry.envKey];
|
|
628
|
+
const draftKeyName = modelChoice.envName;
|
|
629
|
+
const apiKey = keyValueOf(env, draftKeyName);
|
|
564
630
|
if (!apiKey) { refuse(`${draftKeyName} not set. Stopped — nothing spent.`); return; }
|
|
565
631
|
// the customer's own price on this row (USD per 1M in config.json), resolved once here at $0 and
|
|
566
632
|
// handed to every model call this session makes; a bad price refuses, never falls back to the guess
|
|
567
|
-
/** @type {
|
|
633
|
+
/** @type {import('../providerrows.js').PriceResult} */
|
|
568
634
|
let draftPrice = null;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
635
|
+
{
|
|
636
|
+
const chosenRow = keyCfg.find((r) => r.envName === modelChoice.envName);
|
|
637
|
+
const dl = chosenRow?.models.find((l) => l.model === modelChoice.name);
|
|
638
|
+
const dp = dl ? ratesOfLine(dl) : null;
|
|
639
|
+
draftPrice = dp ? { ...dp, envName: modelChoice.envName } : null;
|
|
573
640
|
}
|
|
574
641
|
const keyProblem = apiKeyProblem(apiKey);
|
|
575
642
|
if (keyProblem) { refuse(`${draftKeyName} ${keyProblem}. Stopped — nothing spent.`); return; }
|
|
576
643
|
|
|
577
644
|
const verdictType = card.checkType === 'rubric' ? 'soft-green' : 'green';
|
|
578
645
|
const into = join(outDir, 'source-seed');
|
|
579
|
-
const isRepoLike = looksLikeRepoSource(
|
|
646
|
+
const isRepoLike = looksLikeRepoSource(SOURCE);
|
|
580
647
|
// hamr's ruling B (2026-10-05): ONE rule for both a fresh card and a reuse card, at $0, before the copy and any model call
|
|
581
648
|
const destProblem = repoDestinationProblem(card.destination, isRepoLike);
|
|
582
649
|
if (destProblem !== null) { refuse(destProblem); return; }
|
|
@@ -586,7 +653,7 @@ export function createSession(card, deps = {}) {
|
|
|
586
653
|
// P6 item 1 (hamr 2026-10-05, Q1 = A / Q3 = A): a REPO source gets a worktree in the person's own repo, made now at
|
|
587
654
|
// drafting start — `.bareloop/wt/<session id>` at their current commit. Edits they have not committed are not in it,
|
|
588
655
|
// and the progress line says so (a notice, never a refusal).
|
|
589
|
-
const repoRoot = isRepoLike ? nearestGitAncestor(resolve(
|
|
656
|
+
const repoRoot = isRepoLike ? nearestGitAncestor(resolve(SOURCE))?.dir ?? null : null;
|
|
590
657
|
if (repoRoot !== null) {
|
|
591
658
|
const dirty = uncommittedCount(repoRoot);
|
|
592
659
|
if (dirty > 0) {
|
|
@@ -601,7 +668,7 @@ export function createSession(card, deps = {}) {
|
|
|
601
668
|
// (relative) Destination is therefore handed to the door only when the source is repo-like.
|
|
602
669
|
const destIsDir = isAbsoluteDestination(card.destination);
|
|
603
670
|
const prep = await prepareSource({
|
|
604
|
-
source:
|
|
671
|
+
source: SOURCE, into, ...(isRepoLike || destIsDir ? { destination: card.destination } : {}),
|
|
605
672
|
...(repoRoot === null ? {} : { worktree: worktreePath(repoRoot, id) }),
|
|
606
673
|
});
|
|
607
674
|
if (repoRoot !== null && prep.stop === null && prep.manifest?.worktree) madeWorktree = { repo: repoRoot, dir: prep.manifest.worktree };
|
|
@@ -724,9 +791,10 @@ export function createSession(card, deps = {}) {
|
|
|
724
791
|
const judges = closeJudges(spec.closeDecl);
|
|
725
792
|
const judge = judges ? resolveJobJudge(spec, modelChoice.provider, resolveWorkerModel) : null;
|
|
726
793
|
let judgeProvider = null;
|
|
727
|
-
/** @type {
|
|
794
|
+
/** @type {import('../providerrows.js').PriceResult} */
|
|
728
795
|
let judgePrice = null;
|
|
729
796
|
if (judge) {
|
|
797
|
+
const judgeRow = judgeRowFor(keyCfg, judge, modelChoice.provider, keyCfg.find((r) => r.envName === modelChoice.envName) ?? null);
|
|
730
798
|
try {
|
|
731
799
|
judgePrice = judgeRatesFor(judge, modelChoice.provider, modelChoice.baseUrl, draftPrice, keyCfg);
|
|
732
800
|
} catch (e) {
|
|
@@ -734,22 +802,23 @@ export function createSession(card, deps = {}) {
|
|
|
734
802
|
refuse(`${e.message} — refusing before gate 4 spends anything`);
|
|
735
803
|
return;
|
|
736
804
|
}
|
|
737
|
-
const judgeKeyValue = env.JUDGE_API_KEY ?? (judge.provider === modelChoice.provider ?
|
|
805
|
+
const judgeKeyValue = env.JUDGE_API_KEY ?? (judge.provider === modelChoice.provider ? apiKey : keyValueOf(env, judgeRow?.envName));
|
|
738
806
|
const judgeKeyProblem = judgeKeyValue ? apiKeyProblem(judgeKeyValue) : null;
|
|
739
807
|
if (!judgeKeyValue || judgeKeyProblem) {
|
|
740
808
|
refuse(`the judge key is not usable (${judgeKeyProblem ?? 'not set'}) — refusing before gate 4 spends anything`);
|
|
741
809
|
return;
|
|
742
810
|
}
|
|
743
811
|
judgeProvider = buildRunnerProviders({
|
|
744
|
-
providerName: modelChoice.provider, apiKey, model: MODEL,
|
|
812
|
+
providerName: modelChoice.provider, apiKey, model: MODEL, baseUrl: modelChoice.baseUrl, legacyMaxTokens: modelChoice.legacyMaxTokens,
|
|
745
813
|
judgeApiKey: judgeKeyValue, judgeModel: judge.model, judgeProviderName: judge.provider,
|
|
746
|
-
judgeBaseUrl: judge.provider === modelChoice.provider ? modelChoice.baseUrl : undefined,
|
|
814
|
+
judgeBaseUrl: judge.provider === modelChoice.provider ? modelChoice.baseUrl : (judgeRow?.baseUrl.trim() || undefined),
|
|
815
|
+
judgeLegacyMaxTokens: judge.provider === modelChoice.provider ? modelChoice.legacyMaxTokens : (judgeRow?.legacyMaxTokens ?? false),
|
|
747
816
|
}).judgeProvider;
|
|
748
817
|
}
|
|
749
818
|
const signing = await prepareSigningFn({
|
|
750
819
|
spec, workdir: prep.tree, seedRef: seedRef, timeoutMs,
|
|
751
820
|
shellCapUsd: spec.budgetUsd, ceilingUsd: card.capUsd, priorCalls: [...metered],
|
|
752
|
-
judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null }) : null,
|
|
821
|
+
judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null, thinking: spec.thinking === true }) : null,
|
|
753
822
|
judgeModel: judge?.model ?? null,
|
|
754
823
|
onJudgeCost: (c) => onCall({ label: `${c.label}:${c.id}`, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds }),
|
|
755
824
|
});
|
|
@@ -814,10 +883,15 @@ export function createSession(card, deps = {}) {
|
|
|
814
883
|
schema: 'job-v1',
|
|
815
884
|
job: card.jobName,
|
|
816
885
|
description: `${card.jobName} — authored through the bareloop panel (${verdictType}, ${LANG})`,
|
|
886
|
+
// P7: the person's own words, signed beside the model's goal sentence
|
|
887
|
+
jobLines: signedJobLines(jobLines),
|
|
888
|
+
inputs: INPUTS,
|
|
817
889
|
provider: modelChoice.provider,
|
|
818
890
|
...(modelChoice.baseUrl ? { baseUrl: modelChoice.baseUrl } : {}),
|
|
819
|
-
//
|
|
820
|
-
|
|
891
|
+
// a job always carries its own model (P8: there is no provider default to omit it against)
|
|
892
|
+
model: modelChoice.name,
|
|
893
|
+
// the "Thinking on" tick is signed ONLY when ticked (unticked writes no field: the hash stays what it would have been)
|
|
894
|
+
...(card.thinking === true && draftThinkingPlan?.support !== false ? { thinking: true } : {}),
|
|
821
895
|
cadence: { unit: 'day', every: 1 },
|
|
822
896
|
budgetUsd: card.capUsd,
|
|
823
897
|
...(typeof card.maxWallMs === 'number' ? { maxWallMs: card.maxWallMs } : {}),
|
|
@@ -833,18 +907,18 @@ export function createSession(card, deps = {}) {
|
|
|
833
907
|
}
|
|
834
908
|
|
|
835
909
|
/** @type {Record<string, string>} */
|
|
836
|
-
const answers = { 1:
|
|
837
|
-
if (verdictType === 'soft-green') answers[
|
|
910
|
+
const answers = { 1: renderJobLines(jobLines) };
|
|
911
|
+
if (verdictType === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(jobLines);
|
|
838
912
|
const required = requiredAnswersFor(verdictType);
|
|
839
913
|
for (const n of required) {
|
|
840
914
|
if (!answers[n]) { refuse(`the ${verdictType} check type needs ${required.length} answers and one is missing (key ${n})`); return; }
|
|
841
915
|
}
|
|
842
916
|
|
|
843
917
|
const draftJudge = (() => {
|
|
844
|
-
try { return resolveJobJudge(draft, modelChoice.provider, resolveWorkerModel); } catch { return { provider: modelChoice.provider, model:
|
|
918
|
+
try { return resolveJobJudge(draft, modelChoice.provider, resolveWorkerModel); } catch { return { provider: modelChoice.provider, model: modelChoice.name }; }
|
|
845
919
|
})();
|
|
846
920
|
const { provider } = buildRunnerProviders({
|
|
847
|
-
providerName: modelChoice.provider, apiKey, model: MODEL,
|
|
921
|
+
providerName: modelChoice.provider, apiKey, model: MODEL, baseUrl: modelChoice.baseUrl, legacyMaxTokens: modelChoice.legacyMaxTokens,
|
|
848
922
|
judgeApiKey: apiKey, judgeModel: MODEL, judgeProviderName: modelChoice.provider, judgeBaseUrl: modelChoice.baseUrl,
|
|
849
923
|
});
|
|
850
924
|
// TEST SEAMS (see the constructor's own note): a test overrides
|
|
@@ -854,8 +928,35 @@ export function createSession(card, deps = {}) {
|
|
|
854
928
|
// machinery below with a deterministic fake, never a live provider call.
|
|
855
929
|
// no NEW model call starts after an abandon; a call already in flight books its usage via onCall and is discarded
|
|
856
930
|
const noCallAfterAbandon = (fn) => (...a) => { if (abandoned) throw new Error('abandoned — no new model call'); return fn(...a); };
|
|
857
|
-
const generate = noCallAfterAbandon(deps.generate ?? makeLoopGenerate(provider, { rates: draftPrice?.rates ?? null }));
|
|
858
|
-
const confirmGenerate = noCallAfterAbandon(deps.confirmGenerate ?? makeLoopGenerate(provider, { system: CONFIRM_SYSTEM, rates: draftPrice?.rates ?? null }));
|
|
931
|
+
const generate = noCallAfterAbandon(deps.generate ?? makeLoopGenerate(provider, { rates: draftPrice?.rates ?? null, draftThinking }));
|
|
932
|
+
const confirmGenerate = noCallAfterAbandon(deps.confirmGenerate ?? makeLoopGenerate(provider, { system: CONFIRM_SYSTEM, rates: draftPrice?.rates ?? null, draftThinking }));
|
|
933
|
+
|
|
934
|
+
// P7 item 5 (fwdloop am24/25): the last drafting try, after two failed ones, may ask. Each question comes through the same
|
|
935
|
+
// ask path as every other (the chat box + Send); the answer is REQUIRED (a blank one asks again, there is no skip) and is
|
|
936
|
+
// added WORD FOR WORD as a `~` rule on the job line it names — never rewritten. The lines are mutated in place, so the
|
|
937
|
+
// signed `jobLines`, the answers text and the plan card all read the same words.
|
|
938
|
+
const answerQuestions = async (/** @type {{questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}} */ o) => {
|
|
939
|
+
if (o.dropped.length) appendDraftLog(outDir, { kind: 'questions-dropped', no: Math.max(0, state.steps.length - 1), dropped: o.dropped, at: new Date().toISOString() });
|
|
940
|
+
const total = o.questions.length;
|
|
941
|
+
for (let k = 0; k < total; k += 1) {
|
|
942
|
+
const q = o.questions[k];
|
|
943
|
+
let text = '';
|
|
944
|
+
for (;;) {
|
|
945
|
+
const a = await ask({ kind: 'answer', index: k + 1, total, question: q.question, line: q.line });
|
|
946
|
+
if (a === null) return null;
|
|
947
|
+
// one `~` line is one line: the person's own words, newlines folded to spaces, nothing else touched
|
|
948
|
+
text = String(a).replace(/\s*\n\s*/g, ' ').trim();
|
|
949
|
+
if (text) break;
|
|
950
|
+
say('bot', 'An answer is needed here — there is no skip. Type it in the box below and press Send.');
|
|
951
|
+
}
|
|
952
|
+
const line = jobLines.find((l) => l.n === q.line);
|
|
953
|
+
if (line) { line.rule = line.rule ? `${line.rule}; ${text}` : text; line.rules.push(text); }
|
|
954
|
+
}
|
|
955
|
+
draft.jobLines = signedJobLines(jobLines);
|
|
956
|
+
answers[1] = renderJobLines(jobLines);
|
|
957
|
+
state.messages.push({ ...planMessage(o.confirmed ?? {}, jobLines), text: 'Your answers are in as ~ rules on those job lines. Drafting again.' });
|
|
958
|
+
return { jobLines, answers };
|
|
959
|
+
};
|
|
859
960
|
|
|
860
961
|
state.phase = 'drafting';
|
|
861
962
|
// no early "drafting" line: the real author phase (PHASE_STEP) starts it, carrying draftDetail (see onPhase)
|
|
@@ -866,11 +967,13 @@ export function createSession(card, deps = {}) {
|
|
|
866
967
|
judgeModel: draftJudge.model,
|
|
867
968
|
answers, verdictType, repoPath: prep.tree, lang: LANG,
|
|
868
969
|
questions: questionsFor(verdictType),
|
|
970
|
+
jobLines, inputs: INPUTS,
|
|
869
971
|
writeScope, provider, generate,
|
|
870
972
|
ceilingUsd: card.capUsd,
|
|
871
973
|
rates: draftPrice?.rates ?? null,
|
|
974
|
+
draftThinking,
|
|
872
975
|
onPhase, onCall,
|
|
873
|
-
ask, confirmGenerate, isRepo: true, langResult,
|
|
976
|
+
ask, answerQuestions, confirmGenerate, isRepo: true, langResult,
|
|
874
977
|
...(deps.scout ? { scout: deps.scout } : {}),
|
|
875
978
|
...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
|
|
876
979
|
});
|
|
@@ -893,7 +996,19 @@ export function createSession(card, deps = {}) {
|
|
|
893
996
|
return;
|
|
894
997
|
}
|
|
895
998
|
if (!authored.ok) {
|
|
896
|
-
|
|
999
|
+
// ITEM 6 (P7): every drafting stop says WHY in plain words (the reds travel into the drafting log beside it) — the bare
|
|
1000
|
+
// "Stopped: provider-red" of live session smv2bzdqmp0f8 carried a code and no reason.
|
|
1001
|
+
const stopReds = authored.reds ?? [];
|
|
1002
|
+
if (stopReds.length) {
|
|
1003
|
+
const no = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
1004
|
+
appendDraftLog(outDir, { kind: 'step-reds', no: no === -1 ? state.steps.length - 1 : no, stop: authored.stop ?? 'authoring-failed', reds: redsRecord(stopReds), at: new Date().toISOString() });
|
|
1005
|
+
}
|
|
1006
|
+
refuse(authored.stop === 'confirm-abandoned' || authored.stop === 'confirm-restart'
|
|
1007
|
+
? `Stopped: ${authored.stop}${authored.refusal ? ` — ${authored.refusal.detail}` : ''}`
|
|
1008
|
+
: draftStopText({
|
|
1009
|
+
stop: String(authored.stop ?? 'authoring-failed'), reds: stopReds, refusal: authored.refusal,
|
|
1010
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
1011
|
+
}), authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
897
1012
|
return;
|
|
898
1013
|
}
|
|
899
1014
|
|