bareloop 0.9.0 → 0.11.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 +555 -0
- package/README.md +111 -117
- package/bareloop.context.md +314 -24
- package/package.json +3 -3
- package/src/authorflow.js +300 -35
- package/src/authoring.js +290 -33
- package/src/authorjob.js +59 -7
- package/src/authorscout.js +83 -10
- package/src/bridges.js +15 -2
- package/src/clock.js +21 -3
- package/src/declaredclose.js +88 -11
- package/src/index.js +11 -1
- package/src/job.js +7 -2
- package/src/kinds.js +292 -20
- package/src/ledger.js +29 -2
- package/src/planrun.js +404 -29
- package/src/ralph.js +14 -0
- package/src/reuse.js +247 -9
- package/src/root.js +1 -1
- package/src/run.js +12 -5
- package/src/selection.js +1 -1
- package/src/text.js +79 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/types/authorflow.d.ts +135 -17
- package/types/authoring.d.ts +161 -35
- package/types/authorjob.d.ts +11 -2
- package/types/authorscout.d.ts +22 -3
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +64 -6
- package/types/index.d.ts +4 -4
- package/types/job.d.ts +5 -0
- package/types/kinds.d.ts +134 -10
- package/types/planrun.d.ts +61 -2
- package/types/reuse.d.ts +75 -1
- package/types/run.d.ts +12 -2
- package/types/text.d.ts +67 -0
- package/types/validate.d.ts +74 -9
package/src/authorscout.js
CHANGED
|
@@ -50,11 +50,11 @@ import { createRequire } from 'node:module';
|
|
|
50
50
|
import { join } from 'node:path';
|
|
51
51
|
import { Gate } from 'bareguard';
|
|
52
52
|
import { LiteCtx } from 'litectx';
|
|
53
|
-
import { seedListing } from './kinds.js';
|
|
53
|
+
import { seedListing, GATE_AUDIT_FILE, ARBITER_BOOK_STORES } from './kinds.js';
|
|
54
54
|
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
|
-
import { extractArtifact, priceOf, scrubRaw, stampRaw } from './text.js';
|
|
57
|
+
import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop } from './text.js';
|
|
58
58
|
|
|
59
59
|
const require = createRequire(import.meta.url);
|
|
60
60
|
const { Loop, wireGate } = require('bare-agent');
|
|
@@ -115,6 +115,16 @@ export const SURVEY_CAUSES = Object.freeze({
|
|
|
115
115
|
NON_OBJECT: 'non-object',
|
|
116
116
|
/** valid JSON, no content — F59's whole point, and a SEMANTIC failure */
|
|
117
117
|
EMPTY_OBJECT: 'empty-object',
|
|
118
|
+
/**
|
|
119
|
+
* THE SURVEY WAS NEVER ASKED FOR: the operator's money ceiling was already
|
|
120
|
+
* spent when the first attempt came due, so no call was made. Named as its own
|
|
121
|
+
* cause for the reason this enum exists at all — a new route to ABSENT has to
|
|
122
|
+
* be named, and naming it is the moment somebody decides whether it retries.
|
|
123
|
+
* This one does not, and cannot: a retry is precisely what the cap forbade.
|
|
124
|
+
* It is deliberately ABSENT of the model's own doing, and the prose says so —
|
|
125
|
+
* "the scout did not complete" would blame a model that never spoke.
|
|
126
|
+
*/
|
|
127
|
+
NOT_FUNDED: 'not-funded',
|
|
118
128
|
});
|
|
119
129
|
|
|
120
130
|
/**
|
|
@@ -280,9 +290,9 @@ export function classifySurvey(blob, meta) {
|
|
|
280
290
|
* `runAuthorScout` where they can be proven.
|
|
281
291
|
* @param {{workdir: string, granted: readonly string[], auditPath?: string, ctx?: boolean}} o
|
|
282
292
|
*/
|
|
283
|
-
export async function defaultSurveyor({ workdir, granted, auditPath = join(workdir,
|
|
293
|
+
export async function defaultSurveyor({ workdir, granted, auditPath = join(workdir, GATE_AUDIT_FILE), ctx = true }) {
|
|
284
294
|
const gate = new Gate({
|
|
285
|
-
fs: { writeScope: [], readScope: [workdir], deny: [auditPath,
|
|
295
|
+
fs: { writeScope: [], readScope: [workdir], deny: [auditPath, ...ARBITER_BOOK_STORES.map((s) => join(workdir, s))] },
|
|
286
296
|
limits: { maxTurns: AUTHOR_SCOUT_ROUNDS + 2 },
|
|
287
297
|
audit: { path: auditPath },
|
|
288
298
|
secrets: { patterns: SECRET_PATTERNS },
|
|
@@ -338,11 +348,13 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
338
348
|
*
|
|
339
349
|
* @param {{workdir: string, provider?: any,
|
|
340
350
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
341
|
-
* attempts?: number,
|
|
351
|
+
* attempts?: number, ceilingUsd?: number|null,
|
|
352
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
342
353
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
343
354
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
344
355
|
* @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
|
|
345
|
-
* calls: {label: string, costUsd: number|null, unpricedRounds: number}[]
|
|
356
|
+
* calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
|
|
357
|
+
* budgetStop: 'cap-halt'|'pricing-red'|null}>}
|
|
346
358
|
*/
|
|
347
359
|
export async function runAuthorScout({
|
|
348
360
|
workdir, provider = null,
|
|
@@ -352,6 +364,8 @@ export async function runAuthorScout({
|
|
|
352
364
|
maxTokens = AUTHOR_SCOUT_MAX_TOKENS,
|
|
353
365
|
ctx = true,
|
|
354
366
|
attempts = SCOUT_ATTEMPTS,
|
|
367
|
+
ceilingUsd = null,
|
|
368
|
+
onCall = () => {},
|
|
355
369
|
createLoop = defaultLoop,
|
|
356
370
|
createSurveyor = defaultSurveyor,
|
|
357
371
|
}) {
|
|
@@ -372,7 +386,17 @@ export async function runAuthorScout({
|
|
|
372
386
|
* @param {string} label @param {number} attempt @param {any} r @returns {number}
|
|
373
387
|
*/
|
|
374
388
|
const record = (label, attempt, r) => {
|
|
375
|
-
|
|
389
|
+
const call = { label, ...priceOf(r) };
|
|
390
|
+
calls.push(call);
|
|
391
|
+
// …and the SHELL is told, at the same instant and off the same entry. A run
|
|
392
|
+
// killed from outside used to lose 100% of its spend record (F6/F12: a
|
|
393
|
+
// halted attempt's spend was invisible by 300×) — the totals only ever
|
|
394
|
+
// existed in this array, which dies with the process. Fired from inside the
|
|
395
|
+
// one recorder rather than at the call sites, so a paid call cannot be
|
|
396
|
+
// metered without the report going out: exactly the pairing invariant the
|
|
397
|
+
// raw already rides. It REPORTS and decides nothing; the ceiling still binds
|
|
398
|
+
// through `capStop` between calls, untouched.
|
|
399
|
+
onCall({ ...call });
|
|
376
400
|
return raws.push(scrubRaw({ label, attempt, text: r?.text })) - 1;
|
|
377
401
|
};
|
|
378
402
|
// TIGHTEN-ONLY: an operator may lower the ceiling, never raise it. A garbage
|
|
@@ -395,8 +419,21 @@ export async function runAuthorScout({
|
|
|
395
419
|
* judged. An instrument that misaddresses its own reading is the blind
|
|
396
420
|
* -instrument class, one field wide. */
|
|
397
421
|
let verdictAt = -1;
|
|
422
|
+
/** the operator's money ceiling, when it is what ended the ladder. `null` on
|
|
423
|
+
* every other path INCLUDING the unbounded one, so a reader never has to
|
|
424
|
+
* infer a governance stop from an absence.
|
|
425
|
+
* @type {'cap-halt'|'pricing-red'|null} */
|
|
426
|
+
let budgetStop = null;
|
|
398
427
|
|
|
399
428
|
while (attempt < allowed) {
|
|
429
|
+
// THE CEILING, between attempts. The attempt cap bounds how many times the
|
|
430
|
+
// survey may ask; it says nothing about what those asks cost, and this is
|
|
431
|
+
// the pipeline's FIRST paid stage — a ladder that spends the whole wallet
|
|
432
|
+
// before the declaration is ever authored is the failure this closes.
|
|
433
|
+
// Checked before the counter moves, so `attempt` stays the number of
|
|
434
|
+
// attempts that actually happened.
|
|
435
|
+
const halt = capStop({ ceilingUsd, ...tallyCalls(calls) });
|
|
436
|
+
if (halt) { budgetStop = halt; break; }
|
|
400
437
|
attempt += 1;
|
|
401
438
|
const label = attempt === 1 ? 'author-scout' : `author-scout#${attempt}`;
|
|
402
439
|
|
|
@@ -432,7 +469,29 @@ export async function runAuthorScout({
|
|
|
432
469
|
// F59's reserved TOOLLESS final round. It fires on the ONE population it
|
|
433
470
|
// was sized for — a survey that was CUT OFF and came back short — never
|
|
434
471
|
// on a survey that simply finished with little to say.
|
|
435
|
-
|
|
472
|
+
// …and the reserved round is a PAID CALL like any other, so it sits behind
|
|
473
|
+
// the same ceiling. Left outside it, the one call the cap most needs to
|
|
474
|
+
// stop is the one that escapes: the recovery fires exactly when the first
|
|
475
|
+
// call was cut off, which is exactly when the wallet is thinnest.
|
|
476
|
+
//
|
|
477
|
+
// THE CEILING IS ASKED ABOUT A CALL THAT WOULD ACTUALLY BE MADE, and the
|
|
478
|
+
// ORDER is what makes that true: the recovery predicate is settled FIRST,
|
|
479
|
+
// and `capStop` is consulted only when a recovery was genuinely pending.
|
|
480
|
+
// Asked unconditionally it answers a question nobody put — the wallet is
|
|
481
|
+
// over, but nothing was refused — and `budgetStop` means the ceiling is
|
|
482
|
+
// WHAT ENDED THE LADDER, which is then a lie on every other attempt-1
|
|
483
|
+
// ending: a dead socket on an unbounded first call, or a survey that came
|
|
484
|
+
// back PRESENT and needed no second call at all. Both then render as
|
|
485
|
+
// `cap-halt` at `path: budgetUsd`, steering the operator to raise a number
|
|
486
|
+
// that cannot move the outcome. Nothing about the REFUSAL moves here — the
|
|
487
|
+
// same predicate, the same tally, the same call not made (the top-of-loop
|
|
488
|
+
// latch still names the ceiling whenever a retry was due). This is which
|
|
489
|
+
// ending gets the label, and nothing else.
|
|
490
|
+
const recoveryWanted = bounded && Buffer.byteLength(blob) < minBytes
|
|
491
|
+
&& Array.isArray(r?.msgs) && r.msgs.length > 0;
|
|
492
|
+
const recoveryHalt = recoveryWanted ? capStop({ ceilingUsd, ...tallyCalls(calls) }) : null;
|
|
493
|
+
if (recoveryHalt) budgetStop = recoveryHalt;
|
|
494
|
+
if (recoveryWanted && !recoveryHalt) {
|
|
436
495
|
const recovery = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
437
496
|
s2 = await recovery.run([...r.msgs, { role: 'user', content: SCOUT_RECOVERY_PROMPT }], [],
|
|
438
497
|
{ cacheMessages: true, maxTokens });
|
|
@@ -484,12 +543,26 @@ export async function runAuthorScout({
|
|
|
484
543
|
if (!SCOUT_RETRY_CAUSES.includes(/** @type {string} */ (survey.cause))) break;
|
|
485
544
|
}
|
|
486
545
|
|
|
487
|
-
|
|
546
|
+
// THE SURVEY THAT WAS NEVER ASKED FOR. The ceiling was already spent when the
|
|
547
|
+
// first attempt came due, so no call was made and there is no reply to
|
|
548
|
+
// classify. It is spelled out rather than run through `classifySurvey`, which
|
|
549
|
+
// would read the empty blob and report that the model came back EMPTY — a
|
|
550
|
+
// model that never spoke cannot have said nothing, and blaming it would send
|
|
551
|
+
// the operator to the scout when the fact is the wallet.
|
|
552
|
+
const final = /** @type {Survey} */ (survey ?? {
|
|
553
|
+
state: 'ABSENT',
|
|
554
|
+
facts: null,
|
|
555
|
+
cause: SURVEY_CAUSES.NOT_FUNDED,
|
|
556
|
+
reason: budgetStop === 'pricing-red'
|
|
557
|
+
? 'the survey was never asked for: spend could not be priced, so the ceiling could not be enforced (F6)'
|
|
558
|
+
: `the survey was never asked for: the $${ceilingUsd} ceiling was already spent`,
|
|
559
|
+
meta: { bytes: 0, rounds: 0, bounded: false, recovered: false, error: null, attempts: 0, attemptsAllowed: allowed },
|
|
560
|
+
});
|
|
488
561
|
// Exhaustion is still the same honest ABSENT — it now says how many times we
|
|
489
562
|
// paid to find out. One attempt says nothing about retries, because there
|
|
490
563
|
// were none.
|
|
491
564
|
const reason = final.reason !== null && attempt > 1 ? `${final.reason} — after ${attempt} attempts` : final.reason;
|
|
492
|
-
return { ...final, reason, raw: blob, raws, calls };
|
|
565
|
+
return { ...final, reason, raw: blob, raws, calls, budgetStop };
|
|
493
566
|
} finally {
|
|
494
567
|
// a leaked surveyor leaks a gate handle and a litectx index — always, even
|
|
495
568
|
// when the loop threw
|
package/src/bridges.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — the REUSE registry (design record:
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/02-features/2026-08-01-layer-3-reuse-design.md, D1/D2/D6, frozen 2026-08-01).
|
|
3
3
|
//
|
|
4
4
|
// A BRIDGE is the plan a green actually executed, kept so the next run of the same
|
|
5
5
|
// SHAPE starts from it instead of cold. This module is the box and the label on it:
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
36
36
|
import { join } from 'node:path';
|
|
37
|
-
import { isObj, isNonEmptyString } from './validate.js';
|
|
37
|
+
import { isObj, isNonEmptyString, sweepNestedQuantifiers } from './validate.js';
|
|
38
38
|
import { TOOL_MENU, LOCKED_TOOLS } from './job.js';
|
|
39
39
|
import { closeStagesOf } from './plan.js';
|
|
40
40
|
|
|
@@ -183,6 +183,19 @@ export function validateBridge(input) {
|
|
|
183
183
|
if (!Array.isArray(e.history)) red('invalid-value', 'history', 'array of per-run rows');
|
|
184
184
|
else e.history.forEach((/** @type {unknown} */ h, /** @type {number} */ i) => validateHistoryRow(h, `history.${i}.`, red));
|
|
185
185
|
|
|
186
|
+
// PRD v1.60 §1 — the operator-regex admissibility sweep, whose population was
|
|
187
|
+
// `jobs/*.json` and is now every signed spec wherever it lives. A bridge ships
|
|
188
|
+
// its plans VERBATIM, and a stored `artifact-written.pattern` reaches the same
|
|
189
|
+
// untimed exit evaluator on the same main event loop that F49 measured. The
|
|
190
|
+
// reject is UNCHANGED (`hasNestedQuantifier`, one inventory) — only the
|
|
191
|
+
// population is wider, so a clean bridge is byte-identically clean.
|
|
192
|
+
//
|
|
193
|
+
// Whole-entry rather than per-field, because a bridge's regexes sit inside a
|
|
194
|
+
// plan stored as opaque data (`isObj(o.plan)` is all this validator asserts
|
|
195
|
+
// about it): enumerating a path into a document this one deliberately does not
|
|
196
|
+
// parse is how a sweep goes blind the next time the plan schema moves.
|
|
197
|
+
sweepNestedQuantifiers(e, red);
|
|
198
|
+
|
|
186
199
|
// R1's coherence, checked only once everything it reads is itself valid (a
|
|
187
200
|
// second red on an already-broken field would bury the real defect): every green
|
|
188
201
|
// mints exactly one version, and a red mints none.
|
package/src/clock.js
CHANGED
|
@@ -43,6 +43,21 @@
|
|
|
43
43
|
// it from the remaining budget is what stops the LAST round outliving the run.
|
|
44
44
|
// 3. ~90% accuracy is the bar, by hamr's ruling (addendum 3). This module does
|
|
45
45
|
// not chase precision; it reports honestly at the precision it has.
|
|
46
|
+
// 4. SUSPENDED MINUTES NEVER CHARGE A SIGNED WALL (hamr's ruling, 2026-08-15:
|
|
47
|
+
// "if computer suspended, time shouldn't count"). The default source is
|
|
48
|
+
// therefore MONOTONIC — `performance.now()`, which reads libuv's `uv_hrtime`
|
|
49
|
+
// and so CLOCK_MONOTONIC, which per clock_gettime(2) "does not count time
|
|
50
|
+
// that the system is suspended" on Linux (CLOCK_BOOTTIME is the one that
|
|
51
|
+
// would). `Date.now()` is CLOCK_REALTIME and counts every suspended minute:
|
|
52
|
+
// that was not a hypothetical, it charged 45 phantom minutes against a
|
|
53
|
+
// 45-minute wall when an s2idle suspend landed mid-run, and it is the same
|
|
54
|
+
// exposure for an NTP step or a hand-set date. Nothing here corrects for a
|
|
55
|
+
// suspend after the fact; the reading simply never includes one.
|
|
56
|
+
// The epoch change is safe by construction: every use of `now()` below is
|
|
57
|
+
// RELATIVE (`now() - startedAt`), and no value derived from this clock is
|
|
58
|
+
// ever published as a calendar timestamp — `report()` carries durations only,
|
|
59
|
+
// and the `at:`/`ts:` stamps a run writes come from their own `Date.now`
|
|
60
|
+
// seams (src/reuse.js), which stay calendar-correct on purpose.
|
|
46
61
|
//
|
|
47
62
|
// Unbounded is a REAL state, not a large number: hamr ruled `maxWallMs` has no
|
|
48
63
|
// default, so a run without one is time-unbounded by explicit operator choice and
|
|
@@ -135,11 +150,14 @@ export function isWallTimeout(err, clock) {
|
|
|
135
150
|
* run. `closeStages`: how many stages the job's close runs (W5) — each one gets
|
|
136
151
|
* the FULL `closeTimeoutMs`, so this is the multiplier on the overshoot, not a
|
|
137
152
|
* decoration. Defaults to 1, the single-predicate close. `now`: injected for
|
|
138
|
-
* tests — the default is the real clock
|
|
139
|
-
*
|
|
153
|
+
* tests — the default is the real clock, and it stays injectable precisely so
|
|
154
|
+
* every test here can drive time deterministically without a sleep. That default
|
|
155
|
+
* is MONOTONIC (fact 4 above), not `Date.now`: a wall-clock jump — a suspend, an
|
|
156
|
+
* NTP step, a hand-set date — must not be able to reach a signed deadline.
|
|
157
|
+
* `priorElapsedMs`: time this attempt's PREDECESSOR already consumed (see below).
|
|
140
158
|
* @returns {Clock}
|
|
141
159
|
*/
|
|
142
|
-
export function createClock({ maxWallMs = null, closeStages = 1, now = () =>
|
|
160
|
+
export function createClock({ maxWallMs = null, closeStages = 1, now = () => performance.now(), priorElapsedMs = 0 } = {}) {
|
|
143
161
|
// RESUME (module C) — hamr's checkpoint ruling in a time coat: a run killed
|
|
144
162
|
// mid-try restarts THAT try under the REMAINDER of its signed per-try numbers,
|
|
145
163
|
// because "a budget ceiling folds in prior spend so re-invoking cannot silently
|
package/src/declaredclose.js
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED } from './kinds.js';
|
|
72
72
|
import {
|
|
73
73
|
validateDeclaration, classGuards, genreOwnedEnvNames, ungroundedGenreEnv, GENRE_LANGUAGES, TYPES_GENRE,
|
|
74
|
-
VERDICT_CLASSES, LIVE_CLASSES,
|
|
74
|
+
VERDICT_CLASSES, LIVE_CLASSES, NEVER_OFFERED_KINDS,
|
|
75
75
|
} from './authoring.js';
|
|
76
76
|
import { stageGap } from './ralph.js';
|
|
77
77
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
@@ -112,12 +112,68 @@ export const DECLARED_GAP_PREFIX = 'close: ';
|
|
|
112
112
|
* whole judged output rather than a hand-picked slice of it. */
|
|
113
113
|
export const DECLARED_GAP_KEEP = `^${DECLARED_GAP_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`;
|
|
114
114
|
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
|
|
115
|
+
/**
|
|
116
|
+
* The close verdict word for a run WAITING on a person (N4 §1.3/§1.4). A
|
|
117
|
+
* literal, exported, because three readers key on it — the runner's pause
|
|
118
|
+
* branch, the ledger's excluded set, and the trend reader's blind rule — and
|
|
119
|
+
* three hand-spelled copies of one terminal is how they come to disagree.
|
|
120
|
+
*
|
|
121
|
+
* It is not in `CLOSE_FAULTS` and must never be added: a fault says the close
|
|
122
|
+
* could not render a verdict, and this says the close is not FINISHED rendering
|
|
123
|
+
* one. The first escalates; the second is a checkpoint the operator returns to.
|
|
124
|
+
*/
|
|
125
|
+
export const HUMAN_PAUSE = 'human-pause';
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The RUN-level terminal names the hitl class mints one layer up from
|
|
129
|
+
* `HUMAN_PAUSE` (N4 §1.3–§1.5): the checkpoint, and the honest refusal of the
|
|
130
|
+
* operator's rerun input. Spelled once here, beside the close-verdict word they
|
|
131
|
+
* translate, for the same reason `HUMAN_PAUSE` is — the emitters (`planrun.js`),
|
|
132
|
+
* the ledger's excluded set, the resume reader (`reuse.js`), and the runner
|
|
133
|
+
* script all key on them, and hand-spelled copies of one terminal is how they
|
|
134
|
+
* come to disagree.
|
|
135
|
+
*
|
|
136
|
+
* There were three (2026-08-17): `hitl-cancel` was the signer's own terminal and
|
|
137
|
+
* is DELETED with the door it belonged to. A person who does not want to carry on
|
|
138
|
+
* now PAUSES — the same checkpoint, resumable, retired by the TTL if nobody comes
|
|
139
|
+
* back — so nothing in this tree mints that name any more. The ledger keeps the
|
|
140
|
+
* bare string in its excluded set so a spine written before this change still
|
|
141
|
+
* reads as governance rather than as a capability gap; that is a READER's
|
|
142
|
+
* backward compatibility, and deliberately not a constant a writer could reach.
|
|
143
|
+
*/
|
|
144
|
+
export const HITL_PAUSE = 'hitl-pause';
|
|
145
|
+
export const HITL_DECISION_RED = 'hitl-decision-red';
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The terminals above that leave the run WAITING ON A PERSON — as a list, minted
|
|
149
|
+
* beside the names it holds so a reader keys on membership rather than on a
|
|
150
|
+
* spelling, and a rename here travels to every reader for free.
|
|
151
|
+
*
|
|
152
|
+
* It is deliberately NOT `reuse.js`'s `CHECKPOINT_OUTCOMES`, which is the wider
|
|
153
|
+
* RESUME list: every stop that left work on disk and an allowance unspent, money
|
|
154
|
+
* and time included. Those are governance stops the reuse loop is meant to try
|
|
155
|
+
* again under its remaining tries; this is the half no further attempt can
|
|
156
|
+
* change, because the allowance still unspent is a human answer. `CHECKPOINT_OUTCOMES`
|
|
157
|
+
* is composed FROM this list so the two can never disagree about which is which.
|
|
158
|
+
*
|
|
159
|
+
* `HITL_DECISION_RED` is not here: it is a verdict already rendered on the operator's
|
|
160
|
+
* own input, not a question still open.
|
|
161
|
+
*/
|
|
162
|
+
export const HUMAN_CHECKPOINTS = Object.freeze([HITL_PAUSE]);
|
|
163
|
+
|
|
164
|
+
/** The verdict classes a DECLARED close may claim. Named as data so the job
|
|
165
|
+
* validator reads one hierarchy table rather than hard-coding a class.
|
|
166
|
+
*
|
|
167
|
+
* `hard` is by CONSTRUCTION: every mechanical kind is a command whose measurement
|
|
168
|
+
* is the truth. `hitl` joined it at N4 slice 1, and only because `human-confirms`
|
|
169
|
+
* is genuinely LIVE — the widening and the kind go in one commit, or this table
|
|
170
|
+
* admits a class the executor cannot render. The `soft` class is still absent:
|
|
171
|
+
* `judged-floor` is locked, so no declaration can reach above hitl.
|
|
172
|
+
*
|
|
173
|
+
* The direction that matters is unchanged. This says which class a declaration
|
|
174
|
+
* MAY claim; `closeCeiling` says which class its stages actually NEED. A pick
|
|
175
|
+
* below the ceiling is still the silent upgrade `class-ceiling` refuses. */
|
|
176
|
+
export const DECLARED_CLOSE_CLASSES = Object.freeze(['hard', 'hitl']);
|
|
121
177
|
|
|
122
178
|
/** @param {any} job @returns {boolean} */
|
|
123
179
|
export function isDeclaredClose(job) {
|
|
@@ -135,7 +191,15 @@ export function isDeclaredClose(job) {
|
|
|
135
191
|
*/
|
|
136
192
|
export function declaredStages(closeDecl) {
|
|
137
193
|
if (!isObj(closeDecl) || !Array.isArray(closeDecl.stages)) return null;
|
|
138
|
-
return closeDecl.stages.map((s) => (isObj(s)
|
|
194
|
+
return closeDecl.stages.map((s) => (isObj(s)
|
|
195
|
+
// RULING 5, stamped by the ARBITER rather than trusted from the artefact:
|
|
196
|
+
// a judged or human stage is `offer: false` BY LAW, so `checkMenu` can never
|
|
197
|
+
// hand the agent `check-passes(<a person>)`. The validator refuses a
|
|
198
|
+
// declaration that says anything else (`human-stage-offered`), so this is
|
|
199
|
+
// never overwriting a signer-visible value — it is filling in a law the
|
|
200
|
+
// declaration schema deliberately gives the composer no way to state.
|
|
201
|
+
? { ...s, gapKeep: DECLARED_GAP_KEEP, ...(NEVER_OFFERED_KINDS.includes(s.kind) ? { offer: false } : {}) }
|
|
202
|
+
: s));
|
|
139
203
|
}
|
|
140
204
|
|
|
141
205
|
// ── the spec-level gate ──────────────────────────────────────────────────────
|
|
@@ -361,7 +425,7 @@ export function guardNames(closeDecl, verdictType) {
|
|
|
361
425
|
* @param {any} r an M1 `StageResult`
|
|
362
426
|
* @param {(s: string) => string} redact
|
|
363
427
|
* @returns {{verdict: string, notes?: string, gap?: string, detail?: string,
|
|
364
|
-
* exitCode?: number, judgedCount?: number}} `notes` is absent, never empty —
|
|
428
|
+
* exitCode?: number, judgedCount?: number, ask?: string|null}} `notes` is absent, never empty —
|
|
365
429
|
* a field that is always there says nothing; a RED carries its diagnostics
|
|
366
430
|
* inside `gap` and so has none of its own.
|
|
367
431
|
*/
|
|
@@ -370,6 +434,13 @@ function translate(r, redact) {
|
|
|
370
434
|
/** absent rather than empty: a field that is always there says nothing */
|
|
371
435
|
const notes = gapText ? { notes: gapText } : {};
|
|
372
436
|
if (r.verdict === 'green') return { verdict: 'satisfied', judgedCount: 1, ...notes };
|
|
437
|
+
// THE PAUSE (N4 §1.3) — its own verdict word, and deliberately NOT one of the
|
|
438
|
+
// three below. `satisfied` would mint a green nobody gave; `needs_revision`
|
|
439
|
+
// would hand the fix worker a gap the person has not written yet; a
|
|
440
|
+
// `CLOSE_FAULTS` word would file a working arbiter as a broken instrument.
|
|
441
|
+
// No `gap` and no `judgedCount`: nothing was judged, and every consumer guards
|
|
442
|
+
// with `if (gap)`.
|
|
443
|
+
if (r.verdict === 'pause') return { verdict: HUMAN_PAUSE, ask: r.detail?.ask ?? null, ...notes };
|
|
373
444
|
if (r.verdict === 'red') {
|
|
374
445
|
return {
|
|
375
446
|
verdict: 'needs_revision',
|
|
@@ -408,11 +479,12 @@ function translate(r, redact) {
|
|
|
408
479
|
* @param {any[]} stages declared stage descriptors, already validated
|
|
409
480
|
* @param {(s: string) => string} [redact] the scrub, applied at THIS boundary
|
|
410
481
|
* @param {{timeoutMs?: number, cwd?: string, seedRef?: string,
|
|
411
|
-
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree'
|
|
482
|
+
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
|
|
483
|
+
* humanRuling?: {decision: string, text?: string|null}|null}} [opts]
|
|
412
484
|
* @returns {Promise<any>}
|
|
413
485
|
*/
|
|
414
486
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
415
|
-
const { timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode } = opts;
|
|
487
|
+
const { timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null } = opts;
|
|
416
488
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
417
489
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
418
490
|
// missing one is the close unable to run, not a red about the worker.
|
|
@@ -436,6 +508,11 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
436
508
|
...(gapCap !== undefined ? { gapCap } : {}),
|
|
437
509
|
...(maxBuffer !== undefined ? { maxBuffer } : {}),
|
|
438
510
|
...(baselineMode !== undefined ? { baselineMode } : {}),
|
|
511
|
+
// the SIGNER's answer, when this close has a human stage and a leg is
|
|
512
|
+
// carrying one. Absent (null) is the ordinary path AND the pause path — the
|
|
513
|
+
// human stage reads the absence as "nobody has answered yet", which is the
|
|
514
|
+
// whole of what a pause means.
|
|
515
|
+
humanRuling,
|
|
439
516
|
seedTrees,
|
|
440
517
|
};
|
|
441
518
|
try {
|
package/src/index.js
CHANGED
|
@@ -60,7 +60,7 @@ export { renderListing, selectionPrompt } from './selection.js';
|
|
|
60
60
|
// `resumeTreeGate` is the ruling that a resumed patient is continued dirty and never
|
|
61
61
|
// reset. Both are read by a runner BEFORE the approval gate, so neither can live behind
|
|
62
62
|
// `runReuse`'s own entry.
|
|
63
|
-
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from './reuse.js';
|
|
63
|
+
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate } from './reuse.js';
|
|
64
64
|
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from './ledger.js';
|
|
65
65
|
// THE WORK BRANCH (PRD v1.57 §3). `workBranchName` is exported so an operator runner can
|
|
66
66
|
// SHOW, before the approval gate, which branch the run will work on — the same reason the
|
|
@@ -89,6 +89,10 @@ export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE }
|
|
|
89
89
|
export {
|
|
90
90
|
runDeclaredClose, runStage, seedRead, seedAtHead, seedListing, changedSet, makeSeedTrees,
|
|
91
91
|
LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, EXIT_GREEN, EXIT_RED, EXIT_STOP,
|
|
92
|
+
// N4 slice 1 — the hitl surface an adopting runner needs: the three doors, the
|
|
93
|
+
// gate that reads a signer's answer (and refuses an empty one), and the seed
|
|
94
|
+
// exemption ruling 8 states.
|
|
95
|
+
HUMAN_DECISIONS, SEED_EXEMPT_KINDS, normalizeHumanRuling,
|
|
92
96
|
} from './kinds.js';
|
|
93
97
|
// M2 — what a declaration may SAY, and whether one said it legally. The
|
|
94
98
|
// catalogue and the genre are DATA an integrating UI renders (the kind menu, the
|
|
@@ -96,10 +100,15 @@ export {
|
|
|
96
100
|
export {
|
|
97
101
|
validateDeclaration, normalizeDeclaration, KIND_CATALOGUE, CATALOGUE_KINDS, CATALOGUE_LIVE_KINDS,
|
|
98
102
|
LOCKED_KINDS, TYPES_GENRE, TYPES_GENRE_TEMPLATE, GENRE_LANGUAGES, genreEnv, genreOwnedEnvNames,
|
|
103
|
+
// how the genre's own tools PRINT — genre property for the same reason the
|
|
104
|
+
// suppression battery is (2026-08-09, run msmbpjk6)
|
|
105
|
+
genreInstruments,
|
|
99
106
|
DIRECTIONS, BASELINES, MAX_STAGES,
|
|
100
107
|
// the verdict-class surface (PRD v1.57 §1–§2): the radio's own menu, the guard
|
|
101
108
|
// battery keyed off it, and the ceiling that makes the pick a promise
|
|
102
109
|
VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, CLASS_BATTERIES, classGuards, closeCeiling,
|
|
110
|
+
// ruling 5 as data: the kinds no agent may ever be offered as an in-run ruler
|
|
111
|
+
NEVER_OFFERED_KINDS,
|
|
103
112
|
} from './authoring.js';
|
|
104
113
|
// M3 — the looking half and the writing half. `authorClose` is the grounded
|
|
105
114
|
// loop; `runAuthorScout`/`buildSeedListing` are exported because a caller that
|
|
@@ -125,6 +134,7 @@ export {
|
|
|
125
134
|
export {
|
|
126
135
|
validateCloseDecl, runDeclaredStages, declaredStages, isDeclaredClose, guardNames, closeGrade,
|
|
127
136
|
DECLARED_GENRES, DECLARED_GAP_PREFIX, DECLARED_GAP_KEEP, DECLARED_CLOSE_CLASSES, CLOSE_DECL_FIELDS,
|
|
137
|
+
HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED, HUMAN_CHECKPOINTS,
|
|
128
138
|
} from './declaredclose.js';
|
|
129
139
|
export { closeStagesOf } from './plan.js';
|
|
130
140
|
// M4b — the interview, the refusal, the composition, and D9's three gates.
|
package/src/job.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Job-spec validator — the OPERATOR-owned half of the config story (N1,
|
|
2
|
-
// design record: docs/
|
|
2
|
+
// design record: docs/02-features/2026-07-12-n1-job-close-schema-design.md).
|
|
3
3
|
// A job spec is the arbiter's rulebook: the close chain, the budget, the
|
|
4
4
|
// outer write fence, the escalation route. It is pure declarative data —
|
|
5
5
|
// no freeform code is expressible anywhere in it — and it validates
|
|
@@ -103,7 +103,12 @@ export const LOCKED_TOOLS = Object.freeze(['run']);
|
|
|
103
103
|
* pattern: disclosure ≠ admission), so the ledger counts the demand and the
|
|
104
104
|
* non-code rung later fills a declared slot instead of reshaping the schema. */
|
|
105
105
|
export const VERDICT_TYPES = Object.freeze(['green', 'soft-green', 'hitl']);
|
|
106
|
-
|
|
106
|
+
/** N4 slice 1: `hitl` is ADMITTED — the class whose *done* needs a person runs
|
|
107
|
+
* through the same signed spec, the same agent-authored plan and the same
|
|
108
|
+
* arbiter as a green job (the nine rulings, 2026-08-07; the three-button surface,
|
|
109
|
+
* 2026-08-12). `soft-green` stays declared-but-locked until its judged floor is
|
|
110
|
+
* built and proven (slice 2): a wobbly ruler must not mint anything. */
|
|
111
|
+
export const LOCKED_VERDICTS = Object.freeze(['soft-green']);
|
|
107
112
|
/** which close CLASS a declared verdictType demands — the close hierarchy one
|
|
108
113
|
* level up: the same laundering guard as CLASS_BY_CLOSE, applied to the plan
|
|
109
114
|
* shape's single close (green on a rubric would be a fake-hard verdict). */
|