bareloop 0.38.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 +59 -0
- package/bareloop.context.md +87 -24
- package/package.json +3 -3
- package/src/authorflow.js +335 -59
- package/src/authoring.js +125 -3
- package/src/authorjob.js +93 -32
- package/src/authorreadout.js +498 -3
- package/src/authorrun.js +26 -6
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +85 -11
- package/src/cardauthor.js +8 -2
- package/src/declaredclose.js +12 -5
- 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 +323 -29
- package/src/kinds.js +13 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +17 -11
- package/src/panel/authorsession.js +202 -75
- package/src/panel/index.html +302 -312
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +6 -5
- package/src/promptregisters.js +1 -1
- package/src/providers.js +111 -2
- 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/trend.js +1 -0
- package/src/userrun.js +3 -3
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +30 -1
- package/types/authorjob.d.ts +52 -13
- package/types/authorreadout.d.ts +123 -1
- package/types/authorscout.d.ts +5 -3
- package/types/calibrate.d.ts +4 -1
- package/types/declaredclose.d.ts +5 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/judged.d.ts +21 -2
- package/types/panel/authorsession.d.ts +57 -7
- 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 +52 -1
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
- package/types/trend.d.ts +1 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// The SIGN route additionally requires the EXACT `specHash` from that
|
|
14
14
|
// session's own `signing.json` (never trusted from the request body alone
|
|
15
15
|
// without that check) — a mismatch, a missing gate, or a session not yet
|
|
16
|
-
// `prepared` all refuse. No other route in this file (start/send/
|
|
16
|
+
// `prepared` all refuse. No other route in this file (start/send/
|
|
17
17
|
// sign-prepare) can reach the sign path — `signRun` is the only function
|
|
18
18
|
// here that ever calls `spawnFn`.
|
|
19
19
|
|
|
@@ -23,6 +23,8 @@ import { openSync } from 'node:fs';
|
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { readFileSync } from 'node:fs';
|
|
25
25
|
import { createSession, validateJobCard, validateReuseCard, lockedFieldChanged, buildReuseSpec, cardFields, STEP_LABELS } from './authorsession.js';
|
|
26
|
+
import { parseInputs } from '../jobblock.js';
|
|
27
|
+
import { proveInputFiles } from '../source.js';
|
|
26
28
|
import { validateJob } from '../job.js';
|
|
27
29
|
import { checkMonthlyRoom, monthlyRefusalText } from '../monthly.js';
|
|
28
30
|
import { ConfigError } from '../config.js';
|
|
@@ -274,15 +276,23 @@ export function createAuthorRoutes(opts) {
|
|
|
274
276
|
const v = validateJobCard(card, { jobsDir: opts.jobsDir, rows });
|
|
275
277
|
if (!v.ok) { send(400, { ok: false, error: v.error }); return true; }
|
|
276
278
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
// P7: the Inputs are proven at $0 before a session (so before any token): line 1 the repo, lines 2+ tracked files inside it.
|
|
280
|
+
// A miss is a plain 400 naming the line, shown under the card exactly like every other refused start.
|
|
281
|
+
const ip = parseInputs(card.inputs);
|
|
282
|
+
if (!ip.ok) { send(400, { ok: false, error: ip.error }); return true; }
|
|
283
|
+
proveInputFiles(ip.inputs).then((proof) => {
|
|
284
|
+
if (!proof.ok) { send(400, { ok: false, error: proof.error }); return; }
|
|
285
|
+
if (hasLiveSession()) { send(409, { ok: false, error: 'an authoring session is already live — one at a time' }); return; }
|
|
286
|
+
const session = createSession(cardFields(card), {
|
|
287
|
+
env: envNow(), sessionsRoot: opts.sessionsRoot, ...(opts.home !== undefined ? { home: opts.home } : {}), ...(reuse ? { reuse } : {}),
|
|
288
|
+
});
|
|
289
|
+
sessions.set(session.id, session);
|
|
290
|
+
send(200, { ok: true, sessionId: session.id, reuse: reuse !== null, state: session.state });
|
|
291
|
+
}).catch((e) => { send(500, { ok: false, error: `could not check the inputs: ${/** @type {Error} */ (e)?.message ?? e}` }); });
|
|
282
292
|
return true;
|
|
283
293
|
}
|
|
284
294
|
|
|
285
|
-
const m = /^\/api\/author\/([A-Za-z0-9]+)(\/(send|
|
|
295
|
+
const m = /^\/api\/author\/([A-Za-z0-9]+)(\/(send|sign-prepare|sign|check-deps|abandon))?$/.exec(pathname);
|
|
286
296
|
if (!m) { send(404, { ok: false, error: 'not found' }); return true; }
|
|
287
297
|
const session = sessions.get(m[1]);
|
|
288
298
|
if (!session) { send(404, { ok: false, error: 'no such session' }); return true; }
|
|
@@ -300,10 +310,6 @@ export function createAuthorRoutes(opts) {
|
|
|
300
310
|
send(r.ok ? 200 : 400, { ...r, state: session.state });
|
|
301
311
|
return true;
|
|
302
312
|
}
|
|
303
|
-
if (sub === 'revise') {
|
|
304
|
-
session.revise(String(body?.text ?? '')).then((r) => send(r.ok ? 200 : 400, { ...r, state: session.state }));
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
313
|
if (sub === 'abandon') {
|
|
308
314
|
const r = session.abandon();
|
|
309
315
|
send(r.ok ? 200 : 400, { ...r, state: session.state });
|
|
@@ -42,17 +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, draftStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
55
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';
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* THE PROGRESS LIST's one table (hamr 2026-10-04, "one place showing all"): every pipeline step the left Chat panel
|
|
@@ -61,6 +66,7 @@ import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
|
61
66
|
* @type {Readonly<Record<string, string>>}
|
|
62
67
|
*/
|
|
63
68
|
export const STEP_LABELS = Object.freeze({
|
|
69
|
+
thinking: 'thinking',
|
|
64
70
|
setup: 'checking setup',
|
|
65
71
|
copy: 'making worktree',
|
|
66
72
|
check: 'checking source',
|
|
@@ -104,6 +110,20 @@ export const PHASE_STEP = Object.freeze({
|
|
|
104
110
|
'rubric-scrubbed': 'calibrate',
|
|
105
111
|
});
|
|
106
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
|
+
|
|
107
127
|
/** @typedef {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}} StepLine */
|
|
108
128
|
|
|
109
129
|
/** @param {StepLine[]} steps @param {string} id the LATEST line carrying this id (the list is a time-ordered log; an id can repeat) */
|
|
@@ -132,13 +152,23 @@ const SOURCE_CHECK_CODES = new Set([
|
|
|
132
152
|
'source-file-oversize', 'source-changed-after-scan', 'source-untracked-in-repo',
|
|
133
153
|
]);
|
|
134
154
|
|
|
135
|
-
/** the
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
|
|
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
|
+
|
|
158
|
+
/**
|
|
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[]}}}
|
|
165
|
+
*/
|
|
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
|
+
};
|
|
171
|
+
}
|
|
142
172
|
|
|
143
173
|
/** @param {string} name @returns {boolean} kebab-case slug, same rule `job.js`'s `SLUG_RE` enforces */
|
|
144
174
|
function isSlug(name) {
|
|
@@ -164,7 +194,18 @@ export function jobNameTaken(name, opts = {}) {
|
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
/** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
|
|
167
|
-
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
|
+
}
|
|
168
209
|
|
|
169
210
|
/**
|
|
170
211
|
* Only the form's own fields, nothing else a request body might carry (`startFrom`, stray keys).
|
|
@@ -180,15 +221,15 @@ export function cardFields(card) {
|
|
|
180
221
|
|
|
181
222
|
/**
|
|
182
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
|
|
183
|
-
* 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
|
|
184
225
|
* editable. The page greys them; THIS is what refuses (the server never trusts the page). Changing any locked box
|
|
185
226
|
* is a different job: Clear the card, which drafts.
|
|
186
227
|
*/
|
|
187
|
-
export const REUSE_OPEN_FIELDS = Object.freeze(['
|
|
228
|
+
export const REUSE_OPEN_FIELDS = Object.freeze(['inputs', 'destination', 'model', 'capUsd', 'maxWallMs']);
|
|
188
229
|
/** every card field that is NOT open on a reuse card, in card order */
|
|
189
230
|
export const REUSE_LOCKED_FIELDS = Object.freeze(CARD_FIELDS.filter((f) => !REUSE_OPEN_FIELDS.includes(f)));
|
|
190
231
|
const REUSE_LABELS = Object.freeze(/** @type {Record<string, string>} */ ({
|
|
191
|
-
jobName: 'Job name', checkType: 'Check type',
|
|
232
|
+
jobName: 'Job name', checkType: 'Check type', jobText: 'The job',
|
|
192
233
|
}));
|
|
193
234
|
|
|
194
235
|
/**
|
|
@@ -207,12 +248,12 @@ export function lockedFieldChanged(card, origin) {
|
|
|
207
248
|
|
|
208
249
|
/**
|
|
209
250
|
* The spec a reuse signs: a COPY of the origin's signed spec with ONLY the open fields set from the card —
|
|
210
|
-
* `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
|
|
211
252
|
* worker (`provider`/`baseUrl`/`model`) from the Model Name chosen in Settings > Providers, spelled exactly as normal
|
|
212
253
|
* authoring spells it (`baseUrl` only when the row has one; `model` only when the Name is not the provider's default
|
|
213
254
|
* tier). A RUBRIC (soft-green) origin with no explicit `judge` first has its judge pinned to the ORIGIN's resolved judge
|
|
214
255
|
* identity — the judge must not change because the worker did (`resolveJobJudge`, the one spelling); an explicit
|
|
215
|
-
* `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
|
|
216
257
|
* hashes to a NEW `jobSpecHash` and the SAME `workflowKey` — the caller checks the second after building.
|
|
217
258
|
* @param {any} originSpec
|
|
218
259
|
* @param {Record<string, any>} card
|
|
@@ -222,6 +263,8 @@ export function lockedFieldChanged(card, origin) {
|
|
|
222
263
|
export function buildReuseSpec(originSpec, card, rows) {
|
|
223
264
|
const spec = JSON.parse(JSON.stringify(originSpec));
|
|
224
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;
|
|
225
268
|
spec.budgetUsd = card.capUsd;
|
|
226
269
|
if (typeof card.maxWallMs === 'number' && Number.isFinite(card.maxWallMs) && card.maxWallMs > 0) spec.maxWallMs = card.maxWallMs;
|
|
227
270
|
else delete spec.maxWallMs;
|
|
@@ -238,7 +281,7 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
238
281
|
|
|
239
282
|
/**
|
|
240
283
|
* `$0` validation for a REUSE start: the signed job carries the goal, checks and guardrails, so those boxes are not
|
|
241
|
-
* 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,
|
|
242
285
|
* and the cap. (The spec built from the open boxes is validated by the route with `validateJob`.)
|
|
243
286
|
* @param {any} card
|
|
244
287
|
* @param {{rows?: readonly import('../providerrows.js').KeyRow[]}} [opts]
|
|
@@ -247,7 +290,8 @@ export function buildReuseSpec(originSpec, card, rows) {
|
|
|
247
290
|
export function validateReuseCard(card, opts = {}) {
|
|
248
291
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
249
292
|
if (!modelChoiceFor(opts.rows ?? [], card.model)) return { ok: false, error: 'Model must be one of the Names in Settings > Providers' };
|
|
250
|
-
|
|
293
|
+
const ip = parseInputs(card.inputs);
|
|
294
|
+
if (!ip.ok) return { ok: false, error: ip.error };
|
|
251
295
|
if (typeof card.destination !== 'string' || card.destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
252
296
|
if (typeof card.capUsd !== 'number' || !Number.isFinite(card.capUsd) || card.capUsd <= 0) {
|
|
253
297
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
@@ -265,7 +309,7 @@ export { nonRepoSourceMessage };
|
|
|
265
309
|
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
266
310
|
* source that does not peek as a repo) keeps today's behaviour: its Destination is an absolute output directory.
|
|
267
311
|
* @param {string} destination
|
|
268
|
-
* @param {boolean} isRepoLike `looksLikeRepoSource(card
|
|
312
|
+
* @param {boolean} isRepoLike `looksLikeRepoSource(cardSource(card))`
|
|
269
313
|
* @returns {string|null} the plain refusal, or null when the Destination is fine
|
|
270
314
|
*/
|
|
271
315
|
export function repoDestinationProblem(destination, isRepoLike) {
|
|
@@ -282,19 +326,20 @@ export function repoDestinationProblem(destination, isRepoLike) {
|
|
|
282
326
|
*/
|
|
283
327
|
export function validateJobCard(card, opts = {}) {
|
|
284
328
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
285
|
-
const { jobName, checkType, model,
|
|
329
|
+
const { jobName, checkType, model, destination, capUsd } = card;
|
|
286
330
|
if (!isSlug(jobName)) return { ok: false, error: 'Job name must be a kebab-case slug (letters, digits, dashes)' };
|
|
287
331
|
if (jobNameTaken(jobName, { jobsDir: opts.jobsDir })) {
|
|
288
332
|
return { ok: false, error: `a job named "${jobName}" already exists — P3 is new jobs only (Q4=A)` };
|
|
289
333
|
}
|
|
290
334
|
if (checkType !== 'deterministic' && checkType !== 'rubric') return { ok: false, error: 'Check type must be deterministic or rubric' };
|
|
291
335
|
if (!modelChoiceFor(opts.rows ?? [], model)) return { ok: false, error: 'Model must be one of the Names in Settings > Providers' };
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
}
|
|
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' };
|
|
298
343
|
if (typeof capUsd !== 'number' || !Number.isFinite(capUsd) || capUsd <= 0) {
|
|
299
344
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
300
345
|
}
|
|
@@ -304,25 +349,36 @@ export function validateJobCard(card, opts = {}) {
|
|
|
304
349
|
/**
|
|
305
350
|
* ONE session's engine — created already RUNNING (the caller awaits nothing;
|
|
306
351
|
* the pipeline drives itself, reporting through `state`). `state.pendingAsk`
|
|
307
|
-
* is the ONE thing a person answers next; `answer()`/`send()
|
|
352
|
+
* is the ONE thing a person answers next; `answer()`/`send()`/
|
|
308
353
|
* `signPrepare()` are the only ways in.
|
|
309
354
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
310
355
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
311
356
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
312
|
-
* 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
|
|
313
358
|
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
314
359
|
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
315
360
|
* @returns {any} the session object
|
|
316
361
|
*/
|
|
317
362
|
export function createSession(card, deps = {}) {
|
|
318
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 ?? '';
|
|
319
375
|
const sessionsRoot = deps.sessionsRoot ?? join(process.env.HOME ?? '/tmp', '.config', 'bareloop', 'panel-sessions');
|
|
320
376
|
// config.json lives beside the sessions dir (~/.config/bareloop) unless a home is injected
|
|
321
377
|
const configHome = deps.home ?? dirname(sessionsRoot);
|
|
322
378
|
const timeoutMs = deps.timeoutMs ?? 300_000;
|
|
323
379
|
// TEST SEAMS ONLY (never set by `src/panel/authorroutes.js`'s real caller):
|
|
324
380
|
// override the declaration composer and/or `prepareSigning` itself so a
|
|
325
|
-
// test can drive the REAL confirm turn / ask() channel /
|
|
381
|
+
// test can drive the REAL confirm turn / ask() channel /
|
|
326
382
|
// hash-matching machinery this file owns, without also re-running (and
|
|
327
383
|
// re-proving) `authorClose`'s own composer ladder or `prepareSigning`'s own
|
|
328
384
|
// gates — both already have their own test suites. Absent, both default to
|
|
@@ -342,7 +398,6 @@ export function createSession(card, deps = {}) {
|
|
|
342
398
|
messages: /** @type {{role: string, text: string}[]} */ ([]),
|
|
343
399
|
pendingAsk: /** @type {any} */ (null),
|
|
344
400
|
cost: /** @type {any} */ (null),
|
|
345
|
-
revisesLeft: CONFIRM_ROUND_CAP,
|
|
346
401
|
specHash: /** @type {string|null} */ (null),
|
|
347
402
|
resolvedSpecPath: /** @type {string|null} */ (null),
|
|
348
403
|
error: /** @type {string|null} */ (null),
|
|
@@ -411,6 +466,8 @@ export function createSession(card, deps = {}) {
|
|
|
411
466
|
|
|
412
467
|
/** @param {'bot'|'you'|'system'} role @param {string} text */
|
|
413
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)); };
|
|
414
471
|
|
|
415
472
|
/** @type {((value: string|null) => void)|null} */
|
|
416
473
|
let resolvePending = null;
|
|
@@ -436,15 +493,9 @@ export function createSession(card, deps = {}) {
|
|
|
436
493
|
resolvePending = resolveFn;
|
|
437
494
|
if (step.kind === 'language') say('bot', `${step.field?.prompt ?? 'Which language is this job about?'} (${(step.candidates ?? []).join(', ')})`);
|
|
438
495
|
else if (step.kind === 'menu') {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if ((p.notChecked ?? []).length) lines.push(`Not checked: ${p.notChecked.join(' · ')}`);
|
|
442
|
-
if ((p.questions ?? []).length) lines.push(`Questions: ${p.questions.join(' · ')}`);
|
|
443
|
-
lines.push('Sign & run to confirm this plan, or Revise to describe a change.');
|
|
444
|
-
say('bot', lines.join('\n'));
|
|
445
|
-
} 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}`);
|
|
446
498
|
else if (step.kind === 'goal') say('bot', 'Type the goal sentence yourself.');
|
|
447
|
-
else if (step.kind === 'fix') say('bot', 'What should change?');
|
|
448
499
|
});
|
|
449
500
|
|
|
450
501
|
/** @param {string|null} value @returns {boolean} */
|
|
@@ -457,21 +508,7 @@ export function createSession(card, deps = {}) {
|
|
|
457
508
|
return true;
|
|
458
509
|
};
|
|
459
510
|
|
|
460
|
-
/** @param {'menu'|'fix'|'answer'|'language'|'goal'} kind @param {number} timeoutMsInner */
|
|
461
|
-
const waitForPendingKind = async (kind, timeoutMsInner = 2000) => {
|
|
462
|
-
const start = Date.now();
|
|
463
|
-
while (Date.now() - start < timeoutMsInner) {
|
|
464
|
-
if (state.pendingAsk?.kind === kind) return true;
|
|
465
|
-
// eslint-disable-next-line no-await-in-loop
|
|
466
|
-
await new Promise((r) => { setTimeout(r, 10); });
|
|
467
|
-
}
|
|
468
|
-
return state.pendingAsk?.kind === kind;
|
|
469
|
-
};
|
|
470
|
-
|
|
471
511
|
const onPhase = (name, data = {}) => {
|
|
472
|
-
if (name === 'confirm-round' && typeof data.round === 'number') {
|
|
473
|
-
state.revisesLeft = Math.max(0, CONFIRM_ROUND_CAP - (data.round - 1));
|
|
474
|
-
}
|
|
475
512
|
// COLLAPSED INTO THE PROGRESS INDICATOR ONLY (build item 3) — this used
|
|
476
513
|
// to also `say('system', `… ${name}`)`, one jargon chat bubble per
|
|
477
514
|
// library phase (`… confirm-turn-done`, `… seed-read`, …), read back to
|
|
@@ -520,8 +557,16 @@ export function createSession(card, deps = {}) {
|
|
|
520
557
|
// the call's label, the drafting model, its cost (null stays null: unpriced is never $0). One owner: this onCall.
|
|
521
558
|
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
522
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
|
+
}
|
|
523
567
|
appendDraftLog(outDir, {
|
|
524
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 }),
|
|
525
570
|
costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
|
|
526
571
|
});
|
|
527
572
|
try {
|
|
@@ -533,6 +578,10 @@ export function createSession(card, deps = {}) {
|
|
|
533
578
|
};
|
|
534
579
|
/** @type {string|null} */
|
|
535
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;
|
|
536
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} */
|
|
537
586
|
let madeWorktree = null;
|
|
538
587
|
/** a session that ends WITHOUT ever running (refused, errored, abandoned) takes its worktree with it; a signed one keeps it for the run */
|
|
@@ -558,9 +607,18 @@ export function createSession(card, deps = {}) {
|
|
|
558
607
|
|
|
559
608
|
async function run() {
|
|
560
609
|
// ── keys, at $0, before anything is prepared ────────────────────────
|
|
561
|
-
stepStart('setup');
|
|
562
610
|
const keyRows = rowsForHome(configHome);
|
|
563
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');
|
|
564
622
|
if (!modelChoice) { refuse(`Model "${card.model}" is not a Name in Settings > Providers. Stopped — nothing spent.`); return; }
|
|
565
623
|
draftIdentity = { provider: modelChoice.provider, baseUrl: modelChoice.baseUrl ?? null, model: modelChoice.name };
|
|
566
624
|
let providerEntry;
|
|
@@ -585,7 +643,7 @@ export function createSession(card, deps = {}) {
|
|
|
585
643
|
|
|
586
644
|
const verdictType = card.checkType === 'rubric' ? 'soft-green' : 'green';
|
|
587
645
|
const into = join(outDir, 'source-seed');
|
|
588
|
-
const isRepoLike = looksLikeRepoSource(
|
|
646
|
+
const isRepoLike = looksLikeRepoSource(SOURCE);
|
|
589
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
|
|
590
648
|
const destProblem = repoDestinationProblem(card.destination, isRepoLike);
|
|
591
649
|
if (destProblem !== null) { refuse(destProblem); return; }
|
|
@@ -595,7 +653,7 @@ export function createSession(card, deps = {}) {
|
|
|
595
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
|
|
596
654
|
// drafting start — `.bareloop/wt/<session id>` at their current commit. Edits they have not committed are not in it,
|
|
597
655
|
// and the progress line says so (a notice, never a refusal).
|
|
598
|
-
const repoRoot = isRepoLike ? nearestGitAncestor(resolve(
|
|
656
|
+
const repoRoot = isRepoLike ? nearestGitAncestor(resolve(SOURCE))?.dir ?? null : null;
|
|
599
657
|
if (repoRoot !== null) {
|
|
600
658
|
const dirty = uncommittedCount(repoRoot);
|
|
601
659
|
if (dirty > 0) {
|
|
@@ -610,7 +668,7 @@ export function createSession(card, deps = {}) {
|
|
|
610
668
|
// (relative) Destination is therefore handed to the door only when the source is repo-like.
|
|
611
669
|
const destIsDir = isAbsoluteDestination(card.destination);
|
|
612
670
|
const prep = await prepareSource({
|
|
613
|
-
source:
|
|
671
|
+
source: SOURCE, into, ...(isRepoLike || destIsDir ? { destination: card.destination } : {}),
|
|
614
672
|
...(repoRoot === null ? {} : { worktree: worktreePath(repoRoot, id) }),
|
|
615
673
|
});
|
|
616
674
|
if (repoRoot !== null && prep.stop === null && prep.manifest?.worktree) madeWorktree = { repo: repoRoot, dir: prep.manifest.worktree };
|
|
@@ -764,9 +822,28 @@ export function createSession(card, deps = {}) {
|
|
|
764
822
|
});
|
|
765
823
|
const signingFile = join(outDir, 'signing.json');
|
|
766
824
|
writeFileSync(signingFile, `${JSON.stringify(signing, null, 2)}\n`);
|
|
825
|
+
// The `calibrating` line is the rubric phase the person watched; its ✓ was set long before the gate graded
|
|
826
|
+
// anything. Once the gate has a result, the line says THAT, on its own line (hamr, 2026-10-09).
|
|
827
|
+
const calSum = calibrationSummary(signing.gates?.calibration);
|
|
828
|
+
const calStep = latestStep(state.steps, 'calibrate');
|
|
829
|
+
if (calSum && calStep) {
|
|
830
|
+
calStep.status = calSum.ok ? 'done' : 'failed';
|
|
831
|
+
calStep.detail = calSum.note;
|
|
832
|
+
logSteps();
|
|
833
|
+
}
|
|
767
834
|
|
|
768
835
|
if (!signing.ok) {
|
|
769
|
-
|
|
836
|
+
// plain words for the person (src/authorreadout.js, the one owner the CLI prints too); the full reds go into
|
|
837
|
+
// the drafting log on the failed step, so the cause survives the session
|
|
838
|
+
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
839
|
+
appendDraftLog(outDir, {
|
|
840
|
+
kind: 'step-reds', no: runningNo === -1 ? state.steps.length - 1 : runningNo, stop: 'signing-gates-failed',
|
|
841
|
+
reds: redsRecord(signing.reds ?? [], { signing: true }), at: new Date().toISOString(),
|
|
842
|
+
});
|
|
843
|
+
refuse(signingStopText({
|
|
844
|
+
reds: signing.reds ?? [], refusal: signing.refusal,
|
|
845
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
846
|
+
}));
|
|
770
847
|
return;
|
|
771
848
|
}
|
|
772
849
|
state.specHash = signing.specHash;
|
|
@@ -804,6 +881,9 @@ export function createSession(card, deps = {}) {
|
|
|
804
881
|
schema: 'job-v1',
|
|
805
882
|
job: card.jobName,
|
|
806
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,
|
|
807
887
|
provider: modelChoice.provider,
|
|
808
888
|
...(modelChoice.baseUrl ? { baseUrl: modelChoice.baseUrl } : {}),
|
|
809
889
|
// the Name is the model id: signed into the spec only when it is not the provider's own default
|
|
@@ -823,8 +903,8 @@ export function createSession(card, deps = {}) {
|
|
|
823
903
|
}
|
|
824
904
|
|
|
825
905
|
/** @type {Record<string, string>} */
|
|
826
|
-
const answers = { 1:
|
|
827
|
-
if (verdictType === 'soft-green') answers[
|
|
906
|
+
const answers = { 1: renderJobLines(jobLines) };
|
|
907
|
+
if (verdictType === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(jobLines);
|
|
828
908
|
const required = requiredAnswersFor(verdictType);
|
|
829
909
|
for (const n of required) {
|
|
830
910
|
if (!answers[n]) { refuse(`the ${verdictType} check type needs ${required.length} answers and one is missing (key ${n})`); return; }
|
|
@@ -840,12 +920,39 @@ export function createSession(card, deps = {}) {
|
|
|
840
920
|
// TEST SEAMS (see the constructor's own note): a test overrides
|
|
841
921
|
// `generate`/`confirmGenerate` directly (bypassing this real, constructed
|
|
842
922
|
// `provider` for the model boundary) and/or `scout` (bypassing the real
|
|
843
|
-
// paid scout) so it can drive the REAL confirm turn/ask()/
|
|
923
|
+
// paid scout) so it can drive the REAL confirm turn/ask()/hash
|
|
844
924
|
// machinery below with a deterministic fake, never a live provider call.
|
|
845
925
|
// no NEW model call starts after an abandon; a call already in flight books its usage via onCall and is discarded
|
|
846
926
|
const noCallAfterAbandon = (fn) => (...a) => { if (abandoned) throw new Error('abandoned — no new model call'); return fn(...a); };
|
|
847
|
-
const generate = noCallAfterAbandon(deps.generate ?? makeLoopGenerate(provider, { rates: draftPrice?.rates ?? null }));
|
|
848
|
-
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
|
+
};
|
|
849
956
|
|
|
850
957
|
state.phase = 'drafting';
|
|
851
958
|
// no early "drafting" line: the real author phase (PHASE_STEP) starts it, carrying draftDetail (see onPhase)
|
|
@@ -856,17 +963,48 @@ export function createSession(card, deps = {}) {
|
|
|
856
963
|
judgeModel: draftJudge.model,
|
|
857
964
|
answers, verdictType, repoPath: prep.tree, lang: LANG,
|
|
858
965
|
questions: questionsFor(verdictType),
|
|
966
|
+
jobLines, inputs: INPUTS,
|
|
859
967
|
writeScope, provider, generate,
|
|
860
968
|
ceilingUsd: card.capUsd,
|
|
861
969
|
rates: draftPrice?.rates ?? null,
|
|
970
|
+
draftThinking,
|
|
862
971
|
onPhase, onCall,
|
|
863
|
-
ask, confirmGenerate, isRepo: true, langResult,
|
|
972
|
+
ask, answerQuestions, confirmGenerate, isRepo: true, langResult,
|
|
864
973
|
...(deps.scout ? { scout: deps.scout } : {}),
|
|
865
974
|
...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
|
|
866
975
|
});
|
|
867
976
|
|
|
977
|
+
if (!authored.ok && PLAIN_PROPOSAL_STOPS.includes(String(authored.stop))) {
|
|
978
|
+
// a refused rubric proposal: the person gets the reason in plain words (src/authorreadout.js, the one owner the
|
|
979
|
+
// CLI prints too), and the full reds go into the drafting log on the failed step so the cause survives the session
|
|
980
|
+
const cases = authored.judged?.signed?.cases ?? authored.judged?.proposal?.proposal?.cases ?? null;
|
|
981
|
+
const reds = authored.reds ?? [];
|
|
982
|
+
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
983
|
+
appendDraftLog(outDir, {
|
|
984
|
+
kind: 'step-reds', no: runningNo === -1 ? state.steps.length - 1 : runningNo, stop: authored.stop,
|
|
985
|
+
reds: redsRecord(reds, { cases }), at: new Date().toISOString(),
|
|
986
|
+
});
|
|
987
|
+
refuse(proposalStopText({
|
|
988
|
+
stop: String(authored.stop), reds, cases,
|
|
989
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
990
|
+
source: authored.judged?.source === 'signer' ? 'signer' : 'proposal',
|
|
991
|
+
}));
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
868
994
|
if (!authored.ok) {
|
|
869
|
-
|
|
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');
|
|
870
1008
|
return;
|
|
871
1009
|
}
|
|
872
1010
|
|
|
@@ -888,7 +1026,7 @@ export function createSession(card, deps = {}) {
|
|
|
888
1026
|
/** @param {string} text */
|
|
889
1027
|
send: (text) => {
|
|
890
1028
|
if (!state.pendingAsk) return { ok: false, error: 'nothing is being asked right now' };
|
|
891
|
-
if (state.pendingAsk.kind === 'menu') return { ok: false, error: 'use Sign & run
|
|
1029
|
+
if (state.pendingAsk.kind === 'menu') return { ok: false, error: 'use Sign & run for a plan, never Send — the chat can never sign or pick a plan action' };
|
|
892
1030
|
if (state.pendingAsk.kind === 'install-needed') return { ok: false, error: 'install the packages, then click Check again — nothing to send here' };
|
|
893
1031
|
say('you', text);
|
|
894
1032
|
answer(text);
|
|
@@ -917,17 +1055,6 @@ export function createSession(card, deps = {}) {
|
|
|
917
1055
|
r();
|
|
918
1056
|
return { ok: true };
|
|
919
1057
|
},
|
|
920
|
-
/** @param {string} text */
|
|
921
|
-
revise: async (text) => {
|
|
922
|
-
if (!state.pendingAsk || state.pendingAsk.kind !== 'menu') return { ok: false, error: 'no plan is waiting for a Revise right now' };
|
|
923
|
-
if (state.revisesLeft <= 0) return { ok: false, error: 'no revises left (D3: max 2 rounds)' };
|
|
924
|
-
answer('fix');
|
|
925
|
-
const reached = await waitForPendingKind('fix', 3000);
|
|
926
|
-
if (!reached) return { ok: false, error: 'the confirm turn did not ask for a fix in time' };
|
|
927
|
-
say('you', text);
|
|
928
|
-
answer(text);
|
|
929
|
-
return { ok: true };
|
|
930
|
-
},
|
|
931
1058
|
/** click 1 — the confirm pick. Never signs on its own. */
|
|
932
1059
|
signPrepare: () => {
|
|
933
1060
|
if (!state.pendingAsk || state.pendingAsk.kind !== 'menu') return { ok: false, error: 'no plan is waiting for a decision right now' };
|