bareloop 0.7.0 → 0.9.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 +704 -0
- package/README.md +13 -4
- package/bareloop.context.md +351 -32
- package/package.json +1 -1
- package/src/authorflow.js +1280 -0
- package/src/authoring.js +1296 -0
- package/src/authorjob.js +793 -0
- package/src/authorscout.js +706 -0
- package/src/bridges.js +2 -2
- package/src/declaredclose.js +519 -0
- package/src/exits.js +8 -2
- package/src/index.js +72 -0
- package/src/job.js +91 -10
- package/src/kinds.js +1354 -0
- package/src/ledger.js +70 -9
- package/src/plan.js +57 -101
- package/src/planrun.js +554 -75
- package/src/ralph.js +27 -5
- package/src/reuse.js +135 -11
- package/src/root.js +8 -1
- package/src/run.js +29 -3
- package/src/text.js +120 -0
- package/src/validate.js +121 -0
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +720 -0
- package/types/authoring.d.ts +447 -0
- package/types/authorjob.d.ts +241 -0
- package/types/authorscout.d.ts +307 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +14 -2
- package/types/kinds.d.ts +343 -0
- package/types/plan.d.ts +41 -38
- package/types/planrun.d.ts +125 -18
- package/types/ralph.d.ts +17 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +23 -2
- package/types/text.d.ts +84 -0
- package/types/validate.d.ts +50 -0
- package/types/workbranch.d.ts +102 -0
package/src/job.js
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// judgment call. Minting policy is product doctrine, not job-authorable.
|
|
15
15
|
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
|
-
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals } from './validate.js';
|
|
17
|
+
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier } from './validate.js';
|
|
18
|
+
import { validateCloseDecl, DECLARED_CLOSE_CLASSES } from './declaredclose.js';
|
|
18
19
|
|
|
19
20
|
// The menus below ARE the close-authoring hierarchy (PRD §7) and ship frozen:
|
|
20
21
|
// they are read at call time, so a mutable export would let adopter code
|
|
@@ -25,7 +26,20 @@ export const CLOSE_TYPES = Object.freeze(['predicate', 'gold', 'rubric', 'hitl']
|
|
|
25
26
|
* type cannot legally claim (green-on-rubric is laundering). The `CLASSES`
|
|
26
27
|
* vocabulary export went with steps[] (PRD v1.32): a class was a STEP field, and
|
|
27
28
|
* nothing declares one now. */
|
|
28
|
-
export const CLASS_BY_CLOSE = Object.freeze({
|
|
29
|
+
export const CLASS_BY_CLOSE = Object.freeze({
|
|
30
|
+
predicate: Object.freeze(['hard']),
|
|
31
|
+
gold: Object.freeze(['hard']),
|
|
32
|
+
rubric: Object.freeze(['soft']),
|
|
33
|
+
hitl: Object.freeze(['hitl']),
|
|
34
|
+
// The AUTHORED close (M4, `closeDecl`) is not a `close.type` — it is the other
|
|
35
|
+
// field a plan-shape spec may carry — but it claims a verdict class exactly
|
|
36
|
+
// like one, so it belongs in the SAME table rather than in a second constant
|
|
37
|
+
// beside it. Hard by construction: every live kind is a mechanical command
|
|
38
|
+
// whose measurement is the truth, and the two judgment kinds are locked out at
|
|
39
|
+
// the catalogue and absent from the executor. The suite pins the key set
|
|
40
|
+
// against CLOSE_TYPES so the two can never drift apart silently.
|
|
41
|
+
declared: DECLARED_CLOSE_CLASSES,
|
|
42
|
+
});
|
|
29
43
|
export const GOLD_COMPARE = Object.freeze(['exact', 'json-equal']);
|
|
30
44
|
/** the tool grant menu (2b interview #1): read/grep/write, plus the two litectx
|
|
31
45
|
* RETRIEVAL verbs (F19) — `run` stays locked-but-listed; a spec requesting it
|
|
@@ -108,11 +122,11 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
|
|
|
108
122
|
// `steps` stays in the field list ONLY so a retired spec reds by name
|
|
109
123
|
// (`shape-retired`) instead of falling through to a generic unknown-field —
|
|
110
124
|
// the operator gets told what happened, not just that something is wrong.
|
|
111
|
-
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'checks', 'tools'];
|
|
125
|
+
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
|
|
112
126
|
/** the four-field plan shape's core (decision 5) — presence of any of these
|
|
113
127
|
* declares the shape; `tools` (the ceiling) rides the shape but alone does not
|
|
114
128
|
* declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
|
|
115
|
-
const PLAN_CORE_FIELDS = ['goal', 'verdictType', 'close', 'checks'];
|
|
129
|
+
const PLAN_CORE_FIELDS = ['goal', 'verdictType', 'close', 'closeDecl', 'checks'];
|
|
116
130
|
/** exact field set per close type — anything else is an unknown-field red
|
|
117
131
|
* (freeform code, script bodies, and minting claims all land there) */
|
|
118
132
|
const CLOSE_FIELDS = {
|
|
@@ -123,7 +137,12 @@ const CLOSE_FIELDS = {
|
|
|
123
137
|
};
|
|
124
138
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
125
139
|
|
|
126
|
-
/** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red — `verb`
|
|
140
|
+
/** @typedef {{code: string, path: string, detail?: string, verb?: string, lib?: string}} Red — `verb` and `lib` ride request-reds as structured data (the ledger keys on them, never on prose).
|
|
141
|
+
* `lib` is the TERRITORY the demand lands against, stamped HERE at the emit site
|
|
142
|
+
* (the typed-lib rule): one code, two catalogues — a locked TOOL verb is demand
|
|
143
|
+
* against the worker-surface package, a locked VERDICT type is demand against
|
|
144
|
+
* bareloop's OWN menu. Inferring it downstream is how a bareloop refusal gets
|
|
145
|
+
* filed as an upstream bug (the BA-2 misattribution class). */
|
|
127
146
|
|
|
128
147
|
/**
|
|
129
148
|
* Validate an operator-owned job spec (`schema: "job-v1"`). Never throws on
|
|
@@ -278,6 +297,21 @@ function validateClose(close, at, red) {
|
|
|
278
297
|
return true;
|
|
279
298
|
}
|
|
280
299
|
|
|
300
|
+
/**
|
|
301
|
+
* The F49 reject's OPERATOR-side detail. ONE spelling for both regex fields —
|
|
302
|
+
* they differ only in which untimed evaluator the pattern reaches — so the two
|
|
303
|
+
* reds can never drift into saying different things about the same hazard.
|
|
304
|
+
*
|
|
305
|
+
* The second sentence is the part an operator cannot derive: on the agent side
|
|
306
|
+
* F49 is self-DoS, but these two patterns are compiled and run in BARELOOP'S OWN
|
|
307
|
+
* PROCESS, where the hang blocks the event loop the in-process stall fuse lives
|
|
308
|
+
* on (F67 — a guard sharing the fate of the process it guards). The run then
|
|
309
|
+
* dies to the outside watchdog and reads as a model stall, so without this red a
|
|
310
|
+
* bad regex in a SIGNED spec is diagnosed as a provider problem.
|
|
311
|
+
* @param {string} evaluator the consumer that would run it, named concretely
|
|
312
|
+
*/
|
|
313
|
+
const redosDetail = (evaluator) => `nested unbounded quantifier (e.g. (a+)+ , (\\d*)* , (x+){1,}) — a catastrophic-backtracking footgun that can hang ${evaluator}, untimed (F49). It runs in bareloop's own process, so the hang blocks the event loop the stall fuse lives on (F67) and the run dies to the outside watchdog looking like a model stall; rewrite without a repeated group inside a repeat`;
|
|
314
|
+
|
|
281
315
|
/**
|
|
282
316
|
* The predicate BODY contract — cmd/expect/judged/gapKeep. Shared verbatim by
|
|
283
317
|
* the predicate close and the Layer 2 named checks (decision 1: a check runs
|
|
@@ -330,6 +364,13 @@ function predicateBody(o, at, red) {
|
|
|
330
364
|
// the fake green this floor exists to catch). Alternation stays
|
|
331
365
|
// fully expressible with non-capturing branches: `(?:a|b) (\d+)`.
|
|
332
366
|
else if (groups > 1) red('invalid-value', `${at}.judged.pattern`, `${groups} capture groups — the count is read from group 1 only; use exactly one capture group and non-capturing (?:…) branches`);
|
|
367
|
+
// F49, operator side: `runClose` execs this against the close's whole
|
|
368
|
+
// stdout+stderr with no timeout. Gated on `groups >= 0` — that is the
|
|
369
|
+
// COMPILED flag in disguise (the catch sets -1), and the scan documents
|
|
370
|
+
// that it assumes syntactically valid input.
|
|
371
|
+
if (groups >= 0 && hasNestedQuantifier(j.pattern)) {
|
|
372
|
+
red('invalid-value', `${at}.judged.pattern`, redosDetail('the close reader (runClose)'));
|
|
373
|
+
}
|
|
333
374
|
}
|
|
334
375
|
if (!Number.isInteger(j.min) || j.min < 1) {
|
|
335
376
|
red('invalid-value', `${at}.judged.min`, 'integer >= 1 — a floor of 0 is satisfied by judging nothing, which is the check it is meant to make');
|
|
@@ -348,8 +389,15 @@ function predicateBody(o, at, red) {
|
|
|
348
389
|
if (o.gapKeep !== undefined) {
|
|
349
390
|
if (!isNonEmptyString(o.gapKeep)) red('invalid-value', `${at}.gapKeep`, 'regex source string — lines matching it survive the gap bound (F28)');
|
|
350
391
|
else {
|
|
351
|
-
|
|
392
|
+
let compiled = false;
|
|
393
|
+
try { new RegExp(o.gapKeep, 'm'); compiled = true; }
|
|
352
394
|
catch { red('invalid-value', `${at}.gapKeep`, 'must compile as a RegExp'); }
|
|
395
|
+
// F49, operator side and the measured one: `boundGap` runs this per LINE
|
|
396
|
+
// over the whole close stream, untimed. Measured: `(a+)+$` against a
|
|
397
|
+
// 40-char line does not finish in 20s.
|
|
398
|
+
if (compiled && hasNestedQuantifier(o.gapKeep)) {
|
|
399
|
+
red('invalid-value', `${at}.gapKeep`, redosDetail('the gap bound (boundGap), once per line of close output'));
|
|
400
|
+
}
|
|
353
401
|
}
|
|
354
402
|
}
|
|
355
403
|
}
|
|
@@ -486,8 +534,9 @@ function validatePlanShape(spec, red, reds) {
|
|
|
486
534
|
else if (LOCKED_VERDICTS.includes(spec.verdictType)) {
|
|
487
535
|
// declared-but-locked (disclosure ≠ admission, the tool-menu pattern):
|
|
488
536
|
// the type rides as a structured field — the ledger keys admission demand
|
|
489
|
-
// on it, never on prose
|
|
490
|
-
|
|
537
|
+
// on it, never on prose. `lib: 'bareloop'`: the refused catalogue is OURS
|
|
538
|
+
// (VERDICT_TYPES), so this demand is never an upstream ask.
|
|
539
|
+
reds.push({ code: 'request-red', path: 'verdictType', verb: spec.verdictType, lib: 'bareloop', detail: `"${spec.verdictType}" is declared-but-locked — not at this rung (v1 admits green only); this red IS the admission evidence, never a grant` });
|
|
491
540
|
demanded = CLASS_BY_VERDICT[spec.verdictType];
|
|
492
541
|
} else if (!VERDICT_TYPES.includes(spec.verdictType)) {
|
|
493
542
|
red('invalid-value', 'verdictType', `menu: ${VERDICT_TYPES.join('|')} — an unknown type is a typo, never a request`);
|
|
@@ -495,7 +544,37 @@ function validatePlanShape(spec, red, reds) {
|
|
|
495
544
|
demanded = CLASS_BY_VERDICT[spec.verdictType];
|
|
496
545
|
}
|
|
497
546
|
|
|
498
|
-
|
|
547
|
+
// THE TWO CLOSE FIELDS, and they are ALTERNATIVES (M4). `close` is the
|
|
548
|
+
// operator's own command stages; `closeDecl` is the AUTHORED declaration over
|
|
549
|
+
// owned kinds — the whole point of the close-authoring rung, and the field a
|
|
550
|
+
// user who is not an engineer ends up with. Both at once is refused rather
|
|
551
|
+
// than merged or precedence-ordered: a spec carrying two closes has two
|
|
552
|
+
// arbiters, and picking one silently is how the signed artefact stops meaning
|
|
553
|
+
// what the signer read.
|
|
554
|
+
if (spec.close !== undefined && spec.closeDecl !== undefined) {
|
|
555
|
+
red('close-duplicated', 'closeDecl', 'a job declares EITHER a command close (close) OR an authored declaration '
|
|
556
|
+
+ '(closeDecl) — never both. Two closes are two arbiters, and the close is the only truth');
|
|
557
|
+
} else if (spec.closeDecl !== undefined) {
|
|
558
|
+
// The DECLARED close. The tree-grounded half of the gate (hamr's listing
|
|
559
|
+
// rule, and the scoped-job derivation that arms the F84 one-population law)
|
|
560
|
+
// is DEFERRED here and re-run by the runner against the real seed before any
|
|
561
|
+
// stage executes — a job spec is validated with no repository in hand, and a
|
|
562
|
+
// validator that quietly skipped those two rules would report a declaration
|
|
563
|
+
// it never examined. `grounded: false` is what the runner keys on.
|
|
564
|
+
// the class rides along: the mandatory guard battery keys off the verdict
|
|
565
|
+
// class the user picked (PRD v1.57 §2), so the gate cannot check D5 without
|
|
566
|
+
// it. An unknown or locked class is refused there, on its own axis, beside
|
|
567
|
+
// the counted `request-red` above.
|
|
568
|
+
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType });
|
|
569
|
+
for (const r of cd.reds) reds.push(r);
|
|
570
|
+
if (demanded !== undefined && !CLASS_BY_CLOSE.declared.includes(demanded)) {
|
|
571
|
+
// the same laundering guard as the close-type hierarchy, one level up: a
|
|
572
|
+
// declared close is HARD, so a soft/hitl verdict can never ride one
|
|
573
|
+
red('close-hierarchy', 'verdictType', `verdictType ${spec.verdictType} demands a ${demanded}-class close; an `
|
|
574
|
+
+ `authored declaration admits ${CLASS_BY_CLOSE.declared.join('|')} only — every live kind is a mechanical `
|
|
575
|
+
+ 'measurement, and the two judgment kinds are locked out of the catalogue (PRD §7)');
|
|
576
|
+
}
|
|
577
|
+
} else if (spec.close === undefined) red('missing-required', 'close', 'declared green with nothing to run it — preflight validates the declaration, never infers');
|
|
499
578
|
// The staged close (PRD v1.28): an ordered list of named command stages, from
|
|
500
579
|
// which the check menu derives. The object form remains ONLY for the
|
|
501
580
|
// declared-but-locked verdict classes (gold/rubric/hitl), which name no
|
|
@@ -529,7 +608,9 @@ function validatePlanShape(spec, red, reds) {
|
|
|
529
608
|
red('invalid-value', 'tools', `non-empty unique subset of ${TOOL_MENU.join('|')}`);
|
|
530
609
|
} else {
|
|
531
610
|
for (const t of spec.tools.filter((/** @type {string} */ t) => LOCKED_TOOLS.includes(t))) {
|
|
532
|
-
|
|
611
|
+
// `lib: 'bare-agent'`: a locked TOOL verb is demand against the
|
|
612
|
+
// worker-surface package that would have to expose it (the original case)
|
|
613
|
+
reds.push({ code: 'request-red', path: 'tools', verb: t, lib: 'bare-agent', detail: `"${t}" is locked-but-listed — this red IS the admission evidence, never a grant; granted menu: ${TOOL_MENU.join('|')}` });
|
|
533
614
|
}
|
|
534
615
|
const unknown = spec.tools.filter((/** @type {string} */ t) => !TOOL_MENU.includes(t) && !LOCKED_TOOLS.includes(t));
|
|
535
616
|
if (unknown.length) red('invalid-value', 'tools', `unknown tool(s) ${unknown.join(', ')} — menu: ${TOOL_MENU.join('|')}`);
|