bareloop 0.26.0 → 0.28.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 +271 -0
- package/bareloop.context.md +308 -38
- package/package.json +4 -3
- package/src/authorflow.js +255 -33
- package/src/authorjob.js +12 -9
- package/src/authorreadout.js +323 -0
- package/src/authorrun.js +1337 -0
- package/src/authorscout.js +16 -11
- package/src/cli.js +130 -8
- package/src/index.js +14 -2
- package/src/interviewrun.js +842 -0
- package/src/job.js +42 -26
- package/src/planrun.js +82 -3
- package/src/promptregisters.js +8 -0
- package/src/providers.js +36 -0
- package/src/replayio.js +153 -0
- package/src/reuse.js +1 -1
- package/src/run.js +1 -1
- package/src/source.js +138 -1
- package/src/text.js +45 -0
- package/src/u-patient.js +73 -0
- package/src/u-readout.js +451 -0
- package/src/userrun.js +2273 -0
- package/src/validate.js +25 -1
- package/types/authorflow.d.ts +49 -7
- package/types/authorreadout.d.ts +172 -0
- package/types/authorrun.d.ts +37 -0
- package/types/index.d.ts +3 -2
- package/types/interviewrun.d.ts +46 -0
- package/types/job.d.ts +21 -0
- package/types/planrun.d.ts +43 -3
- package/types/providers.d.ts +28 -0
- package/types/replayio.d.ts +93 -0
- package/types/reuse.d.ts +2 -2
- package/types/run.d.ts +2 -2
- package/types/source.d.ts +45 -0
- package/types/text.d.ts +35 -0
- package/types/u-patient.d.ts +39 -0
- package/types/u-readout.d.ts +247 -0
- package/types/userrun.d.ts +134 -0
- package/types/validate.d.ts +22 -0
package/src/validate.js
CHANGED
|
@@ -41,11 +41,34 @@ export function globToPrefix(scope) {
|
|
|
41
41
|
return p;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Path segments a fence/scope may never contain, checked WHOLE-SEGMENT
|
|
46
|
+
* (`prefix.split('/')`), never substring — a look-alike (`.github`,
|
|
47
|
+
* `.gitignore`, `my.git`, `node_modules_util`) is a DIFFERENT name and stays
|
|
48
|
+
* admitted. hamr's ruling (option B, 2026-09-14, block both): F178 — a
|
|
49
|
+
* signed fence naming `.git` could let a worker write git's own files (refs,
|
|
50
|
+
* the seed, `info/exclude`) and hide those writes from `changedSet`. F177's
|
|
51
|
+
* follow-up — a signed fence naming `node_modules` directly is the same class
|
|
52
|
+
* of gap (an installed/worker-written file inside it is invisible to
|
|
53
|
+
* `changedSet` the same way, and installs are the person's job now, F177).
|
|
54
|
+
* This catches only a fence that NAMES one of these directly; a fence like
|
|
55
|
+
* `packages/api/**` that later reaches a NESTED `node_modules` beneath it is a
|
|
56
|
+
* distinct, only partially closeable gap — see the runtime belt in
|
|
57
|
+
* planrun.js for `node_modules` (`.git` has no such gap: there is exactly one
|
|
58
|
+
* `.git` per repo, always at the workdir root, so its own runtime belt is a
|
|
59
|
+
* single `fs.deny` entry). Both are inexpressible at declaration time, the
|
|
60
|
+
* same law #1 already applies to `..`.
|
|
61
|
+
* @type {readonly string[]}
|
|
62
|
+
*/
|
|
63
|
+
export const FORBIDDEN_SCOPE_SEGMENTS = Object.freeze(['.git', 'node_modules']);
|
|
64
|
+
|
|
44
65
|
/**
|
|
45
66
|
* A scope may never reach the arbiter's inputs (design law #1): it must
|
|
46
67
|
* resolve to a PROPER subdirectory of the run directory. Absolute paths and
|
|
47
68
|
* ".." segments escape it; "." / "./**" cover the whole run directory — where
|
|
48
69
|
* the close suite lives. Windows spellings ("..\", "C:\") count as escapes.
|
|
70
|
+
* A `.git` or `node_modules` segment anywhere in the prefix is refused too
|
|
71
|
+
* (F178/F177, see {@link FORBIDDEN_SCOPE_SEGMENTS}).
|
|
49
72
|
* Exported for the job validator: the operator's outer fence obeys the SAME
|
|
50
73
|
* law through the same code — two containment transforms would be the F9
|
|
51
74
|
* red-class one level up.
|
|
@@ -59,7 +82,8 @@ export function scopeContained(s) {
|
|
|
59
82
|
// that normalizes to something absolute can never pass (defense in depth
|
|
60
83
|
// against a future globToPrefix regression; the un-gameable gate, law #1).
|
|
61
84
|
if (prefix === '' || prefix === '.' || prefix.startsWith('/') || /^[a-zA-Z]:/.test(prefix)) return false;
|
|
62
|
-
|
|
85
|
+
const segments = prefix.split('/');
|
|
86
|
+
return segments.every((seg) => seg !== '..' && !FORBIDDEN_SCOPE_SEGMENTS.includes(seg));
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
/** Shared with the job validator — one definition of "a JSON object" for both
|
package/types/authorflow.d.ts
CHANGED
|
@@ -254,13 +254,21 @@ export function writeScopeBlock(writeScope: string[]): string;
|
|
|
254
254
|
* no listed check can verify — shown so the composer names the gap in its
|
|
255
255
|
* own notes rather than the plan quietly reading as if everything asked for
|
|
256
256
|
* is covered.
|
|
257
|
-
*
|
|
257
|
+
* `answeredQuestions` (F175's open half, run mu0voeo4, hamr's 2026-09-16
|
|
258
|
+
* ruling) is every `Q:/A:` pair the person answered inline when a "Confirm"
|
|
259
|
+
* or "Type the goal yourself" pick forced a raised question before
|
|
260
|
+
* acceptance — BINDING, never re-decided by the composer, and a DIFFERENT
|
|
261
|
+
* meaning from `openQuestions` (which means "could not resolve"): the two
|
|
262
|
+
* never merge.
|
|
263
|
+
* @param {{checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[],
|
|
264
|
+
* answeredQuestions?: string[]}} confirmed
|
|
258
265
|
*/
|
|
259
266
|
export function confirmedBlock(confirmed: {
|
|
260
267
|
checks: string[];
|
|
261
268
|
protections: string[];
|
|
262
269
|
openQuestions?: string[];
|
|
263
270
|
notChecked?: string[];
|
|
271
|
+
answeredQuestions?: string[];
|
|
264
272
|
}): string;
|
|
265
273
|
/**
|
|
266
274
|
* The whole authoring prompt, as one string.
|
|
@@ -284,7 +292,7 @@ export function confirmedBlock(confirmed: {
|
|
|
284
292
|
* guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
|
|
285
293
|
* ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any>,
|
|
286
294
|
* writeScope?: string[]|null,
|
|
287
|
-
* confirmed?: {checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[]}|null}} o
|
|
295
|
+
* confirmed?: {checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null}} o
|
|
288
296
|
*/
|
|
289
297
|
export function authorPrompt({ answers, questions, facts, listingBlock, lang, verdictType, guards, ownedEnvNames, mode, catalogue, writeScope, confirmed, }: {
|
|
290
298
|
answers: Record<string | number, string>;
|
|
@@ -308,6 +316,7 @@ export function authorPrompt({ answers, questions, facts, listingBlock, lang, ve
|
|
|
308
316
|
protections: string[];
|
|
309
317
|
openQuestions?: string[];
|
|
310
318
|
notChecked?: string[];
|
|
319
|
+
answeredQuestions?: string[];
|
|
311
320
|
} | null;
|
|
312
321
|
}): string;
|
|
313
322
|
/**
|
|
@@ -592,12 +601,20 @@ export function confirmTool(box: {
|
|
|
592
601
|
* listing already paid for, the write fence, the detected language, "worse
|
|
593
602
|
* than before" when the person gave one (repo only), and — on a fix round —
|
|
594
603
|
* what the person asked to change.
|
|
604
|
+
* F183 (2026-09-16, ruling A, run mu2qmept): also carries the REAL,
|
|
605
|
+
* code-derived `protections` list ({@link confirmProtections} — the same
|
|
606
|
+
* guards the close will actually compose plus the real write fence, never a
|
|
607
|
+
* second hand-typed list) as an ALREADY COVERED block, with the instruction
|
|
608
|
+
* that anything on it never belongs in `notChecked`. Twice live (mu2bjmed,
|
|
609
|
+
* mu2qmept) the model claimed a guard or the write fence was missing while
|
|
610
|
+
* blind to what was actually enforced — this is the fix: show it the facts
|
|
611
|
+
* before it drafts, never hand-write a matcher over its answer.
|
|
595
612
|
* @param {{answers: Record<string|number, string>, questions: Record<string|number, string>,
|
|
596
613
|
* labels?: Record<string|number, string>, facts?: any, listing?: string|null,
|
|
597
614
|
* writeScope?: string[]|null, isRepo: boolean, lang: string, worseThanBefore?: string,
|
|
598
|
-
* fixText?: string|null}} o
|
|
615
|
+
* fixText?: string|null, protections?: string[]}} o
|
|
599
616
|
*/
|
|
600
|
-
export function confirmPrompt({ answers, questions, labels, facts, listing, writeScope, isRepo, lang, worseThanBefore, fixText }: {
|
|
617
|
+
export function confirmPrompt({ answers, questions, labels, facts, listing, writeScope, isRepo, lang, worseThanBefore, fixText, protections }: {
|
|
601
618
|
answers: Record<string | number, string>;
|
|
602
619
|
questions: Record<string | number, string>;
|
|
603
620
|
labels?: Record<string | number, string>;
|
|
@@ -608,6 +625,7 @@ export function confirmPrompt({ answers, questions, labels, facts, listing, writ
|
|
|
608
625
|
lang: string;
|
|
609
626
|
worseThanBefore?: string;
|
|
610
627
|
fixText?: string | null;
|
|
628
|
+
protections?: string[];
|
|
611
629
|
}): string;
|
|
612
630
|
/**
|
|
613
631
|
* THE CONFIRM TURN: the $0 half (worse-than-before for a repo, a language
|
|
@@ -636,7 +654,7 @@ export function confirmPrompt({ answers, questions, labels, facts, listing, writ
|
|
|
636
654
|
* stop: null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart',
|
|
637
655
|
* rounds: number,
|
|
638
656
|
* accepted: {goal: string, checks: string[], protections: string[], lang: string,
|
|
639
|
-
* worseThanBefore: string, openQuestions: string[], notChecked: string[]}|null,
|
|
657
|
+
* worseThanBefore: string, openQuestions: string[], notChecked: string[], answeredQuestions?: string[]}|null,
|
|
640
658
|
* reds: Red[], cost: any}>}
|
|
641
659
|
*/
|
|
642
660
|
export function runConfirmTurn({ verdictType, answers, questions, labels, facts, listing, writeScope, isRepo, lang, generate, book, ask, onPhase, mode, worseThanBefore: presetWorseThanBefore, }: {
|
|
@@ -674,6 +692,7 @@ export function runConfirmTurn({ verdictType, answers, questions, labels, facts,
|
|
|
674
692
|
worseThanBefore: string;
|
|
675
693
|
openQuestions: string[];
|
|
676
694
|
notChecked: string[];
|
|
695
|
+
answeredQuestions?: string[];
|
|
677
696
|
} | null;
|
|
678
697
|
reds: Red[];
|
|
679
698
|
cost: any;
|
|
@@ -689,7 +708,13 @@ export function runConfirmTurn({ verdictType, answers, questions, labels, facts,
|
|
|
689
708
|
* The return is deliberately complete rather than tidy:
|
|
690
709
|
* - `ok` is true iff a declaration passed validation and was measured;
|
|
691
710
|
* - `declaration` is the LAST ACCEPTED one, genre-env injected — the exact form
|
|
692
|
-
* that will run and that the signature will cover;
|
|
711
|
+
* that will run and that the signature will cover; unless F176's fallback
|
|
712
|
+
* fired (see `fellBack`), in which case it is the newest SOUND accepted one
|
|
713
|
+
* instead — a "last accepted" close whose seed read instrument-stopped is
|
|
714
|
+
* never handed to a signer as though it were the best close this run found;
|
|
715
|
+
* - `fellBack` is `null` unless the fallback above fired, in which case it names
|
|
716
|
+
* `{from, to, brokenStages}` — which iteration was dropped, which one now
|
|
717
|
+
* stands, and which of the dropped one's stages instrument-stopped;
|
|
693
718
|
* - `reds` is what the LAST validated iteration said. `ok:true` with a non-empty
|
|
694
719
|
* `reds` means the final revision was rejected and the previous close stands:
|
|
695
720
|
* the rejection is reported, never hidden, and never silently shipped either;
|
|
@@ -717,7 +742,7 @@ export function runConfirmTurn({ verdictType, answers, questions, labels, facts,
|
|
|
717
742
|
* structuredMode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
|
|
718
743
|
* priorCalls?: {label: string, costUsd: number|null, unpricedRounds: number}[]|null,
|
|
719
744
|
* priorRaws?: any[]|null,
|
|
720
|
-
* confirmed?: {checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[]}|null}} o
|
|
745
|
+
* confirmed?: {checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null}} o
|
|
721
746
|
*/
|
|
722
747
|
export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, writeScope, scout, listing, priorCalls, priorRaws, confirmed, generate, seedReadFn, closeCtx, ceilingUsd, onPhase, onCall, maxRevisions, structureRetries, structuredMode, catalogue, }: {
|
|
723
748
|
workdir: string;
|
|
@@ -767,6 +792,7 @@ export function authorClose({ workdir, seedRef, lang, verdictType, answers, ques
|
|
|
767
792
|
protections: string[];
|
|
768
793
|
openQuestions?: string[];
|
|
769
794
|
notChecked?: string[];
|
|
795
|
+
answeredQuestions?: string[];
|
|
770
796
|
} | null;
|
|
771
797
|
}): Promise<any>;
|
|
772
798
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -1023,6 +1049,22 @@ export const LANGUAGE_PICK_FIELD: Readonly<{
|
|
|
1023
1049
|
label: "Language";
|
|
1024
1050
|
prompt: "This repo has more than one supported language's manifest at the same level. Which one is this job about?";
|
|
1025
1051
|
}>;
|
|
1052
|
+
/**
|
|
1053
|
+
* F175's open half (docs/logs/FINDINGS.md, run mu0voeo4), hamr's ruling
|
|
1054
|
+
* (2026-09-16): "Confirm" and "Type the goal yourself" may not bypass a
|
|
1055
|
+
* `questions` entry the model itself raised on the plan being accepted — the
|
|
1056
|
+
* person answers it inline, one question at a time, with NO extra model
|
|
1057
|
+
* call (the round cap and D3 are untouched). This is the `field` handed to
|
|
1058
|
+
* `ask({kind: 'answer', ...})` for each forced question — `question`,
|
|
1059
|
+
* `index` (1-based) and `total` ride alongside on the step itself, not on
|
|
1060
|
+
* this frozen constant, because they differ per question within one turn.
|
|
1061
|
+
*/
|
|
1062
|
+
export const ANSWER_QUESTION_FIELD: Readonly<{
|
|
1063
|
+
id: "answerQuestion";
|
|
1064
|
+
kind: "mechanical";
|
|
1065
|
+
label: "Answer";
|
|
1066
|
+
prompt: string;
|
|
1067
|
+
}>;
|
|
1026
1068
|
/**
|
|
1027
1069
|
* THE CONFIRM TURN'S MENU — a structured CHOICE the person picks from after
|
|
1028
1070
|
* each round, never free text matched against a pattern (ruling 6's "no
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{goal?: string|null, closeDecl?: any}} spec the RESOLVED spec — the bytes
|
|
3
|
+
* that get hashed and signed, never the operator's draft and never the authored
|
|
4
|
+
* half on its own.
|
|
5
|
+
* @returns {string[]} one console line each, in print order.
|
|
6
|
+
*/
|
|
7
|
+
export function declarationLines({ goal, closeDecl }: {
|
|
8
|
+
goal?: string | null;
|
|
9
|
+
closeDecl?: any;
|
|
10
|
+
}): string[];
|
|
11
|
+
/**
|
|
12
|
+
* F176'S FALLBACK, SAID OUT LOUD — the revise ladder kept the NEWEST SOUND
|
|
13
|
+
* declaration rather than the last one it happened to try, because "kept the
|
|
14
|
+
* last accepted revision" is not the same claim as "kept the best revision"
|
|
15
|
+
* (docs/logs/FINDINGS.md F176). Absent on every run where the fallback never
|
|
16
|
+
* fired (the overwhelming majority), which is why this returns an EMPTY list
|
|
17
|
+
* rather than a "no fallback" line — a green close has nothing to say here and
|
|
18
|
+
* printing it every run would be noise, not information.
|
|
19
|
+
* @param {{fellBack?: {from: string, to: string, brokenStages: string[]}|null}|null} authoring `authored.authoring`
|
|
20
|
+
* @returns {string[]}
|
|
21
|
+
*/
|
|
22
|
+
export function fellBackLines(authoring: {
|
|
23
|
+
fellBack?: {
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
brokenStages: string[];
|
|
27
|
+
} | null;
|
|
28
|
+
} | null): string[];
|
|
29
|
+
/**
|
|
30
|
+
* THE TWO SIGNED JUDGED ARTIFACTS, as a person reads them before signing
|
|
31
|
+
* (softgreen modules 4+5). Absent on a close that judges nothing, which is why
|
|
32
|
+
* this returns an EMPTY list rather than a "no card" line: a green close has no
|
|
33
|
+
* rubric and saying so every run is noise, not information.
|
|
34
|
+
*
|
|
35
|
+
* The CARD is printed in full — it is short, it is the whole standard the judge
|
|
36
|
+
* will hold the work to, and it is the signer's own words. The CASES are printed
|
|
37
|
+
* as a ROSTER (id, verdict, itemized reds) rather than in full: ten real source
|
|
38
|
+
* artifacts is a page of code the terminal cannot usefully show, and what a
|
|
39
|
+
* signer checks at this surface is that both polarities are there and that each
|
|
40
|
+
* red names something they recognise. The artifacts themselves are in the spec
|
|
41
|
+
* file, which is named right above this block.
|
|
42
|
+
* @param {{closeDecl?: any}} spec the RESOLVED spec
|
|
43
|
+
* @returns {string[]}
|
|
44
|
+
*/
|
|
45
|
+
export function rubricLines({ closeDecl }: {
|
|
46
|
+
closeDecl?: any;
|
|
47
|
+
}): string[];
|
|
48
|
+
/**
|
|
49
|
+
* THE CONFIRM TURN'S OPEN QUESTIONS (PRD item 33 M3 piece 4, ruling 5's D3
|
|
50
|
+
* addendum) — shown at the SIGNING readout, from `authored.json`'s own
|
|
51
|
+
* `confirmed.openQuestions`, never added to the signed spec format itself
|
|
52
|
+
* (D4: the closeDecl's `notes` are the model's own record; this is the
|
|
53
|
+
* person's "fix" text from a round that hit the 2-round cap, ruling 5,
|
|
54
|
+
* passed to the composer verbatim rather than spending a 3rd call, D3). A
|
|
55
|
+
* close with none is shown as `(none)` rather than an empty line — the
|
|
56
|
+
* absence is itself part of what the signer reads before signing.
|
|
57
|
+
* @param {{openQuestions?: string[]}|null} confirmed `authored.confirmed`
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
export function openQuestionLines(confirmed: {
|
|
61
|
+
openQuestions?: string[];
|
|
62
|
+
} | null): string[];
|
|
63
|
+
/**
|
|
64
|
+
* F175's open half, said at the SIGNING readout too (run mu0voeo4, hamr's
|
|
65
|
+
* 2026-09-16 ruling) — every question the plan raised that "Confirm" or
|
|
66
|
+
* "Type the goal yourself" forced the person to answer inline, from
|
|
67
|
+
* `authored.json`'s own `confirmed.answeredQuestions`. Shown only when
|
|
68
|
+
* non-empty (the overwhelming majority of plans raise no question at all,
|
|
69
|
+
* and printing an empty block every run would be noise, not information) —
|
|
70
|
+
* unlike {@link openQuestionLines}, an empty list here says nothing, because
|
|
71
|
+
* absence is the ordinary case, not a fact the signer needs flagged.
|
|
72
|
+
* @param {{answeredQuestions?: string[]}|null} confirmed `authored.confirmed`
|
|
73
|
+
* @returns {string[]}
|
|
74
|
+
*/
|
|
75
|
+
export function answeredQuestionLines(confirmed: {
|
|
76
|
+
answeredQuestions?: string[];
|
|
77
|
+
} | null): string[];
|
|
78
|
+
/**
|
|
79
|
+
* THE CALIBRATION GATE'S OWN READOUT — the one gate that spends money, and the
|
|
80
|
+
* one whose rows a signer has to read case by case.
|
|
81
|
+
*
|
|
82
|
+
* ITEMIZED, never an aggregate percentage: "9/10" tells a person nothing about
|
|
83
|
+
* which line of their own rubric is wrong, and the fix for a miss is a card line
|
|
84
|
+
* or a corrected case (§4.3's ceiling). A CASUALTY prints under its own name and
|
|
85
|
+
* says so, because a dead judge is not a failed set.
|
|
86
|
+
* @param {any} calibration `signing.gates.calibration`
|
|
87
|
+
* @returns {string[]}
|
|
88
|
+
*/
|
|
89
|
+
export function calibrationLines(calibration: any): string[];
|
|
90
|
+
/**
|
|
91
|
+
* THE AUTHORING CEILING, parsed from `--budget`. Housed here for the same reason
|
|
92
|
+
* the readout above is: the runner is a script, and a rule no test can reach is a
|
|
93
|
+
* rule nothing checks.
|
|
94
|
+
*
|
|
95
|
+
* ABSENT IS UNBOUNDED, and that is the only way to get there. A malformed value
|
|
96
|
+
* is an ERROR rather than a fallback: `--budget banana` silently collapsing to
|
|
97
|
+
* "no ceiling" is precisely the failure this flag exists to prevent, wearing the
|
|
98
|
+
* operator's own typo. Zero and negatives are rejected on the same rule — a
|
|
99
|
+
* ceiling that can fund nothing is a typo for "unbounded" far more often than it
|
|
100
|
+
* is a request, and the honest way to ask for nothing is not to ask.
|
|
101
|
+
*
|
|
102
|
+
* There is NO DEFAULT anywhere in this function, deliberately: a defaulted cap is
|
|
103
|
+
* a silent second ceiling (the `maxWallMs` precedent), and the number is the
|
|
104
|
+
* operator's to set or to decline.
|
|
105
|
+
* @param {string|null} raw the argv value, or null when the flag was not given
|
|
106
|
+
* @returns {{ceilingUsd: number|null, error: string|null}}
|
|
107
|
+
*/
|
|
108
|
+
export function parseCeiling(raw: string | null): {
|
|
109
|
+
ceilingUsd: number | null;
|
|
110
|
+
error: string | null;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The ceiling's header line. An UNBOUNDED run is ANNOUNCED — printed before the
|
|
114
|
+
* provider is built, so it reaches stdout ahead of the first paid byte rather
|
|
115
|
+
* than being inferred from the total afterwards. An unbounded run is legal; it is
|
|
116
|
+
* never allowed to be an accident.
|
|
117
|
+
* @param {number|null} ceilingUsd
|
|
118
|
+
*/
|
|
119
|
+
export function ceilingLine(ceilingUsd: number | null): string;
|
|
120
|
+
/**
|
|
121
|
+
* ONE PROGRESS PHASE, as a console line.
|
|
122
|
+
*
|
|
123
|
+
* The pipeline used to run up to ~15 minutes saying nothing between
|
|
124
|
+
* `author-start` and its result — a real survey ladder, a real declaration
|
|
125
|
+
* ladder, and a real toolchain per close stage, all inside one await. Silence
|
|
126
|
+
* and a hang are the same bytes on a terminal, and the operator's only lever is
|
|
127
|
+
* to kill a run that may be working.
|
|
128
|
+
*
|
|
129
|
+
* It says WHAT is happening, never how far along it is: there is no honest
|
|
130
|
+
* fraction to print (a survey attempt has no progress, and a suite stage
|
|
131
|
+
* finishes when it finishes), and an invented percentage is a number nobody
|
|
132
|
+
* measured. What it does carry is the two facts a person waiting actually uses —
|
|
133
|
+
* which phase, and how long the last thing took.
|
|
134
|
+
*
|
|
135
|
+
* `durationMs` prints as UNKNOWN when it is absent rather than as 0 (F6's rule,
|
|
136
|
+
* in its time form): a stage whose duration was never measured did not take no
|
|
137
|
+
* time.
|
|
138
|
+
* @param {string} phase @param {any} [data]
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function phaseLine(phase: string, data?: any): string;
|
|
142
|
+
/**
|
|
143
|
+
* ONE crashed authoring run, ready for the spine.
|
|
144
|
+
*
|
|
145
|
+
* It exists because a crash used to leave NO body. `run-author.mjs` awaited the
|
|
146
|
+
* paid pipeline at the top level, so a throw anywhere inside it went to the
|
|
147
|
+
* operator's terminal as an unhandled rejection and the spine kept exactly one
|
|
148
|
+
* line — `author-start` — which is byte-for-byte what a run still in flight looks
|
|
149
|
+
* like. The terminal scrolls; the spine is the record.
|
|
150
|
+
*
|
|
151
|
+
* The STACK is the evidence (the incident that minted this had no idea WHERE the
|
|
152
|
+
* throw came from), and a stack is the one field here that can be arbitrarily
|
|
153
|
+
* long and can quote anything the process had open. So it goes through `scrubRaw`
|
|
154
|
+
* — the ONE persist boundary for a raw blob — which redacts it over the same
|
|
155
|
+
* `SECRET_PATTERNS` inventory the validator reds on and bounds it with the bound
|
|
156
|
+
* announcing its own size (F28). Nothing here is hand-rolled: a second spelling
|
|
157
|
+
* of either rule is how the two drift.
|
|
158
|
+
*
|
|
159
|
+
* A non-Error throw (a string, a plain object) is honest rather than coerced into
|
|
160
|
+
* an Error shape: it has no `name`, so `name` is `null`, and `String(err)` is what
|
|
161
|
+
* lands in the raw.
|
|
162
|
+
* @param {unknown} err whatever was thrown
|
|
163
|
+
* @returns {{name: string|null, message: string|null, code: string|null,
|
|
164
|
+
* raw: ReturnType<typeof scrubRaw>}}
|
|
165
|
+
*/
|
|
166
|
+
export function crashRecord(err: unknown): {
|
|
167
|
+
name: string | null;
|
|
168
|
+
message: string | null;
|
|
169
|
+
code: string | null;
|
|
170
|
+
raw: ReturnType<typeof scrubRaw>;
|
|
171
|
+
};
|
|
172
|
+
import { scrubRaw } from './text.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} Deps
|
|
3
|
+
* @property {Record<string,string|undefined>} [env]
|
|
4
|
+
* @property {NodeJS.ReadStream} [stdin]
|
|
5
|
+
* @property {NodeJS.WritableStream} [stdout]
|
|
6
|
+
* @property {NodeJS.WritableStream} [stderr]
|
|
7
|
+
* @property {string} [invokedAs] the command name this run was actually
|
|
8
|
+
* reached through, for the usage message ONLY — `scripts/run-author.mjs`
|
|
9
|
+
* supplies none (defaults to its own real invocation), `src/cli.js` passes
|
|
10
|
+
* `'bareloop author'` so a person who typed that is told to re-run THAT,
|
|
11
|
+
* never a script path they never invoked and may not have on disk.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Parses this script's own argv shape and runs the authoring pipeline
|
|
15
|
+
* (scout, declaration loop, D9's gates) against the given (or real)
|
|
16
|
+
* stdin/stdout. `scripts/run-author.mjs` is the thin adapter over this, the
|
|
17
|
+
* same shape `bin/bareloop.mjs` already is over `src/cli.js`'s own
|
|
18
|
+
* `main(argv, deps)`.
|
|
19
|
+
* @param {string[]} argv
|
|
20
|
+
* @param {Deps} [deps]
|
|
21
|
+
* @returns {Promise<number>} an exit code — never calls process.exit
|
|
22
|
+
*/
|
|
23
|
+
export function main(argv: string[], deps?: Deps): Promise<number>;
|
|
24
|
+
export type Deps = {
|
|
25
|
+
env?: Record<string, string | undefined> | undefined;
|
|
26
|
+
stdin?: NodeJS.ReadStream | undefined;
|
|
27
|
+
stdout?: NodeJS.WritableStream | undefined;
|
|
28
|
+
stderr?: NodeJS.WritableStream | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* the command name this run was actually
|
|
31
|
+
* reached through, for the usage message ONLY — `scripts/run-author.mjs`
|
|
32
|
+
* supplies none (defaults to its own real invocation), `src/cli.js` passes
|
|
33
|
+
* `'bareloop author'` so a person who typed that is told to re-run THAT,
|
|
34
|
+
* never a script path they never invoked and may not have on disk.
|
|
35
|
+
*/
|
|
36
|
+
invokedAs?: string | undefined;
|
|
37
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -24,9 +24,10 @@ export { scrubRaw, RAW_PERSIST_MAX } from "./text.js";
|
|
|
24
24
|
export { authorClose, MAX_REVISIONS, questionsFor, requiredAnswersFor } from "./authorflow.js";
|
|
25
25
|
export { validateCloseDecl, runDeclaredStages, declaredStages, HUMAN_PAUSE } from "./declaredclose.js";
|
|
26
26
|
export { runBehaviour, formatBehaviour } from "./behaviour.js";
|
|
27
|
-
export { replayRun, formatReplay } from "./replay.js";
|
|
27
|
+
export { replayRun, formatReplay, summarizeForAllLine, formatAllLines } from "./replay.js";
|
|
28
|
+
export { replayOne, listSpines, readHistoryLog } from "./replayio.js";
|
|
28
29
|
export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind, AUTHORED_SPEC_FIELDS } from "./authorjob.js";
|
|
29
30
|
export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
|
|
30
31
|
export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
|
|
31
32
|
export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf, hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts } from "./close-integrity.js";
|
|
32
|
-
export { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest, datedDestination, pickDelivery } from "./source.js";
|
|
33
|
+
export { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest, datedDestination, pickDelivery, missingDependencies } from "./source.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} Deps
|
|
3
|
+
* @property {Record<string,string|undefined>} [env]
|
|
4
|
+
* @property {NodeJS.ReadStream} [stdin]
|
|
5
|
+
* @property {NodeJS.WritableStream} [stdout]
|
|
6
|
+
* @property {NodeJS.WritableStream} [stderr]
|
|
7
|
+
* @property {typeof realSpawnSync} [spawnSync] the TEST SEAM for the final
|
|
8
|
+
* paid-step offer — a caller that supplies this never actually spawns
|
|
9
|
+
* `scripts/run-author.mjs`.
|
|
10
|
+
* @property {string} [invokedAs] the command name this run was actually
|
|
11
|
+
* reached through, for the usage message ONLY — `scripts/run-interview.mjs`
|
|
12
|
+
* supplies none (defaults to its own real invocation), `src/cli.js` passes
|
|
13
|
+
* `'bareloop interview'` so a person who typed that is told to re-run THAT,
|
|
14
|
+
* never a script path they never invoked and may not have on disk.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Parses this script's own argv shape and runs the interview against the
|
|
18
|
+
* given (or real) stdin/stdout. `scripts/run-interview.mjs` is the thin
|
|
19
|
+
* adapter over this, the same shape `bin/bareloop.mjs` already is over
|
|
20
|
+
* `src/cli.js`'s own `main(argv, deps)`.
|
|
21
|
+
* @param {string[]} argv
|
|
22
|
+
* @param {Deps} [deps]
|
|
23
|
+
* @returns {Promise<number>} an exit code — never calls process.exit
|
|
24
|
+
*/
|
|
25
|
+
export function main(argv: string[], deps?: Deps): Promise<number>;
|
|
26
|
+
export type Deps = {
|
|
27
|
+
env?: Record<string, string | undefined> | undefined;
|
|
28
|
+
stdin?: NodeJS.ReadStream | undefined;
|
|
29
|
+
stdout?: NodeJS.WritableStream | undefined;
|
|
30
|
+
stderr?: NodeJS.WritableStream | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* the TEST SEAM for the final
|
|
33
|
+
* paid-step offer — a caller that supplies this never actually spawns
|
|
34
|
+
* `scripts/run-author.mjs`.
|
|
35
|
+
*/
|
|
36
|
+
spawnSync?: typeof realSpawnSync | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* the command name this run was actually
|
|
39
|
+
* reached through, for the usage message ONLY — `scripts/run-interview.mjs`
|
|
40
|
+
* supplies none (defaults to its own real invocation), `src/cli.js` passes
|
|
41
|
+
* `'bareloop interview'` so a person who typed that is told to re-run THAT,
|
|
42
|
+
* never a script path they never invoked and may not have on disk.
|
|
43
|
+
*/
|
|
44
|
+
invokedAs?: string | undefined;
|
|
45
|
+
};
|
|
46
|
+
import { spawnSync as realSpawnSync } from 'node:child_process';
|
package/types/job.d.ts
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
* against the worker-surface package, a locked VERDICT type is demand against
|
|
5
5
|
* bareloop's OWN menu. Inferring it downstream is how a bareloop refusal gets
|
|
6
6
|
* filed as an upstream bug (the BA-2 misattribution class). */
|
|
7
|
+
/**
|
|
8
|
+
* The `baseUrl` SHAPE rule, standalone (PRD item 28 ruling (d); PRD item 33
|
|
9
|
+
* close-out, "authoring provider selectable" — a bad `--base-url` used to
|
|
10
|
+
* refuse only at the end of the interview, through `validateJob`, after every
|
|
11
|
+
* question had already been asked). ONE spelling of the rule: `validateJob`
|
|
12
|
+
* below calls this, and so does `scripts/run-interview.mjs`, at flag-parse
|
|
13
|
+
* time, before it asks anything.
|
|
14
|
+
*
|
|
15
|
+
* No network access, so this checks SHAPE only: `https://` for a real
|
|
16
|
+
* endpoint, or `http://` restricted to a loopback host (a local
|
|
17
|
+
* OpenAI-compatible server, the only legitimate reason to skip TLS) — and
|
|
18
|
+
* never a credential embedded IN the URL itself. A URL travels into logs and
|
|
19
|
+
* spine records far more casually than a header ever would, and secrets never
|
|
20
|
+
* enter the tree/spine (hard line #3) — a baseUrl carrying `user:pass@host`
|
|
21
|
+
* is exactly that leak, just spelled differently. The returned message never
|
|
22
|
+
* echoes the value it judged — it is printed as-is by both callers, and a
|
|
23
|
+
* value that failed for carrying credentials must not repeat them back.
|
|
24
|
+
* @param {unknown} value
|
|
25
|
+
* @returns {string|null} a red `detail`, or `null` when the value is admitted
|
|
26
|
+
*/
|
|
27
|
+
export function validateBaseUrl(value: unknown): string | null;
|
|
7
28
|
/**
|
|
8
29
|
* Validate an operator-owned job spec (`schema: "job-v1"`). Never throws on
|
|
9
30
|
* JSON text or plain parsed data — the ingest contract; a live object with a
|
package/types/planrun.d.ts
CHANGED
|
@@ -73,7 +73,6 @@
|
|
|
73
73
|
* No gap → the empty string, so the brief renders byte-identically to the pre-F86
|
|
74
74
|
* one. A labelled empty section would be an invitation to explain an absence the
|
|
75
75
|
* run never observed (a stall never judged its exits at all).
|
|
76
|
-
*
|
|
77
76
|
* @param {string | null | undefined} gap the step's last exit gap text
|
|
78
77
|
* @returns {string} the labelled block, or '' when there is nothing to show
|
|
79
78
|
*/
|
|
@@ -366,7 +365,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
|
|
|
366
365
|
* it the deterministic name would collide with that very branch and the collision walk
|
|
367
366
|
* would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
|
|
368
367
|
* path. A recorded branch that no longer exists is a STOP, never a fresh start.
|
|
369
|
-
* @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
368
|
+
* @param {{decision: string, text?: string|null}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
370
369
|
* answer at a hitl pause (2026-08-12 §1, re-cut 2026-08-17: accept | rerun <text> | pause), carried in
|
|
371
370
|
* by the runner on the leg that RESUMES a paused run. Absent on every ordinary run and
|
|
372
371
|
* on the leg that pauses; it is never authored, never defaulted, and never inferred.
|
|
@@ -457,7 +456,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
|
|
|
457
456
|
resumeBranch?: string | null | undefined;
|
|
458
457
|
humanRuling?: {
|
|
459
458
|
decision: string;
|
|
460
|
-
text?: string;
|
|
459
|
+
text?: string | null;
|
|
461
460
|
} | null | undefined;
|
|
462
461
|
heldRuling?: {
|
|
463
462
|
decision: string;
|
|
@@ -473,6 +472,47 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
|
|
|
473
472
|
} | null | undefined;
|
|
474
473
|
resumable?: boolean | undefined;
|
|
475
474
|
}): Promise<string>;
|
|
475
|
+
/**
|
|
476
|
+
* The runtime belt for F178 (`.git`) and F177's follow-up (`node_modules`) —
|
|
477
|
+
* hamr's option B ruling (2026-09-14): block WRITING to either, never reading.
|
|
478
|
+
* bareguard's `fs.deny` cannot be used for this: it blocks ALL fs actions,
|
|
479
|
+
* reads included (node_modules/bareguard/src/primitives/fs.js:43 "paths/
|
|
480
|
+
* prefixes denied for all fs actions", checked at :62-66 before the read/write
|
|
481
|
+
* split) — putting `.git` there (the first version of this fix) silently also
|
|
482
|
+
* blocked READING it, which was never ruled, and the worker's persona never
|
|
483
|
+
* names `.git`/`node_modules` as a denied book the way it does the gate audit
|
|
484
|
+
* / `.smoke` / `.litectx` (`tests/tools.test.js`'s persona/fence-drift guard
|
|
485
|
+
* caught exactly this in the full gate). `fs.deny` is exact prefix-containment
|
|
486
|
+
* only anyway — no glob, no path-segment match — confirmed by reading
|
|
487
|
+
* `within()` (node_modules/bareguard/src/primitives/fs.js:26-31): `p === b ||
|
|
488
|
+
* p.startsWith(b + '/')` against each configured entry, nothing else; it
|
|
489
|
+
* could not express `node_modules`-at-any-depth even if reads were not a
|
|
490
|
+
* problem (`node_modules/bareguard/src/glob.js`'s `matchAny`/`globToRegex`
|
|
491
|
+
* exist in the same package but `fs.js` never imports them).
|
|
492
|
+
*
|
|
493
|
+
* `tools.denyArgPatterns` (`node_modules/bareguard/src/primitives/tools.js`,
|
|
494
|
+
* step 3 of `Gate#_stepEval` — the SAME step `fsCheck` runs at, checked right
|
|
495
|
+
* after it in the `??` chain, `node_modules/bareguard/src/gate.js`) is the
|
|
496
|
+
* supported hook used instead: a RegExp tested against
|
|
497
|
+
* `JSON.stringify(action)`, keyed by `action.type` so it is scoped to
|
|
498
|
+
* `write`/`edit` ONLY — a `read` action never reaches this pattern at all.
|
|
499
|
+
* Because `fsCheck` runs first in the same `??` chain, this pattern is
|
|
500
|
+
* reached only when the path already passed `writeScope`/`deny` — exactly the
|
|
501
|
+
* residual case (an in-fence write/edit whose path still carries a `.git` or
|
|
502
|
+
* `node_modules` segment; `.git` itself is never IN a signed fence in the
|
|
503
|
+
* first place, `scopeContained` in src/validate.js refuses that at
|
|
504
|
+
* declaration time — this is the belt for a validator regression). Matched
|
|
505
|
+
* whole-segment (a look-alike like `.github`/`.gitignore`/`my.git`/
|
|
506
|
+
* `node_modules_util` stays admitted). Safe to match the WHOLE serialized
|
|
507
|
+
* action (not just the `path` field in isolation) because bareloop's own
|
|
508
|
+
* action shape (`toolAction`, src/tools.js) never carries a write's byte
|
|
509
|
+
* CONTENT in the action object — `args.bytes` is a length, not the text — so
|
|
510
|
+
* there is no written-file-content collision risk the way there would be for
|
|
511
|
+
* bareguard's own `content.denyPatterns` primitive (which explicitly strips
|
|
512
|
+
* payload fields for exactly that reason).
|
|
513
|
+
* @type {RegExp}
|
|
514
|
+
*/
|
|
515
|
+
export const FORBIDDEN_WRITE_SEGMENT_PATTERN: RegExp;
|
|
476
516
|
/**
|
|
477
517
|
* The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
|
|
478
518
|
* provider string (`denied:<tool>` today, ~20 bytes), not a gap, so this is a
|
package/types/providers.d.ts
CHANGED
|
@@ -8,6 +8,34 @@
|
|
|
8
8
|
* @returns {ProviderTableEntry}
|
|
9
9
|
*/
|
|
10
10
|
export function resolveProvider(providerName: string): ProviderTableEntry;
|
|
11
|
+
/**
|
|
12
|
+
* Report whether a resolved API key VALUE (not its presence — every call
|
|
13
|
+
* site already presence-checks separately) carries a shape an HTTP header
|
|
14
|
+
* value cannot: F181 (`docs/logs/FINDINGS.md`) — a two-line secret-store
|
|
15
|
+
* entry put the key plus a trailing metadata line into `OPENAI_API_KEY`
|
|
16
|
+
* (`"<key>\nmeta"`), the presence-only check at every door passed, and Node
|
|
17
|
+
* crashed `TypeError [ERR_INVALID_CHAR]` inside its own header-encode at the
|
|
18
|
+
* FIRST paid call — after the run had already announced itself as spending,
|
|
19
|
+
* inside the paid span. curl tolerated the same value (HTTP 200 twice), so a
|
|
20
|
+
* curl probe is a blind instrument for this defect class.
|
|
21
|
+
*
|
|
22
|
+
* NEVER TRIMS OR REPAIRS — this reports a reason, it does not fix the value
|
|
23
|
+
* (hamr's retry-never-repair rule, in spirit: refuse loud, don't guess at
|
|
24
|
+
* what the operator's secret store meant). Every call site that gets a
|
|
25
|
+
* non-null reason back refuses at $0, in the SAME place and the SAME way it
|
|
26
|
+
* already refuses a missing key — this function only adds a second shape of
|
|
27
|
+
* refusal to a door that already had one.
|
|
28
|
+
*
|
|
29
|
+
* The checked shape is deliberately wider than what Node's own header
|
|
30
|
+
* encoder rejects (Node tolerates a bare tab in a header value): CR, LF, TAB,
|
|
31
|
+
* every other C0/DEL control character, and leading/trailing whitespace all
|
|
32
|
+
* refuse, because every one of them is evidence the resolved value is not a
|
|
33
|
+
* clean single-line secret — the same class of "reads as one plausible thing,
|
|
34
|
+
* is actually stale/wrong/multi-line" this finding named.
|
|
35
|
+
* @param {string} value the resolved key value — never logged, never echoed by any caller
|
|
36
|
+
* @returns {string|null} a plain-English reason class, or `null` when the value is clean
|
|
37
|
+
*/
|
|
38
|
+
export function apiKeyProblem(value: string): string | null;
|
|
11
39
|
/**
|
|
12
40
|
* Construct ONE provider instance for `providerName`/`model`, applying
|
|
13
41
|
* `exposeErrorBody: true` (F153 — every constructed provider, unconditionally)
|