bareloop 0.22.0 → 0.24.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 +247 -0
- package/README.md +1 -1
- package/bareloop.context.md +113 -29
- package/package.json +2 -2
- package/src/authorflow.js +24 -9
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/authorscout.js +83 -9
- package/src/calibrate.js +43 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +73 -21
- package/src/declaredclose.js +25 -11
- package/src/index.js +8 -5
- package/src/job.js +92 -2
- package/src/judged.js +240 -19
- package/src/kinds.js +89 -12
- package/src/planrun.js +194 -41
- package/src/providers.js +301 -0
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +9 -1
- package/src/run.js +68 -9
- package/src/transport.js +29 -12
- package/src/validate.js +4 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/authorscout.d.ts +27 -2
- package/types/calibrate.d.ts +9 -2
- package/types/cardauthor.d.ts +13 -11
- package/types/cli.d.ts +2 -1
- package/types/declaredclose.d.ts +8 -1
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +130 -16
- package/types/kinds.d.ts +51 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +166 -0
- package/types/ratelimit.d.ts +59 -0
- package/types/run.d.ts +17 -3
- package/types/transport.d.ts +10 -4
package/src/authorflow.js
CHANGED
|
@@ -80,9 +80,9 @@ import { JUDGE_RULE_IDS } from './judged.js';
|
|
|
80
80
|
import {
|
|
81
81
|
KIND_CATALOGUE, MAX_STAGES, DIRECTIONS, BASELINES,
|
|
82
82
|
GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
83
|
-
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES,
|
|
83
|
+
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES, MENU_CLASSES,
|
|
84
84
|
} from './authoring.js';
|
|
85
|
-
import { buildSeedListing, cleanEntry, SURVEY_CAUSES } from './authorscout.js';
|
|
85
|
+
import { buildSeedListing, cleanEntry, SURVEY_CAUSES, AUTHOR_CALL_TIMEOUT_MS } from './authorscout.js';
|
|
86
86
|
import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop } from './text.js';
|
|
87
87
|
import { redactSecrets } from './validate.js';
|
|
88
88
|
|
|
@@ -1147,7 +1147,12 @@ export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens =
|
|
|
1147
1147
|
...t,
|
|
1148
1148
|
execute: async (/** @type {any} */ a) => { const r = await t.execute(a); loop.stop(); return r; },
|
|
1149
1149
|
}));
|
|
1150
|
-
|
|
1150
|
+
// F152/PRD 30.4 — the SAME idle bound `authorscout.js` reuses (this pipeline
|
|
1151
|
+
// has no wall clock of its own): a live endpoint that accepts and never
|
|
1152
|
+
// answers hangs the process forever otherwise. `...opts` still spreads AFTER
|
|
1153
|
+
// it, so a caller that ever needs a tighter number for one call may pass it —
|
|
1154
|
+
// tighten-only, and nothing does today.
|
|
1155
|
+
return loop.run(messages, wired, { cacheMessages: true, maxTokens, timeoutMs: AUTHOR_CALL_TIMEOUT_MS, ...opts });
|
|
1151
1156
|
};
|
|
1152
1157
|
}
|
|
1153
1158
|
|
|
@@ -1413,18 +1418,28 @@ export async function authorClose({
|
|
|
1413
1418
|
// what D5 exists to make impossible. `verb` rides as a structured field so the
|
|
1414
1419
|
// caller can carry this to the counted admission path rather than re-deriving
|
|
1415
1420
|
// the demand from prose.
|
|
1416
|
-
|
|
1421
|
+
// MENU_CLASSES, not LIVE_CLASSES (PRD item 31.1): this is the AUTHORING path,
|
|
1422
|
+
// and the question it asks is "may a NEW close be drafted for this class",
|
|
1423
|
+
// never "can this class run". `hitl` answers no to the first and yes to the
|
|
1424
|
+
// second — it is unlisted, not locked, so `validateCloseDecl` still validates an
|
|
1425
|
+
// existing hitl close against its real battery and the runtime still executes it.
|
|
1426
|
+
if (!MENU_CLASSES.includes(String(verdictType))) {
|
|
1417
1427
|
const known = VERDICT_CLASSES.includes(String(verdictType));
|
|
1428
|
+
const unlisted = LIVE_CLASSES.includes(String(verdictType));
|
|
1418
1429
|
return refuse([{
|
|
1419
1430
|
code: 'class-unsupported',
|
|
1420
1431
|
path: 'verdictType',
|
|
1421
1432
|
verb: String(verdictType),
|
|
1422
1433
|
lib: 'bareloop',
|
|
1423
|
-
detail:
|
|
1424
|
-
? `"${verdictType}" is
|
|
1425
|
-
+ '
|
|
1426
|
-
|
|
1427
|
-
|
|
1434
|
+
detail: unlisted
|
|
1435
|
+
? `"${verdictType}" is built and still runs, but it is NOT ON THE AUTHORING MENU — this flow drafts `
|
|
1436
|
+
+ `${MENU_CLASSES.join(', ')} only. Its class moved to fwdloop; an existing close of this class still `
|
|
1437
|
+
+ 'validates and still executes, but no new one is drafted here'
|
|
1438
|
+
: known
|
|
1439
|
+
? `"${verdictType}" is a named but LOCKED verdict class — v1 admits ${MENU_CLASSES.join(', ')} only, and a `
|
|
1440
|
+
+ 'class with no guard battery must refuse rather than author a close without the guards it cannot supply'
|
|
1441
|
+
: `"${verdictType}" is not a verdict class — the menu is ${VERDICT_CLASSES.join(' | ')}, and nothing derives `
|
|
1442
|
+
+ 'a class from the answers',
|
|
1428
1443
|
}], 'precheck');
|
|
1429
1444
|
}
|
|
1430
1445
|
|
package/src/authoring.js
CHANGED
|
@@ -108,6 +108,38 @@ export const LOCKED_CLASSES = Object.freeze([]);
|
|
|
108
108
|
/** the classes v1 actually builds */
|
|
109
109
|
export const LIVE_CLASSES = Object.freeze(VERDICT_CLASSES.filter((c) => !LOCKED_CLASSES.includes(c)));
|
|
110
110
|
|
|
111
|
+
/** OFF THE MENU, but still built — a THIRD state, distinct from both lists above
|
|
112
|
+
* (PRD item 31.1, hamr 2026-09-09: *"retire hitl from list, keep its code"*, and
|
|
113
|
+
* *"if it's not on menu, you can't reach it, from menu"*).
|
|
114
|
+
*
|
|
115
|
+
* `hitl` was retired by ruling at v1.71 and the class moved to fwdloop (PRD item
|
|
116
|
+
* 29), but no constant was ever updated, so `runInterview` still returned
|
|
117
|
+
* `ok:true` for a hitl pick — an authorable job for a class this product no
|
|
118
|
+
* longer closes.
|
|
119
|
+
*
|
|
120
|
+
* Why this is NOT `LOCKED_CLASSES`. Locked means INEXPRESSIBLE: no guard battery
|
|
121
|
+
* exists, so `validateCloseDecl` cannot check the close and must refuse
|
|
122
|
+
* (`src/declaredclose.js:306`). hitl's battery, question set, composition rule and
|
|
123
|
+
* runtime all exist and are tested — locking it would make every one of them
|
|
124
|
+
* unreachable, which is not "keeping the code", it is abandoning it in place with
|
|
125
|
+
* its tests deleted. So the split is:
|
|
126
|
+
*
|
|
127
|
+
* LOCKED_CLASSES — no battery. Cannot be validated, cannot run. (empty today)
|
|
128
|
+
* UNLISTED_CLASSES — battery exists, validates and RUNS. Just never OFFERED.
|
|
129
|
+
* LIVE_CLASSES — everything the system can validate and run (locked removed).
|
|
130
|
+
*
|
|
131
|
+
* Read by the AUTHORING surface only (`runInterview`, `authorClose`'s precheck).
|
|
132
|
+
* The spec validator, the close validator and the runtime deliberately do NOT
|
|
133
|
+
* read it: a hitl spec that already exists still validates and still runs, which
|
|
134
|
+
* is what keeps the code alive and under test. Nothing can AUTHOR a new one.
|
|
135
|
+
* @type {readonly string[]} */
|
|
136
|
+
export const UNLISTED_CLASSES = Object.freeze(['hitl']);
|
|
137
|
+
|
|
138
|
+
/** what the authoring interview actually OFFERS: live, minus unlisted. This is
|
|
139
|
+
* "the menu" in hamr's sentence, and it is the only list an authoring refusal
|
|
140
|
+
* should ever name. */
|
|
141
|
+
export const MENU_CLASSES = Object.freeze(LIVE_CLASSES.filter((c) => !UNLISTED_CLASSES.includes(c)));
|
|
142
|
+
|
|
111
143
|
/** the hierarchy as a comparable number. `green` is the FLOOR: a mechanical
|
|
112
144
|
* measurement is the cheapest honest verdict, a judge is above it, a person is
|
|
113
145
|
* above that. Read by the ceiling rule and by nothing else. */
|
package/src/authorjob.js
CHANGED
|
@@ -35,11 +35,19 @@
|
|
|
35
35
|
// same way `repoPath` does — a closed set handed over enumerated, never read out
|
|
36
36
|
// of prose.
|
|
37
37
|
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
38
|
+
// THE MENU IS TWO SHAPES: `green` (a command decides done) and `soft-green` (a
|
|
39
|
+
// rubric decides it, through the judged floor). Both are LIVE and both author
|
|
40
|
+
// here. `hitl` is named by the radio, still validates and still runs, and is NOT
|
|
41
|
+
// OFFERED — the class moved to fwdloop (PRD items 29 and 31.1); picking it
|
|
42
|
+
// returns the honest counted refusal on the `request-red` admission path (D13's
|
|
43
|
+
// mechanism), carrying demand for a VERDICT CLASS rather than only for a genre.
|
|
44
|
+
// It refuses at ADMISSION — before that class's questions are ever asked.
|
|
45
|
+
//
|
|
46
|
+
// This comment said the opposite until 2026-09-09 (*"v1 STILL ADMITS ONLY
|
|
47
|
+
// `green`"*), which was false for a long time and sent one build in the wrong
|
|
48
|
+
// direction: `LOCKED_CLASSES` was empty, so `runInterview` returned `ok:true` for
|
|
49
|
+
// every class including the retired one. Check `MENU_CLASSES` at runtime rather
|
|
50
|
+
// than trusting any sentence here, this one included.
|
|
43
51
|
//
|
|
44
52
|
// AND THE INTERVIEW KEYS ON THE CLASS. Three frozen question sets replace one
|
|
45
53
|
// set per genre: genres are a fat tail that cannot be enumerated, classes are
|
|
@@ -91,14 +99,15 @@
|
|
|
91
99
|
// in the stored declaration (forward-compat point 3), and the suite pins it.
|
|
92
100
|
|
|
93
101
|
import { jobSpecHash, validateJob } from './job.js';
|
|
94
|
-
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, SEED_EXEMPT_KINDS,
|
|
102
|
+
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, SEED_EXEMPT_KINDS, judgedStages, closeJudges } from './kinds.js';
|
|
95
103
|
// SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
|
|
96
104
|
// and the gate that decides whether the ruler they describe is a ruler.
|
|
97
105
|
import { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts } from './cardauthor.js';
|
|
98
106
|
import { runCalibration } from './calibrate.js';
|
|
99
|
-
import {
|
|
107
|
+
import { CALIBRATION_SIZE } from './judged.js';
|
|
100
108
|
import {
|
|
101
109
|
GENRE_LANGUAGES, LOCKED_KINDS, TYPES_GENRE, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES,
|
|
110
|
+
UNLISTED_CLASSES, MENU_CLASSES,
|
|
102
111
|
} from './authoring.js';
|
|
103
112
|
import { QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook } from './authorflow.js';
|
|
104
113
|
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
@@ -123,7 +132,7 @@ export { QUESTION_SETS, questionsFor, requiredAnswersFor };
|
|
|
123
132
|
* rule about it: illegal becomes inexpressible instead of rejected after the
|
|
124
133
|
* fact (the `check-passes(name)` model). A UI renders it as three buttons, and
|
|
125
134
|
* two of them are marked locked. */
|
|
126
|
-
export { VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES };
|
|
135
|
+
export { VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, UNLISTED_CLASSES, MENU_CLASSES };
|
|
127
136
|
|
|
128
137
|
/** The territory every refusal here lands against. bareloop's OWN catalogue —
|
|
129
138
|
* the genre menu, the verdict menu, the kind menu — never a bare-suite package.
|
|
@@ -141,6 +150,15 @@ const LEVERS = ([
|
|
|
141
150
|
'wait for the verdict-classes rung — this refusal is the evidence it waits on',
|
|
142
151
|
]);
|
|
143
152
|
|
|
153
|
+
/** the levers for an UNLISTED class (PRD item 31.1): the class is BUILT, so
|
|
154
|
+
* "wait for the rung" is the wrong sentence — there is no rung left to wait on.
|
|
155
|
+
* The honest next step is the other product, or a class this one still offers. */
|
|
156
|
+
/** @type {string[]} */
|
|
157
|
+
const UNLISTED_LEVERS = ([
|
|
158
|
+
'restate the job for a class bareloop still offers: green (a command decides done) or soft-green (a rubric does)',
|
|
159
|
+
'take a job that needs a person\'s sign-off to fwdloop — that product is built for the human-in-the-loop shape',
|
|
160
|
+
]);
|
|
161
|
+
|
|
144
162
|
/**
|
|
145
163
|
* One refusal, in the shape both the caller and the ledger need.
|
|
146
164
|
* @param {{verb: string|null, path: string, detail: string, kind?: 'request-red'|'decision-ready', options?: string[]}} o
|
|
@@ -236,17 +254,33 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
|
|
|
236
254
|
return out({});
|
|
237
255
|
}
|
|
238
256
|
|
|
239
|
-
// A
|
|
240
|
-
// for the class itself — the verdict-classes rung is what this
|
|
241
|
-
|
|
257
|
+
// A class that is not ON THE MENU refuses HERE, before its questions are asked.
|
|
258
|
+
// Counted demand for the class itself — the verdict-classes rung is what this
|
|
259
|
+
// evidence is for. TWO reasons a pick lands here, and they are different facts
|
|
260
|
+
// (PRD item 31.1):
|
|
261
|
+
// * LOCKED — no guard battery exists, so nothing could validate the close.
|
|
262
|
+
// * UNLISTED — the class is built and still RUNS; this product just no longer
|
|
263
|
+
// offers it. `hitl` moved to fwdloop (item 29); its code, tests
|
|
264
|
+
// and runtime stay, and an existing hitl spec still validates.
|
|
265
|
+
// Either way the person cannot author one, and the refusal says which it is
|
|
266
|
+
// rather than pooling two different situations into one sentence.
|
|
267
|
+
if (LOCKED_CLASSES.includes(picked) || UNLISTED_CLASSES.includes(picked)) {
|
|
268
|
+
const unlisted = UNLISTED_CLASSES.includes(picked);
|
|
242
269
|
return out({
|
|
243
270
|
refusal: refuse({
|
|
244
271
|
verb: picked,
|
|
245
272
|
path: 'verdictType',
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
273
|
+
options: unlisted ? UNLISTED_LEVERS : LEVERS,
|
|
274
|
+
detail: unlisted
|
|
275
|
+
? `You picked "${picked}", and this product no longer offers that class: the menu is `
|
|
276
|
+
+ `${MENU_CLASSES.join(', ')} — a job whose done a command can decide, or one whose done needs `
|
|
277
|
+
+ 'judgement against a rubric. A person\'s sign-off moved to fwdloop, which is built for it. The '
|
|
278
|
+
+ 'class itself is not deleted — an existing spec still validates and still runs — so picking one is '
|
|
279
|
+
+ 'recorded as demand rather than dropped.'
|
|
280
|
+
: `You picked "${picked}", and v1 cannot close that yet: it admits ${MENU_CLASSES.join(', ')} only — a `
|
|
281
|
+
+ 'job whose done a command can decide. A class with no guard battery is named in the menu and locked, '
|
|
282
|
+
+ 'so picking one is recorded as demand rather than dropped, and this refusal is the evidence the '
|
|
283
|
+
+ 'verdict-classes rung waits on.',
|
|
250
284
|
}),
|
|
251
285
|
});
|
|
252
286
|
}
|
|
@@ -277,21 +311,32 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
|
|
|
277
311
|
}
|
|
278
312
|
if (reds.length) return out({ answers: given });
|
|
279
313
|
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
314
|
+
// A GENRE precondition, not a class one — and the difference is why this text
|
|
315
|
+
// was wrong until 2026-09-09 (PRD item 31.2). It used to answer a missing repo
|
|
316
|
+
// with "that needs a judged (soft-green) or a human close", which told a person
|
|
317
|
+
// who had just PICKED soft-green that they needed soft-green. Both live classes
|
|
318
|
+
// land here identically: D1's "a repo is never a precondition" holds for the
|
|
319
|
+
// INTERVIEW and not for D9's validity gates, all three of which rest on a
|
|
320
|
+
// runnable patient with a git seed, and the close CATALOGUE is code-genre only
|
|
321
|
+
// (doc-genre kinds are not admitted — PRD item 26). So a doc or a website job
|
|
322
|
+
// refuses for want of a SEED, whichever class it picked, on the same counted
|
|
285
323
|
// path. The class the user picked cannot be honoured, so it is not returned.
|
|
324
|
+
//
|
|
325
|
+
// The `verb` stays `non-green-verdict` deliberately. It is the ledger's key for
|
|
326
|
+
// this demand and renaming it would split one running count into two that look
|
|
327
|
+
// like different demands; the name is a poor description of a genre refusal,
|
|
328
|
+
// and that is a smaller cost than a discontinuous ledger.
|
|
286
329
|
if (!isNonEmptyString(repoPath)) {
|
|
287
330
|
return out({
|
|
288
331
|
answers: given,
|
|
289
332
|
refusal: refuse({
|
|
290
333
|
verb: 'non-green-verdict',
|
|
291
334
|
path: 'repoPath',
|
|
292
|
-
detail: 'This job has no code repository, so there is no seed to measure against and no changed set to
|
|
293
|
-
+ '
|
|
294
|
-
+ '
|
|
335
|
+
detail: 'This job has no code repository, so there is no seed to measure against and no changed set to '
|
|
336
|
+
+ 'read. This authoring flow drafts CODE-GENRE closes against a git seed only, and that is true of both '
|
|
337
|
+
+ 'classes it offers: a green close runs commands over the tree, and a soft-green close judges the '
|
|
338
|
+
+ 'artifacts the run changed in it. A job with no repository needs a doc-genre close, which is not built '
|
|
339
|
+
+ 'here yet.',
|
|
295
340
|
}),
|
|
296
341
|
});
|
|
297
342
|
}
|
|
@@ -370,7 +415,7 @@ function composerRefusal(reds) {
|
|
|
370
415
|
* @param {{answers: Record<string|number, any>, repoPath?: string|null, lang: string,
|
|
371
416
|
* verdictType?: string|null,
|
|
372
417
|
* questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
|
|
373
|
-
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
|
|
418
|
+
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
|
|
374
419
|
* onPhase?: (phase: string, data?: any) => void,
|
|
375
420
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
376
421
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
@@ -384,6 +429,11 @@ export async function authorCloseForJob({
|
|
|
384
429
|
answers, repoPath = null, lang, verdictType = null, questions = null,
|
|
385
430
|
generate, provider = null, seedRef = null, scout = null, listing = null,
|
|
386
431
|
ceilingUsd = null,
|
|
432
|
+
// THE JUDGE IDENTITY (PRD item 32.1), required only on the path that actually
|
|
433
|
+
// composes a judged stage — the stored calibration set carries the judge that
|
|
434
|
+
// certified it, and there is no library pin to fall back to. Absent on a purely
|
|
435
|
+
// mechanical close is correct and costs nothing: that close never judges.
|
|
436
|
+
judgeModel = null,
|
|
387
437
|
// THE PROGRESS SEAMS. This composition is the pipeline's long silence — a real
|
|
388
438
|
// survey, a real declaration ladder, and a real toolchain per close stage — and
|
|
389
439
|
// it emitted nothing until it returned, so an operator watching a live run
|
|
@@ -573,7 +623,7 @@ export async function authorCloseForJob({
|
|
|
573
623
|
// as the caller-error guard it is.
|
|
574
624
|
/** @type {any} */
|
|
575
625
|
let judged = null;
|
|
576
|
-
if ((closeDecl
|
|
626
|
+
if (closeJudges(closeDecl)) {
|
|
577
627
|
// ONE CEILING, ALL THREE PAID SEAMS. The declaration loop's own calls are
|
|
578
628
|
// ABSORBED before the compile is asked for, so the ceiling folds in prior
|
|
579
629
|
// spend and re-invoking a seam cannot silently widen it. `absorb` deliberately
|
|
@@ -620,7 +670,14 @@ export async function authorCloseForJob({
|
|
|
620
670
|
// non-array before anything can be `ok` — and the narrowing is stated rather
|
|
621
671
|
// than defaulted: `?? []` here would fold an EMPTY signed set into a close,
|
|
622
672
|
// which is the one direction that must never be reachable.
|
|
623
|
-
|
|
673
|
+
if (typeof judgeModel !== 'string' || judgeModel.trim() === '') {
|
|
674
|
+
throw new Error('[authorjob] authorCloseForJob composed a JUDGED close and was given no judgeModel (PRD item '
|
|
675
|
+
+ '32.1). The stored calibration set carries the judge that certified it, and a set stamped with a model '
|
|
676
|
+
+ 'nobody named is exactly the unattributable floor that field exists to prevent.');
|
|
677
|
+
}
|
|
678
|
+
closeDecl = foldJudgedArtifacts(closeDecl, {
|
|
679
|
+
card: signed.card, cases: /** @type {any[]} */ (signed.cases), judgeModel,
|
|
680
|
+
});
|
|
624
681
|
return {
|
|
625
682
|
ok: true,
|
|
626
683
|
refusal: null,
|
|
@@ -760,12 +817,15 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
760
817
|
* The book is built HERE and PRIOR SPEND IS ABSORBED into it, so the ceiling folds in
|
|
761
818
|
* what the earlier seams already spent and re-invoking a seam cannot silently widen
|
|
762
819
|
* it; the gate reads it between calls and stops as a refusal that names money.
|
|
763
|
-
*
|
|
820
|
+
* `judgeModel` is the RESOLVED judge identity (PRD item 32.1), required and with no
|
|
821
|
+
* default: it is stamped onto the graded set and onto the calibration-missing record
|
|
822
|
+
* alike, so both say which judge the close would have been certified by.
|
|
823
|
+
* @param {{spec: any, judgedStages: any[], judgeLoop: Function|null, judgeModel: string,
|
|
764
824
|
* onJudgeCost: ((c: any) => void)|null, calibrateFn: Function,
|
|
765
825
|
* ceilingUsd: number|null, priorCalls: any[]}} o
|
|
766
826
|
* @returns {Promise<{ok: boolean, record: any, reds: Red[], refusal: Refusal|null}>}
|
|
767
827
|
*/
|
|
768
|
-
async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, calibrateFn, ceilingUsd, priorCalls }) {
|
|
828
|
+
async function calibrationGate({ spec, judgedStages, judgeLoop, judgeModel, onJudgeCost, calibrateFn, ceilingUsd, priorCalls }) {
|
|
769
829
|
const cases = spec.closeDecl?.calibration?.cases ?? null;
|
|
770
830
|
// ONE card, and it is the SIGNED one — the card the calibration is graded
|
|
771
831
|
// against must be the card the close will RUN, or the gate certifies a ruler
|
|
@@ -779,7 +839,7 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, cal
|
|
|
779
839
|
+ 'ruler is unmeasured, and an unmeasured ruler cannot be signed.';
|
|
780
840
|
return {
|
|
781
841
|
ok: false,
|
|
782
|
-
record: { ok: false, stop: 'calibration-missing', judgeModel
|
|
842
|
+
record: { ok: false, stop: 'calibration-missing', judgeModel, required: CALIBRATION_SIZE },
|
|
783
843
|
reds: [{ code: 'calibration-missing', path: 'closeDecl.calibration.cases', detail }],
|
|
784
844
|
refusal: refuse({
|
|
785
845
|
kind: 'decision-ready',
|
|
@@ -805,6 +865,7 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, cal
|
|
|
805
865
|
cases,
|
|
806
866
|
card,
|
|
807
867
|
judgeLoop,
|
|
868
|
+
judgeModel,
|
|
808
869
|
// the gate's own calls join the same tally the ceiling is read against — a
|
|
809
870
|
// ceiling that cannot see the spend it bounds is F45's blind detector in a
|
|
810
871
|
// money coat. The caller's reporter fires with the call's whole record, after.
|
|
@@ -942,8 +1003,8 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, cal
|
|
|
942
1003
|
*
|
|
943
1004
|
* @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
|
|
944
1005
|
* timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function,
|
|
945
|
-
* judgeLoop?: Function|null, onJudgeCost?: ((c: any) => void)|null,
|
|
946
|
-
* ceilingUsd?: number|null, priorCalls?: any[]}} o
|
|
1006
|
+
* judgeLoop?: Function|null, judgeModel?: string|null, onJudgeCost?: ((c: any) => void)|null,
|
|
1007
|
+
* calibrateFn?: Function, ceilingUsd?: number|null, priorCalls?: any[]}} o
|
|
947
1008
|
* @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
|
|
948
1009
|
* gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
|
|
949
1010
|
*/
|
|
@@ -953,8 +1014,11 @@ export async function prepareSigning({
|
|
|
953
1014
|
// THE PAID SEAM, and it is the operator's to wire — this module owns no
|
|
954
1015
|
// provider and picks none (the same contract `authorCloseForJob` keeps for the
|
|
955
1016
|
// drafting model). Absent is never a fall-back: a judged close whose gate could
|
|
956
|
-
// not run is refused, not waved through.
|
|
957
|
-
|
|
1017
|
+
// not run is refused, not waved through. `judgeModel` is the seam's other half
|
|
1018
|
+
// (PRD item 32.1) — WHICH model that loop drives, resolved by the caller
|
|
1019
|
+
// (`resolveJudge`, src/judged.js). The two travel together: a gate that grades
|
|
1020
|
+
// without recording its judge certifies a floor nobody can attribute.
|
|
1021
|
+
judgeLoop = null, judgeModel = null, onJudgeCost = null, calibrateFn = runCalibration,
|
|
958
1022
|
// THE OPERATOR'S CEILING, and the spend it has already met. `null`/absent is
|
|
959
1023
|
// UNBOUNDED and is a stated choice, never a default invented here (the same
|
|
960
1024
|
// contract `authorCloseForJob` keeps). `priorCalls` is what the earlier paid
|
|
@@ -1109,9 +1173,17 @@ export async function prepareSigning({
|
|
|
1109
1173
|
// validator checks a stored set for LEGALITY only and deliberately allows a
|
|
1110
1174
|
// judged stage with no set at all, because making it mandatory is a signing
|
|
1111
1175
|
// decision and this is the signing gate.
|
|
1112
|
-
const
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1176
|
+
const judged = judgedStages(spec.closeDecl);
|
|
1177
|
+
if (judged.length) {
|
|
1178
|
+
if (judgeLoop !== null && (typeof judgeModel !== 'string' || judgeModel.trim() === '')) {
|
|
1179
|
+
throw new Error('[authorjob] prepareSigning was wired a judgeLoop with no judgeModel (PRD item 32.1). The two '
|
|
1180
|
+
+ 'halves of the judge seam travel together: the graded set is stamped with the judge that certified it, and '
|
|
1181
|
+
+ 'there is no library pin to fall back to.');
|
|
1182
|
+
}
|
|
1183
|
+
const cal = await calibrationGate({
|
|
1184
|
+
spec, judgedStages: judged, judgeLoop, judgeModel: /** @type {string} */ (judgeModel),
|
|
1185
|
+
onJudgeCost, calibrateFn, ceilingUsd, priorCalls,
|
|
1186
|
+
});
|
|
1115
1187
|
base.gates.calibration = cal.record;
|
|
1116
1188
|
if (!cal.ok) return { ...base, work, guards, reds: cal.reds, refusal: cal.refusal };
|
|
1117
1189
|
}
|
package/src/authorscout.js
CHANGED
|
@@ -55,11 +55,37 @@ import { TOOL_MENU, WRITE_VERBS, STORE_VERBS } from './job.js';
|
|
|
55
55
|
import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, strategyFor } from './tools.js';
|
|
56
56
|
import { redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
57
57
|
import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop } from './text.js';
|
|
58
|
+
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
58
59
|
|
|
59
60
|
const require = createRequire(import.meta.url);
|
|
60
61
|
const { Loop, wireGate } = require('bare-agent');
|
|
61
62
|
const { createShellTools } = require('bare-agent/tools');
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* F152/PRD 30.4 — the scout's per-call IDLE bound. Every `loop.run()` in this
|
|
66
|
+
* module carried NO time bound at all: a live endpoint that accepts and never
|
|
67
|
+
* answers hangs the process forever (measured) — an open socket is an active
|
|
68
|
+
* handle, so node never drains and no backstop can fire.
|
|
69
|
+
*
|
|
70
|
+
* UNLIKE the run path (`callBounds()`, src/planrun.js:1518), this pipeline has
|
|
71
|
+
* NO wall clock to derive a bound FROM — the authoring pipeline is genuinely
|
|
72
|
+
* clockless (`src/authorjob.js`/`src/authorflow.js`/`scripts/run-author.mjs`
|
|
73
|
+
* carry a money ceiling, `ceilingUsd`/`capStop`, and never a `maxWallMs`).
|
|
74
|
+
* Rather than inventing a fresh number, this reuses the EXACT ceiling the run
|
|
75
|
+
* path itself falls back to when ITS OWN wall is unbounded
|
|
76
|
+
* (`PROVIDER_TIMEOUT_MS`, `src/clock.js` — BA-18's idle timeout, already an
|
|
77
|
+
* approved "no wall present" default one layer up): an authoring-time call is
|
|
78
|
+
* the identical situation, one call earlier.
|
|
79
|
+
*
|
|
80
|
+
* It bounds SOCKET INACTIVITY only (reset by every byte, per BA-18) — a call
|
|
81
|
+
* that is merely slow keeps streaming and is untouched; only a truly silent
|
|
82
|
+
* endpoint trips it. TIGHTEN-ONLY, like every cap in this file: this is an
|
|
83
|
+
* operator-set ceiling fixed here, not a param a caller or a signed spec may
|
|
84
|
+
* widen (a defaulted cap is a silent second ceiling, and there is no clock
|
|
85
|
+
* whose narrower reading could ever tighten it further).
|
|
86
|
+
*/
|
|
87
|
+
export const AUTHOR_CALL_TIMEOUT_MS = PROVIDER_TIMEOUT_MS;
|
|
88
|
+
|
|
63
89
|
/** src/planrun.js:42 — the survey's hard round bound. */
|
|
64
90
|
export const AUTHOR_SCOUT_ROUNDS = 8;
|
|
65
91
|
/** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
|
|
@@ -348,7 +374,7 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
348
374
|
*
|
|
349
375
|
* @param {{workdir: string, provider?: any,
|
|
350
376
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
351
|
-
* attempts?: number, ceilingUsd?: number|null,
|
|
377
|
+
* attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
|
|
352
378
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
353
379
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
354
380
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
@@ -365,10 +391,18 @@ export async function runAuthorScout({
|
|
|
365
391
|
ctx = true,
|
|
366
392
|
attempts = SCOUT_ATTEMPTS,
|
|
367
393
|
ceilingUsd = null,
|
|
394
|
+
// F152/PRD 30.4 — TIGHTEN-ONLY, the same clamp shape `attempts` carries just
|
|
395
|
+
// below: an operator (a test included) may buy a SHORTER idle bound and never
|
|
396
|
+
// a longer one. A garbage/absent value floors at the module ceiling
|
|
397
|
+
// (`AUTHOR_CALL_TIMEOUT_MS`), never at "no bound at all".
|
|
398
|
+
callTimeoutMs = AUTHOR_CALL_TIMEOUT_MS,
|
|
368
399
|
onCall = () => {},
|
|
369
400
|
createLoop = defaultLoop,
|
|
370
401
|
createSurveyor = defaultSurveyor,
|
|
371
402
|
}) {
|
|
403
|
+
const callTimeout = Number.isFinite(Number(callTimeoutMs)) && Number(callTimeoutMs) > 0
|
|
404
|
+
? Math.min(Math.trunc(Number(callTimeoutMs)), AUTHOR_CALL_TIMEOUT_MS)
|
|
405
|
+
: AUTHOR_CALL_TIMEOUT_MS;
|
|
372
406
|
const granted = AUTHOR_SCOUT_VERBS;
|
|
373
407
|
const system = SCOUT_SYSTEM + strategyFor([...granted]);
|
|
374
408
|
const surveyor = await createSurveyor({ workdir, granted, ctx });
|
|
@@ -404,6 +438,33 @@ export async function runAuthorScout({
|
|
|
404
438
|
// an ABSENT nobody can act on.
|
|
405
439
|
const allowed = Math.max(1, Math.min(Math.trunc(Number(attempts)) || 1, SCOUT_ATTEMPTS));
|
|
406
440
|
|
|
441
|
+
/**
|
|
442
|
+
* F152/PRD 30.4 — ONE call seam for every `loop.run()` below, so a REJECTED
|
|
443
|
+
* call (a `TimeoutError` off `AUTHOR_CALL_TIMEOUT_MS` included — bare-agent
|
|
444
|
+
* 0.42 rejects every known transport/idle-timeout casualty, F141) lands in the
|
|
445
|
+
* SAME place a call that merely RESOLVED with an error already did: as an
|
|
446
|
+
* `{error}` shape, read by `record()` (cost null, same as a `provider-red`
|
|
447
|
+
* locate call — a rejected call has no knowable cost) and by `classifySurvey`
|
|
448
|
+
* under the EXISTING `call-failed` cause. Without this seam a rejection would
|
|
449
|
+
* escape uncaught to `run-author.mjs`'s top-level crash handler instead of the
|
|
450
|
+
* honest, already-typed terminal this module was built to report through.
|
|
451
|
+
* @param {Promise<any>} p @returns {Promise<any>}
|
|
452
|
+
*/
|
|
453
|
+
const settled = async (p) => {
|
|
454
|
+
try { return await p; } catch (e) {
|
|
455
|
+
// ONLY the call's own deadline lands here. Swallowing every throw would
|
|
456
|
+
// launder a budget HaltError, a programming bug, or any other named
|
|
457
|
+
// terminal into "the survey call failed" — the exact class of dishonesty
|
|
458
|
+
// this repo refuses (an unknown reported as a known). A timeout is the one
|
|
459
|
+
// throw this seam exists for: it IS a call that produced nothing, which is
|
|
460
|
+
// what `call-failed` already means. Everything else re-raises and is
|
|
461
|
+
// relayed after cleanup, as it was before this seam existed.
|
|
462
|
+
const err = /** @type {any} */ (e);
|
|
463
|
+
if (err?.code !== 'ETIMEDOUT' && err?.name !== 'TimeoutError') throw e;
|
|
464
|
+
return { error: String(err?.message ?? err) };
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
407
468
|
try {
|
|
408
469
|
/** @type {Survey|null} */
|
|
409
470
|
let survey = null;
|
|
@@ -455,8 +516,8 @@ export async function runAuthorScout({
|
|
|
455
516
|
};
|
|
456
517
|
loop = createLoop({ system, policy: surveyor.policy, onLlmResult: metered, provider });
|
|
457
518
|
|
|
458
|
-
const r = await loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
|
|
459
|
-
{ cacheMessages: true, maxTokens });
|
|
519
|
+
const r = await settled(loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
|
|
520
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
|
|
460
521
|
verdictAt = record(label, attempt, r);
|
|
461
522
|
blob = redactSecrets(String(r?.text ?? '')).slice(0, blobMax);
|
|
462
523
|
lastMsgs = Array.isArray(r?.msgs) && r.msgs.length ? r.msgs : null;
|
|
@@ -493,8 +554,16 @@ export async function runAuthorScout({
|
|
|
493
554
|
if (recoveryHalt) budgetStop = recoveryHalt;
|
|
494
555
|
if (recoveryWanted && !recoveryHalt) {
|
|
495
556
|
const recovery = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
496
|
-
|
|
497
|
-
|
|
557
|
+
// F147 (second instance; sibling fix: src/planrun.js `askFrom`, commit
|
|
558
|
+
// 031ffe1): `r.msgs` is a prior `loop.run()`'s returned transcript,
|
|
559
|
+
// which bare-agent already prepended `system` to (Loop.run, loop.js
|
|
560
|
+
// ~482-486). `recovery` below is built with the same `system` and
|
|
561
|
+
// would prepend it again, leaving two identical system messages at
|
|
562
|
+
// index 0/1 — real OpenAI tolerates it, vLLM-class OpenAI-compatible
|
|
563
|
+
// backends 400 on it. Strip the leading one before re-running.
|
|
564
|
+
const rMsgs = r.msgs[0]?.role === 'system' ? r.msgs.slice(1) : r.msgs;
|
|
565
|
+
s2 = await settled(recovery.run([...rMsgs, { role: 'user', content: SCOUT_RECOVERY_PROMPT }], [],
|
|
566
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
|
|
498
567
|
const recoveryAt = record('author-scout-recovery', attempt, s2);
|
|
499
568
|
const t = redactSecrets(String(s2?.text ?? '')).slice(0, blobMax);
|
|
500
569
|
// the verdict follows the blob that WON, never the call that came last
|
|
@@ -518,10 +587,15 @@ export async function runAuthorScout({
|
|
|
518
587
|
// THE RE-ASK. Toolless by construction, over the survey's own
|
|
519
588
|
// conversation, naming only what failed mechanically.
|
|
520
589
|
const reask = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
590
|
+
// F147 (second instance; sibling fix: src/planrun.js `askFrom`, commit
|
|
591
|
+
// 031ffe1): `lastMsgs` is a prior `loop.run()`'s returned transcript
|
|
592
|
+
// (system already prepended by bare-agent); `reask` is built with the
|
|
593
|
+
// same `system` and would prepend it again. Strip the leading one.
|
|
594
|
+
const askMsgs = lastMsgs[0]?.role === 'system' ? lastMsgs.slice(1) : lastMsgs;
|
|
595
|
+
const rr = await settled(reask.run(
|
|
596
|
+
[...askMsgs, { role: 'user', content: scoutReaskTurn(/** @type {Survey} */ (survey).reason ?? '') }], [],
|
|
597
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout },
|
|
598
|
+
));
|
|
525
599
|
verdictAt = record(label, attempt, rr);
|
|
526
600
|
blob = redactSecrets(String(rr?.text ?? '')).slice(0, blobMax);
|
|
527
601
|
if (Array.isArray(rr?.msgs) && rr.msgs.length) lastMsgs = rr.msgs;
|