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
package/src/authorflow.js
CHANGED
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
// F6 throughout: an unpriced call makes the TOTAL unknown, and unknown is null.
|
|
73
73
|
// No spine emission here — M4 wires events.
|
|
74
74
|
|
|
75
|
+
import { renderInputs } from './jobblock.js';
|
|
76
|
+
import { draftCallOptions } from './providers.js';
|
|
75
77
|
import { createRequire } from 'node:module';
|
|
76
78
|
import { realpathSync } from 'node:fs';
|
|
77
79
|
import { join, relative, isAbsolute } from 'node:path';
|
|
@@ -110,6 +112,96 @@ export const AUTHOR_MAX_TOKENS = 32000;
|
|
|
110
112
|
/** the one slot the model fills on an injected guard */
|
|
111
113
|
export const FILL_MARKER = '<FILL IN>';
|
|
112
114
|
|
|
115
|
+
/** P7 item 5 (fwdloop am24/25): the most questions the MACHINE keeps from the try-3 drafter. The drafter is never told this number. */
|
|
116
|
+
export const MAX_QUESTIONS = 2;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The model-visible words of the optional `questions` property — offered ONLY on the last drafting try and only after the
|
|
120
|
+
* two before it both failed validation (a mechanism, never a wish: the schema carries it on that one call and nowhere
|
|
121
|
+
* else). It lives in the property's description, so the frozen revise turn stays measurement-only. No digit and no number
|
|
122
|
+
* word anywhere in it (a test greps this): the machine, not the prompt, bounds the count.
|
|
123
|
+
*/
|
|
124
|
+
export const QUESTIONS_DESCRIPTION = 'Optional. Add a question only if a refusal in the measured results names a job line that cannot be drafted '
|
|
125
|
+
+ 'without the person\'s answer (an undefined size, format, destination, or what "done" means). Each question is {line, question}: '
|
|
126
|
+
+ '"line" is the number of the job line it is about. If the refusals can be fixed without asking, fix them and leave this out. '
|
|
127
|
+
+ 'Never ask about a budget, a cap or a time limit (the person signs those). Still deliver the complete corrected declaration.';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Which job lines did these validation reds name? A red names a line in its text ("job line N") or through the stage it
|
|
131
|
+
* sits on (path `stages[i]…` → that stage's own `fromLine`; `refused[i]…` → that refusal's `line`). A red naming none (a
|
|
132
|
+
* missing top-level key) names no line. Sorted real line numbers.
|
|
133
|
+
* @param {Red[]} reds @param {any} declaration @param {number[]} lineNums
|
|
134
|
+
* @returns {number[]}
|
|
135
|
+
*/
|
|
136
|
+
export function linesNamedByReds(reds, declaration, lineNums) {
|
|
137
|
+
/** @type {Set<number>} */
|
|
138
|
+
const found = new Set();
|
|
139
|
+
const stages = Array.isArray(declaration?.stages) ? declaration.stages : [];
|
|
140
|
+
const refused = Array.isArray(declaration?.refused) ? declaration.refused : [];
|
|
141
|
+
for (const red of reds ?? []) {
|
|
142
|
+
for (const m of String(red?.detail ?? '').matchAll(/\bjob line (\d+)\b/g)) found.add(Number(m[1]));
|
|
143
|
+
const st = /^stages\[(\d+)\]/.exec(String(red?.path ?? ''));
|
|
144
|
+
if (st && Array.isArray(stages[Number(st[1])]?.fromLine)) {
|
|
145
|
+
for (const n of stages[Number(st[1])].fromLine) if (Number.isInteger(n)) found.add(n);
|
|
146
|
+
}
|
|
147
|
+
const rf = /^refused\[(\d+)\]/.exec(String(red?.path ?? ''));
|
|
148
|
+
if (rf && Number.isInteger(refused[Number(rf[1])]?.line)) found.add(refused[Number(rf[1])].line);
|
|
149
|
+
}
|
|
150
|
+
return [...found].filter((n) => lineNums.includes(n)).sort((a, b) => a - b);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The machine's keep-or-drop of the try-3 questions. Kept only when its line is one the previous try's reds named and its
|
|
155
|
+
* text is non-blank, at most {@link MAX_QUESTIONS}; every drop comes back with its reason, for the draft's log.
|
|
156
|
+
* @param {any} raw @param {number[]} failedLines
|
|
157
|
+
* @returns {{kept: {line: number, question: string}[], dropped: {reason: string, raw: any}[]}}
|
|
158
|
+
*/
|
|
159
|
+
export function normalizeQuestions(raw, failedLines) {
|
|
160
|
+
/** @type {{line: number, question: string}[]} */
|
|
161
|
+
const kept = [];
|
|
162
|
+
/** @type {{reason: string, raw: any}[]} */
|
|
163
|
+
const dropped = [];
|
|
164
|
+
if (raw === undefined || raw === null) return { kept, dropped };
|
|
165
|
+
if (!Array.isArray(raw)) return { kept, dropped: [{ reason: 'questions is not a list', raw }] };
|
|
166
|
+
for (const q of raw) {
|
|
167
|
+
if (!q || typeof q !== 'object' || !Number.isInteger(q.line)) { dropped.push({ reason: 'no line', raw: q }); continue; }
|
|
168
|
+
if (!failedLines.includes(q.line)) { dropped.push({ reason: `line ${q.line} is not a line the previous try's checks failed on`, raw: q }); continue; }
|
|
169
|
+
if (typeof q.question !== 'string' || q.question.trim() === '') { dropped.push({ reason: 'blank question', raw: q }); continue; }
|
|
170
|
+
if (kept.length >= MAX_QUESTIONS) { dropped.push({ reason: 'beyond the machine\'s limit', raw: q }); continue; }
|
|
171
|
+
kept.push({ line: q.line, question: redactSecrets(q.question.trim()) });
|
|
172
|
+
}
|
|
173
|
+
return { kept, dropped };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Did this try FAIL THE SEED READ (hamr's ruling 2026-10-10, differing from fwdloop's validation-only rule)? It did when a work
|
|
178
|
+
* stage instrument-stopped, or when no work stage is red on the repo as it is — the vacuous close ("nothing in the close fails
|
|
179
|
+
* at all": it grades nothing). The mandatory guards (`guardNames`) are green at the seed by design and never count as work.
|
|
180
|
+
* The job lines it names are the `fromLine`s of the stages the seed read named (the instrument-stopped ones, else every work
|
|
181
|
+
* stage); if none of them maps to a real line, EVERY numbered job line counts.
|
|
182
|
+
* @param {any[]} rows @param {any} declaration @param {string[]} guardNames @param {number[]} lineNums
|
|
183
|
+
* @returns {{failed: boolean, lines: number[]}}
|
|
184
|
+
*/
|
|
185
|
+
export function seedReadFailure(rows, declaration, guardNames, lineNums) {
|
|
186
|
+
const work = (Array.isArray(rows) ? rows : []).filter((r) => !guardNames.includes(r?.stage));
|
|
187
|
+
const stopped = work.filter((r) => r?.verdict === 'instrument-stop');
|
|
188
|
+
const vacuous = !work.some((r) => r?.verdict === 'red');
|
|
189
|
+
if (stopped.length === 0 && !vacuous) return { failed: false, lines: [] };
|
|
190
|
+
const named = new Set((stopped.length ? stopped : work).map((r) => r?.stage));
|
|
191
|
+
/** @type {Set<number>} */
|
|
192
|
+
const found = new Set();
|
|
193
|
+
for (const st of Array.isArray(declaration?.stages) ? declaration.stages : []) {
|
|
194
|
+
if (named.has(st?.name) && Array.isArray(st.fromLine)) for (const n of st.fromLine) if (Number.isInteger(n)) found.add(n);
|
|
195
|
+
}
|
|
196
|
+
const lines = [...found].filter((n) => lineNums.includes(n)).sort((a, b) => a - b);
|
|
197
|
+
return { failed: true, lines: lines.length ? lines : [...lineNums] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The honest truncation signal: bare-agent's Loop tags a round cut off at the output cap `error: 'truncated:max_tokens'`
|
|
201
|
+
* with the neutral `stopReason: 'max_tokens'` (node_modules/bare-agent/src/loop.js, BA-6/BA-13).
|
|
202
|
+
* @param {any} r */
|
|
203
|
+
export const isOutputCutOff = (r) => r?.error === 'truncated:max_tokens' || (Boolean(r?.error) && r?.stopReason === 'max_tokens');
|
|
204
|
+
|
|
113
205
|
/**
|
|
114
206
|
* ONE red, scrubbed — the same spelling `prepareSigning` uses (src/authorjob.js),
|
|
115
207
|
* and for the same reason: a validation red QUOTES what the model declared, and
|
|
@@ -205,6 +297,22 @@ export const SOURCE_FIELD = Object.freeze({
|
|
|
205
297
|
prompt: 'A local folder, subfolder or file, or one URL the machine can reach (no login, no setup).',
|
|
206
298
|
});
|
|
207
299
|
|
|
300
|
+
/** P7: the Inputs box's wording (replaces Source). Line 1 is the repo; lines 2+ are tracked files inside it. */
|
|
301
|
+
export const INPUTS_FIELD = Object.freeze({
|
|
302
|
+
id: 'inputs',
|
|
303
|
+
kind: 'mechanical',
|
|
304
|
+
label: 'Inputs',
|
|
305
|
+
prompt: 'What the job points at, one per line as `label: value`. Line 1 is the repo, as an absolute path\n'
|
|
306
|
+
+ '(e.g. `repo: /home/me/myrepo`). Lines 2 and on are files or folders inside that repo, repo-relative\n'
|
|
307
|
+
+ '(e.g. `spec: docs/spec.md`) — git must track them.',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/** P7: how to write "The job" (printed under its question by the CLI; the panel's box shows the same in its examples). */
|
|
311
|
+
export const JOB_BLOCK_HELP = Object.freeze([
|
|
312
|
+
'One line per thing the job must do — each plain line gets a number. A line starting with `~` is a rule for the',
|
|
313
|
+
'numbered line above it. For a rubric check, a `~ PASS: ...` and a `~ FAIL: ...` line are your examples.',
|
|
314
|
+
]);
|
|
315
|
+
|
|
208
316
|
/** Destination's wording when Source resolved to a git repo: the answer IS the
|
|
209
317
|
* signed `writeScope` fence (ITEM33-BUILD.md M3 ruling 2). */
|
|
210
318
|
export const DESTINATION_FIELD_REPO = Object.freeze({
|
|
@@ -231,8 +339,11 @@ export const DESTINATION_FIELD_PLAIN = Object.freeze({
|
|
|
231
339
|
export function destinationFieldFor(isRepo) { return isRepo ? DESTINATION_FIELD_REPO : DESTINATION_FIELD_PLAIN; }
|
|
232
340
|
|
|
233
341
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
342
|
+
* P7 (2026-10-10, docs/product/PANEL-BUILD.md): the old free-text trio (Goal, What success looks like, Guardrails) is ONE
|
|
343
|
+
* answer now — "The job", the person's numbered lines with their `~` rules (src/jobblock.js renders it). What follows
|
|
344
|
+
* is the history of the trio, kept because the numbering rule below still holds.
|
|
345
|
+
*
|
|
346
|
+
* THE FREE-TEXT SLOTS EVERY CLASS SHARES, numbered contiguously from 1. This is what `answers[1..3]` are
|
|
236
347
|
* filed under for every class; soft-green appends a fourth
|
|
237
348
|
* (`SOFTGREEN_QUESTIONS`) and hitl appends a different fourth
|
|
238
349
|
* (`HITL_QUESTIONS`), each its own next number rather than a fixed one, so a
|
|
@@ -242,7 +353,7 @@ export function destinationFieldFor(isRepo) { return isRepo ? DESTINATION_FIELD_
|
|
|
242
353
|
* 2026-09-10 "The intake form (RULED)" ruling, `docs/product/PRD.md`) — not the
|
|
243
354
|
* old worded questions ("What do you want done?", "How do you check today
|
|
244
355
|
* whether it's working?", "...worse than before?"). Motivated by run `mtv8jihy`
|
|
245
|
-
* (F159, `docs/
|
|
356
|
+
* (F159, `docs/wiki/FINDINGS.md`): a rulebook this prompt commit did not touch
|
|
246
357
|
* still had to compile against a card built from a person's actual bar, and the
|
|
247
358
|
* old wording never matched what got signed — this reshape (commit 1ac4d05)
|
|
248
359
|
* trimmed the SET to three slots but left their PROSE unreconciled with the
|
|
@@ -251,9 +362,7 @@ export function destinationFieldFor(isRepo) { return isRepo ? DESTINATION_FIELD_
|
|
|
251
362
|
* `authorPrompt`'s interview block and `scripts/run-interview.mjs`.
|
|
252
363
|
*/
|
|
253
364
|
export const GREEN_QUESTIONS = Object.freeze({
|
|
254
|
-
1: '
|
|
255
|
-
2: 'checks a machine can count',
|
|
256
|
-
3: 'what must not happen or change',
|
|
365
|
+
1: 'the job, as numbered lines — each line may carry the rules (~) the person wrote under it',
|
|
257
366
|
});
|
|
258
367
|
|
|
259
368
|
/**
|
|
@@ -268,13 +377,11 @@ export const GREEN_QUESTIONS = Object.freeze({
|
|
|
268
377
|
* calls `questionsFor` — additive, nothing existing breaks.
|
|
269
378
|
*/
|
|
270
379
|
export const FIELD_LABELS = Object.freeze({
|
|
271
|
-
1: '
|
|
272
|
-
2: 'What success looks like',
|
|
273
|
-
3: 'Guardrails',
|
|
380
|
+
1: 'The job',
|
|
274
381
|
});
|
|
275
382
|
|
|
276
|
-
/** The key
|
|
277
|
-
*
|
|
383
|
+
/** The key Judge Examples and hitl's own question both prove out at: green holds
|
|
384
|
+
* exactly one free-text slot (P7), so 2 is the next number for both the
|
|
278
385
|
* soft-green and hitl extensions below — computed, never hardcoded twice. */
|
|
279
386
|
const NEXT_FREE_TEXT_KEY = Object.keys(GREEN_QUESTIONS).length + 1;
|
|
280
387
|
|
|
@@ -288,7 +395,7 @@ export const SOFTGREEN_JUDGE_EXAMPLES_KEY = NEXT_FREE_TEXT_KEY;
|
|
|
288
395
|
* you'd pass and one you'd fail, and say why."). Exported so `cardauthor.js`'s
|
|
289
396
|
* prompt and this module's own `SOFTGREEN_QUESTIONS` render the identical
|
|
290
397
|
* string; its label ("Judge examples") lives in `SOFTGREEN_FIELD_LABELS`. */
|
|
291
|
-
export const JUDGE_EXAMPLES_QUESTION = '
|
|
398
|
+
export const JUDGE_EXAMPLES_QUESTION = 'the PASS: and FAIL: examples the person wrote under their job lines, each with its line number';
|
|
292
399
|
|
|
293
400
|
/** {@link FIELD_LABELS} plus Judge Examples' own label, for the soft-green set. */
|
|
294
401
|
export const SOFTGREEN_FIELD_LABELS = Object.freeze({
|
|
@@ -384,7 +491,8 @@ export const CLASS_STATEMENTS = Object.freeze({
|
|
|
384
491
|
+ 'the result and deciding whether it is any good — and no command can render it. Compose the mechanical part '
|
|
385
492
|
+ 'first: everything about this repository a command can decide is still a mechanical stage, exactly as it would '
|
|
386
493
|
+ 'be for a green job. Then put ONE judged-floor stage AFTER them, carrying the few things the person said they '
|
|
387
|
-
+ 'actually look for and the real files the work lands in
|
|
494
|
+
+ 'actually look for and the real files the work lands in (files the job may EDIT, inside the write fence, '
|
|
495
|
+
+ 'because a finding on a file the run is fenced out of can never be fixed). A judged stage decides only the lines its card names, '
|
|
388
496
|
+ 'so name each thing they look for as its own line, and leave nothing to be inferred.',
|
|
389
497
|
// N4 slice 1. The mechanical-first composition law is stated as an ORDER
|
|
390
498
|
// because first-red-wins makes the order the mechanism: the cheap stages shield
|
|
@@ -456,7 +564,7 @@ export const LANGUAGE_PICK_FIELD = Object.freeze({
|
|
|
456
564
|
});
|
|
457
565
|
|
|
458
566
|
/**
|
|
459
|
-
* F175's open half (docs/
|
|
567
|
+
* F175's open half (docs/wiki/FINDINGS.md, run mu0voeo4), hamr's ruling
|
|
460
568
|
* (2026-09-16): "Confirm" and "Type the goal yourself" may not bypass a
|
|
461
569
|
* `questions` entry the model itself raised on the plan being accepted — the
|
|
462
570
|
* person answers it inline, one question at a time, with NO extra model
|
|
@@ -745,10 +853,11 @@ export function schemaCoverage(catalogue = KIND_CATALOGUE) {
|
|
|
745
853
|
* THROWS on a catalogue it cannot cover: a malformed catalogue is a bareloop bug,
|
|
746
854
|
* and failing loudly beats emitting a schema that quietly cannot say something
|
|
747
855
|
* the validator will then demand.
|
|
856
|
+
* `questions` (P7 item 5, last drafting try only) adds the optional `questions` property; absent, the schema is byte-identical.
|
|
748
857
|
* @param {Record<string, any>} [catalogue]
|
|
749
|
-
* @param {{verdictType?: string|null}} [o]
|
|
858
|
+
* @param {{verdictType?: string|null, questions?: boolean}} [o]
|
|
750
859
|
*/
|
|
751
|
-
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null } = {}) {
|
|
860
|
+
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null, questions = false } = {}) {
|
|
752
861
|
const cov = schemaCoverage(catalogue);
|
|
753
862
|
if (!cov.ok) {
|
|
754
863
|
throw new Error('[authorflow] the declaration schema does not cover the catalogue — '
|
|
@@ -767,6 +876,7 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
767
876
|
type: 'object',
|
|
768
877
|
properties: {
|
|
769
878
|
name: { type: 'string', description: 'a unique lowercase-hyphenated slug saying what this stage asserts' },
|
|
879
|
+
fromLine: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'integer', minimum: 1 }, description: 'the numbers of the job line(s) this stage checks, e.g. [1] or [1, 3] (absent on a mandatory guard)' },
|
|
770
880
|
kind: { const: kind },
|
|
771
881
|
params: { type: 'object', properties, required: [...spec.required], additionalProperties: false },
|
|
772
882
|
},
|
|
@@ -794,6 +904,30 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
794
904
|
items: { type: 'string', minLength: 1 },
|
|
795
905
|
description: 'anything you could not express, or any fact you needed and did not have',
|
|
796
906
|
},
|
|
907
|
+
refused: {
|
|
908
|
+
type: 'array',
|
|
909
|
+
items: {
|
|
910
|
+
type: 'object',
|
|
911
|
+
properties: {
|
|
912
|
+
line: { type: 'integer', minimum: 1, description: 'the number of a job line no stage checks' },
|
|
913
|
+
reason: { type: 'string', minLength: 1, description: 'why, in plain words the person can read' },
|
|
914
|
+
},
|
|
915
|
+
required: ['line', 'reason'],
|
|
916
|
+
additionalProperties: false,
|
|
917
|
+
},
|
|
918
|
+
description: 'job lines you could not turn into a stage, each with a reason — a note does not cover a line',
|
|
919
|
+
},
|
|
920
|
+
...(questions ? {
|
|
921
|
+
questions: {
|
|
922
|
+
type: 'array',
|
|
923
|
+
items: {
|
|
924
|
+
type: 'object',
|
|
925
|
+
properties: { line: { type: 'integer', minimum: 1 }, question: { type: 'string', minLength: 1 } },
|
|
926
|
+
required: ['line', 'question'],
|
|
927
|
+
},
|
|
928
|
+
description: QUESTIONS_DESCRIPTION,
|
|
929
|
+
},
|
|
930
|
+
} : {}),
|
|
797
931
|
},
|
|
798
932
|
required: ['stages'],
|
|
799
933
|
additionalProperties: false,
|
|
@@ -804,13 +938,13 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
804
938
|
* The output channel. It records and acknowledges; it takes no action, which is
|
|
805
939
|
* why handing it to a toolless authoring call does not reopen D3.
|
|
806
940
|
* @param {{calls: any[]}} box
|
|
807
|
-
* @param {{catalogue?: Record<string, any>, verdictType?: string|null}} [o]
|
|
941
|
+
* @param {{catalogue?: Record<string, any>, verdictType?: string|null, questions?: boolean}} [o]
|
|
808
942
|
*/
|
|
809
|
-
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null } = {}) {
|
|
943
|
+
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null, questions = false } = {}) {
|
|
810
944
|
return {
|
|
811
945
|
name: DECLARATION_TOOL_NAME,
|
|
812
946
|
description: 'Deliver the finished close. Call this exactly once, with the whole declaration as its arguments.',
|
|
813
|
-
parameters: declarationSchema(catalogue, { verdictType }),
|
|
947
|
+
parameters: declarationSchema(catalogue, { verdictType, questions }),
|
|
814
948
|
execute: async (/** @type {any} */ args) => { box.calls.push(args); return DECLARATION_ACK; },
|
|
815
949
|
};
|
|
816
950
|
}
|
|
@@ -985,7 +1119,7 @@ export function writeScopeBlock(writeScope) {
|
|
|
985
1119
|
* acceptance — BINDING, never re-decided by the composer, and a DIFFERENT
|
|
986
1120
|
* meaning from `openQuestions` (which means "could not resolve"): the two
|
|
987
1121
|
* never merge.
|
|
988
|
-
* @param {{checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[],
|
|
1122
|
+
* @param {{checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[],
|
|
989
1123
|
* answeredQuestions?: string[]}} confirmed
|
|
990
1124
|
*/
|
|
991
1125
|
export function confirmedBlock(confirmed) {
|
|
@@ -997,7 +1131,9 @@ export function confirmedBlock(confirmed) {
|
|
|
997
1131
|
return 'THE CONFIRMED PLAN — the person already saw and confirmed this in the confirm turn\n\n'
|
|
998
1132
|
+ 'Compose stages for these checks and no other — a genre never adds a check the goal did not ask for, and '
|
|
999
1133
|
+ 'neither do you:\n\n'
|
|
1000
|
-
+ `${
|
|
1134
|
+
+ `${(confirmed.checkItems?.length
|
|
1135
|
+
? confirmed.checkItems.map((c) => ` - ${c.fromLine && c.fromLine.length ? `[line ${c.fromLine.join(', ')}] ` : ''}${c.text}`)
|
|
1136
|
+
: checks.map((c) => ` - ${c}`)).join('\n') || ' (none)'}\n\n`
|
|
1001
1137
|
+ 'These protections are already always-on and unchanged by this plan — never compose one of these as a check:\n\n'
|
|
1002
1138
|
+ `${protections.map((p) => ` - ${p}`).join('\n') || ' (none)'}\n\n`
|
|
1003
1139
|
+ (notChecked.length
|
|
@@ -1012,6 +1148,21 @@ export function confirmedBlock(confirmed) {
|
|
|
1012
1148
|
: 'The confirm turn left no open questions.');
|
|
1013
1149
|
}
|
|
1014
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* P7: the job-line tagging order (model-facing, registered). Rule 1 of the job block: every stage the drafter writes
|
|
1153
|
+
* names the ONE numbered job line it serves, or the line is refused with a reason; the mandatory guards serve no line.
|
|
1154
|
+
* The $0 validator (validateDeclaration, `jobLines`) enforces it and the revise loop feeds a miss back.
|
|
1155
|
+
* @param {number} count how many numbered job lines the person wrote
|
|
1156
|
+
*/
|
|
1157
|
+
export function jobLineTagBlock(count) {
|
|
1158
|
+
return 'EVERY STAGE SERVES ONE JOB LINE\n\n'
|
|
1159
|
+
+ `The person's job has ${count} numbered line${count === 1 ? '' : 's'} (the interview above). Tag every stage YOU write `
|
|
1160
|
+
+ 'with "fromLine": [N, ...] — the number(s) of the job line(s) that stage checks (one stage may serve several lines, e.g. a judged stage that reads lines 1-3). A job line that no kind in the catalogue '
|
|
1161
|
+
+ 'can check is not left out silently: list it in "refused" as {"line": N, "reason": "..."}, with a reason in plain '
|
|
1162
|
+
+ 'words the person can read. Every numbered line needs at least one stage or one refusal — a note in "notes" does '
|
|
1163
|
+
+ 'NOT cover a line. The mandatory guards serve no job line and carry NO "fromLine".';
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1015
1166
|
/**
|
|
1016
1167
|
* The whole authoring prompt, as one string.
|
|
1017
1168
|
*
|
|
@@ -1034,11 +1185,15 @@ export function confirmedBlock(confirmed) {
|
|
|
1034
1185
|
* guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
|
|
1035
1186
|
* ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any>,
|
|
1036
1187
|
* writeScope?: string[]|null,
|
|
1037
|
-
*
|
|
1188
|
+
* jobLines?: {n: number}[]|null, inputs?: {n: number, label: string, value: string}[]|null,
|
|
1189
|
+
* confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null}} o
|
|
1038
1190
|
*/
|
|
1039
1191
|
export function authorPrompt({
|
|
1040
1192
|
answers, questions = GREEN_QUESTIONS, facts, listingBlock, lang, verdictType, guards,
|
|
1041
1193
|
ownedEnvNames = [], mode = 'tool', catalogue = KIND_CATALOGUE, writeScope = null,
|
|
1194
|
+
// P7: the person's numbered job lines and the inputs they pointed at. ABSENT = byte-identical prompt (every caller
|
|
1195
|
+
// that predates the job block). Present: the drafter is told to tag every stage it writes with the line it serves.
|
|
1196
|
+
jobLines = null, inputs = null,
|
|
1042
1197
|
// PRD item 33 M3 piece 4: ABSENT for every caller that predates the confirm
|
|
1043
1198
|
// turn (or ran without `ask`) — byte-identical prompt, nothing added. Present
|
|
1044
1199
|
// only when a confirm turn actually ran and the person accepted a plan.
|
|
@@ -1114,7 +1269,9 @@ measure anything.`;
|
|
|
1114
1269
|
// asks what KIND of job this is; the person answered what DONE means, and the
|
|
1115
1270
|
// composer reads that plus the catalogue and works the rest out.
|
|
1116
1271
|
`WHAT THE PERSON DECLARED "DONE" TO MEAN — their answer, not yours\n\n${statement}`,
|
|
1117
|
-
`THE INTERVIEW — the person's own words\n\n${interview}
|
|
1272
|
+
`THE INTERVIEW — the person's own words\n\n${interview}`
|
|
1273
|
+
+ (inputs && inputs.length ? `\n\n${renderInputs(inputs)}` : ''),
|
|
1274
|
+
...(jobLines && jobLines.length ? [jobLineTagBlock(jobLines.length)] : []),
|
|
1118
1275
|
// Q2 IS GONE (PRD item 33 M3 piece 3) — what it used to carry (which files
|
|
1119
1276
|
// change, which are read-only) is proven against the machine now, not typed,
|
|
1120
1277
|
// and this is where that proof reaches the composer instead of vanishing.
|
|
@@ -1379,7 +1536,7 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }) {
|
|
|
1379
1536
|
*/
|
|
1380
1537
|
/**
|
|
1381
1538
|
* @param {{ceilingUsd?: number|null,
|
|
1382
|
-
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void}} [o]
|
|
1539
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void}} [o]
|
|
1383
1540
|
* `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
|
|
1384
1541
|
* `onCall` is a REPORTING seam fired as each metered call lands — the shell's
|
|
1385
1542
|
* only chance to know what has been spent while the run is still alive. It is
|
|
@@ -1400,7 +1557,9 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
|
|
|
1400
1557
|
const call = { label, ...priceOf(r) };
|
|
1401
1558
|
entries.push(call);
|
|
1402
1559
|
raws.push(scrubRaw({ label, attempt, text: r?.text, reason: r?.error ? String(r.error) : null }));
|
|
1403
|
-
|
|
1560
|
+
// bare-agent's per-run `thinkingApplied` (true | false | null) rides the REPORTING seam only — never the stored entry,
|
|
1561
|
+
// so the cost report and every persisted shape stay as they were; absent when thinkingMode was not sent
|
|
1562
|
+
onCall({ ...call, ...(r?.thinkingApplied === undefined ? {} : { thinkingApplied: r.thinkingApplied }) });
|
|
1404
1563
|
return entries.at(-1);
|
|
1405
1564
|
},
|
|
1406
1565
|
/** @param {{label: string, costUsd: number|null, unpricedRounds: number}[]} calls
|
|
@@ -1440,10 +1599,13 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
|
|
|
1440
1599
|
* `cacheMessages` (F18) and `maxTokens` (F30) are not optional on this stack.
|
|
1441
1600
|
* `rates` is the customer's own price for this provider's key row (USD per 1K, `ratesFor`);
|
|
1442
1601
|
* absent = bare-agent's built-in guess, exactly as before.
|
|
1443
|
-
*
|
|
1602
|
+
* `draftThinking` ('on' | 'off', operator knob; default 'off') adds the drafter's per-call thinking option via `draftCallOptions`.
|
|
1603
|
+
* @param {any} provider @param {{system?: string, maxTokens?: number, rates?: {in: number, out: number}|null, draftThinking?: string}} [o]
|
|
1444
1604
|
* @returns {(messages: any[], tools: any[], opts?: any) => Promise<any>}
|
|
1445
1605
|
*/
|
|
1446
|
-
export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens = AUTHOR_MAX_TOKENS, rates = null } = {}) {
|
|
1606
|
+
export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens = AUTHOR_MAX_TOKENS, rates = null, draftThinking = 'off' } = {}) {
|
|
1607
|
+
// the drafter's per-call options (src/providers.js `draftCallOptions`, one owner); a bad mode throws HERE, at construction
|
|
1608
|
+
const callOptions = draftCallOptions(provider, draftThinking);
|
|
1447
1609
|
return async (/** @type {any[]} */ messages, /** @type {any[]} */ tools, /** @type {any} */ opts = {}) => {
|
|
1448
1610
|
const loop = new Loop({ provider, system, ...(rates ? { rates } : {}) });
|
|
1449
1611
|
const wired = tools.map((t) => ({
|
|
@@ -1459,7 +1621,7 @@ export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens =
|
|
|
1459
1621
|
// throws on a malformed tool-call arguments string (BA-27): it returns the
|
|
1460
1622
|
// round priced with `toolCalls: []` plus its own `malformedToolCall` marker,
|
|
1461
1623
|
// which `Loop.run` surfaces unchanged on its return. No local shim needed.
|
|
1462
|
-
return loop.run(messages, wired, { cacheMessages: true, maxTokens, timeoutMs: AUTHOR_CALL_TIMEOUT_MS, ...opts });
|
|
1624
|
+
return loop.run(messages, wired, { cacheMessages: true, maxTokens, timeoutMs: AUTHOR_CALL_TIMEOUT_MS, ...callOptions, ...opts });
|
|
1463
1625
|
};
|
|
1464
1626
|
}
|
|
1465
1627
|
|
|
@@ -1488,7 +1650,7 @@ function canonical(x) {
|
|
|
1488
1650
|
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
1489
1651
|
* channel: {name: string, instruction: string, textPath?: string, tool: (box: {calls: any[]}) => any}}} o
|
|
1490
1652
|
* @returns {Promise<{artifact: any, attempts: number, convo: any[], raw: string,
|
|
1491
|
-
* providerError: string|null, red: Red|null, budget: 'cap-halt'|'pricing-red'|null}>}
|
|
1653
|
+
* providerError: string|null, red: Red|null, budget: 'cap-halt'|'pricing-red'|null, allCutOff?: boolean}>}
|
|
1492
1654
|
*/
|
|
1493
1655
|
export async function askStructured({ messages, generate, mode, retries, label, book, channel }) {
|
|
1494
1656
|
/** @type {any[]} */
|
|
@@ -1496,6 +1658,8 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1496
1658
|
let attempts = 0;
|
|
1497
1659
|
/** @type {Red|null} */
|
|
1498
1660
|
let red = null;
|
|
1661
|
+
/** how many of this ask's attempts were cut off at the output cap (P7: all of them = a failed try for the question gate) */
|
|
1662
|
+
let cutOffs = 0;
|
|
1499
1663
|
|
|
1500
1664
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
1501
1665
|
// THE CEILING, immediately before the paid call and nowhere else. Sited HERE
|
|
@@ -1540,11 +1704,23 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1540
1704
|
book.add(attempt === 0 ? label : `${label}#${attempt + 1}`, r, attempts);
|
|
1541
1705
|
const raw = redactSecrets(String(r?.text ?? ''));
|
|
1542
1706
|
|
|
1543
|
-
|
|
1707
|
+
// ITEM 6 (P7, live session smv2bzdqmp0f8, 2026-10-10): a round cut off at the output cap is the MODEL's own overlong
|
|
1708
|
+
// answer, not a transport fault — it is a failed try on the same ladder as a malformed artifact (the spend is already
|
|
1709
|
+
// booked above). Only a real error (a throw, a provider/halt tag) stays provider-red.
|
|
1710
|
+
const cutOff = isOutputCutOff(r);
|
|
1711
|
+
if (cutOff) cutOffs += 1;
|
|
1712
|
+
if (r?.error && !cutOff) {
|
|
1544
1713
|
return { artifact: null, attempts, convo, raw, providerError: String(r.error), red: null, budget: null };
|
|
1545
1714
|
}
|
|
1546
1715
|
|
|
1547
|
-
if (
|
|
1716
|
+
if (cutOff) {
|
|
1717
|
+
red = {
|
|
1718
|
+
code: 'artifact-red',
|
|
1719
|
+
path: channel.name,
|
|
1720
|
+
detail: "the AI's answer was cut off at the output limit",
|
|
1721
|
+
axis: 'output-truncated',
|
|
1722
|
+
};
|
|
1723
|
+
} else if (mode === 'tool') {
|
|
1548
1724
|
// F179 (2026-09-15 ruling) — retry, never repair. bare-agent 0.43.0
|
|
1549
1725
|
// (BA-27) already priced this round on the real usage and returns it
|
|
1550
1726
|
// with `toolCalls: []` plus its own `malformedToolCall` marker instead
|
|
@@ -1597,22 +1773,23 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1597
1773
|
|
|
1598
1774
|
if (attempt < retries) {
|
|
1599
1775
|
const block = renderRejectBlock({ kind: 'artifact', reds: [/** @type {Red} */ (red).detail] });
|
|
1600
|
-
|
|
1776
|
+
// a cut-off reply is NOT replayed (it would re-bill the whole cap as input and teach nothing)
|
|
1777
|
+
convo.push({ role: 'assistant', content: (cutOff ? '' : raw.trim()) || '(the reply carried no text)' });
|
|
1601
1778
|
convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ? channel.instruction : STRUCTURE_INSTRUCTION_TEXT}` });
|
|
1602
1779
|
}
|
|
1603
1780
|
}
|
|
1604
|
-
return { artifact: null, attempts, convo, raw: '', providerError: null, red, budget: null };
|
|
1781
|
+
return { artifact: null, attempts, convo, raw: '', providerError: null, red, budget: null, allCutOff: attempts > 0 && cutOffs === attempts };
|
|
1605
1782
|
}
|
|
1606
1783
|
|
|
1607
1784
|
/** THE DECLARATION CHANNEL — the one this loop has always used. Its schema is
|
|
1608
1785
|
* CLASS-SCOPED, like the prompt's catalogue block: the two must offer one set of
|
|
1609
1786
|
* kinds, or the composer reads about a kind it cannot call.
|
|
1610
|
-
* @param {{catalogue: Record<string, any>, verdictType: string}} o */
|
|
1611
|
-
const declarationChannel = ({ catalogue, verdictType }) => ({
|
|
1787
|
+
* @param {{catalogue: Record<string, any>, verdictType: string, questions?: boolean}} o */
|
|
1788
|
+
const declarationChannel = ({ catalogue, verdictType, questions = false }) => ({
|
|
1612
1789
|
name: DECLARATION_TOOL_NAME,
|
|
1613
1790
|
instruction: STRUCTURE_INSTRUCTION_TOOL,
|
|
1614
1791
|
textPath: 'declaration',
|
|
1615
|
-
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType }),
|
|
1792
|
+
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType, questions }),
|
|
1616
1793
|
});
|
|
1617
1794
|
|
|
1618
1795
|
/** The declaration ask, over the shared ladder. `declaration` is `artifact`
|
|
@@ -1620,11 +1797,11 @@ const declarationChannel = ({ catalogue, verdictType }) => ({
|
|
|
1620
1797
|
* anonymous artifact, and one word for two things is how a reader loses which.
|
|
1621
1798
|
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
1622
1799
|
* label: string, book: ReturnType<typeof makeCostBook>, catalogue: Record<string, any>,
|
|
1623
|
-
* verdictType: string}} o
|
|
1800
|
+
* verdictType: string, questions?: boolean}} o
|
|
1624
1801
|
*/
|
|
1625
|
-
async function askDeclaration({ messages, generate, mode, retries, label, book, catalogue, verdictType }) {
|
|
1802
|
+
async function askDeclaration({ messages, generate, mode, retries, label, book, catalogue, verdictType, questions = false }) {
|
|
1626
1803
|
const r = await askStructured({
|
|
1627
|
-
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType }),
|
|
1804
|
+
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType, questions }),
|
|
1628
1805
|
});
|
|
1629
1806
|
return { ...r, declaration: r.artifact };
|
|
1630
1807
|
}
|
|
@@ -1662,9 +1839,17 @@ const CONFIRM_SCHEMA = Object.freeze({
|
|
|
1662
1839
|
properties: {
|
|
1663
1840
|
checks: {
|
|
1664
1841
|
type: 'array',
|
|
1665
|
-
items: {
|
|
1666
|
-
|
|
1667
|
-
|
|
1842
|
+
items: {
|
|
1843
|
+
type: 'object',
|
|
1844
|
+
properties: {
|
|
1845
|
+
text: { type: 'string', minLength: 1 },
|
|
1846
|
+
fromLine: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'integer', minimum: 1 }, description: 'the number(s) of the numbered job line(s) this check is for, e.g. [1] or [1, 3]' },
|
|
1847
|
+
},
|
|
1848
|
+
required: ['text', 'fromLine'],
|
|
1849
|
+
additionalProperties: false,
|
|
1850
|
+
},
|
|
1851
|
+
description: 'the mechanical checks you plan to compose, each one traceable to the numbered job line it checks '
|
|
1852
|
+
+ '(fromLine, an array: one check may serve several lines) — never a check invented beyond what the job lines and their rules asked for',
|
|
1668
1853
|
},
|
|
1669
1854
|
goal: {
|
|
1670
1855
|
type: 'string',
|
|
@@ -1675,7 +1860,7 @@ const CONFIRM_SCHEMA = Object.freeze({
|
|
|
1675
1860
|
notChecked: {
|
|
1676
1861
|
type: 'array',
|
|
1677
1862
|
items: { type: 'string', minLength: 1 },
|
|
1678
|
-
description: 'anything the person asked for (in the
|
|
1863
|
+
description: 'anything the person asked for (in the job lines, the rules under them, or '
|
|
1679
1864
|
+ 'worse-than-before answers) that none of your listed checks can verify — named plainly in the person\'s '
|
|
1680
1865
|
+ 'own words; an empty list means every ask is covered by a listed check',
|
|
1681
1866
|
},
|
|
@@ -1690,6 +1875,25 @@ const CONFIRM_SCHEMA = Object.freeze({
|
|
|
1690
1875
|
additionalProperties: false,
|
|
1691
1876
|
});
|
|
1692
1877
|
|
|
1878
|
+
/**
|
|
1879
|
+
* The confirm plan's checks as `{text, fromLine}` items. The schema asks for objects (P7: each check names the job line
|
|
1880
|
+
* it is for); a bare string (a text-mode reply, an older caller) reads as a check with no line.
|
|
1881
|
+
* @param {any} raw the plan's `checks`
|
|
1882
|
+
* @returns {{text: string, fromLine: number[]|null}[]}
|
|
1883
|
+
*/
|
|
1884
|
+
export function normalizeChecks(raw) {
|
|
1885
|
+
/** a fromLine as a non-empty, duplicate-free array of line numbers, else null (never a guess)
|
|
1886
|
+
* @param {any} v @returns {number[]|null} */
|
|
1887
|
+
const normLines = (v) => (Array.isArray(v) && v.length > 0 && v.every((x) => Number.isInteger(x) && x >= 1) ? [...new Set(v)] : null);
|
|
1888
|
+
return (Array.isArray(raw) ? raw : []).flatMap((/** @type {any} */ c) => {
|
|
1889
|
+
if (typeof c === 'string') return c.trim() ? [{ text: c, fromLine: null }] : [];
|
|
1890
|
+
if (c && typeof c === 'object' && typeof c.text === 'string' && c.text.trim()) {
|
|
1891
|
+
return [{ text: c.text, fromLine: normLines(c.fromLine) }];
|
|
1892
|
+
}
|
|
1893
|
+
return [];
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1693
1897
|
/** The output channel. It records and acknowledges; it takes no action.
|
|
1694
1898
|
* @param {{calls: any[]}} box */
|
|
1695
1899
|
export function confirmTool(box) {
|
|
@@ -1721,9 +1925,10 @@ const confirmChannel = () => ({
|
|
|
1721
1925
|
*/
|
|
1722
1926
|
export const CONFIRM_SYSTEM = 'You read what a NON-ENGINEER answered, plus a read-only survey or listing of their '
|
|
1723
1927
|
+ 'own repository, and draft a PLAN for a job\'s definition of done: the checks you plan to compose and one '
|
|
1724
|
-
+ 'signed goal sentence. You never propose a check the
|
|
1928
|
+
+ 'signed goal sentence. You never propose a check the numbered job lines (and the ~ rules written under them) did not ask for '
|
|
1725
1929
|
+ '— a genre never adds a check the goal did not ask for (run mtv8jihy drafted an unasked tsc --strict stage; '
|
|
1726
|
-
+ 'that is exactly the mistake this order exists to prevent).
|
|
1930
|
+
+ 'that is exactly the mistake this order exists to prevent). Tag each check with "fromLine", an array of the number(s) of the '
|
|
1931
|
+
+ 'numbered job line(s) it is for. The always-on guards (changed-from-seed, '
|
|
1727
1932
|
+ 'no-suppressions, and every mandatory guard) and the write fence are shown to the person by the SYSTEM, never '
|
|
1728
1933
|
+ 'by you: you never claim, name, or list a protection or guard of your own — that is not your call to state, and '
|
|
1729
1934
|
+ 'a protection is never a check and never named in the goal sentence. The goal sentence names every check you '
|
|
@@ -1755,15 +1960,16 @@ export const CONFIRM_SYSTEM = 'You read what a NON-ENGINEER answered, plus a rea
|
|
|
1755
1960
|
* @param {{answers: Record<string|number, string>, questions: Record<string|number, string>,
|
|
1756
1961
|
* labels?: Record<string|number, string>, facts?: any, listing?: string|null,
|
|
1757
1962
|
* writeScope?: string[]|null, isRepo: boolean, lang: string, worseThanBefore?: string,
|
|
1758
|
-
* fixText?: string|null, protections?: string[]}} o
|
|
1963
|
+
* fixText?: string|null, protections?: string[], inputs?: {n: number, label: string, value: string}[]|null}} o
|
|
1759
1964
|
*/
|
|
1760
|
-
export function confirmPrompt({ answers, questions, labels = {}, facts = null, listing = null, writeScope = null, isRepo, lang, worseThanBefore = '', fixText = null, protections = [] }) {
|
|
1965
|
+
export function confirmPrompt({ answers, questions, labels = {}, facts = null, listing = null, writeScope = null, isRepo, lang, worseThanBefore = '', fixText = null, protections = [], inputs = null }) {
|
|
1761
1966
|
const lines = ['THE PERSON\'S OWN ANSWERS (a non-engineer; read exactly what they wrote, invent nothing beyond it):'];
|
|
1762
1967
|
for (const k of Object.keys(questions)) {
|
|
1763
1968
|
const label = labels[k] ? ` (${labels[k]})` : '';
|
|
1764
1969
|
lines.push(`Q${k}${label}. ${questions[k]}`);
|
|
1765
1970
|
lines.push(`A${k}. ${answers?.[k] ?? '(no answer)'}`);
|
|
1766
1971
|
}
|
|
1972
|
+
if (inputs && inputs.length) lines.push('', renderInputs(inputs));
|
|
1767
1973
|
if (isRepo && worseThanBefore) {
|
|
1768
1974
|
lines.push('', `WORSE THAN BEFORE (a constraint the person named, repo jobs only): ${worseThanBefore}`);
|
|
1769
1975
|
}
|
|
@@ -1820,14 +2026,14 @@ async function askConfirmPlan({ convo, generate, mode, book, label }) {
|
|
|
1820
2026
|
* generate: Function, book: ReturnType<typeof makeCostBook>,
|
|
1821
2027
|
* ask: (step: {kind: string, [k: string]: any}) => Promise<string|null>,
|
|
1822
2028
|
* onPhase?: (phase: string, data?: any) => void, mode?: 'tool'|'text',
|
|
1823
|
-
* worseThanBefore?: string}} o retired field (hamr's ruling 2026-09-28):
|
|
2029
|
+
* worseThanBefore?: string, inputs?: {n: number, label: string, value: string}[]|null}} o retired field (hamr's ruling 2026-09-28):
|
|
1824
2030
|
* `worseThanBefore` is never asked any more and defaults to `''` — a
|
|
1825
2031
|
* caller may still pass a string through (kept for a future, non-ask
|
|
1826
2032
|
* source of the same constraint), it just never comes from an ask() step.
|
|
1827
2033
|
* @returns {Promise<{ok: boolean,
|
|
1828
2034
|
* stop: null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart',
|
|
1829
2035
|
* rounds: number,
|
|
1830
|
-
* accepted: {goal: string, checks: string[], protections: string[], lang: string,
|
|
2036
|
+
* accepted: {goal: string, checks: string[], checkItems: {text: string, fromLine: number[]|null}[], protections: string[], lang: string,
|
|
1831
2037
|
* worseThanBefore: string, openQuestions: string[], notChecked: string[], answeredQuestions?: string[]}|null,
|
|
1832
2038
|
* reds: Red[], cost: any}>}
|
|
1833
2039
|
*/
|
|
@@ -1840,6 +2046,8 @@ export async function runConfirmTurn({
|
|
|
1840
2046
|
// still pass a resolved string through (kept as a plain pass-through, not
|
|
1841
2047
|
// an ask trigger).
|
|
1842
2048
|
worseThanBefore: presetWorseThanBefore = '',
|
|
2049
|
+
// P7: the inputs the person pointed at; absent = the prompt is byte-identical
|
|
2050
|
+
inputs = null,
|
|
1843
2051
|
}) {
|
|
1844
2052
|
/** @typedef {null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart'} ConfirmStop */
|
|
1845
2053
|
/** @returns {{ok: boolean, stop: ConfirmStop, rounds: number, accepted: null, reds: Red[], cost: any}} */
|
|
@@ -1910,14 +2118,14 @@ export async function runConfirmTurn({
|
|
|
1910
2118
|
let convo = [{
|
|
1911
2119
|
role: 'user',
|
|
1912
2120
|
content: confirmPrompt({
|
|
1913
|
-
answers, questions, labels, facts, listing, writeScope, isRepo, lang: resolvedLang, worseThanBefore, protections,
|
|
2121
|
+
answers, questions, labels, facts, listing, writeScope, isRepo, lang: resolvedLang, worseThanBefore, protections, inputs,
|
|
1914
2122
|
}),
|
|
1915
2123
|
}];
|
|
1916
2124
|
|
|
1917
2125
|
for (let round = 1; round <= 2; round += 1) {
|
|
1918
2126
|
if (fixText) {
|
|
1919
2127
|
convo = [...convo, { role: 'user', content: confirmPrompt({
|
|
1920
|
-
answers, questions, labels, facts, listing, writeScope, isRepo, lang: resolvedLang, worseThanBefore, fixText, protections,
|
|
2128
|
+
answers, questions, labels, facts, listing, writeScope, isRepo, lang: resolvedLang, worseThanBefore, fixText, protections, inputs,
|
|
1921
2129
|
}) }];
|
|
1922
2130
|
}
|
|
1923
2131
|
onPhase('confirm-round', { round });
|
|
@@ -1946,6 +2154,8 @@ export async function runConfirmTurn({
|
|
|
1946
2154
|
// shown at confirm-done and recorded on acceptance. `notChecked` is the
|
|
1947
2155
|
// model's own honest gap list, carried through unchanged.
|
|
1948
2156
|
const notChecked = [...(r.plan.notChecked ?? [])];
|
|
2157
|
+
const checkItems = normalizeChecks(r.plan.checks);
|
|
2158
|
+
r.plan = { ...r.plan, checks: checkItems.map((c) => c.text), checkItems };
|
|
1949
2159
|
const plan = { ...r.plan, protections, notChecked };
|
|
1950
2160
|
onPhase('confirm-done', { round, plan });
|
|
1951
2161
|
const picked = await ask({ kind: 'menu', field: CONFIRM_MENU, plan });
|
|
@@ -1958,7 +2168,7 @@ export async function runConfirmTurn({
|
|
|
1958
2168
|
return {
|
|
1959
2169
|
ok: true, stop: null, rounds: round,
|
|
1960
2170
|
accepted: {
|
|
1961
|
-
goal: String(r.plan.goal ?? ''), checks: [...(r.plan.checks ?? [])], protections: [...protections],
|
|
2171
|
+
goal: String(r.plan.goal ?? ''), checks: [...(r.plan.checks ?? [])], checkItems: [...(r.plan.checkItems ?? [])], protections: [...protections],
|
|
1962
2172
|
lang: resolvedLang, worseThanBefore,
|
|
1963
2173
|
// every question forced above was answered in order, so what's left
|
|
1964
2174
|
// unresolved is honestly empty — computed FROM questionsFromPlan
|
|
@@ -1979,7 +2189,7 @@ export async function runConfirmTurn({
|
|
|
1979
2189
|
return {
|
|
1980
2190
|
ok: true, stop: null, rounds: round,
|
|
1981
2191
|
accepted: {
|
|
1982
|
-
goal: redactSecrets(String(typed).trim()), checks: [...(r.plan.checks ?? [])], protections: [...protections],
|
|
2192
|
+
goal: redactSecrets(String(typed).trim()), checks: [...(r.plan.checks ?? [])], checkItems: [...(r.plan.checkItems ?? [])], protections: [...protections],
|
|
1983
2193
|
lang: resolvedLang, worseThanBefore,
|
|
1984
2194
|
openQuestions: questionsFromPlan(r.plan).slice(answeredQuestions.length),
|
|
1985
2195
|
notChecked: [...notChecked], answeredQuestions,
|
|
@@ -2001,7 +2211,7 @@ export async function runConfirmTurn({
|
|
|
2001
2211
|
return {
|
|
2002
2212
|
ok: true, stop: null, rounds: round,
|
|
2003
2213
|
accepted: {
|
|
2004
|
-
goal: String(r.plan.goal ?? ''), checks: [...(r.plan.checks ?? [])], protections: [...protections],
|
|
2214
|
+
goal: String(r.plan.goal ?? ''), checks: [...(r.plan.checks ?? [])], checkItems: [...(r.plan.checkItems ?? [])], protections: [...protections],
|
|
2005
2215
|
lang: resolvedLang, worseThanBefore, openQuestions: [...questionsFromPlan(r.plan), redactedFix], notChecked: [...notChecked],
|
|
2006
2216
|
},
|
|
2007
2217
|
reds: [], cost: book.report(),
|
|
@@ -2055,12 +2265,14 @@ export async function runConfirmTurn({
|
|
|
2055
2265
|
* generate: Function, seedReadFn?: Function, closeCtx?: any,
|
|
2056
2266
|
* ceilingUsd?: number|null,
|
|
2057
2267
|
* onPhase?: (phase: string, data?: any) => void,
|
|
2058
|
-
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
2268
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void,
|
|
2059
2269
|
* maxRevisions?: number, structureRetries?: number,
|
|
2060
2270
|
* structuredMode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
|
|
2061
2271
|
* priorCalls?: {label: string, costUsd: number|null, unpricedRounds: number}[]|null,
|
|
2062
2272
|
* priorRaws?: any[]|null,
|
|
2063
|
-
*
|
|
2273
|
+
* jobLines?: {n: number, text: string}[]|null, inputs?: {n: number, label: string, value: string}[]|null,
|
|
2274
|
+
* offerQuestions?: boolean,
|
|
2275
|
+
* confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null}} o
|
|
2064
2276
|
*/
|
|
2065
2277
|
export async function authorClose({
|
|
2066
2278
|
workdir, seedRef, lang, verdictType,
|
|
@@ -2083,6 +2295,9 @@ export async function authorClose({
|
|
|
2083
2295
|
// straight into `authorPrompt`'s `confirmedBlock`. `null` is every caller
|
|
2084
2296
|
// that ran no confirm turn — the prompt is then byte-identical to today.
|
|
2085
2297
|
confirmed = null,
|
|
2298
|
+
// P7: the person's numbered job lines and inputs (src/jobblock.js). ABSENT = every caller that predates the job block:
|
|
2299
|
+
// no tagging order in the prompt, and no coverage rule in the validator.
|
|
2300
|
+
jobLines = null, inputs = null,
|
|
2086
2301
|
generate, seedReadFn = runSeedReadStages, closeCtx = {},
|
|
2087
2302
|
ceilingUsd = null,
|
|
2088
2303
|
// The two REPORTING seams, defaulted to nothing so every existing caller is
|
|
@@ -2094,6 +2309,10 @@ export async function authorClose({
|
|
|
2094
2309
|
structureRetries = MAX_STRUCTURE_RETRIES,
|
|
2095
2310
|
structuredMode = 'tool',
|
|
2096
2311
|
catalogue = KIND_CATALOGUE,
|
|
2312
|
+
// P7 item 5 (fwdloop am25): OFF unless the caller can really ask the person (the panel). When on, ONLY the last of the
|
|
2313
|
+
// full drafting ladder (the author call plus both revisions), and only after the two before it both failed validation,
|
|
2314
|
+
// is offered the optional `questions` property — see `askOpen` below. Absent = byte-identical to today.
|
|
2315
|
+
offerQuestions = false,
|
|
2097
2316
|
}) {
|
|
2098
2317
|
// TIGHTEN-ONLY: a caller may lower the ceiling, never raise it — unlimited
|
|
2099
2318
|
// revising launders thrash as adaptation (addendum 5). The floor is zero: one
|
|
@@ -2256,7 +2475,7 @@ export async function authorClose({
|
|
|
2256
2475
|
// ── the grounded loop ─────────────────────────────────────────────────────
|
|
2257
2476
|
const prompt = authorPrompt({
|
|
2258
2477
|
answers, questions, facts, listingBlock: /** @type {string} */ (seeds.block),
|
|
2259
|
-
lang, verdictType, guards, ownedEnvNames, mode: structuredMode, catalogue, writeScope, confirmed,
|
|
2478
|
+
lang, verdictType, guards, ownedEnvNames, mode: structuredMode, catalogue, writeScope, confirmed, jobLines, inputs,
|
|
2260
2479
|
});
|
|
2261
2480
|
/** @type {any[]} */
|
|
2262
2481
|
let messages = [{ role: 'user', content: prompt }];
|
|
@@ -2322,6 +2541,15 @@ export async function authorClose({
|
|
|
2322
2541
|
};
|
|
2323
2542
|
};
|
|
2324
2543
|
|
|
2544
|
+
/** the job lines the PREVIOUS try's validation reds named — set only when the next try may ask (null = it may not) @type {number[]|null} */
|
|
2545
|
+
let questionLines = null;
|
|
2546
|
+
/** per try: null = it passed (validated AND was measured with a work stage red); else the job lines its failure named @type {({lines: number[]}|null)[]} */
|
|
2547
|
+
const tryFails = [];
|
|
2548
|
+
/** @type {{line: number, question: string}[]} */
|
|
2549
|
+
let askedQuestions = [];
|
|
2550
|
+
/** P7 item 5: may the try AFTER try `i` ask? Only the last try of a full ladder, and only when try 1 and try `i` both failed. */
|
|
2551
|
+
const nextTryMayAsk = (/** @type {number} */ i) => offerQuestions && revisionCap === MAX_REVISIONS && i === revisionCap - 1
|
|
2552
|
+
&& Array.isArray(jobLines) && jobLines.length > 0 && Boolean(tryFails[0]) && Boolean(tryFails[1]);
|
|
2325
2553
|
const seedTrees = makeSeedTrees();
|
|
2326
2554
|
try {
|
|
2327
2555
|
for (let i = 0; i <= revisionCap; i += 1) {
|
|
@@ -2331,8 +2559,21 @@ export async function authorClose({
|
|
|
2331
2559
|
// a readout quoting a number the loop does not obey is a second
|
|
2332
2560
|
// instrument, and this file's own history is what that costs.
|
|
2333
2561
|
onPhase('author-call', { call: label, i, of: revisionCap });
|
|
2334
|
-
|
|
2562
|
+
// P7 item 5: a mechanism, never a wish. Only the last try of the full ladder carries the property, and only when the
|
|
2563
|
+
// two before it both failed (validation or the seed read; `questionLines` is set at the end of the second one, below).
|
|
2564
|
+
const askOpen = questionLines !== null && i === revisionCap;
|
|
2565
|
+
const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue, verdictType, questions: askOpen });
|
|
2335
2566
|
messages = ask.convo;
|
|
2567
|
+
/** @type {{reason: string, raw: any}[]} */
|
|
2568
|
+
let droppedQuestions = [];
|
|
2569
|
+
if (askOpen && ask.declaration && typeof ask.declaration === 'object' && Object.hasOwn(ask.declaration, 'questions')) {
|
|
2570
|
+
// `questions` is never part of the declaration: stripped before validation, kept only through the machine's filter
|
|
2571
|
+
const { questions: rawQuestions, ...rest } = ask.declaration;
|
|
2572
|
+
ask.declaration = rest;
|
|
2573
|
+
const nq = normalizeQuestions(rawQuestions, /** @type {number[]} */ (questionLines));
|
|
2574
|
+
askedQuestions = nq.kept;
|
|
2575
|
+
droppedQuestions = nq.dropped;
|
|
2576
|
+
}
|
|
2336
2577
|
|
|
2337
2578
|
// The cap tripped BEFORE this ask made any call at all, so there is no
|
|
2338
2579
|
// iteration to record: an iteration row for a call that never happened is
|
|
@@ -2350,6 +2591,7 @@ export async function authorClose({
|
|
|
2350
2591
|
call: label, attempts: ask.attempts, declaration: ask.declaration ?? null,
|
|
2351
2592
|
artifactRed: ask.red ?? null, providerError: ask.providerError ?? null,
|
|
2352
2593
|
validation: null, seedRead: null, reviseTurn: null,
|
|
2594
|
+
...(askOpen ? { questionsOffered: true, questions: askedQuestions, droppedQuestions } : {}),
|
|
2353
2595
|
};
|
|
2354
2596
|
iterations.push(iter);
|
|
2355
2597
|
|
|
@@ -2373,10 +2615,31 @@ export async function authorClose({
|
|
|
2373
2615
|
break;
|
|
2374
2616
|
}
|
|
2375
2617
|
if (!ask.declaration) {
|
|
2618
|
+
// hamr's ruling 2026-10-10 (live session smv2k6bczlvk3): a try whose EVERY structure attempt was cut off at the output cap
|
|
2619
|
+
// is a FAILED try for the question gate. A cut-off names no line, so every numbered job line counts. The ladder goes on to
|
|
2620
|
+
// try 3 only when that gate can open (try 1 failed too); anywhere else it stops artifact-red as it always did.
|
|
2621
|
+
if (ask.allCutOff && i === revisionCap - 1) {
|
|
2622
|
+
const allLines = (jobLines ?? []).map((l) => l.n);
|
|
2623
|
+
tryFails[i] = { lines: allLines };
|
|
2624
|
+
iter.cutOff = true;
|
|
2625
|
+
if (nextTryMayAsk(i)) {
|
|
2626
|
+
questionLines = allLines.length ? allLines : null;
|
|
2627
|
+
const block = renderRejectBlock({ kind: 'artifact', reds: [/** @type {Red} */ (ask.red).detail] });
|
|
2628
|
+
messages = [...ask.convo, { role: 'assistant', content: '(the reply carried no text)' },
|
|
2629
|
+
{ role: 'user', content: `${block}\n\n${structuredMode === 'tool' ? STRUCTURE_INSTRUCTION_TOOL : STRUCTURE_INSTRUCTION_TEXT}` }];
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2376
2633
|
fatal = [/** @type {Red} */ (ask.red)];
|
|
2377
2634
|
stop = 'artifact-red';
|
|
2378
2635
|
break;
|
|
2379
2636
|
}
|
|
2637
|
+
// P7 item 5: the person is asked; nothing from this try is validated, measured or signable. The reds that opened the
|
|
2638
|
+
// asking stay as the record of why (`lastValidation`).
|
|
2639
|
+
if (askedQuestions.length) {
|
|
2640
|
+
stop = 'questions-open';
|
|
2641
|
+
break;
|
|
2642
|
+
}
|
|
2380
2643
|
|
|
2381
2644
|
// early stop: an unchanged declaration has the previous iteration's
|
|
2382
2645
|
// measurements by definition — re-executing it buys nothing and costs a
|
|
@@ -2389,7 +2652,7 @@ export async function authorClose({
|
|
|
2389
2652
|
}
|
|
2390
2653
|
previous = ask.declaration;
|
|
2391
2654
|
|
|
2392
|
-
const v = validateDeclaration(ask.declaration, { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType });
|
|
2655
|
+
const v = validateDeclaration(ask.declaration, { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope, jobLines });
|
|
2393
2656
|
// SCRUBBED HERE, once, where the reds enter this module's records — the same
|
|
2394
2657
|
// boundary rule `renderRejectBlock` already states, applied to the OTHER
|
|
2395
2658
|
// channel they travel down. A validation red quotes what the model declared
|
|
@@ -2409,6 +2672,7 @@ export async function authorClose({
|
|
|
2409
2672
|
// a validation red IS a measurement of the declaration, so it rides the
|
|
2410
2673
|
// same channel and counts as a revision (addendum 5)
|
|
2411
2674
|
measured = renderRejectBlock({ kind: 'validation', reds: vReds });
|
|
2675
|
+
tryFails[i] = { lines: linesNamedByReds(vReds, ask.declaration, (jobLines ?? []).map((l) => l.n)) };
|
|
2412
2676
|
} else {
|
|
2413
2677
|
// what gets MEASURED is what will RUN: the resolved declaration with the
|
|
2414
2678
|
// genre's environment already injected
|
|
@@ -2437,10 +2701,20 @@ export async function authorClose({
|
|
|
2437
2701
|
finalFrom = label;
|
|
2438
2702
|
measured = renderSeedReadBlock(rows);
|
|
2439
2703
|
measuredIterations.push({ label, declaration: injected.declaration, seedRead: rows, droppedEnv: injected.dropped });
|
|
2704
|
+
const sf = seedReadFailure(rows, injected.declaration, guards.map((/** @type {any} */ g) => g.name), (jobLines ?? []).map((l) => l.n));
|
|
2705
|
+
tryFails[i] = sf.failed ? { lines: sf.lines } : null;
|
|
2440
2706
|
}
|
|
2441
2707
|
|
|
2442
2708
|
if (i === revisionCap) { stop = 'max-revisions'; break; }
|
|
2443
2709
|
|
|
2710
|
+
// P7 item 5: the NEXT try is the last of a full ladder, and this one AND the first both FAILED — either failed validation
|
|
2711
|
+
// or failed the seed read (a vacuous close; hamr's ruling 2026-10-10, differing from fwdloop's validation-only rule) — so
|
|
2712
|
+
// it may ask, about the lines THIS try's failure named. A try that passed (a confident guess) never opens asking.
|
|
2713
|
+
if (nextTryMayAsk(i)) {
|
|
2714
|
+
const second = tryFails[1];
|
|
2715
|
+
questionLines = second && second.lines.length ? second.lines : null;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2444
2718
|
const turn = buildReviseTurn(measured);
|
|
2445
2719
|
// A TRIPWIRE, not behaviour: it can only fire once something else is
|
|
2446
2720
|
// already broken, so it is the one guard in this module the mutation
|
|
@@ -2523,6 +2797,8 @@ export async function authorClose({
|
|
|
2523
2797
|
// WHICH stages broke on the dropped one, so this is never a silent swap.
|
|
2524
2798
|
fellBack,
|
|
2525
2799
|
stop,
|
|
2800
|
+
// P7 item 5: what the person is asked (empty on every other stop)
|
|
2801
|
+
questions: stop === 'questions-open' ? askedQuestions : [],
|
|
2526
2802
|
revisions: Math.max(0, iterations.length - 1),
|
|
2527
2803
|
};
|
|
2528
2804
|
}
|