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
|
@@ -20,14 +20,14 @@
|
|
|
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,
|
|
27
27
|
} from 'node:fs';
|
|
28
28
|
import { join, resolve, dirname } from 'node:path';
|
|
29
29
|
import {
|
|
30
|
-
prepareSource, proveDestination, missingDependencies, looksLikeRepoSource, nearestGitAncestor, nonRepoSourceMessage,
|
|
30
|
+
prepareSource, proveDestination, proveRubricInputs, looksLikeRubricInput, missingDependencies, looksLikeRepoSource, nearestGitAncestor, nonRepoSourceMessage,
|
|
31
31
|
} from '../source.js';
|
|
32
32
|
import {
|
|
33
33
|
worktreePath, uncommittedCount, shortHead, removeWorktree,
|
|
@@ -45,14 +45,17 @@ import {
|
|
|
45
45
|
questionsFor, requiredAnswersFor, makeLoopGenerate, CONFIRM_SYSTEM, SOFTGREEN_JUDGE_EXAMPLES_KEY,
|
|
46
46
|
} from '../authorflow.js';
|
|
47
47
|
import {
|
|
48
|
-
resolveProvider, buildRunnerProviders, apiKeyProblem,
|
|
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';
|
|
53
|
+
import { RUBRIC_LANG } from '../authoring.js';
|
|
54
|
+
import { rubricGoal, reconcileRubricPlan } from '../rubricstep.js';
|
|
52
55
|
import { redactSecrets } from '../validate.js';
|
|
53
56
|
import { tallyCalls } from '../text.js';
|
|
54
57
|
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
55
|
-
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
58
|
+
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, declarationRecord, calibrationSummary, tryRefusedText } from '../authorreadout.js';
|
|
56
59
|
import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
57
60
|
import {
|
|
58
61
|
parseJobBlock, parseInputs, jobBlockFitsCheck, renderJobLines, renderExamples, signedJobLines,
|
|
@@ -74,6 +77,7 @@ export const STEP_LABELS = Object.freeze({
|
|
|
74
77
|
reuse: 'reusing signed workflow',
|
|
75
78
|
read: 'reading repo',
|
|
76
79
|
scout: 'scouting repo',
|
|
80
|
+
look: 'looking at your input',
|
|
77
81
|
listing: 'listing files',
|
|
78
82
|
confirm: 'confirming plan',
|
|
79
83
|
draft: 'drafting',
|
|
@@ -83,6 +87,16 @@ export const STEP_LABELS = Object.freeze({
|
|
|
83
87
|
signed: 'signed hash',
|
|
84
88
|
});
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The same steps, worded for a job on the person's own local FILES (Part 1a): there is no worktree and no repo to read.
|
|
92
|
+
* Code-owned, one table beside STEP_LABELS; a step with no entry keeps its STEP_LABELS word.
|
|
93
|
+
* @type {Readonly<Record<string, string>>}
|
|
94
|
+
*/
|
|
95
|
+
export const FILE_JOB_STEP_LABELS = Object.freeze({
|
|
96
|
+
copy: 'copying input files',
|
|
97
|
+
read: 'reading input files',
|
|
98
|
+
});
|
|
99
|
+
|
|
86
100
|
/**
|
|
87
101
|
* The library's own `onPhase(name, ...)` calls (`src/authorjob.js`/`src/authorflow.js`) -> the step each one advances.
|
|
88
102
|
* A phase with no entry leaves the current step as it was.
|
|
@@ -100,6 +114,8 @@ export const PHASE_STEP = Object.freeze({
|
|
|
100
114
|
seed: 'read',
|
|
101
115
|
scout: 'scout',
|
|
102
116
|
'scout-done': 'scout',
|
|
117
|
+
look: 'look',
|
|
118
|
+
'look-done': 'look',
|
|
103
119
|
confirm: 'confirm',
|
|
104
120
|
'confirm-turn-done': 'confirm',
|
|
105
121
|
listing: 'listing',
|
|
@@ -110,19 +126,7 @@ export const PHASE_STEP = Object.freeze({
|
|
|
110
126
|
'rubric-scrubbed': 'calibrate',
|
|
111
127
|
});
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
* The first drafting row's words (hamr 2026-10-11) — code-owned, one owner, ALWAYS starting "thinking on" or "thinking off".
|
|
115
|
-
* `sent` is what the drafting calls carry (`draftThinkingFor`); `thoughtAnyway` = a call reported `thinkingApplied === false`
|
|
116
|
-
* after 'off' was sent (bare-agent saw reasoning in the reply).
|
|
117
|
-
* @param {'on'|'off'|'not-switchable'|'unverified'} sent @param {boolean} [thoughtAnyway]
|
|
118
|
-
* @returns {string}
|
|
119
|
-
*/
|
|
120
|
-
export function thinkingRowText(sent, thoughtAnyway = false) {
|
|
121
|
-
if (sent === 'off') return thoughtAnyway ? 'thinking on (the model thought anyway)' : 'thinking off';
|
|
122
|
-
if (sent === 'on') return 'thinking on';
|
|
123
|
-
if (sent === 'not-switchable') return "thinking on (this model can't turn it off)";
|
|
124
|
-
return 'thinking on (not verified for this model)';
|
|
125
|
-
}
|
|
129
|
+
export { thinkingRowText };
|
|
126
130
|
|
|
127
131
|
/** @typedef {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}} StepLine */
|
|
128
132
|
|
|
@@ -138,12 +142,13 @@ export function latestStep(steps, id) {
|
|
|
138
142
|
* earlier line keeps its status and detail). Existing lines are never moved. Re-starting the step that is still the
|
|
139
143
|
* LAST line just resumes that line.
|
|
140
144
|
* @param {StepLine[]} steps @param {string} id @param {string} [detail] (absent = keep what the line has)
|
|
145
|
+
* @param {Readonly<Record<string, string>>} [labels] the label table (a file job passes its own)
|
|
141
146
|
*/
|
|
142
|
-
export function advanceSteps(steps, id, detail) {
|
|
147
|
+
export function advanceSteps(steps, id, detail, labels = STEP_LABELS) {
|
|
143
148
|
for (const x of steps) if (x.status === 'running' && x.id !== id) x.status = 'done';
|
|
144
149
|
const last = steps.at(-1);
|
|
145
150
|
if (last && last.id === id) { last.status = 'running'; if (detail !== undefined) last.detail = detail; return; }
|
|
146
|
-
steps.push({ id, label:
|
|
151
|
+
steps.push({ id, label: labels[id] ?? id, status: 'running', detail: detail ?? '' });
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
/** source-door refusals that come from CHECKING the copied source (the scan/freeze), not from reaching it */
|
|
@@ -194,7 +199,7 @@ export function jobNameTaken(name, opts = {}) {
|
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
/** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
|
|
197
|
-
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'model', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
|
|
202
|
+
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'provider', 'model', 'thinking', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
|
|
198
203
|
|
|
199
204
|
/**
|
|
200
205
|
* The repo a card names: Inputs line 1 (P7 — the Source box is gone; the first input IS the repo). `''` when the Inputs
|
|
@@ -268,13 +273,13 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
268
273
|
spec.budgetUsd = card.capUsd;
|
|
269
274
|
if (typeof card.maxWallMs === 'number' && Number.isFinite(card.maxWallMs) && card.maxWallMs > 0) spec.maxWallMs = card.maxWallMs;
|
|
270
275
|
else delete spec.maxWallMs;
|
|
271
|
-
const choice = rows && typeof card.model === 'string' ? modelChoiceFor(rows, card.model) : null;
|
|
276
|
+
const choice = rows && typeof card.model === 'string' ? modelChoiceFor(rows, { provider: card.provider, model: card.model }) : null;
|
|
272
277
|
if (choice) {
|
|
273
278
|
if (spec.verdictType === 'soft-green' && !spec.judge) spec.judge = resolveJobJudge(originSpec, originSpec.provider ?? choice.provider, resolveWorkerModel);
|
|
274
|
-
|
|
279
|
+
resolveProvider(choice.provider);
|
|
275
280
|
spec.provider = choice.provider;
|
|
276
281
|
if (choice.baseUrl) spec.baseUrl = choice.baseUrl; else delete spec.baseUrl;
|
|
277
|
-
|
|
282
|
+
spec.model = choice.name; // a job always carries its own model
|
|
278
283
|
}
|
|
279
284
|
return spec;
|
|
280
285
|
}
|
|
@@ -289,7 +294,8 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
289
294
|
*/
|
|
290
295
|
export function validateReuseCard(card, opts = {}) {
|
|
291
296
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
292
|
-
|
|
297
|
+
const pick = judgePick(opts.rows ?? [], { provider: card.provider, model: card.model });
|
|
298
|
+
if (!pick.ok) return { ok: false, error: pick.say };
|
|
293
299
|
const ip = parseInputs(card.inputs);
|
|
294
300
|
if (!ip.ok) return { ok: false, error: ip.error };
|
|
295
301
|
if (typeof card.destination !== 'string' || card.destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
@@ -332,13 +338,18 @@ export function validateJobCard(card, opts = {}) {
|
|
|
332
338
|
return { ok: false, error: `a job named "${jobName}" already exists — P3 is new jobs only (Q4=A)` };
|
|
333
339
|
}
|
|
334
340
|
if (checkType !== 'deterministic' && checkType !== 'rubric') return { ok: false, error: 'Check type must be deterministic or rubric' };
|
|
335
|
-
|
|
341
|
+
const pick = judgePick(opts.rows ?? [], { provider: card.provider, model });
|
|
342
|
+
if (!pick.ok) return { ok: false, error: pick.say };
|
|
343
|
+
if (card.thinking !== undefined && typeof card.thinking !== 'boolean') return { ok: false, error: 'Thinking on must be on or off' };
|
|
336
344
|
const jb = parseJobBlock(card.jobText);
|
|
337
345
|
if (!jb.ok) return { ok: false, error: jb.error };
|
|
338
|
-
const misfit = jobBlockFitsCheck(jb.lines, checkType === 'rubric');
|
|
339
|
-
if (misfit) return { ok: false, error: misfit };
|
|
340
346
|
const ib = parseInputs(card.inputs);
|
|
341
347
|
if (!ib.ok) return { ok: false, error: ib.error };
|
|
348
|
+
// P1=B: PASS/FAIL examples are required of a rubric job only when it holds a judged part. A rubric on the person's own
|
|
349
|
+
// local files (first input not a repo) holds none in this build; a rubric on a repo composes the judged floor.
|
|
350
|
+
const rubricFiles = checkType === 'rubric' && looksLikeRubricInput(ib.inputs[0].value);
|
|
351
|
+
const misfit = jobBlockFitsCheck(jb.lines, checkType === 'rubric', { judged: !rubricFiles });
|
|
352
|
+
if (misfit) return { ok: false, error: misfit };
|
|
342
353
|
if (typeof destination !== 'string' || destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
343
354
|
if (typeof capUsd !== 'number' || !Number.isFinite(capUsd) || capUsd <= 0) {
|
|
344
355
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
@@ -354,16 +365,16 @@ export function validateJobCard(card, opts = {}) {
|
|
|
354
365
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
355
366
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
356
367
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
357
|
-
* prepareSigningFn?: Function, npmCiFn?: Function, draftThinking?: "on"|"off", reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
368
|
+
* prepareSigningFn?: Function, npmCiFn?: Function, lookFn?: Function|null, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>, draftThinking?: "on"|"off", reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
358
369
|
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
359
370
|
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
360
371
|
* @returns {any} the session object
|
|
361
372
|
*/
|
|
362
373
|
export function createSession(card, deps = {}) {
|
|
363
374
|
const env = deps.env ?? process.env;
|
|
364
|
-
// The drafter's THINKING
|
|
365
|
-
//
|
|
366
|
-
const draftThinking =
|
|
375
|
+
// The drafter's THINKING mode ('on' | 'off'): the card's "Thinking on" tick (unticked = 'off'), signed into the job only when ticked.
|
|
376
|
+
// `deps.draftThinking` is an operator override for tests/probes; a bad value throws here, before a session exists.
|
|
377
|
+
const draftThinking = assertThinkingMode(deps.draftThinking ?? (card.thinking === true ? 'on' : 'off'));
|
|
367
378
|
// P7: the person's own words, parsed ONCE by the one parser (src/jobblock.js). The card validator refused a malformed
|
|
368
379
|
// block before a session exists; a reuse card's job text is the origin's and is not drafted from.
|
|
369
380
|
const jobParsed = parseJobBlock(card.jobText);
|
|
@@ -440,10 +451,13 @@ export function createSession(card, deps = {}) {
|
|
|
440
451
|
});
|
|
441
452
|
};
|
|
442
453
|
|
|
454
|
+
/** a rubric job on local files words its copy/read steps for files (set once the card is known) */
|
|
455
|
+
let fileJob = card.checkType === 'rubric' && looksLikeRubricInput(parseInputs(card.inputs).ok ? (/** @type {any} */ (parseInputs(card.inputs))).inputs[0].value : '');
|
|
443
456
|
/** @param {string} id @param {string} [detail] (absent = keep what the step has) start (or restart) a step; whatever step was running is finished */
|
|
444
457
|
const stepStart = (id, detail) => {
|
|
445
|
-
|
|
446
|
-
state.
|
|
458
|
+
const labels = fileJob ? { ...STEP_LABELS, ...FILE_JOB_STEP_LABELS } : STEP_LABELS;
|
|
459
|
+
advanceSteps(state.steps, id, detail, labels);
|
|
460
|
+
state.progressLabel = labels[id] ?? id;
|
|
447
461
|
logSteps();
|
|
448
462
|
};
|
|
449
463
|
/** @param {string} id @param {string} detail set a step's latest line's detail without changing its status */
|
|
@@ -508,6 +522,7 @@ export function createSession(card, deps = {}) {
|
|
|
508
522
|
return true;
|
|
509
523
|
};
|
|
510
524
|
|
|
525
|
+
let lastTryRefused = false;
|
|
511
526
|
const onPhase = (name, data = {}) => {
|
|
512
527
|
// COLLAPSED INTO THE PROGRESS INDICATOR ONLY (build item 3) — this used
|
|
513
528
|
// to also `say('system', `… ${name}`)`, one jargon chat bubble per
|
|
@@ -518,10 +533,24 @@ export function createSession(card, deps = {}) {
|
|
|
518
533
|
// person needs to read; the step-by-step detail lives in one line.
|
|
519
534
|
// ONE owner of the drafting line's detail (model + cap): the drafting line is started here, by the real author phase
|
|
520
535
|
if (PHASE_STEP[name]) stepStart(PHASE_STEP[name], PHASE_STEP[name] === 'draft' ? `${card.model}, $${card.capUsd.toFixed(2)} cap` : undefined);
|
|
521
|
-
// a
|
|
522
|
-
//
|
|
536
|
+
// WHY a try was refused: the drafting log keeps it beside the call that made it (it used to say only "retry 3 of 3"), and the
|
|
537
|
+
// progress line shows it briefly, in code's words
|
|
538
|
+
if (name === 'author-try-failed') {
|
|
539
|
+
lastTryRefused = true;
|
|
540
|
+
const no = state.steps.findLastIndex((/** @type {any} */ x) => x.id === 'draft');
|
|
541
|
+
const reason = tryRefusedText(data);
|
|
542
|
+
appendDraftLog(outDir, {
|
|
543
|
+
kind: 'try-refused', no: no === -1 ? state.steps.length - 1 : no, call: String(data.call ?? ''), reason,
|
|
544
|
+
...(Array.isArray(data.reds) && data.reds.length ? { reds: redsRecord(data.reds) } : {}), ...declarationRecord(data.declaration), at: new Date().toISOString(),
|
|
545
|
+
});
|
|
546
|
+
stepDetail('draft', `${data.call}: refused — ${reason.length > 160 ? `${reason.slice(0, 160)}…` : reason}`);
|
|
547
|
+
}
|
|
548
|
+
// a drafting call after the first (`author-call` i >= 1, of up to `of + 1`) is relabelled in plain words, so a long silence on call 2
|
|
549
|
+
// of 3 does not read as a hang. The loop drafts again after a try that PASSED too (the drafter sees its own seed-read), so
|
|
550
|
+
// "retry" is said only when the previous try was actually refused; otherwise it is "look". The same one line, never a second one.
|
|
523
551
|
if (name === 'author-call' && Number.isInteger(data.i) && data.i >= 1 && Number.isInteger(data.of)) {
|
|
524
|
-
const label = `${STEP_LABELS.draft} (retry ${data.i + 1} of ${data.of + 1})`;
|
|
552
|
+
const label = `${STEP_LABELS.draft} (${lastTryRefused ? 'retry' : 'look'} ${data.i + 1} of ${data.of + 1})`;
|
|
553
|
+
lastTryRefused = false;
|
|
525
554
|
stepLabel('draft', label);
|
|
526
555
|
state.progressLabel = label;
|
|
527
556
|
}
|
|
@@ -557,16 +586,22 @@ export function createSession(card, deps = {}) {
|
|
|
557
586
|
// the call's label, the drafting model, its cost (null stays null: unpriced is never $0). One owner: this onCall.
|
|
558
587
|
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
559
588
|
const callNo = runningNo === -1 ? state.steps.length - 1 : runningNo;
|
|
560
|
-
// bare-agent's own verdict on the call's run
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
589
|
+
// bare-agent's own verdict on the call's run — `thinkingStatus`: 'applied' | 'unsupported' | 'ignored' | 'unknown' — present only
|
|
590
|
+
// when thinkingMode was sent. The first drafting row then says what REALLY happened ("the model thought anyway", "can't change it",
|
|
591
|
+
// "not confirmed"); a row that moved to a worse word never moves back.
|
|
592
|
+
if (draftThinkingPlan !== null && call.thinkingStatus !== undefined && call.thinkingStatus !== thinkingStatusSeen
|
|
593
|
+
&& !(thinkingStatusSeen !== null && thinkingStatusSeen !== 'applied' && call.thinkingStatus === 'applied')) {
|
|
594
|
+
thinkingStatusSeen = call.thinkingStatus;
|
|
564
595
|
const row = latestStep(state.steps, 'thinking');
|
|
565
|
-
if (row) {
|
|
596
|
+
if (row) {
|
|
597
|
+
row.label = thinkingRowText({ mode: draftThinkingPlan.mode, support: draftThinkingPlan.support, status: call.thinkingStatus });
|
|
598
|
+
appendDraftLog(outDir, { kind: 'step-label', no: state.steps.indexOf(row), label: row.label, at: new Date().toISOString() });
|
|
599
|
+
}
|
|
566
600
|
}
|
|
567
601
|
appendDraftLog(outDir, {
|
|
568
602
|
kind: 'call', no: callNo, step: state.steps[callNo]?.id ?? null, label: call.label, model: draftIdentity?.model ?? null,
|
|
569
|
-
thinking:
|
|
603
|
+
thinking: draftThinkingPlan === null ? null : draftThinkingPlan.mode, thinkingSupport: draftThinkingPlan === null ? null : draftThinkingPlan.support,
|
|
604
|
+
...(call.thinkingStatus === undefined ? {} : { thinkingStatus: call.thinkingStatus }),
|
|
570
605
|
costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
|
|
571
606
|
});
|
|
572
607
|
try {
|
|
@@ -578,10 +613,11 @@ export function createSession(card, deps = {}) {
|
|
|
578
613
|
};
|
|
579
614
|
/** @type {string|null} */
|
|
580
615
|
let draftStartedAt = null;
|
|
581
|
-
/** what the drafting calls
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
616
|
+
/** 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
|
|
617
|
+
* @type {ReturnType<typeof thinkingPlanFor>|null} */
|
|
618
|
+
let draftThinkingPlan = null;
|
|
619
|
+
/** the worst-so-far `thinkingStatus` a drafting call reported (null until one did) @type {string|null} */
|
|
620
|
+
let thinkingStatusSeen = null;
|
|
585
621
|
/** 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} */
|
|
586
622
|
let madeWorktree = null;
|
|
587
623
|
/** a session that ends WITHOUT ever running (refused, errored, abandoned) takes its worktree with it; a signed one keeps it for the run */
|
|
@@ -607,36 +643,37 @@ export function createSession(card, deps = {}) {
|
|
|
607
643
|
|
|
608
644
|
async function run() {
|
|
609
645
|
// ── keys, at $0, before anything is prepared ────────────────────────
|
|
610
|
-
const keyRows = rowsForHome(configHome);
|
|
611
|
-
const
|
|
646
|
+
const keyRows = rowsForHome(configHome, env);
|
|
647
|
+
const pick = judgePick(keyRows, { provider: card.provider, model: card.model });
|
|
648
|
+
const modelChoice = pick.ok ? pick.choice : null;
|
|
612
649
|
// hamr 2026-10-11: the FIRST drafting row says whether the drafter thinks, before any other line ("checking setup" follows).
|
|
613
650
|
// A Reuse session drafts nothing, so it has no row. A provider shape that cannot be built is refused below, in words.
|
|
614
651
|
if (modelChoice && deps.reuse === undefined) {
|
|
615
652
|
try {
|
|
616
|
-
|
|
617
|
-
state.steps.push({ id: 'thinking', label: thinkingRowText(
|
|
653
|
+
draftThinkingPlan = thinkingPlanFor({ providerName: modelChoice.provider, model: modelChoice.name, baseUrl: modelChoice.baseUrl ?? null }, draftThinking);
|
|
654
|
+
state.steps.push({ id: 'thinking', label: thinkingRowText({ mode: draftThinkingPlan.mode, support: draftThinkingPlan.support }), status: 'done', detail: '' });
|
|
618
655
|
logSteps();
|
|
619
|
-
} catch {
|
|
656
|
+
} catch { draftThinkingPlan = null; }
|
|
620
657
|
}
|
|
621
658
|
stepStart('setup');
|
|
622
|
-
if (!modelChoice) { refuse(
|
|
659
|
+
if (!modelChoice) { refuse(pick.ok ? 'Pick a model. Nothing was spent.' : pick.say); return; }
|
|
623
660
|
draftIdentity = { provider: modelChoice.provider, baseUrl: modelChoice.baseUrl ?? null, model: modelChoice.name };
|
|
624
661
|
let providerEntry;
|
|
625
662
|
try { providerEntry = resolveProvider(modelChoice.provider); } catch (e) { refuse(/** @type {Error} */ (e).message); return; }
|
|
626
|
-
//
|
|
663
|
+
// the key is the chosen row's own variable (P8: no built-in name)
|
|
627
664
|
const keyCfg = keyRows;
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
const apiKey = draftEnv[providerEntry.envKey];
|
|
665
|
+
const draftKeyName = modelChoice.envName;
|
|
666
|
+
const apiKey = keyValueOf(env, draftKeyName);
|
|
631
667
|
if (!apiKey) { refuse(`${draftKeyName} not set. Stopped — nothing spent.`); return; }
|
|
632
668
|
// the customer's own price on this row (USD per 1M in config.json), resolved once here at $0 and
|
|
633
669
|
// handed to every model call this session makes; a bad price refuses, never falls back to the guess
|
|
634
|
-
/** @type {
|
|
670
|
+
/** @type {import('../providerrows.js').PriceResult} */
|
|
635
671
|
let draftPrice = null;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
672
|
+
{
|
|
673
|
+
const chosenRow = keyCfg.find((r) => r.envName === modelChoice.envName);
|
|
674
|
+
const dl = chosenRow?.models.find((l) => l.model === modelChoice.name);
|
|
675
|
+
const dp = dl ? ratesOfLine(dl) : null;
|
|
676
|
+
draftPrice = dp ? { ...dp, envName: modelChoice.envName } : null;
|
|
640
677
|
}
|
|
641
678
|
const keyProblem = apiKeyProblem(apiKey);
|
|
642
679
|
if (keyProblem) { refuse(`${draftKeyName} ${keyProblem}. Stopped — nothing spent.`); return; }
|
|
@@ -644,6 +681,9 @@ export function createSession(card, deps = {}) {
|
|
|
644
681
|
const verdictType = card.checkType === 'rubric' ? 'soft-green' : 'green';
|
|
645
682
|
const into = join(outDir, 'source-seed');
|
|
646
683
|
const isRepoLike = looksLikeRepoSource(SOURCE);
|
|
684
|
+
// PART 1a: a RUBRIC job whose first input is not a repo works on the person's own local FILES — every numbered input
|
|
685
|
+
// line is one file, frozen by `proveRubricInputs`; there is no repo, no language and no scout (docs/product/ITEM33-BUILD.md)
|
|
686
|
+
const RUBRIC_FILES = card.checkType === 'rubric' && !isRepoLike;
|
|
647
687
|
// 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
|
|
648
688
|
const destProblem = repoDestinationProblem(card.destination, isRepoLike);
|
|
649
689
|
if (destProblem !== null) { refuse(destProblem); return; }
|
|
@@ -667,10 +707,21 @@ export function createSession(card, deps = {}) {
|
|
|
667
707
|
// fence read as a directory and refused as "not absolute" — hiding the real problem with the Source. A scope-shaped
|
|
668
708
|
// (relative) Destination is therefore handed to the door only when the source is repo-like.
|
|
669
709
|
const destIsDir = isAbsoluteDestination(card.destination);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
710
|
+
if (RUBRIC_FILES && !destIsDir) {
|
|
711
|
+
refuse('Destination must be the full path of a folder, like /home/me/out — the finished files are delivered there. Nothing spent.');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
/** @type {any} */
|
|
715
|
+
let prep;
|
|
716
|
+
if (RUBRIC_FILES) {
|
|
717
|
+
const proved = await proveRubricInputs({ inputs: INPUTS, into, destination: card.destination, ...(deps.fetchUrl ? { fetchUrl: deps.fetchUrl } : {}) });
|
|
718
|
+
prep = proved.ok ? proved.prep : { stop: proved.error, code: proved.code };
|
|
719
|
+
} else {
|
|
720
|
+
prep = await prepareSource({
|
|
721
|
+
source: SOURCE, into, ...(isRepoLike || destIsDir ? { destination: card.destination } : {}),
|
|
722
|
+
...(repoRoot === null ? {} : { worktree: worktreePath(repoRoot, id) }),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
674
725
|
if (repoRoot !== null && prep.stop === null && prep.manifest?.worktree) madeWorktree = { repo: repoRoot, dir: prep.manifest.worktree };
|
|
675
726
|
if (abandoned) dropWorktree();
|
|
676
727
|
if (prep.stop !== null) {
|
|
@@ -682,7 +733,7 @@ export function createSession(card, deps = {}) {
|
|
|
682
733
|
stepStart('check');
|
|
683
734
|
|
|
684
735
|
const IS_REPO_SOURCE = prep.manifest.kind === 'repo';
|
|
685
|
-
let LANG = 'none-detected';
|
|
736
|
+
let LANG = RUBRIC_FILES ? RUBRIC_LANG : 'none-detected';
|
|
686
737
|
/** @type {any} */
|
|
687
738
|
let langResult = null;
|
|
688
739
|
if (IS_REPO_SOURCE) {
|
|
@@ -741,7 +792,7 @@ export function createSession(card, deps = {}) {
|
|
|
741
792
|
state.phase = 'drafting';
|
|
742
793
|
}
|
|
743
794
|
}
|
|
744
|
-
if (!IS_REPO_SOURCE) {
|
|
795
|
+
if (!IS_REPO_SOURCE && !RUBRIC_FILES) {
|
|
745
796
|
refuse(nonRepoSourceMessage(prep.manifest.kind));
|
|
746
797
|
return;
|
|
747
798
|
}
|
|
@@ -791,9 +842,10 @@ export function createSession(card, deps = {}) {
|
|
|
791
842
|
const judges = closeJudges(spec.closeDecl);
|
|
792
843
|
const judge = judges ? resolveJobJudge(spec, modelChoice.provider, resolveWorkerModel) : null;
|
|
793
844
|
let judgeProvider = null;
|
|
794
|
-
/** @type {
|
|
845
|
+
/** @type {import('../providerrows.js').PriceResult} */
|
|
795
846
|
let judgePrice = null;
|
|
796
847
|
if (judge) {
|
|
848
|
+
const judgeRow = judgeRowFor(keyCfg, judge, modelChoice.provider, keyCfg.find((r) => r.envName === modelChoice.envName) ?? null);
|
|
797
849
|
try {
|
|
798
850
|
judgePrice = judgeRatesFor(judge, modelChoice.provider, modelChoice.baseUrl, draftPrice, keyCfg);
|
|
799
851
|
} catch (e) {
|
|
@@ -801,22 +853,23 @@ export function createSession(card, deps = {}) {
|
|
|
801
853
|
refuse(`${e.message} — refusing before gate 4 spends anything`);
|
|
802
854
|
return;
|
|
803
855
|
}
|
|
804
|
-
const judgeKeyValue = env.JUDGE_API_KEY ?? (judge.provider === modelChoice.provider ?
|
|
856
|
+
const judgeKeyValue = env.JUDGE_API_KEY ?? (judge.provider === modelChoice.provider ? apiKey : keyValueOf(env, judgeRow?.envName));
|
|
805
857
|
const judgeKeyProblem = judgeKeyValue ? apiKeyProblem(judgeKeyValue) : null;
|
|
806
858
|
if (!judgeKeyValue || judgeKeyProblem) {
|
|
807
859
|
refuse(`the judge key is not usable (${judgeKeyProblem ?? 'not set'}) — refusing before gate 4 spends anything`);
|
|
808
860
|
return;
|
|
809
861
|
}
|
|
810
862
|
judgeProvider = buildRunnerProviders({
|
|
811
|
-
providerName: modelChoice.provider, apiKey, model: MODEL,
|
|
863
|
+
providerName: modelChoice.provider, apiKey, model: MODEL, baseUrl: modelChoice.baseUrl, legacyMaxTokens: modelChoice.legacyMaxTokens,
|
|
812
864
|
judgeApiKey: judgeKeyValue, judgeModel: judge.model, judgeProviderName: judge.provider,
|
|
813
|
-
judgeBaseUrl: judge.provider === modelChoice.provider ? modelChoice.baseUrl : undefined,
|
|
865
|
+
judgeBaseUrl: judge.provider === modelChoice.provider ? modelChoice.baseUrl : (judgeRow?.baseUrl.trim() || undefined),
|
|
866
|
+
judgeLegacyMaxTokens: judge.provider === modelChoice.provider ? modelChoice.legacyMaxTokens : (judgeRow?.legacyMaxTokens ?? false),
|
|
814
867
|
}).judgeProvider;
|
|
815
868
|
}
|
|
816
869
|
const signing = await prepareSigningFn({
|
|
817
870
|
spec, workdir: prep.tree, seedRef: seedRef, timeoutMs,
|
|
818
871
|
shellCapUsd: spec.budgetUsd, ceilingUsd: card.capUsd, priorCalls: [...metered],
|
|
819
|
-
judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null }) : null,
|
|
872
|
+
judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null, thinking: spec.thinking === true }) : null,
|
|
820
873
|
judgeModel: judge?.model ?? null,
|
|
821
874
|
onJudgeCost: (c) => onCall({ label: `${c.label}:${c.id}`, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds }),
|
|
822
875
|
});
|
|
@@ -872,7 +925,9 @@ export function createSession(card, deps = {}) {
|
|
|
872
925
|
return;
|
|
873
926
|
}
|
|
874
927
|
|
|
875
|
-
|
|
928
|
+
// a rubric job on files writes only inside the hidden tree's own `output/`; its Destination is a drop-off folder
|
|
929
|
+
// (proven above, per run), never a fence
|
|
930
|
+
const writeScope = RUBRIC_FILES ? ['output/'] : card.destination.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
|
|
876
931
|
/** @type {any} the confirm turn (below) fills `goal` in once accepted —
|
|
877
932
|
* declared loosely rather than typed field-by-field, since this object's
|
|
878
933
|
* own shape is a `job-v1` draft `validateJob` itself is the real
|
|
@@ -886,8 +941,10 @@ export function createSession(card, deps = {}) {
|
|
|
886
941
|
inputs: INPUTS,
|
|
887
942
|
provider: modelChoice.provider,
|
|
888
943
|
...(modelChoice.baseUrl ? { baseUrl: modelChoice.baseUrl } : {}),
|
|
889
|
-
//
|
|
890
|
-
|
|
944
|
+
// a job always carries its own model (P8: there is no provider default to omit it against)
|
|
945
|
+
model: modelChoice.name,
|
|
946
|
+
// the "Thinking on" tick is signed ONLY when ticked (unticked writes no field: the hash stays what it would have been)
|
|
947
|
+
...(card.thinking === true && draftThinkingPlan?.support !== false ? { thinking: true } : {}),
|
|
891
948
|
cadence: { unit: 'day', every: 1 },
|
|
892
949
|
budgetUsd: card.capUsd,
|
|
893
950
|
...(typeof card.maxWallMs === 'number' ? { maxWallMs: card.maxWallMs } : {}),
|
|
@@ -905,16 +962,17 @@ export function createSession(card, deps = {}) {
|
|
|
905
962
|
/** @type {Record<string, string>} */
|
|
906
963
|
const answers = { 1: renderJobLines(jobLines) };
|
|
907
964
|
if (verdictType === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(jobLines);
|
|
908
|
-
|
|
965
|
+
// a rubric job on files holds no judged part in this build, so the PASS/FAIL examples are not required of it
|
|
966
|
+
const required = RUBRIC_FILES ? [1] : requiredAnswersFor(verdictType);
|
|
909
967
|
for (const n of required) {
|
|
910
968
|
if (!answers[n]) { refuse(`the ${verdictType} check type needs ${required.length} answers and one is missing (key ${n})`); return; }
|
|
911
969
|
}
|
|
912
970
|
|
|
913
971
|
const draftJudge = (() => {
|
|
914
|
-
try { return resolveJobJudge(draft, modelChoice.provider, resolveWorkerModel); } catch { return { provider: modelChoice.provider, model:
|
|
972
|
+
try { return resolveJobJudge(draft, modelChoice.provider, resolveWorkerModel); } catch { return { provider: modelChoice.provider, model: modelChoice.name }; }
|
|
915
973
|
})();
|
|
916
974
|
const { provider } = buildRunnerProviders({
|
|
917
|
-
providerName: modelChoice.provider, apiKey, model: MODEL,
|
|
975
|
+
providerName: modelChoice.provider, apiKey, model: MODEL, baseUrl: modelChoice.baseUrl, legacyMaxTokens: modelChoice.legacyMaxTokens,
|
|
918
976
|
judgeApiKey: apiKey, judgeModel: MODEL, judgeProviderName: modelChoice.provider, judgeBaseUrl: modelChoice.baseUrl,
|
|
919
977
|
});
|
|
920
978
|
// TEST SEAMS (see the constructor's own note): a test overrides
|
|
@@ -969,8 +1027,10 @@ export function createSession(card, deps = {}) {
|
|
|
969
1027
|
rates: draftPrice?.rates ?? null,
|
|
970
1028
|
draftThinking,
|
|
971
1029
|
onPhase, onCall,
|
|
972
|
-
ask, answerQuestions, confirmGenerate, isRepo:
|
|
1030
|
+
ask, answerQuestions, confirmGenerate, isRepo: !RUBRIC_FILES, langResult,
|
|
973
1031
|
...(deps.scout ? { scout: deps.scout } : {}),
|
|
1032
|
+
// PART 1c: a test that replaces the model boundary (`generate`) must not reach the real paid look; `deps.lookFn` brings its own
|
|
1033
|
+
...(deps.lookFn !== undefined ? { lookFn: deps.lookFn } : (deps.generate ? { lookFn: null } : {})),
|
|
974
1034
|
...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
|
|
975
1035
|
});
|
|
976
1036
|
|
|
@@ -1004,13 +1064,34 @@ export function createSession(card, deps = {}) {
|
|
|
1004
1064
|
: draftStopText({
|
|
1005
1065
|
stop: String(authored.stop ?? 'authoring-failed'), reds: stopReds, refusal: authored.refusal,
|
|
1006
1066
|
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
1067
|
+
wholeJob: RUBRIC_FILES,
|
|
1007
1068
|
}), authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
1008
1069
|
return;
|
|
1009
1070
|
}
|
|
1010
1071
|
|
|
1011
|
-
if (
|
|
1072
|
+
if (RUBRIC_FILES) draft.goal = redactSecrets(rubricGoal({ jobLines, files: prep.manifest.inputs, confirmedGoal: authored.confirmed?.goal ?? null, closeDecl: authored.closeDecl }));
|
|
1073
|
+
else if (authored.confirmed?.goal) draft.goal = redactSecrets(String(authored.confirmed.goal));
|
|
1012
1074
|
const spec = assembleSpec(draft, { ...authored, verdictType: /** @type {string} */ (authored.verdictType) });
|
|
1075
|
+
// PART 1a (run mv5bk400): the plan the person confirmed named three checks for line 1; the composer wrote ONE stage and put
|
|
1076
|
+
// "lists 3 releases" in its notes, so the card promised a check the close never ran. The card AFTER drafting is now built
|
|
1077
|
+
// from the close itself, and every confirmed check no part carries out is moved to Not checked, in code, before signing.
|
|
1078
|
+
if (RUBRIC_FILES) {
|
|
1079
|
+
const rec = reconcileRubricPlan({
|
|
1080
|
+
confirmed: authored.confirmed?.checkItems ?? [], notChecked: authored.confirmed?.notChecked ?? [], closeDecl: spec.closeDecl, jobLines,
|
|
1081
|
+
});
|
|
1082
|
+
state.finalPlan = { goal: String(draft.goal ?? ''), checkItems: rec.checkItems, notChecked: rec.notChecked, protections: authored.confirmed?.protections ?? [], moved: rec.moved };
|
|
1083
|
+
}
|
|
1013
1084
|
await finish(spec, authored.seedRef);
|
|
1085
|
+
if (state.finalPlan && state.phase === 'prepared') {
|
|
1086
|
+
// The confirm turn's plan bubble (and a try-3 "answers are in" one) is the DRAFTER's wording of the checks, written before
|
|
1087
|
+
// any close existed. Once the close is signed-ready its card is the only plan the person sees: the earlier ones are
|
|
1088
|
+
// dropped, so drafter prose can never sit above (and be read instead of) the code's sentences (run mv5e1ktb).
|
|
1089
|
+
state.messages = state.messages.filter((/** @type {any} */ m) => m.kind !== 'plan');
|
|
1090
|
+
state.messages.push({
|
|
1091
|
+
...planMessage({ checkItems: state.finalPlan.checkItems, checks: state.finalPlan.checkItems.map((/** @type {any} */ c) => c.text), notChecked: state.finalPlan.notChecked, protections: state.finalPlan.protections, questions: [] }, jobLines),
|
|
1092
|
+
text: 'These are the checks the signed close will run, and what it will not check. Sign & run to accept them, or Revise to edit the boxes and draft again.',
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1014
1095
|
}
|
|
1015
1096
|
|
|
1016
1097
|
// fire-and-forget — the caller (src/panel/server.js) never awaits this; it
|