bareloop 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/bareloop.context.md +63 -11
- package/package.json +2 -2
- package/src/authorflow.js +333 -58
- package/src/authoring.js +65 -2
- package/src/authorjob.js +57 -9
- package/src/authorreadout.js +50 -2
- package/src/authorrun.js +5 -2
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/interviewrun.js +56 -27
- package/src/job.js +38 -5
- package/src/jobblock.js +140 -0
- package/src/judged.js +1 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +15 -5
- package/src/panel/authorsession.js +158 -47
- package/src/panel/index.html +179 -70
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +4 -4
- package/src/promptregisters.js +1 -1
- package/src/providers.js +101 -19
- package/src/reuse.js +1 -1
- package/src/root.js +1 -1
- package/src/run.js +3 -3
- package/src/selection.js +1 -1
- package/src/source.js +64 -1
- package/src/text.js +13 -0
- package/src/transport.js +1 -1
- package/src/userrun.js +3 -3
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +7 -1
- package/types/authorjob.d.ts +35 -3
- package/types/authorreadout.d.ts +17 -1
- package/types/authorscout.d.ts +5 -3
- package/types/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/panel/authorsession.d.ts +50 -14
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +20 -0
- package/types/planrun.d.ts +1 -1
- package/types/providers.d.ts +47 -3
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
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({
|
|
@@ -457,7 +564,7 @@ export const LANGUAGE_PICK_FIELD = Object.freeze({
|
|
|
457
564
|
});
|
|
458
565
|
|
|
459
566
|
/**
|
|
460
|
-
* F175's open half (docs/
|
|
567
|
+
* F175's open half (docs/wiki/FINDINGS.md, run mu0voeo4), hamr's ruling
|
|
461
568
|
* (2026-09-16): "Confirm" and "Type the goal yourself" may not bypass a
|
|
462
569
|
* `questions` entry the model itself raised on the plan being accepted — the
|
|
463
570
|
* person answers it inline, one question at a time, with NO extra model
|
|
@@ -746,10 +853,11 @@ export function schemaCoverage(catalogue = KIND_CATALOGUE) {
|
|
|
746
853
|
* THROWS on a catalogue it cannot cover: a malformed catalogue is a bareloop bug,
|
|
747
854
|
* and failing loudly beats emitting a schema that quietly cannot say something
|
|
748
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.
|
|
749
857
|
* @param {Record<string, any>} [catalogue]
|
|
750
|
-
* @param {{verdictType?: string|null}} [o]
|
|
858
|
+
* @param {{verdictType?: string|null, questions?: boolean}} [o]
|
|
751
859
|
*/
|
|
752
|
-
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null } = {}) {
|
|
860
|
+
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null, questions = false } = {}) {
|
|
753
861
|
const cov = schemaCoverage(catalogue);
|
|
754
862
|
if (!cov.ok) {
|
|
755
863
|
throw new Error('[authorflow] the declaration schema does not cover the catalogue — '
|
|
@@ -768,6 +876,7 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
768
876
|
type: 'object',
|
|
769
877
|
properties: {
|
|
770
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)' },
|
|
771
880
|
kind: { const: kind },
|
|
772
881
|
params: { type: 'object', properties, required: [...spec.required], additionalProperties: false },
|
|
773
882
|
},
|
|
@@ -795,6 +904,30 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
795
904
|
items: { type: 'string', minLength: 1 },
|
|
796
905
|
description: 'anything you could not express, or any fact you needed and did not have',
|
|
797
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
|
+
} : {}),
|
|
798
931
|
},
|
|
799
932
|
required: ['stages'],
|
|
800
933
|
additionalProperties: false,
|
|
@@ -805,13 +938,13 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
805
938
|
* The output channel. It records and acknowledges; it takes no action, which is
|
|
806
939
|
* why handing it to a toolless authoring call does not reopen D3.
|
|
807
940
|
* @param {{calls: any[]}} box
|
|
808
|
-
* @param {{catalogue?: Record<string, any>, verdictType?: string|null}} [o]
|
|
941
|
+
* @param {{catalogue?: Record<string, any>, verdictType?: string|null, questions?: boolean}} [o]
|
|
809
942
|
*/
|
|
810
|
-
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null } = {}) {
|
|
943
|
+
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null, questions = false } = {}) {
|
|
811
944
|
return {
|
|
812
945
|
name: DECLARATION_TOOL_NAME,
|
|
813
946
|
description: 'Deliver the finished close. Call this exactly once, with the whole declaration as its arguments.',
|
|
814
|
-
parameters: declarationSchema(catalogue, { verdictType }),
|
|
947
|
+
parameters: declarationSchema(catalogue, { verdictType, questions }),
|
|
815
948
|
execute: async (/** @type {any} */ args) => { box.calls.push(args); return DECLARATION_ACK; },
|
|
816
949
|
};
|
|
817
950
|
}
|
|
@@ -986,7 +1119,7 @@ export function writeScopeBlock(writeScope) {
|
|
|
986
1119
|
* acceptance — BINDING, never re-decided by the composer, and a DIFFERENT
|
|
987
1120
|
* meaning from `openQuestions` (which means "could not resolve"): the two
|
|
988
1121
|
* never merge.
|
|
989
|
-
* @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[],
|
|
990
1123
|
* answeredQuestions?: string[]}} confirmed
|
|
991
1124
|
*/
|
|
992
1125
|
export function confirmedBlock(confirmed) {
|
|
@@ -998,7 +1131,9 @@ export function confirmedBlock(confirmed) {
|
|
|
998
1131
|
return 'THE CONFIRMED PLAN — the person already saw and confirmed this in the confirm turn\n\n'
|
|
999
1132
|
+ 'Compose stages for these checks and no other — a genre never adds a check the goal did not ask for, and '
|
|
1000
1133
|
+ 'neither do you:\n\n'
|
|
1001
|
-
+ `${
|
|
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`
|
|
1002
1137
|
+ 'These protections are already always-on and unchanged by this plan — never compose one of these as a check:\n\n'
|
|
1003
1138
|
+ `${protections.map((p) => ` - ${p}`).join('\n') || ' (none)'}\n\n`
|
|
1004
1139
|
+ (notChecked.length
|
|
@@ -1013,6 +1148,21 @@ export function confirmedBlock(confirmed) {
|
|
|
1013
1148
|
: 'The confirm turn left no open questions.');
|
|
1014
1149
|
}
|
|
1015
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
|
+
|
|
1016
1166
|
/**
|
|
1017
1167
|
* The whole authoring prompt, as one string.
|
|
1018
1168
|
*
|
|
@@ -1035,11 +1185,15 @@ export function confirmedBlock(confirmed) {
|
|
|
1035
1185
|
* guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
|
|
1036
1186
|
* ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any>,
|
|
1037
1187
|
* writeScope?: string[]|null,
|
|
1038
|
-
*
|
|
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
|
|
1039
1190
|
*/
|
|
1040
1191
|
export function authorPrompt({
|
|
1041
1192
|
answers, questions = GREEN_QUESTIONS, facts, listingBlock, lang, verdictType, guards,
|
|
1042
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,
|
|
1043
1197
|
// PRD item 33 M3 piece 4: ABSENT for every caller that predates the confirm
|
|
1044
1198
|
// turn (or ran without `ask`) — byte-identical prompt, nothing added. Present
|
|
1045
1199
|
// only when a confirm turn actually ran and the person accepted a plan.
|
|
@@ -1115,7 +1269,9 @@ measure anything.`;
|
|
|
1115
1269
|
// asks what KIND of job this is; the person answered what DONE means, and the
|
|
1116
1270
|
// composer reads that plus the catalogue and works the rest out.
|
|
1117
1271
|
`WHAT THE PERSON DECLARED "DONE" TO MEAN — their answer, not yours\n\n${statement}`,
|
|
1118
|
-
`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)] : []),
|
|
1119
1275
|
// Q2 IS GONE (PRD item 33 M3 piece 3) — what it used to carry (which files
|
|
1120
1276
|
// change, which are read-only) is proven against the machine now, not typed,
|
|
1121
1277
|
// and this is where that proof reaches the composer instead of vanishing.
|
|
@@ -1380,7 +1536,7 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }) {
|
|
|
1380
1536
|
*/
|
|
1381
1537
|
/**
|
|
1382
1538
|
* @param {{ceilingUsd?: number|null,
|
|
1383
|
-
* 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]
|
|
1384
1540
|
* `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
|
|
1385
1541
|
* `onCall` is a REPORTING seam fired as each metered call lands — the shell's
|
|
1386
1542
|
* only chance to know what has been spent while the run is still alive. It is
|
|
@@ -1401,7 +1557,9 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
|
|
|
1401
1557
|
const call = { label, ...priceOf(r) };
|
|
1402
1558
|
entries.push(call);
|
|
1403
1559
|
raws.push(scrubRaw({ label, attempt, text: r?.text, reason: r?.error ? String(r.error) : null }));
|
|
1404
|
-
|
|
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 }) });
|
|
1405
1563
|
return entries.at(-1);
|
|
1406
1564
|
},
|
|
1407
1565
|
/** @param {{label: string, costUsd: number|null, unpricedRounds: number}[]} calls
|
|
@@ -1441,10 +1599,13 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
|
|
|
1441
1599
|
* `cacheMessages` (F18) and `maxTokens` (F30) are not optional on this stack.
|
|
1442
1600
|
* `rates` is the customer's own price for this provider's key row (USD per 1K, `ratesFor`);
|
|
1443
1601
|
* absent = bare-agent's built-in guess, exactly as before.
|
|
1444
|
-
*
|
|
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]
|
|
1445
1604
|
* @returns {(messages: any[], tools: any[], opts?: any) => Promise<any>}
|
|
1446
1605
|
*/
|
|
1447
|
-
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);
|
|
1448
1609
|
return async (/** @type {any[]} */ messages, /** @type {any[]} */ tools, /** @type {any} */ opts = {}) => {
|
|
1449
1610
|
const loop = new Loop({ provider, system, ...(rates ? { rates } : {}) });
|
|
1450
1611
|
const wired = tools.map((t) => ({
|
|
@@ -1460,7 +1621,7 @@ export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens =
|
|
|
1460
1621
|
// throws on a malformed tool-call arguments string (BA-27): it returns the
|
|
1461
1622
|
// round priced with `toolCalls: []` plus its own `malformedToolCall` marker,
|
|
1462
1623
|
// which `Loop.run` surfaces unchanged on its return. No local shim needed.
|
|
1463
|
-
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 });
|
|
1464
1625
|
};
|
|
1465
1626
|
}
|
|
1466
1627
|
|
|
@@ -1489,7 +1650,7 @@ function canonical(x) {
|
|
|
1489
1650
|
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
1490
1651
|
* channel: {name: string, instruction: string, textPath?: string, tool: (box: {calls: any[]}) => any}}} o
|
|
1491
1652
|
* @returns {Promise<{artifact: any, attempts: number, convo: any[], raw: string,
|
|
1492
|
-
* 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}>}
|
|
1493
1654
|
*/
|
|
1494
1655
|
export async function askStructured({ messages, generate, mode, retries, label, book, channel }) {
|
|
1495
1656
|
/** @type {any[]} */
|
|
@@ -1497,6 +1658,8 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1497
1658
|
let attempts = 0;
|
|
1498
1659
|
/** @type {Red|null} */
|
|
1499
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;
|
|
1500
1663
|
|
|
1501
1664
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
1502
1665
|
// THE CEILING, immediately before the paid call and nowhere else. Sited HERE
|
|
@@ -1541,11 +1704,23 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1541
1704
|
book.add(attempt === 0 ? label : `${label}#${attempt + 1}`, r, attempts);
|
|
1542
1705
|
const raw = redactSecrets(String(r?.text ?? ''));
|
|
1543
1706
|
|
|
1544
|
-
|
|
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) {
|
|
1545
1713
|
return { artifact: null, attempts, convo, raw, providerError: String(r.error), red: null, budget: null };
|
|
1546
1714
|
}
|
|
1547
1715
|
|
|
1548
|
-
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') {
|
|
1549
1724
|
// F179 (2026-09-15 ruling) — retry, never repair. bare-agent 0.43.0
|
|
1550
1725
|
// (BA-27) already priced this round on the real usage and returns it
|
|
1551
1726
|
// with `toolCalls: []` plus its own `malformedToolCall` marker instead
|
|
@@ -1598,22 +1773,23 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1598
1773
|
|
|
1599
1774
|
if (attempt < retries) {
|
|
1600
1775
|
const block = renderRejectBlock({ kind: 'artifact', reds: [/** @type {Red} */ (red).detail] });
|
|
1601
|
-
|
|
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)' });
|
|
1602
1778
|
convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ? channel.instruction : STRUCTURE_INSTRUCTION_TEXT}` });
|
|
1603
1779
|
}
|
|
1604
1780
|
}
|
|
1605
|
-
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 };
|
|
1606
1782
|
}
|
|
1607
1783
|
|
|
1608
1784
|
/** THE DECLARATION CHANNEL — the one this loop has always used. Its schema is
|
|
1609
1785
|
* CLASS-SCOPED, like the prompt's catalogue block: the two must offer one set of
|
|
1610
1786
|
* kinds, or the composer reads about a kind it cannot call.
|
|
1611
|
-
* @param {{catalogue: Record<string, any>, verdictType: string}} o */
|
|
1612
|
-
const declarationChannel = ({ catalogue, verdictType }) => ({
|
|
1787
|
+
* @param {{catalogue: Record<string, any>, verdictType: string, questions?: boolean}} o */
|
|
1788
|
+
const declarationChannel = ({ catalogue, verdictType, questions = false }) => ({
|
|
1613
1789
|
name: DECLARATION_TOOL_NAME,
|
|
1614
1790
|
instruction: STRUCTURE_INSTRUCTION_TOOL,
|
|
1615
1791
|
textPath: 'declaration',
|
|
1616
|
-
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType }),
|
|
1792
|
+
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType, questions }),
|
|
1617
1793
|
});
|
|
1618
1794
|
|
|
1619
1795
|
/** The declaration ask, over the shared ladder. `declaration` is `artifact`
|
|
@@ -1621,11 +1797,11 @@ const declarationChannel = ({ catalogue, verdictType }) => ({
|
|
|
1621
1797
|
* anonymous artifact, and one word for two things is how a reader loses which.
|
|
1622
1798
|
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
1623
1799
|
* label: string, book: ReturnType<typeof makeCostBook>, catalogue: Record<string, any>,
|
|
1624
|
-
* verdictType: string}} o
|
|
1800
|
+
* verdictType: string, questions?: boolean}} o
|
|
1625
1801
|
*/
|
|
1626
|
-
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 }) {
|
|
1627
1803
|
const r = await askStructured({
|
|
1628
|
-
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType }),
|
|
1804
|
+
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType, questions }),
|
|
1629
1805
|
});
|
|
1630
1806
|
return { ...r, declaration: r.artifact };
|
|
1631
1807
|
}
|
|
@@ -1663,9 +1839,17 @@ const CONFIRM_SCHEMA = Object.freeze({
|
|
|
1663
1839
|
properties: {
|
|
1664
1840
|
checks: {
|
|
1665
1841
|
type: 'array',
|
|
1666
|
-
items: {
|
|
1667
|
-
|
|
1668
|
-
|
|
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',
|
|
1669
1853
|
},
|
|
1670
1854
|
goal: {
|
|
1671
1855
|
type: 'string',
|
|
@@ -1676,7 +1860,7 @@ const CONFIRM_SCHEMA = Object.freeze({
|
|
|
1676
1860
|
notChecked: {
|
|
1677
1861
|
type: 'array',
|
|
1678
1862
|
items: { type: 'string', minLength: 1 },
|
|
1679
|
-
description: 'anything the person asked for (in the
|
|
1863
|
+
description: 'anything the person asked for (in the job lines, the rules under them, or '
|
|
1680
1864
|
+ 'worse-than-before answers) that none of your listed checks can verify — named plainly in the person\'s '
|
|
1681
1865
|
+ 'own words; an empty list means every ask is covered by a listed check',
|
|
1682
1866
|
},
|
|
@@ -1691,6 +1875,25 @@ const CONFIRM_SCHEMA = Object.freeze({
|
|
|
1691
1875
|
additionalProperties: false,
|
|
1692
1876
|
});
|
|
1693
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
|
+
|
|
1694
1897
|
/** The output channel. It records and acknowledges; it takes no action.
|
|
1695
1898
|
* @param {{calls: any[]}} box */
|
|
1696
1899
|
export function confirmTool(box) {
|
|
@@ -1722,9 +1925,10 @@ const confirmChannel = () => ({
|
|
|
1722
1925
|
*/
|
|
1723
1926
|
export const CONFIRM_SYSTEM = 'You read what a NON-ENGINEER answered, plus a read-only survey or listing of their '
|
|
1724
1927
|
+ 'own repository, and draft a PLAN for a job\'s definition of done: the checks you plan to compose and one '
|
|
1725
|
-
+ '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 '
|
|
1726
1929
|
+ '— a genre never adds a check the goal did not ask for (run mtv8jihy drafted an unasked tsc --strict stage; '
|
|
1727
|
-
+ '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, '
|
|
1728
1932
|
+ 'no-suppressions, and every mandatory guard) and the write fence are shown to the person by the SYSTEM, never '
|
|
1729
1933
|
+ 'by you: you never claim, name, or list a protection or guard of your own — that is not your call to state, and '
|
|
1730
1934
|
+ 'a protection is never a check and never named in the goal sentence. The goal sentence names every check you '
|
|
@@ -1756,15 +1960,16 @@ export const CONFIRM_SYSTEM = 'You read what a NON-ENGINEER answered, plus a rea
|
|
|
1756
1960
|
* @param {{answers: Record<string|number, string>, questions: Record<string|number, string>,
|
|
1757
1961
|
* labels?: Record<string|number, string>, facts?: any, listing?: string|null,
|
|
1758
1962
|
* writeScope?: string[]|null, isRepo: boolean, lang: string, worseThanBefore?: string,
|
|
1759
|
-
* fixText?: string|null, protections?: string[]}} o
|
|
1963
|
+
* fixText?: string|null, protections?: string[], inputs?: {n: number, label: string, value: string}[]|null}} o
|
|
1760
1964
|
*/
|
|
1761
|
-
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 }) {
|
|
1762
1966
|
const lines = ['THE PERSON\'S OWN ANSWERS (a non-engineer; read exactly what they wrote, invent nothing beyond it):'];
|
|
1763
1967
|
for (const k of Object.keys(questions)) {
|
|
1764
1968
|
const label = labels[k] ? ` (${labels[k]})` : '';
|
|
1765
1969
|
lines.push(`Q${k}${label}. ${questions[k]}`);
|
|
1766
1970
|
lines.push(`A${k}. ${answers?.[k] ?? '(no answer)'}`);
|
|
1767
1971
|
}
|
|
1972
|
+
if (inputs && inputs.length) lines.push('', renderInputs(inputs));
|
|
1768
1973
|
if (isRepo && worseThanBefore) {
|
|
1769
1974
|
lines.push('', `WORSE THAN BEFORE (a constraint the person named, repo jobs only): ${worseThanBefore}`);
|
|
1770
1975
|
}
|
|
@@ -1821,14 +2026,14 @@ async function askConfirmPlan({ convo, generate, mode, book, label }) {
|
|
|
1821
2026
|
* generate: Function, book: ReturnType<typeof makeCostBook>,
|
|
1822
2027
|
* ask: (step: {kind: string, [k: string]: any}) => Promise<string|null>,
|
|
1823
2028
|
* onPhase?: (phase: string, data?: any) => void, mode?: 'tool'|'text',
|
|
1824
|
-
* 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):
|
|
1825
2030
|
* `worseThanBefore` is never asked any more and defaults to `''` — a
|
|
1826
2031
|
* caller may still pass a string through (kept for a future, non-ask
|
|
1827
2032
|
* source of the same constraint), it just never comes from an ask() step.
|
|
1828
2033
|
* @returns {Promise<{ok: boolean,
|
|
1829
2034
|
* stop: null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart',
|
|
1830
2035
|
* rounds: number,
|
|
1831
|
-
* 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,
|
|
1832
2037
|
* worseThanBefore: string, openQuestions: string[], notChecked: string[], answeredQuestions?: string[]}|null,
|
|
1833
2038
|
* reds: Red[], cost: any}>}
|
|
1834
2039
|
*/
|
|
@@ -1841,6 +2046,8 @@ export async function runConfirmTurn({
|
|
|
1841
2046
|
// still pass a resolved string through (kept as a plain pass-through, not
|
|
1842
2047
|
// an ask trigger).
|
|
1843
2048
|
worseThanBefore: presetWorseThanBefore = '',
|
|
2049
|
+
// P7: the inputs the person pointed at; absent = the prompt is byte-identical
|
|
2050
|
+
inputs = null,
|
|
1844
2051
|
}) {
|
|
1845
2052
|
/** @typedef {null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart'} ConfirmStop */
|
|
1846
2053
|
/** @returns {{ok: boolean, stop: ConfirmStop, rounds: number, accepted: null, reds: Red[], cost: any}} */
|
|
@@ -1911,14 +2118,14 @@ export async function runConfirmTurn({
|
|
|
1911
2118
|
let convo = [{
|
|
1912
2119
|
role: 'user',
|
|
1913
2120
|
content: confirmPrompt({
|
|
1914
|
-
answers, questions, labels, facts, listing, writeScope, isRepo, lang: resolvedLang, worseThanBefore, protections,
|
|
2121
|
+
answers, questions, labels, facts, listing, writeScope, isRepo, lang: resolvedLang, worseThanBefore, protections, inputs,
|
|
1915
2122
|
}),
|
|
1916
2123
|
}];
|
|
1917
2124
|
|
|
1918
2125
|
for (let round = 1; round <= 2; round += 1) {
|
|
1919
2126
|
if (fixText) {
|
|
1920
2127
|
convo = [...convo, { role: 'user', content: confirmPrompt({
|
|
1921
|
-
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,
|
|
1922
2129
|
}) }];
|
|
1923
2130
|
}
|
|
1924
2131
|
onPhase('confirm-round', { round });
|
|
@@ -1947,6 +2154,8 @@ export async function runConfirmTurn({
|
|
|
1947
2154
|
// shown at confirm-done and recorded on acceptance. `notChecked` is the
|
|
1948
2155
|
// model's own honest gap list, carried through unchanged.
|
|
1949
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 };
|
|
1950
2159
|
const plan = { ...r.plan, protections, notChecked };
|
|
1951
2160
|
onPhase('confirm-done', { round, plan });
|
|
1952
2161
|
const picked = await ask({ kind: 'menu', field: CONFIRM_MENU, plan });
|
|
@@ -1959,7 +2168,7 @@ export async function runConfirmTurn({
|
|
|
1959
2168
|
return {
|
|
1960
2169
|
ok: true, stop: null, rounds: round,
|
|
1961
2170
|
accepted: {
|
|
1962
|
-
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],
|
|
1963
2172
|
lang: resolvedLang, worseThanBefore,
|
|
1964
2173
|
// every question forced above was answered in order, so what's left
|
|
1965
2174
|
// unresolved is honestly empty — computed FROM questionsFromPlan
|
|
@@ -1980,7 +2189,7 @@ export async function runConfirmTurn({
|
|
|
1980
2189
|
return {
|
|
1981
2190
|
ok: true, stop: null, rounds: round,
|
|
1982
2191
|
accepted: {
|
|
1983
|
-
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],
|
|
1984
2193
|
lang: resolvedLang, worseThanBefore,
|
|
1985
2194
|
openQuestions: questionsFromPlan(r.plan).slice(answeredQuestions.length),
|
|
1986
2195
|
notChecked: [...notChecked], answeredQuestions,
|
|
@@ -2002,7 +2211,7 @@ export async function runConfirmTurn({
|
|
|
2002
2211
|
return {
|
|
2003
2212
|
ok: true, stop: null, rounds: round,
|
|
2004
2213
|
accepted: {
|
|
2005
|
-
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],
|
|
2006
2215
|
lang: resolvedLang, worseThanBefore, openQuestions: [...questionsFromPlan(r.plan), redactedFix], notChecked: [...notChecked],
|
|
2007
2216
|
},
|
|
2008
2217
|
reds: [], cost: book.report(),
|
|
@@ -2056,12 +2265,14 @@ export async function runConfirmTurn({
|
|
|
2056
2265
|
* generate: Function, seedReadFn?: Function, closeCtx?: any,
|
|
2057
2266
|
* ceilingUsd?: number|null,
|
|
2058
2267
|
* onPhase?: (phase: string, data?: any) => void,
|
|
2059
|
-
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
2268
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void,
|
|
2060
2269
|
* maxRevisions?: number, structureRetries?: number,
|
|
2061
2270
|
* structuredMode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
|
|
2062
2271
|
* priorCalls?: {label: string, costUsd: number|null, unpricedRounds: number}[]|null,
|
|
2063
2272
|
* priorRaws?: any[]|null,
|
|
2064
|
-
*
|
|
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
|
|
2065
2276
|
*/
|
|
2066
2277
|
export async function authorClose({
|
|
2067
2278
|
workdir, seedRef, lang, verdictType,
|
|
@@ -2084,6 +2295,9 @@ export async function authorClose({
|
|
|
2084
2295
|
// straight into `authorPrompt`'s `confirmedBlock`. `null` is every caller
|
|
2085
2296
|
// that ran no confirm turn — the prompt is then byte-identical to today.
|
|
2086
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,
|
|
2087
2301
|
generate, seedReadFn = runSeedReadStages, closeCtx = {},
|
|
2088
2302
|
ceilingUsd = null,
|
|
2089
2303
|
// The two REPORTING seams, defaulted to nothing so every existing caller is
|
|
@@ -2095,6 +2309,10 @@ export async function authorClose({
|
|
|
2095
2309
|
structureRetries = MAX_STRUCTURE_RETRIES,
|
|
2096
2310
|
structuredMode = 'tool',
|
|
2097
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,
|
|
2098
2316
|
}) {
|
|
2099
2317
|
// TIGHTEN-ONLY: a caller may lower the ceiling, never raise it — unlimited
|
|
2100
2318
|
// revising launders thrash as adaptation (addendum 5). The floor is zero: one
|
|
@@ -2257,7 +2475,7 @@ export async function authorClose({
|
|
|
2257
2475
|
// ── the grounded loop ─────────────────────────────────────────────────────
|
|
2258
2476
|
const prompt = authorPrompt({
|
|
2259
2477
|
answers, questions, facts, listingBlock: /** @type {string} */ (seeds.block),
|
|
2260
|
-
lang, verdictType, guards, ownedEnvNames, mode: structuredMode, catalogue, writeScope, confirmed,
|
|
2478
|
+
lang, verdictType, guards, ownedEnvNames, mode: structuredMode, catalogue, writeScope, confirmed, jobLines, inputs,
|
|
2261
2479
|
});
|
|
2262
2480
|
/** @type {any[]} */
|
|
2263
2481
|
let messages = [{ role: 'user', content: prompt }];
|
|
@@ -2323,6 +2541,15 @@ export async function authorClose({
|
|
|
2323
2541
|
};
|
|
2324
2542
|
};
|
|
2325
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]);
|
|
2326
2553
|
const seedTrees = makeSeedTrees();
|
|
2327
2554
|
try {
|
|
2328
2555
|
for (let i = 0; i <= revisionCap; i += 1) {
|
|
@@ -2332,8 +2559,21 @@ export async function authorClose({
|
|
|
2332
2559
|
// a readout quoting a number the loop does not obey is a second
|
|
2333
2560
|
// instrument, and this file's own history is what that costs.
|
|
2334
2561
|
onPhase('author-call', { call: label, i, of: revisionCap });
|
|
2335
|
-
|
|
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 });
|
|
2336
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
|
+
}
|
|
2337
2577
|
|
|
2338
2578
|
// The cap tripped BEFORE this ask made any call at all, so there is no
|
|
2339
2579
|
// iteration to record: an iteration row for a call that never happened is
|
|
@@ -2351,6 +2591,7 @@ export async function authorClose({
|
|
|
2351
2591
|
call: label, attempts: ask.attempts, declaration: ask.declaration ?? null,
|
|
2352
2592
|
artifactRed: ask.red ?? null, providerError: ask.providerError ?? null,
|
|
2353
2593
|
validation: null, seedRead: null, reviseTurn: null,
|
|
2594
|
+
...(askOpen ? { questionsOffered: true, questions: askedQuestions, droppedQuestions } : {}),
|
|
2354
2595
|
};
|
|
2355
2596
|
iterations.push(iter);
|
|
2356
2597
|
|
|
@@ -2374,10 +2615,31 @@ export async function authorClose({
|
|
|
2374
2615
|
break;
|
|
2375
2616
|
}
|
|
2376
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
|
+
}
|
|
2377
2633
|
fatal = [/** @type {Red} */ (ask.red)];
|
|
2378
2634
|
stop = 'artifact-red';
|
|
2379
2635
|
break;
|
|
2380
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
|
+
}
|
|
2381
2643
|
|
|
2382
2644
|
// early stop: an unchanged declaration has the previous iteration's
|
|
2383
2645
|
// measurements by definition — re-executing it buys nothing and costs a
|
|
@@ -2390,7 +2652,7 @@ export async function authorClose({
|
|
|
2390
2652
|
}
|
|
2391
2653
|
previous = ask.declaration;
|
|
2392
2654
|
|
|
2393
|
-
const v = validateDeclaration(ask.declaration, { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope });
|
|
2655
|
+
const v = validateDeclaration(ask.declaration, { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope, jobLines });
|
|
2394
2656
|
// SCRUBBED HERE, once, where the reds enter this module's records — the same
|
|
2395
2657
|
// boundary rule `renderRejectBlock` already states, applied to the OTHER
|
|
2396
2658
|
// channel they travel down. A validation red quotes what the model declared
|
|
@@ -2410,6 +2672,7 @@ export async function authorClose({
|
|
|
2410
2672
|
// a validation red IS a measurement of the declaration, so it rides the
|
|
2411
2673
|
// same channel and counts as a revision (addendum 5)
|
|
2412
2674
|
measured = renderRejectBlock({ kind: 'validation', reds: vReds });
|
|
2675
|
+
tryFails[i] = { lines: linesNamedByReds(vReds, ask.declaration, (jobLines ?? []).map((l) => l.n)) };
|
|
2413
2676
|
} else {
|
|
2414
2677
|
// what gets MEASURED is what will RUN: the resolved declaration with the
|
|
2415
2678
|
// genre's environment already injected
|
|
@@ -2438,10 +2701,20 @@ export async function authorClose({
|
|
|
2438
2701
|
finalFrom = label;
|
|
2439
2702
|
measured = renderSeedReadBlock(rows);
|
|
2440
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;
|
|
2441
2706
|
}
|
|
2442
2707
|
|
|
2443
2708
|
if (i === revisionCap) { stop = 'max-revisions'; break; }
|
|
2444
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
|
+
|
|
2445
2718
|
const turn = buildReviseTurn(measured);
|
|
2446
2719
|
// A TRIPWIRE, not behaviour: it can only fire once something else is
|
|
2447
2720
|
// already broken, so it is the one guard in this module the mutation
|
|
@@ -2524,6 +2797,8 @@ export async function authorClose({
|
|
|
2524
2797
|
// WHICH stages broke on the dropped one, so this is never a silent swap.
|
|
2525
2798
|
fellBack,
|
|
2526
2799
|
stop,
|
|
2800
|
+
// P7 item 5: what the person is asked (empty on every other stop)
|
|
2801
|
+
questions: stop === 'questions-open' ? askedQuestions : [],
|
|
2527
2802
|
revisions: Math.max(0, iterations.length - 1),
|
|
2528
2803
|
};
|
|
2529
2804
|
}
|