bareloop 0.10.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 +351 -0
- package/README.md +17 -7
- package/bareloop.context.md +179 -16
- package/package.json +3 -3
- package/src/authorflow.js +114 -16
- package/src/authoring.js +100 -33
- package/src/authorjob.js +50 -6
- package/src/authorscout.js +13 -1
- package/src/bridges.js +1 -1
- package/src/clock.js +21 -3
- package/src/declaredclose.js +88 -11
- package/src/index.js +8 -1
- package/src/job.js +7 -2
- package/src/kinds.js +190 -9
- package/src/ledger.js +24 -1
- package/src/planrun.js +195 -5
- package/src/ralph.js +9 -0
- package/src/reuse.js +195 -7
- package/src/root.js +1 -1
- package/src/run.js +10 -4
- package/src/selection.js +1 -1
- package/types/authorflow.d.ts +86 -12
- package/types/authoring.d.ts +49 -35
- package/types/authorjob.d.ts +9 -1
- package/types/authorscout.d.ts +7 -1
- 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 +76 -4
- package/types/planrun.d.ts +18 -2
- package/types/reuse.d.ts +74 -0
- package/types/run.d.ts +12 -2
package/src/authoring.js
CHANGED
|
@@ -94,9 +94,11 @@ import { isObj, isNonEmptyString, hasNestedQuantifier, globToPrefix } from './va
|
|
|
94
94
|
|
|
95
95
|
/** the whole menu, in ascending order of what it takes to render the verdict */
|
|
96
96
|
export const VERDICT_CLASSES = Object.freeze(['green', 'soft-green', 'hitl']);
|
|
97
|
-
/** declared-but-locked: selecting one is COUNTED demand, never an admission
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
/** declared-but-locked: selecting one is COUNTED demand, never an admission.
|
|
98
|
+
* `hitl` LEFT this list at N4 slice 1 (see `src/job.js`'s twin) — the class is
|
|
99
|
+
* built, and its battery is below. `soft-green` waits for the judged floor. */
|
|
100
|
+
export const LOCKED_CLASSES = Object.freeze(['soft-green']);
|
|
101
|
+
/** the classes v1 actually builds */
|
|
100
102
|
export const LIVE_CLASSES = Object.freeze(VERDICT_CLASSES.filter((c) => !LOCKED_CLASSES.includes(c)));
|
|
101
103
|
|
|
102
104
|
/** the hierarchy as a comparable number. `green` is the FLOOR: a mechanical
|
|
@@ -237,11 +239,22 @@ export const KIND_CATALOGUE = Object.freeze({
|
|
|
237
239
|
shape: 'LOCKED — not available in v1',
|
|
238
240
|
asserts: 'a judged score must clear a floor. Declaring it is recorded as demand; it will not run.',
|
|
239
241
|
}),
|
|
242
|
+
// LIVE at N4 slice 1, and the ONLY kind that does not measure anything: it
|
|
243
|
+
// does not RUN, it PAUSES the run and hands the tree to a person (ruling 1).
|
|
244
|
+
// `ask` is the whole parameter surface — no cmd, no timeoutMs, no env — so a
|
|
245
|
+
// human stage cannot spawn, cannot be env-capable, and cannot be given a
|
|
246
|
+
// second job. `offer` is never one of its fields either: ruling 5's law is a
|
|
247
|
+
// LAW, checked by the validator, not a knob the composer sets.
|
|
240
248
|
'human-confirms': Object.freeze({
|
|
241
249
|
verdictClass: 'hitl',
|
|
242
|
-
required: Object.freeze([]),
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
required: Object.freeze(['ask']),
|
|
251
|
+
optional: Object.freeze([]),
|
|
252
|
+
pathParams: Object.freeze([]),
|
|
253
|
+
shape: 'ask: string',
|
|
254
|
+
asserts: 'a PERSON renders this verdict, at the end of the run, and nothing about it is measured. Write `ask` as '
|
|
255
|
+
+ 'the plain question they answer while looking at the finished result. It is always the LAST stage, it is '
|
|
256
|
+
+ 'never offered to the agent as an in-run check, and every part of the job a command can decide belongs in a '
|
|
257
|
+
+ 'mechanical stage before it.',
|
|
245
258
|
}),
|
|
246
259
|
});
|
|
247
260
|
|
|
@@ -613,30 +626,40 @@ function language(lang) {
|
|
|
613
626
|
* than hand back nothing if anything ever does.
|
|
614
627
|
* @type {Record<string, {locked: boolean, guards: readonly any[]|null}>}
|
|
615
628
|
*/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
]),
|
|
629
|
+
/** THE MECHANICAL BATTERY — the two guards a machine can always render, named
|
|
630
|
+
* once because TWO classes carry exactly them (OPEN-1's ruling below). It is
|
|
631
|
+
* shared by reference and never mutated: `classGuards` deep-copies before it
|
|
632
|
+
* hands anything back, so one battery cannot be weakened through another class. */
|
|
633
|
+
const MECHANICAL_GUARDS = Object.freeze([
|
|
634
|
+
Object.freeze({
|
|
635
|
+
name: 'changed-from-seed',
|
|
636
|
+
kind: 'files-changed',
|
|
637
|
+
params: Object.freeze({ requireNonEmpty: true }),
|
|
638
|
+
compose: Object.freeze([]),
|
|
639
|
+
fill: Object.freeze(['allowPrefixes']),
|
|
640
|
+
}),
|
|
641
|
+
Object.freeze({
|
|
642
|
+
name: 'no-suppressions',
|
|
643
|
+
kind: 'pattern-absent-in-diff',
|
|
644
|
+
// NO scope: the scan covers every changed file of these extensions. A
|
|
645
|
+
// narrowing here is the round-3 arm A defect and is a red, not a taste.
|
|
646
|
+
params: Object.freeze({}),
|
|
647
|
+
compose: Object.freeze(['patterns', 'extensions']),
|
|
648
|
+
fill: Object.freeze([]),
|
|
637
649
|
}),
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
export const CLASS_BATTERIES = Object.freeze({
|
|
653
|
+
green: Object.freeze({ locked: false, guards: MECHANICAL_GUARDS }),
|
|
638
654
|
'soft-green': Object.freeze({ locked: true, guards: null }),
|
|
639
|
-
|
|
655
|
+
// OPEN-1, RULED (hamr, in-turn, 2026-08-13): **hitl inherits the green
|
|
656
|
+
// mechanical guards and adds nothing a human stage cannot see.** D5 makes the
|
|
657
|
+
// battery mandatory, shown-and-fixed and un-removable, and PRD v1.57 §2 keys it
|
|
658
|
+
// to the CLASS; the mechanical-first composition law makes every hitl close
|
|
659
|
+
// mostly mechanical anyway, so the green battery is the right FLOOR. A guard a
|
|
660
|
+
// person cannot verify by looking would be a guard nobody checks — the one
|
|
661
|
+
// thing D5 exists to make unsayable.
|
|
662
|
+
hitl: Object.freeze({ locked: false, guards: MECHANICAL_GUARDS }),
|
|
640
663
|
});
|
|
641
664
|
|
|
642
665
|
/**
|
|
@@ -925,7 +948,12 @@ function scopeOfJob(declaration, idx, guards) {
|
|
|
925
948
|
* stage — applied to the one population that decides whether the law fires at
|
|
926
949
|
* all.
|
|
927
950
|
*/
|
|
928
|
-
const AT_MOST_ONCE_KINDS = Object.freeze(['files-changed']);
|
|
951
|
+
const AT_MOST_ONCE_KINDS = Object.freeze(['files-changed', 'human-confirms']);
|
|
952
|
+
|
|
953
|
+
/** the kinds NO ONE runs as an in-run ruler — ruling 5's law, as data. A
|
|
954
|
+
* declaration that offers one is a red rather than a normalization: the signer
|
|
955
|
+
* must not read one thing and have the runner store another. */
|
|
956
|
+
export const NEVER_OFFERED_KINDS = Object.freeze(['human-confirms', 'judged-floor']);
|
|
929
957
|
|
|
930
958
|
/** @param {any} v */
|
|
931
959
|
const strArray = (v) => Array.isArray(v) && v.length > 0 && v.every(isNonEmptyString);
|
|
@@ -1128,13 +1156,43 @@ export function validateDeclaration(declaration, opts = {}) {
|
|
|
1128
1156
|
if (AT_MOST_ONCE_KINDS.includes(s.kind)) {
|
|
1129
1157
|
const owner = kindOwners.get(s.kind);
|
|
1130
1158
|
if (owner !== undefined) {
|
|
1131
|
-
red('duplicate-kind', `${at}.kind`,
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1159
|
+
red('duplicate-kind', `${at}.kind`, s.kind === 'human-confirms'
|
|
1160
|
+
? `stage "${label}" is a second ${s.kind} stage — "${owner}" already asks the person. One signer, one judge `
|
|
1161
|
+
+ '(ruling 4), asked ONCE at the end: a second door is a second verdict, and the run has no way to say '
|
|
1162
|
+
+ 'which of them was the one it stopped for'
|
|
1163
|
+
: `stage "${label}" is a second ${s.kind} stage — "${owner}" already declares `
|
|
1164
|
+
+ 'which files this job may change. Stages are ANDed, so two of them mean the intersection of two prefix sets, '
|
|
1165
|
+
+ 'which is itself a prefix set and already says the same thing in one stage; what a second one does add is a '
|
|
1166
|
+
+ 'second answer to what this job\'s scope IS', { kind: s.kind, stage: label, twin: owner });
|
|
1135
1167
|
} else kindOwners.set(s.kind, label);
|
|
1136
1168
|
}
|
|
1137
1169
|
|
|
1170
|
+
// ── RULING 5, AS LAW: judged and human stages are `offer: false`, and the
|
|
1171
|
+
// law is checked rather than applied. `checkMenu` filters `offer !== false`,
|
|
1172
|
+
// so a declaration that says nothing is silent about a rule the runner
|
|
1173
|
+
// enforces anyway (`declaredStages` stamps it) — that part is the arbiter's
|
|
1174
|
+
// own bookkeeping. What must never happen is a declaration that says
|
|
1175
|
+
// something ELSE: `offer: true` on a human stage is the composer asking to
|
|
1176
|
+
// use a person as an in-run ruler, and a silent normalization would leave
|
|
1177
|
+
// the signer reading one thing while the runner stored another.
|
|
1178
|
+
if (NEVER_OFFERED_KINDS.includes(s.kind) && Object.hasOwn(s, 'offer') && s.offer !== false) {
|
|
1179
|
+
red('human-stage-offered', `${at}.offer`, `stage "${label}" is a ${s.kind} stage and offers itself as an in-run `
|
|
1180
|
+
+ 'check. A person (or a paid judge) is never an in-run ruler (ruling 5): the agent may never use one to '
|
|
1181
|
+
+ 'decide a step, and this is refused rather than quietly rewritten to offer: false',
|
|
1182
|
+
{ kind: s.kind, stage: label });
|
|
1183
|
+
}
|
|
1184
|
+
// ── THE COMPOSITION LAW's ordering half (2026-08-07: *mechanical first,
|
|
1185
|
+
// judge minimal, human LAST*), enforced where it is cheap. First-red-wins
|
|
1186
|
+
// means anything after the human stage never runs — so a human stage in the
|
|
1187
|
+
// middle silently deletes the mechanical stages behind it AND empties the
|
|
1188
|
+
// evidence package ruling 2 requires the person to be shown.
|
|
1189
|
+
if (s.kind === 'human-confirms' && i !== stages.length - 1) {
|
|
1190
|
+
red('human-stage-not-last', `${at}.kind`, `stage "${label}" asks a person and is not the LAST stage. The close `
|
|
1191
|
+
+ 'is first-red-wins, so every stage after this one would never run: the person would be shown an incomplete '
|
|
1192
|
+
+ 'evidence package and the mechanical stages behind them would be silently deleted (the composition law: '
|
|
1193
|
+
+ 'mechanical first, human last)', { kind: s.kind, stage: label, at: i, last: stages.length - 1 });
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1138
1196
|
const p = s.params;
|
|
1139
1197
|
if (!isObj(p)) { red('missing-field', `${at}.params`, `an object carrying: ${spec.required.join(', ')}`); return; }
|
|
1140
1198
|
for (const k of spec.required) {
|
|
@@ -1338,6 +1396,15 @@ function checkKind({ kind, params: p, at, red, scoped, label, populations, envOw
|
|
|
1338
1396
|
if (!strArray(p.allowPrefixes)) red('invalid-value', `${at}.params.allowPrefixes`, 'a non-empty array of path prefixes');
|
|
1339
1397
|
if (p.requireNonEmpty !== true) red('invalid-value', `${at}.params.requireNonEmpty`, 'literally true');
|
|
1340
1398
|
break;
|
|
1399
|
+
case 'human-confirms':
|
|
1400
|
+
// the whole surface: the question the signer answers. It is shown to a
|
|
1401
|
+
// person verbatim, so an empty one is a bare "approve?" — ruling 2's
|
|
1402
|
+
// named failure mode, refused here rather than rendered.
|
|
1403
|
+
if (!isNonEmptyString(p.ask)) {
|
|
1404
|
+
red('invalid-value', `${at}.params.ask`, 'the plain question the signer answers while looking at the finished '
|
|
1405
|
+
+ 'result — a human stage with nothing to ask is the bare "approve?" ruling 2 forbids');
|
|
1406
|
+
}
|
|
1407
|
+
break;
|
|
1341
1408
|
default:
|
|
1342
1409
|
red('unknown-kind', `${at}.kind`, `"${kind}" is in the catalogue but has no checker`, { kind });
|
|
1343
1410
|
}
|
package/src/authorjob.js
CHANGED
|
@@ -96,7 +96,7 @@ import {
|
|
|
96
96
|
GENRE_LANGUAGES, LOCKED_KINDS, TYPES_GENRE, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES,
|
|
97
97
|
} from './authoring.js';
|
|
98
98
|
import { QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose } from './authorflow.js';
|
|
99
|
-
import { runAuthorScout, buildSeedListing } from './authorscout.js';
|
|
99
|
+
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
100
100
|
import {
|
|
101
101
|
DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
|
|
102
102
|
} from './declaredclose.js';
|
|
@@ -366,6 +366,8 @@ function composerRefusal(reds) {
|
|
|
366
366
|
* verdictType?: string|null,
|
|
367
367
|
* questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
|
|
368
368
|
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
|
|
369
|
+
* onPhase?: (phase: string, data?: any) => void,
|
|
370
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
369
371
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
370
372
|
* authorFn?: Function, authorOpts?: object}} o
|
|
371
373
|
* @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
|
|
@@ -376,6 +378,13 @@ export async function authorCloseForJob({
|
|
|
376
378
|
answers, repoPath = null, lang, verdictType = null, questions = null,
|
|
377
379
|
generate, provider = null, seedRef = null, scout = null, listing = null,
|
|
378
380
|
ceilingUsd = null,
|
|
381
|
+
// THE PROGRESS SEAMS. This composition is the pipeline's long silence — a real
|
|
382
|
+
// survey, a real declaration ladder, and a real toolchain per close stage — and
|
|
383
|
+
// it emitted nothing until it returned, so an operator watching a live run
|
|
384
|
+
// could not tell work from a hang. The library REPORTS and the shell PRINTS
|
|
385
|
+
// (`runJob`'s `emit` → `runPlan`, the same shape): nothing here reads either
|
|
386
|
+
// callback back, and no decision in this file or below it depends on one.
|
|
387
|
+
onPhase = () => {}, onCall = () => {},
|
|
379
388
|
seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
|
|
380
389
|
authorFn = authorClose, authorOpts = {},
|
|
381
390
|
}) {
|
|
@@ -425,6 +434,10 @@ export async function authorCloseForJob({
|
|
|
425
434
|
// D8 — the seed, READ rather than typed.
|
|
426
435
|
let seed = seedRef;
|
|
427
436
|
if (!isNonEmptyString(seed)) {
|
|
437
|
+
// Announced only on the path that actually READS one: a caller who handed a
|
|
438
|
+
// seedRef in never paid for a git call, and a phase line for work nobody did
|
|
439
|
+
// is the blind-instrument class, one line wide.
|
|
440
|
+
onPhase('seed', {});
|
|
428
441
|
const s = await seedFn(workdir);
|
|
429
442
|
if (s.stop !== null) {
|
|
430
443
|
// same git-stderr channel as `prepareSigning`'s, scrubbed at the same
|
|
@@ -445,15 +458,46 @@ export async function authorCloseForJob({
|
|
|
445
458
|
// form). `null` travels EXPLICITLY rather than being left off — unbounded is a
|
|
446
459
|
// stated operator choice, and an absent field read as falsy is the same state
|
|
447
460
|
// arrived at by accident.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
461
|
+
/** @type {any} */
|
|
462
|
+
let survey = scout;
|
|
463
|
+
// `== null` and not `!survey`: this is the `??` these two lines replaced, kept
|
|
464
|
+
// exactly — a caller-supplied survey is used whatever it is, and only an
|
|
465
|
+
// ABSENT one is paid for.
|
|
466
|
+
if (survey == null) {
|
|
467
|
+
// The scout is the pipeline's FIRST paid stage and its longest single
|
|
468
|
+
// silence: up to three ladder attempts, each a real toolable survey over a
|
|
469
|
+
// real tree. `attempts` is the ceiling the scout enforces on itself, so the
|
|
470
|
+
// reader knows how many the line could repeat for.
|
|
471
|
+
onPhase('scout', { attempts: SCOUT_ATTEMPTS });
|
|
472
|
+
survey = await scoutFn({ workdir, provider, ceilingUsd, onCall });
|
|
473
|
+
// …and the survey's own VERDICT, said the moment it lands. `state` is what
|
|
474
|
+
// every downstream refusal keys on (F59: absent is never "no facts needed"),
|
|
475
|
+
// and the fact count is the one number that says whether the ladder came
|
|
476
|
+
// back with anything to author from.
|
|
477
|
+
onPhase('scout-done', { state: survey?.state ?? null, facts: Object.keys(survey?.facts ?? {}).length });
|
|
478
|
+
}
|
|
479
|
+
/** @type {any} */
|
|
480
|
+
let seeds = listing;
|
|
481
|
+
if (seeds == null) {
|
|
482
|
+
onPhase('listing', {});
|
|
483
|
+
seeds = await listingFn({
|
|
484
|
+
workdir, seedRef: seed, sourcePaths: survey?.facts?.sourcePaths, testPaths: survey?.facts?.testPaths,
|
|
485
|
+
});
|
|
486
|
+
// the count is only knowable AFTER, so it rides the done line rather than
|
|
487
|
+
// being guessed onto the start one
|
|
488
|
+
onPhase('listing-done', { files: seeds?.files?.length ?? null, stop: seeds?.stop ?? null });
|
|
489
|
+
}
|
|
452
490
|
|
|
491
|
+
// The declaration ladder. The REVISION CAP is deliberately not quoted here:
|
|
492
|
+
// the tighten-only clamp lives in `authorClose` (and `authorOpts` may lower
|
|
493
|
+
// it), so a number spelled at this call site could disagree with the one the
|
|
494
|
+
// loop obeys — `author-call` carries the enforced cap instead, from where it
|
|
495
|
+
// is computed.
|
|
496
|
+
onPhase('author', {});
|
|
453
497
|
const authored = await authorFn({
|
|
454
498
|
workdir, seedRef: seed, lang, verdictType: picked,
|
|
455
499
|
answers: interview.answers, questions: questions ?? questionsFor(picked),
|
|
456
|
-
scout: survey, listing: seeds, generate, ceilingUsd, ...authorOpts,
|
|
500
|
+
scout: survey, listing: seeds, generate, ceilingUsd, onPhase, onCall, ...authorOpts,
|
|
457
501
|
});
|
|
458
502
|
|
|
459
503
|
if (!authored.ok) {
|
package/src/authorscout.js
CHANGED
|
@@ -349,6 +349,7 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
349
349
|
* @param {{workdir: string, provider?: any,
|
|
350
350
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
351
351
|
* attempts?: number, ceilingUsd?: number|null,
|
|
352
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
352
353
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
353
354
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
354
355
|
* @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
|
|
@@ -364,6 +365,7 @@ export async function runAuthorScout({
|
|
|
364
365
|
ctx = true,
|
|
365
366
|
attempts = SCOUT_ATTEMPTS,
|
|
366
367
|
ceilingUsd = null,
|
|
368
|
+
onCall = () => {},
|
|
367
369
|
createLoop = defaultLoop,
|
|
368
370
|
createSurveyor = defaultSurveyor,
|
|
369
371
|
}) {
|
|
@@ -384,7 +386,17 @@ export async function runAuthorScout({
|
|
|
384
386
|
* @param {string} label @param {number} attempt @param {any} r @returns {number}
|
|
385
387
|
*/
|
|
386
388
|
const record = (label, attempt, r) => {
|
|
387
|
-
|
|
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 });
|
|
388
400
|
return raws.push(scrubRaw({ label, attempt, text: r?.text })) - 1;
|
|
389
401
|
};
|
|
390
402
|
// TIGHTEN-ONLY: an operator may lower the ceiling, never raise it. A garbage
|
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:
|
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
|
|
@@ -103,6 +107,8 @@ export {
|
|
|
103
107
|
// the verdict-class surface (PRD v1.57 §1–§2): the radio's own menu, the guard
|
|
104
108
|
// battery keyed off it, and the ceiling that makes the pick a promise
|
|
105
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,
|
|
106
112
|
} from './authoring.js';
|
|
107
113
|
// M3 — the looking half and the writing half. `authorClose` is the grounded
|
|
108
114
|
// loop; `runAuthorScout`/`buildSeedListing` are exported because a caller that
|
|
@@ -128,6 +134,7 @@ export {
|
|
|
128
134
|
export {
|
|
129
135
|
validateCloseDecl, runDeclaredStages, declaredStages, isDeclaredClose, guardNames, closeGrade,
|
|
130
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,
|
|
131
138
|
} from './declaredclose.js';
|
|
132
139
|
export { closeStagesOf } from './plan.js';
|
|
133
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). */
|