bareloop 0.26.0 → 0.27.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.
@@ -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
- * @param {{checks: string[], protections: string[], openQuestions?: string[], notChecked?: string[]}} confirmed
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
package/types/index.d.ts CHANGED
@@ -29,4 +29,4 @@ export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalE
29
29
  export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
30
30
  export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
31
31
  export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf, hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts } from "./close-integrity.js";
32
- export { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest, datedDestination, pickDelivery } from "./source.js";
32
+ export { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest, datedDestination, pickDelivery, missingDependencies } from "./source.js";
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
@@ -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
  */
@@ -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
@@ -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)
package/types/source.d.ts CHANGED
@@ -88,6 +88,51 @@ export function prepareSource({ source, into, destination, fetchTimeoutMs }: {
88
88
  manifestPath: string;
89
89
  manifest: object;
90
90
  } | SourceRefusal>;
91
+ /**
92
+ * THE INSTALL-GAP DETECTOR (PRD item 33 close-out, `docs/product/PRD.md:450-452`,
93
+ * hamr's ruling 2026-09-14, option A): `prepareSource` copies ONLY git-tracked
94
+ * files (hamr's ruling, `docs/product/ITEM33-BUILD.md:225-239` — unchanged by
95
+ * this function), so a JS/TS repo's prepared copy never carries `node_modules`,
96
+ * and every close stage needing a tool (`tsc`, a test runner) instrument-stops
97
+ * with nothing telling the person why. bareloop NEVER runs an install itself —
98
+ * it only ever touches gated primitives, and a shell-out to `npm`/etc from
99
+ * inside this module would be exactly the verb hamr's law reserves for the
100
+ * worker's own granted tools, never the arbiter-side machinery that decides
101
+ * whether a run even starts. This is a pure, $0 DETECTOR: it names the gap and
102
+ * the exact command a person runs THEMSELVES in the copy, then returns.
103
+ *
104
+ * JS/TS ONLY for now — a repo in another detected language returns `null`
105
+ * here unconditionally (no `package.json` to find), which is the correct
106
+ * "nothing to report" reading, not a gap in this function; per-language
107
+ * dependency-gap data for other languages is M3b's job
108
+ * (`docs/product/ITEM33-BUILD.md`, "M3b"), not this one's.
109
+ *
110
+ * SUBFOLDER-AS-REPO (ruling 2 addendum, commit 2a020d8): a subfolder job
111
+ * freezes the WHOLE repo, so `package.json` may sit at the repo root, at the
112
+ * subfolder itself, or anywhere between — the SAME nearest-manifest-walking-up
113
+ * rule `detectLanguage` (`src/detectlang.js`) applies, done independently here
114
+ * (not by importing `detectLanguage`) to avoid a `source.js` ⇄ `detectlang.js`
115
+ * import cycle, since `detectlang.js` already imports `nearestGitAncestor`
116
+ * from this module.
117
+ *
118
+ * @param {string} treeDir the prepared tree's root (`prepareSource`'s
119
+ * returned `tree`, e.g. `<into>/tree` — a REPO source sits directly at this
120
+ * root; every other kind sits under `input/` inside it and has no repo
121
+ * dependency story yet, so callers only need this for a `kind: 'repo'`
122
+ * manifest)
123
+ * @param {string} [sourceSubdir] the manifest's own `sourceSubdir` ('' or
124
+ * undefined when Source IS the repo root)
125
+ * @returns {{manager: string, command: string, reason: string}|null} `null`
126
+ * when there is nothing to report: no JS/TS manifest found walking up from
127
+ * `sourceSubdir` to `treeDir`, the nearest one found has no dependencies,
128
+ * `node_modules` is already there, or the manifest cannot even be read as
129
+ * JSON (an invalid `package.json` is "cannot tell", never a false alarm).
130
+ */
131
+ export function missingDependencies(treeDir: string, sourceSubdir?: string): {
132
+ manager: string;
133
+ command: string;
134
+ reason: string;
135
+ } | null;
91
136
  /**
92
137
  * The delivered name (M2b fix 4, kept under D3): a destination FILE of
93
138
  * `/home/me/profile.md` lands as `/home/me/profile-2026-09-12.md`, and a
package/types/text.d.ts CHANGED
@@ -100,6 +100,41 @@ export function capStop({ ceilingUsd, knownUsd, spendComplete }: {
100
100
  knownUsd: number;
101
101
  spendComplete: boolean;
102
102
  }): "cap-halt" | "pricing-red" | null;
103
+ /**
104
+ * THE ONE SPELLING of "was this throw a call casualty, or something else" —
105
+ * F179/F180's seam. `authorscout.js`'s `settled` narrows a rejected
106
+ * `loop.run()` to this one class: swallowing every throw would launder a
107
+ * budget `HaltError`, a programming bug, or any other named terminal into
108
+ * "the call failed" — the exact class of dishonesty this repo refuses (an
109
+ * unknown reported as a known).
110
+ *
111
+ * bare-agent 0.42.0's `OpenAIProvider.generate` used to throw a raw
112
+ * `SyntaxError` from an unguarded `JSON.parse(tc.function.arguments)` after
113
+ * a billed HTTP round (F179/BA-27) — this predicate used to admit that class
114
+ * too. bare-agent 0.43.0 fixed it upstream (`parseToolCalls`, shared by
115
+ * OpenAIProvider and Ollama): a malformed tool-call arguments string now
116
+ * returns a priced round with `toolCalls: []` plus its own `malformedToolCall`
117
+ * marker instead of throwing. Verified against 0.43.0's installed source that
118
+ * no provider bareloop constructs (`AnthropicProvider`, `OpenAIProvider`,
119
+ * `GeminiProvider` — `src/providers.js`) can still throw a JSON `SyntaxError`
120
+ * out of `generate()` after a billed round: Anthropic and Gemini read tool-call
121
+ * arguments as already-parsed objects off their own response shape (nothing to
122
+ * `JSON.parse`), and every provider's raw-HTTP-body parse is already
123
+ * try/catch-wrapped inside `_request`, converted to a plain `Error` before it
124
+ * can reach here. bareloop never streams a provider response. So the
125
+ * SyntaxError branch this predicate used to carry is dead code and was
126
+ * removed in the same change that bumped to bare-agent 0.43.0 — re-add it (or
127
+ * a narrower successor) only if a real provider is found to reintroduce the
128
+ * class.
129
+ *
130
+ * A `HaltError` is deliberately excluded — it is bare-agent's own governance
131
+ * exit (a budget/turn cap), never a transport casualty, and this predicate
132
+ * must never launder one into a mere "the call failed".
133
+ * @param {any} err
134
+ * @returns {string|null} a short reason when `err` is an admitted casualty class,
135
+ * `null` for everything else (including `HaltError`) — the caller re-throws on `null`.
136
+ */
137
+ export function callCasualty(err: any): string | null;
103
138
  /**
104
139
  * ONE raw model emission, ready to persist.
105
140
  *
@@ -13,6 +13,8 @@ export function globToPrefix(scope: string): string;
13
13
  * resolve to a PROPER subdirectory of the run directory. Absolute paths and
14
14
  * ".." segments escape it; "." / "./**" cover the whole run directory — where
15
15
  * the close suite lives. Windows spellings ("..\", "C:\") count as escapes.
16
+ * A `.git` or `node_modules` segment anywhere in the prefix is refused too
17
+ * (F178/F177, see {@link FORBIDDEN_SCOPE_SEGMENTS}).
16
18
  * Exported for the job validator: the operator's outer fence obeys the SAME
17
19
  * law through the same code — two containment transforms would be the F9
18
20
  * red-class one level up.
@@ -205,6 +207,26 @@ export function hasNestedQuantifier(src: string): boolean;
205
207
  * @param {(code: string, path: string, detail?: string) => void} red
206
208
  */
207
209
  export function sweepNestedQuantifiers(root: any, red: (code: string, path: string, detail?: string) => void): void;
210
+ /**
211
+ * Path segments a fence/scope may never contain, checked WHOLE-SEGMENT
212
+ * (`prefix.split('/')`), never substring — a look-alike (`.github`,
213
+ * `.gitignore`, `my.git`, `node_modules_util`) is a DIFFERENT name and stays
214
+ * admitted. hamr's ruling (option B, 2026-09-14, block both): F178 — a
215
+ * signed fence naming `.git` could let a worker write git's own files (refs,
216
+ * the seed, `info/exclude`) and hide those writes from `changedSet`. F177's
217
+ * follow-up — a signed fence naming `node_modules` directly is the same class
218
+ * of gap (an installed/worker-written file inside it is invisible to
219
+ * `changedSet` the same way, and installs are the person's job now, F177).
220
+ * This catches only a fence that NAMES one of these directly; a fence like
221
+ * `packages/api/**` that later reaches a NESTED `node_modules` beneath it is a
222
+ * distinct, only partially closeable gap — see the runtime belt in
223
+ * planrun.js for `node_modules` (`.git` has no such gap: there is exactly one
224
+ * `.git` per repo, always at the workdir root, so its own runtime belt is a
225
+ * single `fs.deny` entry). Both are inexpressible at declaration time, the
226
+ * same law #1 already applies to `..`.
227
+ * @type {readonly string[]}
228
+ */
229
+ export const FORBIDDEN_SCOPE_SEGMENTS: readonly string[];
208
230
  /**
209
231
  * PRD item 27/M2 close-bytes signature, orchestrator-flagged gap
210
232
  * (2026-09-06): a close stage's `cmd` can name an addressable script TWO