bareloop 0.10.0 → 0.12.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 +510 -0
- package/README.md +18 -7
- package/bareloop.context.md +456 -19
- package/package.json +3 -3
- package/src/authorflow.js +290 -49
- package/src/authoring.js +258 -46
- package/src/authorjob.js +390 -32
- package/src/authorscout.js +13 -1
- package/src/bridges.js +238 -11
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/clock.js +21 -3
- package/src/declaredclose.js +188 -13
- package/src/index.js +90 -7
- package/src/job.js +28 -9
- package/src/judged.js +765 -0
- package/src/kinds.js +578 -13
- package/src/ledger.js +24 -1
- package/src/planrun.js +517 -14
- package/src/ralph.js +9 -0
- package/src/reuse.js +514 -96
- package/src/reviewdoor.js +246 -0
- package/src/root.js +1 -1
- package/src/run.js +81 -8
- package/src/selection.js +23 -7
- package/types/authorflow.d.ts +233 -21
- package/types/authoring.d.ts +99 -39
- package/types/authorjob.d.ts +44 -6
- package/types/authorscout.d.ts +7 -1
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +93 -6
- package/types/index.d.ts +11 -7
- package/types/job.d.ts +22 -4
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +227 -8
- package/types/planrun.d.ts +73 -3
- package/types/reuse.d.ts +218 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +60 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build:types"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"bare-agent": "^0.
|
|
27
|
-
"bareguard": "^0.
|
|
26
|
+
"bare-agent": "^0.36.0",
|
|
27
|
+
"bareguard": "^0.13.0",
|
|
28
28
|
"litectx": "^0.32.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/src/authorflow.js
CHANGED
|
@@ -75,11 +75,12 @@
|
|
|
75
75
|
import { createRequire } from 'node:module';
|
|
76
76
|
import { realpathSync } from 'node:fs';
|
|
77
77
|
import { join, relative, isAbsolute } from 'node:path';
|
|
78
|
-
import { seedRead as runSeedReadStages, makeSeedTrees, AGGREGATES, SIGNS, MAX_TERMS } from './kinds.js';
|
|
78
|
+
import { seedRead as runSeedReadStages, makeSeedTrees, AGGREGATES, SIGNS, MAX_TERMS, MAX_JUDGED_PATHS } from './kinds.js';
|
|
79
|
+
import { JUDGE_RULE_IDS } from './judged.js';
|
|
79
80
|
import {
|
|
80
81
|
KIND_CATALOGUE, MAX_STAGES, DIRECTIONS, BASELINES,
|
|
81
82
|
GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
82
|
-
validateDeclaration, envCapableKind, VERDICT_CLASSES, LIVE_CLASSES,
|
|
83
|
+
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES,
|
|
83
84
|
} from './authoring.js';
|
|
84
85
|
import { buildSeedListing, cleanEntry, SURVEY_CAUSES } from './authorscout.js';
|
|
85
86
|
import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop } from './text.js';
|
|
@@ -163,29 +164,107 @@ export const STRUCTURE_INSTRUCTION_TEXT = 'Your reply could not be read as one J
|
|
|
163
164
|
* cannot emit anything else (D2/D3), so a genre it has never seen is a
|
|
164
165
|
* composition problem, not a catalogue problem.
|
|
165
166
|
*
|
|
166
|
-
* The GREEN set
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
167
|
+
* The GREEN set descends from questions 1–6 of the frozen TYPES set (prereg §5).
|
|
168
|
+
* TWO slots have been deleted since, both because the interview was ASKING FOR
|
|
169
|
+
* SOMETHING IT ALREADY HAD or already knew:
|
|
170
|
+
*
|
|
171
|
+
* - D13's genre CONFIRM ("this looks like a type-fixing job, correct?") —
|
|
172
|
+
* superseded in full: the interview never asks about a genre again, and the
|
|
173
|
+
* refusal it carried MOVED to the composer, on the same counted
|
|
174
|
+
* `request-red` path (see `authorCloseForJob`);
|
|
175
|
+
* - "Is there a code repo I can look at? Where?" — DROPPED on hamr's ruling
|
|
176
|
+
* after he drove the terminal interview himself (2026-08-15, verbatim: *"drop
|
|
177
|
+
* Q6"*). The repository is a MANDATORY, STRUCTURED input already —
|
|
178
|
+
* `runInterview`'s own `repoPath`, which the runner takes from `--patient` and
|
|
179
|
+
* refuses to start without. Asking a person to re-type in prose a path the
|
|
180
|
+
* machine is holding in a variable is the SWE tax this product refuses, and
|
|
181
|
+
* it invites a second, drifting answer for the same fact (hamr's own answer
|
|
182
|
+
* on the live run was *"Yes — the patient itself."*). Nothing downstream lost
|
|
183
|
+
* anything: the scout reads `repoPath`, never the answer.
|
|
184
|
+
*
|
|
185
|
+
* Question 2 was WIDENED in the same ruling: it now asks for the read-or-draw-from
|
|
186
|
+
* files as well as the ones that change. The two halves belong in one answer
|
|
187
|
+
* because they are one decision for the person answering — what the work touches,
|
|
188
|
+
* and what it touches only to look at.
|
|
189
|
+
*
|
|
190
|
+
* The five that remain are genre-neutral, which is why every generalisation so far
|
|
191
|
+
* has deleted a slot rather than reworded one. They are NUMBERED CONTIGUOUSLY from
|
|
192
|
+
* 1: the number a person sees is the key their answer is filed under, and a gap
|
|
193
|
+
* would be a hole in a form nobody can explain.
|
|
173
194
|
*/
|
|
174
195
|
export const GREEN_QUESTIONS = Object.freeze({
|
|
175
196
|
1: 'What do you want done?',
|
|
176
|
-
2: 'Which files or folders should change?',
|
|
197
|
+
2: 'Which files or folders should change? And which files should the work read or draw from (they stay untouched)?',
|
|
177
198
|
3: 'Is there anything that must not change?',
|
|
178
199
|
4: 'How do you check today whether it\'s working?',
|
|
179
200
|
5: 'What would make you say this came back worse than before?',
|
|
180
|
-
6: 'Is there a code repo I can look at? Where?',
|
|
181
201
|
});
|
|
182
202
|
|
|
183
203
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
204
|
+
* THE HITL SET (N4 slice 1) — the green questions BYTE FOR BYTE, plus one.
|
|
205
|
+
*
|
|
206
|
+
* The five are unchanged deliberately rather than economically: the
|
|
207
|
+
* mechanical-first composition law (2026-08-07) says every hitl close is
|
|
208
|
+
* *deterministic stages first, judge minimal, human last*, so a hitl job needs
|
|
209
|
+
* exactly the same mechanical facts a green one does — what changes, what must
|
|
210
|
+
* not, how it is checked today, what "worse" looks like. Rewording them would
|
|
211
|
+
* fork one interview into two that drift.
|
|
212
|
+
*
|
|
213
|
+
* The last one is the only thing the green set cannot supply, and it is not a
|
|
214
|
+
* flourish: `human-confirms` requires an `ask` — the question the signer answers
|
|
215
|
+
* at the end of the run — and nothing else in the interview names it. Without it
|
|
216
|
+
* the composer would have to invent what a person is deciding, which is the one
|
|
217
|
+
* thing a human stage must never have done for it. It is the SPREAD's next number
|
|
218
|
+
* rather than a fixed one, so a green-side deletion renumbers it instead of
|
|
219
|
+
* leaving a gap (it was 7 while the green set held six, and is 6 now it holds five).
|
|
220
|
+
*/
|
|
221
|
+
export const HITL_QUESTIONS = Object.freeze({
|
|
222
|
+
...GREEN_QUESTIONS,
|
|
223
|
+
6: 'When you look at the finished result yourself, what are you deciding?',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* THE SOFTGREEN SET (module 3, design record §4.6) — the green questions BYTE
|
|
228
|
+
* FOR BYTE, plus TWO.
|
|
229
|
+
*
|
|
230
|
+
* Same discipline as the hitl set, and for the same reason: a softgreen close is
|
|
231
|
+
* *mechanical stages first, the judged stage after* (§4.5), so a softgreen job
|
|
232
|
+
* needs every mechanical fact a green one does. No renumbering of the green set,
|
|
233
|
+
* no genre question, nothing reworded.
|
|
234
|
+
*
|
|
235
|
+
* The two additions are the only things the green set cannot supply, and neither
|
|
236
|
+
* is a flourish — they are the judged floor's two REQUIRED inputs:
|
|
237
|
+
* - Q6 becomes the RUBRIC CARD (§4.3): the person's own pass/fail lines,
|
|
238
|
+
* compiled into enumerated items, signed, and enumerated in the spec hash.
|
|
239
|
+
* The documented ceiling rides with it — the judge catches violations of
|
|
240
|
+
* STATED card items only, and the fix for a miss is a new card line and a
|
|
241
|
+
* re-sign, never a smarter judge.
|
|
242
|
+
* - Q7 becomes the frozen CALIBRATION SET (§4.4): the whole pipe must grade it
|
|
243
|
+
* correctly before the close is signable.
|
|
244
|
+
* Both are asked in the person's own terms — an example they would pass and one
|
|
245
|
+
* they would fail, not a rubric they are asked to write — because compiling a lay
|
|
246
|
+
* answer into extractable items is the LLM's job (D5, proposed-and-signed) and
|
|
247
|
+
* asking the person to do it is the SWE tax this product refuses.
|
|
248
|
+
*
|
|
249
|
+
* What the two answers COMPILE INTO is module 4. This module carries them
|
|
250
|
+
* exactly as the hitl set carries its own last answer: they ride in `answers`,
|
|
251
|
+
* they reach the composer verbatim through the prompt's interview block, and
|
|
252
|
+
* nothing here derives anything from them.
|
|
253
|
+
*/
|
|
254
|
+
export const SOFTGREEN_QUESTIONS = Object.freeze({
|
|
255
|
+
...GREEN_QUESTIONS,
|
|
256
|
+
6: 'When you judge the result yourself, what separates a pass from a fail? Name the few things you actually look for.',
|
|
257
|
+
7: 'Give one example you\'d pass and one you\'d fail, and say why.',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The three sets, all LIVE as of softgreen module 3. The locked shape stays
|
|
262
|
+
* spelled out in the readers below rather than deleted with the last locked
|
|
263
|
+
* class: `questions: null` is ABSENT, never `{}` — an empty set would read as
|
|
264
|
+
* "this class needs no questions", a different and false statement (F59's
|
|
265
|
+
* distinction, at the interview layer) — and a locked class refuses at ADMISSION,
|
|
266
|
+
* before its questions run, so the two readers throw rather than hand back
|
|
267
|
+
* nothing. A fourth class arrives locked, and the machinery is here for it.
|
|
189
268
|
* @type {Record<string, {locked: boolean, questions: Record<string|number, string>|null, required: readonly number[]|null}>}
|
|
190
269
|
*/
|
|
191
270
|
export const QUESTION_SETS = Object.freeze({
|
|
@@ -194,8 +273,16 @@ export const QUESTION_SETS = Object.freeze({
|
|
|
194
273
|
questions: GREEN_QUESTIONS,
|
|
195
274
|
required: Object.freeze(Object.keys(GREEN_QUESTIONS).map(Number).sort((a, b) => a - b)),
|
|
196
275
|
}),
|
|
197
|
-
'soft-green': Object.freeze({
|
|
198
|
-
|
|
276
|
+
'soft-green': Object.freeze({
|
|
277
|
+
locked: false,
|
|
278
|
+
questions: SOFTGREEN_QUESTIONS,
|
|
279
|
+
required: Object.freeze(Object.keys(SOFTGREEN_QUESTIONS).map(Number).sort((a, b) => a - b)),
|
|
280
|
+
}),
|
|
281
|
+
hitl: Object.freeze({
|
|
282
|
+
locked: false,
|
|
283
|
+
questions: HITL_QUESTIONS,
|
|
284
|
+
required: Object.freeze(Object.keys(HITL_QUESTIONS).map(Number).sort((a, b) => a - b)),
|
|
285
|
+
}),
|
|
199
286
|
});
|
|
200
287
|
|
|
201
288
|
/**
|
|
@@ -213,8 +300,27 @@ export const CLASS_STATEMENTS = Object.freeze({
|
|
|
213
300
|
+ 'to decide it — no judgement, no score, and no person in the loop. Every stage you compose is a mechanical '
|
|
214
301
|
+ 'measurement of this repository. If part of what they asked for cannot be measured that way, leave it out and '
|
|
215
302
|
+ 'say so in your notes rather than approximating it with a stage that means something else.',
|
|
216
|
-
'
|
|
217
|
-
|
|
303
|
+
// softgreen module 3. Stated as an ORDER for the same reason hitl's is: the
|
|
304
|
+
// close is first-red-wins, so the cheap mechanical stages shield the one stage
|
|
305
|
+
// that spends money, and the judge is never the first filter. The last sentence
|
|
306
|
+
// is the documented ceiling (§4.3) said to the composer in its own terms — the
|
|
307
|
+
// judge only ever checks lines the card names, so an unstated expectation is
|
|
308
|
+
// not a stricter judge, it is a line nobody wrote.
|
|
309
|
+
'soft-green': 'The person declared this job SOFT-GREEN: part of what "done" means here needs JUDGEMENT — reading '
|
|
310
|
+
+ 'the result and deciding whether it is any good — and no command can render it. Compose the mechanical part '
|
|
311
|
+
+ 'first: everything about this repository a command can decide is still a mechanical stage, exactly as it would '
|
|
312
|
+
+ 'be for a green job. Then put ONE judged-floor stage AFTER them, carrying the few things the person said they '
|
|
313
|
+
+ 'actually look for and the real files the work lands in. A judged stage decides only the lines its card names, '
|
|
314
|
+
+ 'so name each thing they look for as its own line, and leave nothing to be inferred.',
|
|
315
|
+
// N4 slice 1. The mechanical-first composition law is stated as an ORDER
|
|
316
|
+
// because first-red-wins makes the order the mechanism: the cheap stages shield
|
|
317
|
+
// the expensive one, and the person is never the first filter.
|
|
318
|
+
hitl: 'The person declared this job HITL: a PERSON renders the final verdict, because their judgement is what '
|
|
319
|
+
+ '"done" means here. Compose the mechanical part first — everything about this repository a command can decide '
|
|
320
|
+
+ 'is still a mechanical stage, exactly as it would be for a green job — and put ONE human-confirms stage LAST, '
|
|
321
|
+
+ 'carrying the question they answer when they look at the result. Never make the person the first filter: a '
|
|
322
|
+
+ 'stage a command can render is never handed to them, and they are asked only once, at the end, about the part '
|
|
323
|
+
+ 'no command can hold.',
|
|
218
324
|
});
|
|
219
325
|
|
|
220
326
|
/** @param {string} verdictType @returns {{locked: boolean, questions: any, required: any}} */
|
|
@@ -354,6 +460,45 @@ export const PARAM_SCHEMAS = Object.freeze({
|
|
|
354
460
|
extensions: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
355
461
|
allowPrefixes: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
356
462
|
requireNonEmpty: { const: true },
|
|
463
|
+
// SOFTGREEN's two. `rule` is an ENUM off the judge's own rulebook — where only a
|
|
464
|
+
// closed set can satisfy a field, the illegal value is made inexpressible rather
|
|
465
|
+
// than rejected after the fact (`check-passes(name)`, again) — and `text` is the
|
|
466
|
+
// only free string, carrying the signer's words so a red can be explained in them.
|
|
467
|
+
card: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
description: 'the rubric, as enumerated items: one line per thing the person actually looks for, in the order '
|
|
470
|
+
+ 'they look for them. The rule is SELECTED from the arbiter\'s own rulebook; the text is theirs.',
|
|
471
|
+
properties: {
|
|
472
|
+
items: {
|
|
473
|
+
type: 'array',
|
|
474
|
+
minItems: 1,
|
|
475
|
+
items: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
rule: { enum: [...JUDGE_RULE_IDS], description: 'which owned rule this line asserts' },
|
|
479
|
+
text: { type: 'string', minLength: 1, description: 'what this line means, in the person\'s own words' },
|
|
480
|
+
},
|
|
481
|
+
required: ['rule', 'text'],
|
|
482
|
+
additionalProperties: false,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
required: ['items'],
|
|
487
|
+
additionalProperties: false,
|
|
488
|
+
},
|
|
489
|
+
paths: {
|
|
490
|
+
type: 'array',
|
|
491
|
+
minItems: 1,
|
|
492
|
+
maxItems: MAX_JUDGED_PATHS,
|
|
493
|
+
items: { type: 'string' },
|
|
494
|
+
description: 'the AUTHORITATIVE artifacts to judge, repository-relative, read off the listing. One paid call each.',
|
|
495
|
+
},
|
|
496
|
+
ask: {
|
|
497
|
+
type: 'string',
|
|
498
|
+
minLength: 1,
|
|
499
|
+
description: 'the plain question the person answers while looking at the finished result. They see it verbatim, '
|
|
500
|
+
+ 'so write what they are DECIDING — never a bare "approve?".',
|
|
501
|
+
},
|
|
357
502
|
});
|
|
358
503
|
|
|
359
504
|
/**
|
|
@@ -391,19 +536,27 @@ export function schemaCoverage(catalogue = KIND_CATALOGUE) {
|
|
|
391
536
|
* and they arrive as validation reds through the revise loop — a schema that
|
|
392
537
|
* tried to restate them would be a second validator.
|
|
393
538
|
*
|
|
539
|
+
* `verdictType` narrows the branches to the kinds the picked class may express
|
|
540
|
+
* (softgreen module 3, `classMenu`): a green job cannot SPELL `judged-floor`,
|
|
541
|
+
* rather than spelling it and being told afterwards. COVERAGE is still computed
|
|
542
|
+
* over the WHOLE catalogue — the class filter must never make a parameter look
|
|
543
|
+
* orphaned and turn a menu narrowing into a catalogue-drift alarm.
|
|
544
|
+
*
|
|
394
545
|
* THROWS on a catalogue it cannot cover: a malformed catalogue is a bareloop bug,
|
|
395
546
|
* and failing loudly beats emitting a schema that quietly cannot say something
|
|
396
547
|
* the validator will then demand.
|
|
397
548
|
* @param {Record<string, any>} [catalogue]
|
|
549
|
+
* @param {{verdictType?: string|null}} [o]
|
|
398
550
|
*/
|
|
399
|
-
export function declarationSchema(catalogue = KIND_CATALOGUE) {
|
|
551
|
+
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null } = {}) {
|
|
400
552
|
const cov = schemaCoverage(catalogue);
|
|
401
553
|
if (!cov.ok) {
|
|
402
554
|
throw new Error('[authorflow] the declaration schema does not cover the catalogue — '
|
|
403
555
|
+ `missing: ${cov.missing.join(', ') || 'none'}; orphaned: ${cov.orphan.join(', ') || 'none'}`);
|
|
404
556
|
}
|
|
557
|
+
const menu = classMenu(verdictType, catalogue);
|
|
405
558
|
const branches = Object.entries(catalogue)
|
|
406
|
-
.filter(([, spec]) => !spec.locked)
|
|
559
|
+
.filter(([kind, spec]) => !spec.locked && menu.includes(kind))
|
|
407
560
|
.map(([kind, spec]) => {
|
|
408
561
|
/** @type {Record<string, any>} */
|
|
409
562
|
const properties = {};
|
|
@@ -451,13 +604,13 @@ export function declarationSchema(catalogue = KIND_CATALOGUE) {
|
|
|
451
604
|
* The output channel. It records and acknowledges; it takes no action, which is
|
|
452
605
|
* why handing it to a toolless authoring call does not reopen D3.
|
|
453
606
|
* @param {{calls: any[]}} box
|
|
454
|
-
* @param {{catalogue?: Record<string, any
|
|
607
|
+
* @param {{catalogue?: Record<string, any>, verdictType?: string|null}} [o]
|
|
455
608
|
*/
|
|
456
|
-
export function declarationTool(box, { catalogue = KIND_CATALOGUE } = {}) {
|
|
609
|
+
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null } = {}) {
|
|
457
610
|
return {
|
|
458
611
|
name: DECLARATION_TOOL_NAME,
|
|
459
612
|
description: 'Deliver the finished close. Call this exactly once, with the whole declaration as its arguments.',
|
|
460
|
-
parameters: declarationSchema(catalogue),
|
|
613
|
+
parameters: declarationSchema(catalogue, { verdictType }),
|
|
461
614
|
execute: async (/** @type {any} */ args) => { box.calls.push(args); return DECLARATION_ACK; },
|
|
462
615
|
};
|
|
463
616
|
}
|
|
@@ -468,10 +621,19 @@ export function declarationTool(box, { catalogue = KIND_CATALOGUE } = {}) {
|
|
|
468
621
|
* The catalogue, as the authoring call sees it — rendered FROM the catalogue, so
|
|
469
622
|
* the prompt and the validator judge one set of facts. `shape` and `asserts` are
|
|
470
623
|
* M2's own strings.
|
|
624
|
+
*
|
|
625
|
+
* `verdictType` narrows it to the picked class's menu, exactly as it narrows the
|
|
626
|
+
* schema — the prompt and the tool must offer ONE set of kinds, or the composer
|
|
627
|
+
* reads about a kind it cannot call. A kind above the ceiling is omitted rather
|
|
628
|
+
* than rendered as unavailable: a LOCKED row says "this does not run in v1",
|
|
629
|
+
* which is a fact about the build; "not for your class" is a fact about a choice
|
|
630
|
+
* the person already made, and printing it invites the composer to argue with it.
|
|
471
631
|
* @param {Record<string, any>} [catalogue]
|
|
632
|
+
* @param {{verdictType?: string|null}} [o]
|
|
472
633
|
*/
|
|
473
|
-
export function catalogueBlock(catalogue = KIND_CATALOGUE) {
|
|
474
|
-
const
|
|
634
|
+
export function catalogueBlock(catalogue = KIND_CATALOGUE, { verdictType = null } = {}) {
|
|
635
|
+
const menu = classMenu(verdictType, catalogue);
|
|
636
|
+
const rows = Object.entries(catalogue).filter(([name]) => menu.includes(name)).map(([name, spec]) => (spec.locked
|
|
475
637
|
? `- ${name} — LOCKED. Not available in v1; it will not run.`
|
|
476
638
|
: `- ${name}\n ${spec.shape}\n required: ${spec.required.join(', ')}`
|
|
477
639
|
+ `${spec.optional.length ? ` optional: ${spec.optional.join(', ')}` : ''}\n asserts: ${spec.asserts}`));
|
|
@@ -659,7 +821,9 @@ measure anything.`;
|
|
|
659
821
|
`THE INTERVIEW — the person's own words\n\n${interview}`,
|
|
660
822
|
`THE FACTS OBJECT — from a read-only survey of the repository\n\n${JSON.stringify(facts, null, 2)}`
|
|
661
823
|
+ (listingBlock ? `\n\n${listingBlock}` : ''),
|
|
662
|
-
|
|
824
|
+
// CLASS-SCOPED (softgreen module 3): the composer for a green job is never
|
|
825
|
+
// shown a kind whose pick would red as `class-ceiling` after the call.
|
|
826
|
+
catalogueBlock(catalogue, { verdictType }),
|
|
663
827
|
lawsBlock(),
|
|
664
828
|
`THE GENRE TEMPLATE — fixed by the job's genre (${lang}), not yours to decide\n\n`
|
|
665
829
|
+ 'This job\'s genre carries a fixed close SHAPE. The following policy is law for your declaration. It names\n'
|
|
@@ -912,8 +1076,19 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }) {
|
|
|
912
1076
|
* run could not be read afterwards. A cost entry with no raw beside it is that
|
|
913
1077
|
* run again.
|
|
914
1078
|
*/
|
|
915
|
-
/**
|
|
916
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* @param {{ceilingUsd?: number|null,
|
|
1081
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void}} [o]
|
|
1082
|
+
* `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
|
|
1083
|
+
* `onCall` is a REPORTING seam fired as each metered call lands — the shell's
|
|
1084
|
+
* only chance to know what has been spent while the run is still alive. It is
|
|
1085
|
+
* never consulted and never governs: `capStop` below is still the one
|
|
1086
|
+
* predicate, still asked between calls, and still reads the same `entries`.
|
|
1087
|
+
* `absorb` deliberately does NOT fire it: those calls were metered (and
|
|
1088
|
+
* reported) by whoever made them, and reporting them twice would double the
|
|
1089
|
+
* scout's spend in every reader downstream.
|
|
1090
|
+
*/
|
|
1091
|
+
export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
|
|
917
1092
|
/** @type {{label: string, costUsd: number|null, unpricedRounds: number}[]} */
|
|
918
1093
|
const entries = [];
|
|
919
1094
|
/** @type {ReturnType<typeof scrubRaw>[]} */
|
|
@@ -921,8 +1096,10 @@ export function makeCostBook({ ceilingUsd = null } = {}) {
|
|
|
921
1096
|
return {
|
|
922
1097
|
/** @param {string} label @param {any} r a bare-agent run result @param {number} [attempt] */
|
|
923
1098
|
add(label, r, attempt = 1) {
|
|
924
|
-
|
|
1099
|
+
const call = { label, ...priceOf(r) };
|
|
1100
|
+
entries.push(call);
|
|
925
1101
|
raws.push(scrubRaw({ label, attempt, text: r?.text, reason: r?.error ? String(r.error) : null }));
|
|
1102
|
+
onCall({ ...call });
|
|
926
1103
|
return entries.at(-1);
|
|
927
1104
|
},
|
|
928
1105
|
/** @param {{label: string, costUsd: number|null, unpricedRounds: number}[]} calls
|
|
@@ -985,13 +1162,23 @@ function canonical(x) {
|
|
|
985
1162
|
|
|
986
1163
|
/**
|
|
987
1164
|
* ONE structured ask, with the malformed-emission retry bounded on its OWN axis.
|
|
988
|
-
* A reply that carries no
|
|
1165
|
+
* A reply that carries no artifact is an ARTIFACT-RED — name the axis, retry
|
|
989
1166
|
* under a cap, write nothing — and it never consumes a revision, because a
|
|
990
1167
|
* transport fault is not a defect in a close.
|
|
1168
|
+
*
|
|
1169
|
+
* THE CHANNEL IS A PARAMETER, and that is softgreen module 4's only change here:
|
|
1170
|
+
* the rubric card and the calibration set are a SECOND schema-forced emission
|
|
1171
|
+
* with the same failure modes, and a second ladder is a second place for the cap,
|
|
1172
|
+
* the reject block and the re-ask to drift. The declaration channel below is the
|
|
1173
|
+
* one this loop has always used, spelled out rather than inlined — nothing about
|
|
1174
|
+
* the authoring path's behaviour moves.
|
|
991
1175
|
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
992
|
-
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
1176
|
+
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
1177
|
+
* channel: {name: string, instruction: string, textPath?: string, tool: (box: {calls: any[]}) => any}}} o
|
|
1178
|
+
* @returns {Promise<{artifact: any, attempts: number, convo: any[], raw: string,
|
|
1179
|
+
* providerError: string|null, red: Red|null, budget: 'cap-halt'|'pricing-red'|null}>}
|
|
993
1180
|
*/
|
|
994
|
-
async function
|
|
1181
|
+
export async function askStructured({ messages, generate, mode, retries, label, book, channel }) {
|
|
995
1182
|
/** @type {any[]} */
|
|
996
1183
|
const convo = [...messages];
|
|
997
1184
|
let attempts = 0;
|
|
@@ -1007,49 +1194,78 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
|
|
|
1007
1194
|
// unbounded. `attempts` is returned as-is so the caller can tell a cap that
|
|
1008
1195
|
// tripped before this ask began (0) from one that cut a retry ladder short.
|
|
1009
1196
|
const halt = book.capStop();
|
|
1010
|
-
if (halt) return {
|
|
1197
|
+
if (halt) return { artifact: null, attempts, convo, raw: '', providerError: null, red: null, budget: halt };
|
|
1011
1198
|
|
|
1012
1199
|
/** @type {{calls: any[]}} */
|
|
1013
1200
|
const box = { calls: [] };
|
|
1014
|
-
|
|
1201
|
+
// the tool's schema is CLASS-SCOPED, like the prompt's catalogue block: the
|
|
1202
|
+
// two must offer one set of kinds, or the composer reads about a kind it
|
|
1203
|
+
// cannot call — or worse, calls one the ceiling will refuse after we paid.
|
|
1204
|
+
const tools = mode === 'tool' ? [channel.tool(box)] : [];
|
|
1015
1205
|
const r = await generate(convo, tools, {});
|
|
1016
1206
|
attempts += 1;
|
|
1017
1207
|
book.add(attempt === 0 ? label : `${label}#${attempt + 1}`, r, attempts);
|
|
1018
1208
|
const raw = redactSecrets(String(r?.text ?? ''));
|
|
1019
1209
|
|
|
1020
1210
|
if (r?.error) {
|
|
1021
|
-
return {
|
|
1211
|
+
return { artifact: null, attempts, convo, raw, providerError: String(r.error), red: null, budget: null };
|
|
1022
1212
|
}
|
|
1023
1213
|
|
|
1024
1214
|
if (mode === 'tool') {
|
|
1025
|
-
if (box.calls.length === 1) return {
|
|
1215
|
+
if (box.calls.length === 1) return { artifact: box.calls[0], attempts, convo, raw, providerError: null, red: null, budget: null };
|
|
1026
1216
|
red = {
|
|
1027
1217
|
code: 'artifact-red',
|
|
1028
|
-
path:
|
|
1218
|
+
path: channel.name,
|
|
1029
1219
|
detail: box.calls.length === 0
|
|
1030
|
-
? `the reply delivered no ${
|
|
1031
|
-
: `the reply delivered ${box.calls.length} ${
|
|
1220
|
+
? `the reply delivered no ${channel.name} call — structure is enforced, prose is never parsed`
|
|
1221
|
+
: `the reply delivered ${box.calls.length} ${channel.name} calls; exactly one is expected`,
|
|
1032
1222
|
axis: box.calls.length === 0 ? 'no-declaration-tool-call' : 'multiple-declaration-tool-calls',
|
|
1033
1223
|
};
|
|
1034
1224
|
} else {
|
|
1035
1225
|
// THE FALLBACK PATH — providers without tool mode only. The ONE shipped
|
|
1036
1226
|
// parser, never a second one.
|
|
1227
|
+
const at = channel.textPath ?? channel.name;
|
|
1037
1228
|
const { code, red: extractRed } = extractArtifact(raw);
|
|
1038
1229
|
if (code) {
|
|
1039
|
-
try { return {
|
|
1040
|
-
catch (e) { red = { code: 'artifact-red', path:
|
|
1230
|
+
try { return { artifact: JSON.parse(code), attempts, convo, raw, providerError: null, red: null, budget: null }; }
|
|
1231
|
+
catch (e) { red = { code: 'artifact-red', path: at, detail: `the ${at} did not parse as JSON: ${String(/** @type {any} */ (e)?.message ?? e)}`, axis: 'unparseable-json' }; }
|
|
1041
1232
|
} else {
|
|
1042
|
-
red = { code: 'artifact-red', path:
|
|
1233
|
+
red = { code: 'artifact-red', path: at, detail: `no artifact in the reply: ${extractRed}`, axis: 'no-artifact' };
|
|
1043
1234
|
}
|
|
1044
1235
|
}
|
|
1045
1236
|
|
|
1046
1237
|
if (attempt < retries) {
|
|
1047
1238
|
const block = renderRejectBlock({ kind: 'artifact', reds: [/** @type {Red} */ (red).detail] });
|
|
1048
1239
|
convo.push({ role: 'assistant', content: raw.trim() || '(the reply carried no text)' });
|
|
1049
|
-
convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ?
|
|
1240
|
+
convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ? channel.instruction : STRUCTURE_INSTRUCTION_TEXT}` });
|
|
1050
1241
|
}
|
|
1051
1242
|
}
|
|
1052
|
-
return {
|
|
1243
|
+
return { artifact: null, attempts, convo, raw: '', providerError: null, red, budget: null };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** THE DECLARATION CHANNEL — the one this loop has always used. Its schema is
|
|
1247
|
+
* CLASS-SCOPED, like the prompt's catalogue block: the two must offer one set of
|
|
1248
|
+
* kinds, or the composer reads about a kind it cannot call.
|
|
1249
|
+
* @param {{catalogue: Record<string, any>, verdictType: string}} o */
|
|
1250
|
+
const declarationChannel = ({ catalogue, verdictType }) => ({
|
|
1251
|
+
name: DECLARATION_TOOL_NAME,
|
|
1252
|
+
instruction: STRUCTURE_INSTRUCTION_TOOL,
|
|
1253
|
+
textPath: 'declaration',
|
|
1254
|
+
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType }),
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
/** The declaration ask, over the shared ladder. `declaration` is `artifact`
|
|
1258
|
+
* under this channel's own name — the flow below reads a declaration, not an
|
|
1259
|
+
* anonymous artifact, and one word for two things is how a reader loses which.
|
|
1260
|
+
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
1261
|
+
* label: string, book: ReturnType<typeof makeCostBook>, catalogue: Record<string, any>,
|
|
1262
|
+
* verdictType: string}} o
|
|
1263
|
+
*/
|
|
1264
|
+
async function askDeclaration({ messages, generate, mode, retries, label, book, catalogue, verdictType }) {
|
|
1265
|
+
const r = await askStructured({
|
|
1266
|
+
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType }),
|
|
1267
|
+
});
|
|
1268
|
+
return { ...r, declaration: r.artifact };
|
|
1053
1269
|
}
|
|
1054
1270
|
|
|
1055
1271
|
/**
|
|
@@ -1085,6 +1301,8 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
|
|
|
1085
1301
|
* listing?: {stop: string|null, files: string[]|null, block: string|null, meta: any}|null,
|
|
1086
1302
|
* generate: Function, seedReadFn?: Function, closeCtx?: any,
|
|
1087
1303
|
* ceilingUsd?: number|null,
|
|
1304
|
+
* onPhase?: (phase: string, data?: any) => void,
|
|
1305
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
1088
1306
|
* maxRevisions?: number, structureRetries?: number,
|
|
1089
1307
|
* structuredMode?: 'tool'|'text', catalogue?: Record<string, any>}} o
|
|
1090
1308
|
*/
|
|
@@ -1094,6 +1312,11 @@ export async function authorClose({
|
|
|
1094
1312
|
scout, listing = null,
|
|
1095
1313
|
generate, seedReadFn = runSeedReadStages, closeCtx = {},
|
|
1096
1314
|
ceilingUsd = null,
|
|
1315
|
+
// The two REPORTING seams, defaulted to nothing so every existing caller is
|
|
1316
|
+
// byte-identical. Neither is consulted: the revise ladder, the ceiling and the
|
|
1317
|
+
// seed read all run exactly as they did — the shell is merely told, while the
|
|
1318
|
+
// run is still alive, which of the up-to-15 silent minutes it is inside.
|
|
1319
|
+
onPhase = () => {}, onCall = () => {},
|
|
1097
1320
|
maxRevisions = MAX_REVISIONS,
|
|
1098
1321
|
structureRetries = MAX_STRUCTURE_RETRIES,
|
|
1099
1322
|
structuredMode = 'tool',
|
|
@@ -1118,7 +1341,7 @@ export async function authorClose({
|
|
|
1118
1341
|
// no default for exactly this reason, and `shellCapUsd` left off judged against
|
|
1119
1342
|
// a number nobody chose). An unbounded authoring run is legal; it is just never
|
|
1120
1343
|
// an accident, which is why the driver PRINTS it.
|
|
1121
|
-
const book = makeCostBook({ ceilingUsd });
|
|
1344
|
+
const book = makeCostBook({ ceilingUsd, onCall });
|
|
1122
1345
|
// The scout's calls AND its raws, together. The raws matter most on the path
|
|
1123
1346
|
// that spends nothing more: run mslhn707 refused at the $0 preflight below,
|
|
1124
1347
|
// and the survey text that would have said WHY died with the process.
|
|
@@ -1306,7 +1529,12 @@ export async function authorClose({
|
|
|
1306
1529
|
try {
|
|
1307
1530
|
for (let i = 0; i <= revisionCap; i += 1) {
|
|
1308
1531
|
const label = i === 0 ? 'author' : `revise-${i}`;
|
|
1309
|
-
|
|
1532
|
+
// WHICH call, and how many the ladder may make. `of` is the ENFORCED cap
|
|
1533
|
+
// (`revisionCap`, already clamped tighten-only) rather than the constant —
|
|
1534
|
+
// a readout quoting a number the loop does not obey is a second
|
|
1535
|
+
// instrument, and this file's own history is what that costs.
|
|
1536
|
+
onPhase('author-call', { call: label, i, of: revisionCap });
|
|
1537
|
+
const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue, verdictType });
|
|
1310
1538
|
messages = ask.convo;
|
|
1311
1539
|
|
|
1312
1540
|
// The cap tripped BEFORE this ask made any call at all, so there is no
|
|
@@ -1392,7 +1620,20 @@ export async function authorClose({
|
|
|
1392
1620
|
// `gapKeep` defaults to EMPTY rather than being left undefined: M1
|
|
1393
1621
|
// prefixes every gap line with it verbatim, so an unset one prints the
|
|
1394
1622
|
// word "undefined" on every failure line the model is asked to read
|
|
1395
|
-
|
|
1623
|
+
// THE LONG ONE. Every stage of the declaration spawns a real toolchain
|
|
1624
|
+
// against the patient, so this single await is where a run spends
|
|
1625
|
+
// minutes saying nothing — the per-stage line below is the whole point,
|
|
1626
|
+
// and the block boundary is what makes it readable.
|
|
1627
|
+
const stageCount = injected.declaration?.stages?.length ?? null;
|
|
1628
|
+
onPhase('seed-read', { call: label, stages: stageCount });
|
|
1629
|
+
// `onStage` sits AFTER the `closeCtx` spread beside `workdir`/`seedRef`:
|
|
1630
|
+
// it is the runner's own reporting channel, not a knob a caller's close
|
|
1631
|
+
// context may quietly redirect.
|
|
1632
|
+
const rows = await seedReadFn(injected.declaration, {
|
|
1633
|
+
gapKeep: '', ...closeCtx, workdir, seedRef, seedTrees,
|
|
1634
|
+
onStage: (/** @type {any} */ row) => onPhase('stage', { call: label, ...row }),
|
|
1635
|
+
});
|
|
1636
|
+
onPhase('seed-read-done', { call: label, stages: stageCount });
|
|
1396
1637
|
iter.seedRead = rows;
|
|
1397
1638
|
accepted = injected.declaration;
|
|
1398
1639
|
acceptedSeedRead = rows;
|