bareloop 0.39.0 → 0.40.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 +27 -0
- package/bareloop.context.md +63 -11
- 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 +5 -2
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/interviewrun.js +56 -27
- package/src/job.js +38 -5
- package/src/jobblock.js +140 -0
- package/src/judged.js +1 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +15 -5
- package/src/panel/authorsession.js +158 -47
- package/src/panel/index.html +179 -70
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +4 -4
- package/src/promptregisters.js +1 -1
- package/src/providers.js +101 -19
- 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/transport.js +1 -1
- package/src/userrun.js +3 -3
- 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/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/panel/authorsession.d.ts +50 -14
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +20 -0
- package/types/planrun.d.ts +1 -1
- package/types/providers.d.ts +47 -3
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
|
@@ -42,18 +42,22 @@ 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
|
-
resolveProvider, buildRunnerProviders, apiKeyProblem,
|
|
48
|
+
resolveProvider, buildRunnerProviders, apiKeyProblem, assertDraftThinking, draftThinkingFor,
|
|
49
49
|
} from '../providers.js';
|
|
50
50
|
import { resolveJobJudge, defaultJudgeLoop } from '../judged.js';
|
|
51
51
|
import { closeJudges } from '../kinds.js';
|
|
52
52
|
import { redactSecrets } from '../validate.js';
|
|
53
53
|
import { tallyCalls } from '../text.js';
|
|
54
54
|
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
55
|
-
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
55
|
+
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
56
56
|
import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
57
|
+
import {
|
|
58
|
+
parseJobBlock, parseInputs, jobBlockFitsCheck, renderJobLines, renderExamples, signedJobLines,
|
|
59
|
+
} from '../jobblock.js';
|
|
60
|
+
import { planFromConfirm } from './jobplan.js';
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
63
|
* THE PROGRESS LIST's one table (hamr 2026-10-04, "one place showing all"): every pipeline step the left Chat panel
|
|
@@ -62,6 +66,7 @@ import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
|
62
66
|
* @type {Readonly<Record<string, string>>}
|
|
63
67
|
*/
|
|
64
68
|
export const STEP_LABELS = Object.freeze({
|
|
69
|
+
thinking: 'thinking',
|
|
65
70
|
setup: 'checking setup',
|
|
66
71
|
copy: 'making worktree',
|
|
67
72
|
check: 'checking source',
|
|
@@ -105,6 +110,20 @@ export const PHASE_STEP = Object.freeze({
|
|
|
105
110
|
'rubric-scrubbed': 'calibrate',
|
|
106
111
|
});
|
|
107
112
|
|
|
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
|
+
}
|
|
126
|
+
|
|
108
127
|
/** @typedef {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}} StepLine */
|
|
109
128
|
|
|
110
129
|
/** @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 +152,22 @@ const SOURCE_CHECK_CODES = new Set([
|
|
|
133
152
|
'source-file-oversize', 'source-changed-after-scan', 'source-untracked-in-repo',
|
|
134
153
|
]);
|
|
135
154
|
|
|
155
|
+
/** the line under the plan bubble, in plain words (the bubble itself is drawn by the page's one `renderJobPlan`) */
|
|
156
|
+
export const PLAN_FOOTER = 'Sign & run to confirm this plan, or Revise to edit the boxes and draft again.';
|
|
157
|
+
|
|
136
158
|
/**
|
|
137
|
-
* The confirm turn's plan as
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @param {{
|
|
142
|
-
* @returns {string}
|
|
159
|
+
* The confirm turn's plan as a chat message (P7): a STRUCTURED `{role:'bot', kind:'plan', plan}` the page draws with the
|
|
160
|
+
* same renderer the Job tab uses — each numbered job line, its `~` rule, its drafted checks folded; the AI's own "not
|
|
161
|
+
* checked"; the always-on guards. `questions` (what the plan still asks) ride inside the plan. One owner of the shape.
|
|
162
|
+
* @param {{goal?: string, checks?: string[], checkItems?: {text: string, fromLine: number[]|null}[], notChecked?: string[], questions?: string[], protections?: string[]}} p the confirm turn's plan
|
|
163
|
+
* @param {{n: number, text: string, rule: string}[]|null} jobLines
|
|
164
|
+
* @returns {{role: 'bot', kind: 'plan', text: string, plan: ReturnType<typeof planFromConfirm> & {goal: string, questions: string[]}}}
|
|
143
165
|
*/
|
|
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');
|
|
166
|
+
export function planMessage(p, jobLines) {
|
|
167
|
+
return {
|
|
168
|
+
role: 'bot', kind: 'plan', text: PLAN_FOOTER,
|
|
169
|
+
plan: { ...planFromConfirm(jobLines, p), goal: String(p.goal ?? ''), questions: [...(p.questions ?? [])] },
|
|
170
|
+
};
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
/** @param {string} name @returns {boolean} kebab-case slug, same rule `job.js`'s `SLUG_RE` enforces */
|
|
@@ -176,7 +194,18 @@ export function jobNameTaken(name, opts = {}) {
|
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
/** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
|
|
179
|
-
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'model', '
|
|
197
|
+
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'model', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The repo a card names: Inputs line 1 (P7 — the Source box is gone; the first input IS the repo). `''` when the Inputs
|
|
201
|
+
* box does not parse (the card validator refuses that before anything uses this).
|
|
202
|
+
* @param {any} card
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
export function cardSource(card) {
|
|
206
|
+
const p = parseInputs(card?.inputs);
|
|
207
|
+
return p.ok ? p.inputs[0].value : '';
|
|
208
|
+
}
|
|
180
209
|
|
|
181
210
|
/**
|
|
182
211
|
* Only the form's own fields, nothing else a request body might carry (`startFrom`, stray keys).
|
|
@@ -192,15 +221,15 @@ export function cardFields(card) {
|
|
|
192
221
|
|
|
193
222
|
/**
|
|
194
223
|
* 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 —
|
|
224
|
+
* FIVE open boxes — Inputs, Destination, Model, $ cap, Time cap — and every other box is the signed workflow, shown greyed and never
|
|
196
225
|
* editable. The page greys them; THIS is what refuses (the server never trusts the page). Changing any locked box
|
|
197
226
|
* is a different job: Clear the card, which drafts.
|
|
198
227
|
*/
|
|
199
|
-
export const REUSE_OPEN_FIELDS = Object.freeze(['
|
|
228
|
+
export const REUSE_OPEN_FIELDS = Object.freeze(['inputs', 'destination', 'model', 'capUsd', 'maxWallMs']);
|
|
200
229
|
/** every card field that is NOT open on a reuse card, in card order */
|
|
201
230
|
export const REUSE_LOCKED_FIELDS = Object.freeze(CARD_FIELDS.filter((f) => !REUSE_OPEN_FIELDS.includes(f)));
|
|
202
231
|
const REUSE_LABELS = Object.freeze(/** @type {Record<string, string>} */ ({
|
|
203
|
-
jobName: 'Job name', checkType: 'Check type',
|
|
232
|
+
jobName: 'Job name', checkType: 'Check type', jobText: 'The job',
|
|
204
233
|
}));
|
|
205
234
|
|
|
206
235
|
/**
|
|
@@ -219,12 +248,12 @@ export function lockedFieldChanged(card, origin) {
|
|
|
219
248
|
|
|
220
249
|
/**
|
|
221
250
|
* 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
|
|
251
|
+
* `writeScope` from Destination, `inputs` from the Inputs box (P7), `budgetUsd` from the $ cap, `maxWallMs` from the Time cap (absent when blank), and the
|
|
223
252
|
* worker (`provider`/`baseUrl`/`model`) from the Model Name chosen in Settings > Providers, spelled exactly as normal
|
|
224
253
|
* authoring spells it (`baseUrl` only when the row has one; `model` only when the Name is not the provider's default
|
|
225
254
|
* tier). A RUBRIC (soft-green) origin with no explicit `judge` first has its judge pinned to the ORIGIN's resolved judge
|
|
226
255
|
* 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.
|
|
256
|
+
* `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
257
|
* hashes to a NEW `jobSpecHash` and the SAME `workflowKey` — the caller checks the second after building.
|
|
229
258
|
* @param {any} originSpec
|
|
230
259
|
* @param {Record<string, any>} card
|
|
@@ -234,6 +263,8 @@ export function lockedFieldChanged(card, origin) {
|
|
|
234
263
|
export function buildReuseSpec(originSpec, card, rows) {
|
|
235
264
|
const spec = JSON.parse(JSON.stringify(originSpec));
|
|
236
265
|
spec.writeScope = String(card.destination ?? '').split(/[,\n]/).map((x) => x.trim()).filter(Boolean);
|
|
266
|
+
const ip = parseInputs(card.inputs);
|
|
267
|
+
if (ip.ok) spec.inputs = ip.inputs; else delete spec.inputs;
|
|
237
268
|
spec.budgetUsd = card.capUsd;
|
|
238
269
|
if (typeof card.maxWallMs === 'number' && Number.isFinite(card.maxWallMs) && card.maxWallMs > 0) spec.maxWallMs = card.maxWallMs;
|
|
239
270
|
else delete spec.maxWallMs;
|
|
@@ -250,7 +281,7 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
250
281
|
|
|
251
282
|
/**
|
|
252
283
|
* `$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:
|
|
284
|
+
* required here — only what the new session itself needs: Inputs, a Destination, a Model that is a Settings Name,
|
|
254
285
|
* and the cap. (The spec built from the open boxes is validated by the route with `validateJob`.)
|
|
255
286
|
* @param {any} card
|
|
256
287
|
* @param {{rows?: readonly import('../providerrows.js').KeyRow[]}} [opts]
|
|
@@ -259,7 +290,8 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
259
290
|
export function validateReuseCard(card, opts = {}) {
|
|
260
291
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
261
292
|
if (!modelChoiceFor(opts.rows ?? [], card.model)) return { ok: false, error: 'Model must be one of the Names in Settings > Providers' };
|
|
262
|
-
|
|
293
|
+
const ip = parseInputs(card.inputs);
|
|
294
|
+
if (!ip.ok) return { ok: false, error: ip.error };
|
|
263
295
|
if (typeof card.destination !== 'string' || card.destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
264
296
|
if (typeof card.capUsd !== 'number' || !Number.isFinite(card.capUsd) || card.capUsd <= 0) {
|
|
265
297
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
@@ -277,7 +309,7 @@ export { nonRepoSourceMessage };
|
|
|
277
309
|
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
278
310
|
* source that does not peek as a repo) keeps today's behaviour: its Destination is an absolute output directory.
|
|
279
311
|
* @param {string} destination
|
|
280
|
-
* @param {boolean} isRepoLike `looksLikeRepoSource(card
|
|
312
|
+
* @param {boolean} isRepoLike `looksLikeRepoSource(cardSource(card))`
|
|
281
313
|
* @returns {string|null} the plain refusal, or null when the Destination is fine
|
|
282
314
|
*/
|
|
283
315
|
export function repoDestinationProblem(destination, isRepoLike) {
|
|
@@ -294,19 +326,20 @@ export function repoDestinationProblem(destination, isRepoLike) {
|
|
|
294
326
|
*/
|
|
295
327
|
export function validateJobCard(card, opts = {}) {
|
|
296
328
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
297
|
-
const { jobName, checkType, model,
|
|
329
|
+
const { jobName, checkType, model, destination, capUsd } = card;
|
|
298
330
|
if (!isSlug(jobName)) return { ok: false, error: 'Job name must be a kebab-case slug (letters, digits, dashes)' };
|
|
299
331
|
if (jobNameTaken(jobName, { jobsDir: opts.jobsDir })) {
|
|
300
332
|
return { ok: false, error: `a job named "${jobName}" already exists — P3 is new jobs only (Q4=A)` };
|
|
301
333
|
}
|
|
302
334
|
if (checkType !== 'deterministic' && checkType !== 'rubric') return { ok: false, error: 'Check type must be deterministic or rubric' };
|
|
303
335
|
if (!modelChoiceFor(opts.rows ?? [], model)) return { ok: false, error: 'Model must be one of the Names in Settings > Providers' };
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
}
|
|
336
|
+
const jb = parseJobBlock(card.jobText);
|
|
337
|
+
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
|
+
const ib = parseInputs(card.inputs);
|
|
341
|
+
if (!ib.ok) return { ok: false, error: ib.error };
|
|
342
|
+
if (typeof destination !== 'string' || destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
310
343
|
if (typeof capUsd !== 'number' || !Number.isFinite(capUsd) || capUsd <= 0) {
|
|
311
344
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
312
345
|
}
|
|
@@ -321,13 +354,24 @@ export function validateJobCard(card, opts = {}) {
|
|
|
321
354
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
322
355
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
323
356
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
324
|
-
* prepareSigningFn?: Function, npmCiFn?: Function, reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
357
|
+
* prepareSigningFn?: Function, npmCiFn?: Function, draftThinking?: "on"|"off", reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
325
358
|
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
326
359
|
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
327
360
|
* @returns {any} the session object
|
|
328
361
|
*/
|
|
329
362
|
export function createSession(card, deps = {}) {
|
|
330
363
|
const env = deps.env ?? process.env;
|
|
364
|
+
// The drafter's THINKING knob ('on' | 'off'): an explicit operator option, never read from env and not in the panel UI. A bad
|
|
365
|
+
// value throws here, before a session exists. Default 'off' (hamr ruling 2026-10-11, after the paid ON/OFF contrast).
|
|
366
|
+
const draftThinking = assertDraftThinking(deps.draftThinking ?? 'off');
|
|
367
|
+
// P7: the person's own words, parsed ONCE by the one parser (src/jobblock.js). The card validator refused a malformed
|
|
368
|
+
// block before a session exists; a reuse card's job text is the origin's and is not drafted from.
|
|
369
|
+
const jobParsed = parseJobBlock(card.jobText);
|
|
370
|
+
const jobLines = jobParsed.ok ? jobParsed.lines : [];
|
|
371
|
+
const inputsParsed = parseInputs(card.inputs);
|
|
372
|
+
const INPUTS = inputsParsed.ok ? inputsParsed.inputs : [];
|
|
373
|
+
/** the repo — Inputs line 1 */
|
|
374
|
+
const SOURCE = INPUTS[0]?.value ?? '';
|
|
331
375
|
const sessionsRoot = deps.sessionsRoot ?? join(process.env.HOME ?? '/tmp', '.config', 'bareloop', 'panel-sessions');
|
|
332
376
|
// config.json lives beside the sessions dir (~/.config/bareloop) unless a home is injected
|
|
333
377
|
const configHome = deps.home ?? dirname(sessionsRoot);
|
|
@@ -422,6 +466,8 @@ export function createSession(card, deps = {}) {
|
|
|
422
466
|
|
|
423
467
|
/** @param {'bot'|'you'|'system'} role @param {string} text */
|
|
424
468
|
const say = (role, text) => { state.messages.push({ role, text }); };
|
|
469
|
+
/** a structured message (the plan bubble): the page draws it, the thread keeps it as data */
|
|
470
|
+
const sayPlan = (/** @type {any} */ plan) => { state.messages.push(planMessage(plan, jobLines)); };
|
|
425
471
|
|
|
426
472
|
/** @type {((value: string|null) => void)|null} */
|
|
427
473
|
let resolvePending = null;
|
|
@@ -447,8 +493,8 @@ export function createSession(card, deps = {}) {
|
|
|
447
493
|
resolvePending = resolveFn;
|
|
448
494
|
if (step.kind === 'language') say('bot', `${step.field?.prompt ?? 'Which language is this job about?'} (${(step.candidates ?? []).join(', ')})`);
|
|
449
495
|
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}`);
|
|
496
|
+
sayPlan(step.plan ?? {});
|
|
497
|
+
} 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
498
|
else if (step.kind === 'goal') say('bot', 'Type the goal sentence yourself.');
|
|
453
499
|
});
|
|
454
500
|
|
|
@@ -511,8 +557,16 @@ export function createSession(card, deps = {}) {
|
|
|
511
557
|
// the call's label, the drafting model, its cost (null stays null: unpriced is never $0). One owner: this onCall.
|
|
512
558
|
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
513
559
|
const callNo = runningNo === -1 ? state.steps.length - 1 : runningNo;
|
|
560
|
+
// bare-agent's own verdict on the call's run (true | false | null), present only when thinkingMode was sent. 'off' sent but
|
|
561
|
+
// reasoning seen (false) turns the first drafting row into "thinking on (the model thought anyway)" and stays so.
|
|
562
|
+
if (draftThinkingLogged === 'off' && call.thinkingApplied === false && !thoughtAnyway) {
|
|
563
|
+
thoughtAnyway = true;
|
|
564
|
+
const row = latestStep(state.steps, 'thinking');
|
|
565
|
+
if (row) { row.label = thinkingRowText('off', true); appendDraftLog(outDir, { kind: 'step-label', no: state.steps.indexOf(row), label: row.label, at: new Date().toISOString() }); }
|
|
566
|
+
}
|
|
514
567
|
appendDraftLog(outDir, {
|
|
515
568
|
kind: 'call', no: callNo, step: state.steps[callNo]?.id ?? null, label: call.label, model: draftIdentity?.model ?? null,
|
|
569
|
+
thinking: draftThinkingLogged, ...(call.thinkingApplied === undefined ? {} : { thinkingApplied: call.thinkingApplied }),
|
|
516
570
|
costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
|
|
517
571
|
});
|
|
518
572
|
try {
|
|
@@ -524,6 +578,10 @@ export function createSession(card, deps = {}) {
|
|
|
524
578
|
};
|
|
525
579
|
/** @type {string|null} */
|
|
526
580
|
let draftStartedAt = null;
|
|
581
|
+
/** what the drafting calls really carry for thinking ('on' | 'off' | 'not-switchable' | 'unverified'), set once the Model Name resolves; null before @type {string|null} */
|
|
582
|
+
let draftThinkingLogged = null;
|
|
583
|
+
/** a drafting call reported thinkingApplied === false after 'off' was sent */
|
|
584
|
+
let thoughtAnyway = false;
|
|
527
585
|
/** 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
586
|
let madeWorktree = null;
|
|
529
587
|
/** a session that ends WITHOUT ever running (refused, errored, abandoned) takes its worktree with it; a signed one keeps it for the run */
|
|
@@ -549,9 +607,18 @@ export function createSession(card, deps = {}) {
|
|
|
549
607
|
|
|
550
608
|
async function run() {
|
|
551
609
|
// ── keys, at $0, before anything is prepared ────────────────────────
|
|
552
|
-
stepStart('setup');
|
|
553
610
|
const keyRows = rowsForHome(configHome);
|
|
554
611
|
const modelChoice = modelChoiceFor(keyRows, card.model);
|
|
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
|
+
draftThinkingLogged = draftThinkingFor({ providerName: modelChoice.provider, model: modelChoice.name, baseUrl: modelChoice.baseUrl ?? null }, draftThinking);
|
|
617
|
+
state.steps.push({ id: 'thinking', label: thinkingRowText(/** @type {any} */ (draftThinkingLogged)), status: 'done', detail: '' });
|
|
618
|
+
logSteps();
|
|
619
|
+
} catch { draftThinkingLogged = null; }
|
|
620
|
+
}
|
|
621
|
+
stepStart('setup');
|
|
555
622
|
if (!modelChoice) { refuse(`Model "${card.model}" is not a Name in Settings > Providers. Stopped — nothing spent.`); return; }
|
|
556
623
|
draftIdentity = { provider: modelChoice.provider, baseUrl: modelChoice.baseUrl ?? null, model: modelChoice.name };
|
|
557
624
|
let providerEntry;
|
|
@@ -576,7 +643,7 @@ export function createSession(card, deps = {}) {
|
|
|
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 };
|
|
@@ -814,6 +881,9 @@ export function createSession(card, deps = {}) {
|
|
|
814
881
|
schema: 'job-v1',
|
|
815
882
|
job: card.jobName,
|
|
816
883
|
description: `${card.jobName} — authored through the bareloop panel (${verdictType}, ${LANG})`,
|
|
884
|
+
// P7: the person's own words, signed beside the model's goal sentence
|
|
885
|
+
jobLines: signedJobLines(jobLines),
|
|
886
|
+
inputs: INPUTS,
|
|
817
887
|
provider: modelChoice.provider,
|
|
818
888
|
...(modelChoice.baseUrl ? { baseUrl: modelChoice.baseUrl } : {}),
|
|
819
889
|
// the Name is the model id: signed into the spec only when it is not the provider's own default
|
|
@@ -833,8 +903,8 @@ export function createSession(card, deps = {}) {
|
|
|
833
903
|
}
|
|
834
904
|
|
|
835
905
|
/** @type {Record<string, string>} */
|
|
836
|
-
const answers = { 1:
|
|
837
|
-
if (verdictType === 'soft-green') answers[
|
|
906
|
+
const answers = { 1: renderJobLines(jobLines) };
|
|
907
|
+
if (verdictType === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(jobLines);
|
|
838
908
|
const required = requiredAnswersFor(verdictType);
|
|
839
909
|
for (const n of required) {
|
|
840
910
|
if (!answers[n]) { refuse(`the ${verdictType} check type needs ${required.length} answers and one is missing (key ${n})`); return; }
|
|
@@ -854,8 +924,35 @@ export function createSession(card, deps = {}) {
|
|
|
854
924
|
// machinery below with a deterministic fake, never a live provider call.
|
|
855
925
|
// no NEW model call starts after an abandon; a call already in flight books its usage via onCall and is discarded
|
|
856
926
|
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 }));
|
|
927
|
+
const generate = noCallAfterAbandon(deps.generate ?? makeLoopGenerate(provider, { rates: draftPrice?.rates ?? null, draftThinking }));
|
|
928
|
+
const confirmGenerate = noCallAfterAbandon(deps.confirmGenerate ?? makeLoopGenerate(provider, { system: CONFIRM_SYSTEM, rates: draftPrice?.rates ?? null, draftThinking }));
|
|
929
|
+
|
|
930
|
+
// P7 item 5 (fwdloop am24/25): the last drafting try, after two failed ones, may ask. Each question comes through the same
|
|
931
|
+
// ask path as every other (the chat box + Send); the answer is REQUIRED (a blank one asks again, there is no skip) and is
|
|
932
|
+
// added WORD FOR WORD as a `~` rule on the job line it names — never rewritten. The lines are mutated in place, so the
|
|
933
|
+
// signed `jobLines`, the answers text and the plan card all read the same words.
|
|
934
|
+
const answerQuestions = async (/** @type {{questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}} */ o) => {
|
|
935
|
+
if (o.dropped.length) appendDraftLog(outDir, { kind: 'questions-dropped', no: Math.max(0, state.steps.length - 1), dropped: o.dropped, at: new Date().toISOString() });
|
|
936
|
+
const total = o.questions.length;
|
|
937
|
+
for (let k = 0; k < total; k += 1) {
|
|
938
|
+
const q = o.questions[k];
|
|
939
|
+
let text = '';
|
|
940
|
+
for (;;) {
|
|
941
|
+
const a = await ask({ kind: 'answer', index: k + 1, total, question: q.question, line: q.line });
|
|
942
|
+
if (a === null) return null;
|
|
943
|
+
// one `~` line is one line: the person's own words, newlines folded to spaces, nothing else touched
|
|
944
|
+
text = String(a).replace(/\s*\n\s*/g, ' ').trim();
|
|
945
|
+
if (text) break;
|
|
946
|
+
say('bot', 'An answer is needed here — there is no skip. Type it in the box below and press Send.');
|
|
947
|
+
}
|
|
948
|
+
const line = jobLines.find((l) => l.n === q.line);
|
|
949
|
+
if (line) { line.rule = line.rule ? `${line.rule}; ${text}` : text; line.rules.push(text); }
|
|
950
|
+
}
|
|
951
|
+
draft.jobLines = signedJobLines(jobLines);
|
|
952
|
+
answers[1] = renderJobLines(jobLines);
|
|
953
|
+
state.messages.push({ ...planMessage(o.confirmed ?? {}, jobLines), text: 'Your answers are in as ~ rules on those job lines. Drafting again.' });
|
|
954
|
+
return { jobLines, answers };
|
|
955
|
+
};
|
|
859
956
|
|
|
860
957
|
state.phase = 'drafting';
|
|
861
958
|
// no early "drafting" line: the real author phase (PHASE_STEP) starts it, carrying draftDetail (see onPhase)
|
|
@@ -866,11 +963,13 @@ export function createSession(card, deps = {}) {
|
|
|
866
963
|
judgeModel: draftJudge.model,
|
|
867
964
|
answers, verdictType, repoPath: prep.tree, lang: LANG,
|
|
868
965
|
questions: questionsFor(verdictType),
|
|
966
|
+
jobLines, inputs: INPUTS,
|
|
869
967
|
writeScope, provider, generate,
|
|
870
968
|
ceilingUsd: card.capUsd,
|
|
871
969
|
rates: draftPrice?.rates ?? null,
|
|
970
|
+
draftThinking,
|
|
872
971
|
onPhase, onCall,
|
|
873
|
-
ask, confirmGenerate, isRepo: true, langResult,
|
|
972
|
+
ask, answerQuestions, confirmGenerate, isRepo: true, langResult,
|
|
874
973
|
...(deps.scout ? { scout: deps.scout } : {}),
|
|
875
974
|
...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
|
|
876
975
|
});
|
|
@@ -893,7 +992,19 @@ export function createSession(card, deps = {}) {
|
|
|
893
992
|
return;
|
|
894
993
|
}
|
|
895
994
|
if (!authored.ok) {
|
|
896
|
-
|
|
995
|
+
// ITEM 6 (P7): every drafting stop says WHY in plain words (the reds travel into the drafting log beside it) — the bare
|
|
996
|
+
// "Stopped: provider-red" of live session smv2bzdqmp0f8 carried a code and no reason.
|
|
997
|
+
const stopReds = authored.reds ?? [];
|
|
998
|
+
if (stopReds.length) {
|
|
999
|
+
const no = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
1000
|
+
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() });
|
|
1001
|
+
}
|
|
1002
|
+
refuse(authored.stop === 'confirm-abandoned' || authored.stop === 'confirm-restart'
|
|
1003
|
+
? `Stopped: ${authored.stop}${authored.refusal ? ` — ${authored.refusal.detail}` : ''}`
|
|
1004
|
+
: draftStopText({
|
|
1005
|
+
stop: String(authored.stop ?? 'authoring-failed'), reds: stopReds, refusal: authored.refusal,
|
|
1006
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
1007
|
+
}), authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
897
1008
|
return;
|
|
898
1009
|
}
|
|
899
1010
|
|