@yanlinglabs/winter-agent-runtime 0.0.41 → 0.0.44

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.
Files changed (60) hide show
  1. package/dist/commands/resolver.d.ts +8 -13
  2. package/dist/context/agent-listing.d.ts +7 -11
  3. package/dist/context/attachments.d.ts +29 -23
  4. package/dist/context/dynamic-sections.d.ts +5 -5
  5. package/dist/context/git-branch-fixture.d.ts +22 -0
  6. package/dist/context/git-status.d.ts +2 -2
  7. package/dist/context/memory.d.ts +1 -1
  8. package/dist/context/output-styles.d.ts +2 -2
  9. package/dist/context/request-layout.d.ts +16 -17
  10. package/dist/context/seam.d.ts +7 -6
  11. package/dist/embedded-host.js +1 -1
  12. package/dist/embedded-worker.js +4 -4
  13. package/dist/embedded.js +3 -3
  14. package/dist/engine.d.ts +3 -3
  15. package/dist/{index-97t2rmtf.js → index-dne3dw18.js} +11 -0
  16. package/dist/{index-z9yngxj4.js → index-hbkdq3xs.js} +1072 -827
  17. package/dist/{index-hdvvezf8.js → index-r1pter2v.js} +239 -155
  18. package/dist/{index-ey5c52j3.js → index-x7hdare6.js} +2 -2
  19. package/dist/index.js +2 -2
  20. package/dist/mcp/lifecycle.d.ts +2 -1
  21. package/dist/permissions/edit-recognition.d.ts +1 -1
  22. package/dist/permissions/evaluator.d.ts +19 -30
  23. package/dist/permissions/file-rules.d.ts +177 -277
  24. package/dist/permissions/grammar.d.ts +58 -0
  25. package/dist/permissions/policy-state.d.ts +1 -1
  26. package/dist/permissions/ruleset.d.ts +1 -1
  27. package/dist/permissions/shell-structure.d.ts +1 -1
  28. package/dist/plugins/bundle.d.ts +4 -6
  29. package/dist/plugins/loader.corpus-fixture.d.ts +26 -0
  30. package/dist/plugins/loader.d.ts +3 -4
  31. package/dist/plugins/manifest.d.ts +23 -46
  32. package/dist/production-wiring.d.ts +1 -1
  33. package/dist/protocol/channel.d.ts +6 -0
  34. package/dist/provider/lean-prompt.d.ts +1 -6
  35. package/dist/sandbox/profile.d.ts +39 -79
  36. package/dist/sandbox/spawn.d.ts +5 -6
  37. package/dist/skills/listing.d.ts +8 -26
  38. package/dist/skills/store.d.ts +2 -3
  39. package/dist/store/dialect.d.ts +1 -1
  40. package/dist/subagents/availability.d.ts +10 -8
  41. package/dist/subagents/child-handle.d.ts +5 -5
  42. package/dist/subagents/definitions.d.ts +7 -6
  43. package/dist/subagents/fork.d.ts +4 -8
  44. package/dist/subagents/notification-queue.d.ts +30 -38
  45. package/dist/testing.js +3 -3
  46. package/dist/tools/descriptors/web-fetch.d.ts +1 -1
  47. package/dist/tools/descriptors/web-search.d.ts +1 -1
  48. package/dist/tools/impl/_search-budget.d.ts +1 -1
  49. package/dist/tools/impl/_web-fetch-net.d.ts +6 -6
  50. package/dist/tools/impl/_web-search-assembler.d.ts +7 -20
  51. package/dist/tools/impl/background-task-runtime.d.ts +13 -13
  52. package/dist/tools/impl/bash.d.ts +3 -3
  53. package/dist/tools/impl/monitor.d.ts +2 -2
  54. package/dist/toolsearch/exposure.d.ts +3 -3
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.js +1 -1
  57. package/dist/web/fetchable-url.d.ts +1 -1
  58. package/dist/web/preapproved-hosts.d.ts +9 -31
  59. package/dist/workflows/store.d.ts +23 -31
  60. package/package.json +4 -4
@@ -1,84 +1,56 @@
1
1
  export type FileRuleKind = "edit" | "read";
2
2
  /**
3
- * SV-7 (the router same-view test): claude's file-rule grammar has only TWO pattern kinds --
4
- * `Edit(...)` and `Read(...)`. Dump-confirmed: `ln`'s own dispatch switch has exactly two cases
5
- * (`case"edit":return tn;case"read":return wt`, each a SINGLE literal tool-name string `ub` filters
6
- * `ruleValue.toolName` against by exact equality) -- there is no third "write" kind anywhere in the
7
- * data model. Claude's own WRITE decision function (`zC`) ALWAYS consults `"edit"`-kind rules,
8
- * regardless of which literal write-shaped tool called it -- this is what makes an `Edit(...)` ask
9
- * rule fire before a **Write** on claude (the router's own SV-7 measurement), and what makes a
10
- * `Write(...)`-toolName rule a Winter-only spelling with no claude analogue at all: `ub` filtering
11
- * on the literal string "Write" never runs, because nothing ever calls it with that string.
12
- *
13
- * Every `FILE_RULE_TOOLS` member (grammar.ts) routes to exactly one kind, on every direction (allow,
14
- * ask, deny alike -- the ruling's own "for both ALLOW and DENY" instruction, extended to ask since
15
- * ask shares deny's conservative "cross tools" posture throughout this codebase already).
3
+ * File rules come in exactly two kinds, `edit` and `read`. Every write-shaped tool (Edit, Write,
4
+ * NotebookEdit) is checked against the `edit` kind and every read-shaped one (Read, Glob, Grep)
5
+ * against the `read` kind, on every direction (allow, ask, deny). So an `Edit(...)` ask rule fires
6
+ * before a Write, as it does in Claude Code.
16
7
  */
17
8
  export declare function fileRuleKindFor(toolName: string): FileRuleKind | undefined;
18
9
  /**
19
- * The ONE literal tool name a rule must be AUTHORED under to ever be consulted for `kind` -- claude's
20
- * own `ub` filters `ruleValue.toolName` by EXACT STRING EQUALITY against a single literal per kind
21
- * (`tn`/`"Edit"` for `"edit"`, `wt`/`"Read"` for `"read"`; dump-confirmed, `ln`'s own two-case
22
- * switch), never against every tool that happens to share the kind. This is what makes SV-7's
23
- * "reverse" finding true: a rule AUTHORED as `Write(...)`, `NotebookEdit(...)`, `Glob(...)` or
24
- * `Grep(...)` is dead code claude never reads for ANY call -- not even a call from that SAME literal
25
- * tool -- because `ub` was never invoked with that string. `Write`/`NotebookEdit`/`Glob`/`Grep`
26
- * remain valid rule-authoring tool names SYNTACTICALLY (grammar.ts's `FILE_RULE_TOOLS` still parses
27
- * them -- Winter does not forbid authoring one), but this function is what `findMatchingFileRuleEntry`
28
- * (evaluator.ts) filters CANDIDATES with, so only `Edit(...)`/`Read(...)`-authored rules ever reach
29
- * a group.
10
+ * The ONE tool name a rule must be written under to be consulted for `kind`: `Edit` for `edit`,
11
+ * `Read` for `read`. A rule written as `Write(...)`, `NotebookEdit(...)`, `Glob(...)` or `Grep(...)`
12
+ * still parses (grammar.ts's `FILE_RULE_TOOLS`), but is never consulted for any call -- not even one
13
+ * from that same tool -- matching Claude Code's rule grammar, where only `Edit(...)` and `Read(...)`
14
+ * rules are file rules. `findMatchingFileRuleEntry` (evaluator.ts) filters candidates with this.
30
15
  */
31
16
  export declare function canonicalFileRuleAuthoringToolName(kind: FileRuleKind): "Edit" | "Read";
32
- /** A sentinel distinct from `null` ("resolve against cwd"): a `/`-anchored rule with no resolvable settings-source directory is INERT, never falls back to cwd. */
17
+ /** A sentinel distinct from `null` ("resolve against cwd"): a `/`-anchored rule with no settings-source directory is INERT, and never falls back to cwd. */
33
18
  declare const INERT_ANCHOR: unique symbol;
34
19
  export interface FileRuleAnchor {
35
20
  /** The pattern text, relative to `root`, in `ignore`-package (gitignore) grammar. */
36
21
  relativePattern: string;
37
- /** `null` means "resolve against cwd" (`Ma`'s own `P ?? te()`, ported as `root ?? opts.cwd`); the sentinel means the anchor can never match anything. */
22
+ /** `null` means "resolve against cwd"; the sentinel means the rule can never match anything. */
38
23
  root: string | null | typeof INERT_ANCHOR;
39
24
  }
40
25
  /**
41
- * `jOe` (dump-confirmed): the FOUR anchor spellings WS-07 §3.1 documents, resolved to a
42
- * `{relativePattern, root}` pair -- ported exactly, including the leading-slash-KEPT behaviour on
43
- * the three anchored forms (`//x`, `~/x`, `/x`) that is what makes them root-anchored in gitignore
44
- * terms, and its ABSENCE on `./x`/bare `x` that is what lets `deny ./.env` reach `pkg/.env` (C-1's
45
- * own example) -- a bare pattern with no leading slash and no inner slash is exactly the shape the
46
- * `ignore` package's own `^(?=[^^])` -> `(?:^|\/)` replacer un-anchors.
47
- *
48
- * A bare `~` (no trailing slash) is NOT specially handled by claude's own `jOe` either -- it falls
49
- * through to the final else branch as a literal filename pattern `"~"`, cwd-anchored. Ported
50
- * faithfully rather than "fixed", matching this module's own "port what was measured" discipline.
51
- *
52
- * `sourceDir` is claude's `bl(source)` -- the settings-source-derived root for a `/`-anchored rule.
53
- * Since fix round 11 a settings-tier rule carries it (`SourcedRuleEntry.sourceDir`, set by
54
- * production-wiring.ts's `buildSettingsRuleSeed` per claude's `Wyt`), and a `/`-anchored rule resolves
55
- * against it. When it is absent -- a rule from Options, canUseTool or a plugin -- the rule is inert
56
- * (the `INERT_ANCHOR` root below, treated as "no group to match against" by `matchFileRulesGrouped`).
26
+ * Resolves a pattern's anchor spelling to the root it is relative to and the pattern text relative
27
+ * to that root:
28
+ * - `//x` -> root `/`, pattern `/x` (one slash dropped, the leading `/` KEPT);
29
+ * - `~/x` -> root `home`, pattern `/x` (the `~` dropped, the leading `/` KEPT);
30
+ * - `/x` -> root `sourceDir` (the directory of the settings file the rule came from), pattern
31
+ * `/x` unchanged; with no `sourceDir` -- a rule from Options, canUseTool or a plugin --
32
+ * the rule is INERT (`INERT_ANCHOR`) and matches nothing;
33
+ * - `./x` -> root cwd (`null`), pattern `x` (the `./` dropped, NO leading `/`);
34
+ * - else -> root cwd (`null`), pattern unchanged (a bare `~` included: it is a file named `~`).
35
+ * The kept leading `/` is what anchors a pattern to its root in gitignore terms; its absence is what
36
+ * lets `deny ./.env` also reach `pkg/.env`.
57
37
  */
58
38
  export declare function resolveFileRuleAnchor(pattern: string, opts: {
59
39
  home: string;
60
40
  sourceDir?: string | undefined;
61
41
  }): FileRuleAnchor;
62
42
  /**
63
- * WS-21 fix round 10, item C: a Read/Edit rule's own pattern, resolved to ONE absolute filesystem
64
- * path -- for the sandbox's own `subpath` rule (sandbox/profile.ts's `denyWritePaths`/
65
- * `denyReadPaths`/`writableRoots`), which has no glob grammar of its own to hand a pattern string
66
- * to; a real Seatbelt `subpath` already means "this directory and everything under it," so it needs
67
- * ONE real path, never a pattern.
43
+ * A Read/Edit rule's pattern resolved to ONE absolute filesystem path, for the sandbox's `subpath`
44
+ * rules (sandbox/profile.ts's `denyWritePaths`/`denyReadPaths`/`writableRoots`), which take a real
45
+ * path, not a pattern; `subpath` already means "this directory and everything under it".
68
46
  *
69
- * `undefined` in two cases, matching claude's own observable posture (dump-confirmed, `Jm`: `let{
70
- * allowOnly:t}=at.getFsWriteConfig();if(t.some(eg))return!0` -- ANY glob-shaped entry in the
71
- * write-allow set makes claude's OWN sandbox stop trying to restrict writes via that mechanism at
72
- * all, relying on the separate, glob-aware PERMISSION-RULE layer instead, which is unaffected by
73
- * this and stays the real enforcement point):
74
- * - the pattern is INERT (a bare `/`-anchored rule with no resolvable settings-source root --
75
- * `resolveFileRuleAnchor`'s own pre-existing posture, unchanged here);
76
- * - the pattern is genuinely GLOB-SHAPED once a single TRAILING `/**` is stripped (redundant with
77
- * `subpath`'s own "and everything under it" semantics, so it is not itself disqualifying --
78
- * `Edit(//repo/secrets/**)` becomes the plain path `/repo/secrets`) -- a glob ANYWHERE else
79
- * (`src/*.ts`, `[wip]`, `a?b`) cannot become one exact path at all.
80
- * A caller that gets `undefined` back simply does not add this rule to the sandbox's own filesystem
81
- * lists; the permission-rule layer (`evaluate()`) still enforces it in full, exactly as it always has.
47
+ * `undefined` when there is no such path:
48
+ * - the pattern is INERT (a `/`-anchored rule with no settings-source directory);
49
+ * - the pattern is still glob-shaped once ONE trailing `/**` is stripped (`Edit(//repo/secrets/**)`
50
+ * becomes the plain path `/repo/secrets`; `src/*.ts`, `[wip]`, `a?b` cannot become one path).
51
+ * A glob-shaped write-ALLOW entry is simply left out of the sandbox, which then does not try to
52
+ * restrict writes through it; the permission-rule layer (`evaluate()`) still enforces the rule in
53
+ * full.
82
54
  */
83
55
  export declare function resolveFileRuleAbsolutePath(pattern: string, opts: {
84
56
  cwd: string;
@@ -86,155 +58,132 @@ export declare function resolveFileRuleAbsolutePath(pattern: string, opts: {
86
58
  sourceDir?: string;
87
59
  }): string | undefined;
88
60
  /**
89
- * WS-21 fix round 11 ("important" item): the sibling of `resolveFileRuleAbsolutePath` that does NOT
90
- * drop a genuinely glob-shaped pattern -- it resolves the SAME anchor/root as that function but
91
- * returns the absolute text WITH any remaining glob characters intact (a redundant trailing `/**` is
92
- * still stripped first, identically, since `subpath`'s/the recursive-regex-suffix's own "and
93
- * everything under it" semantics already cover it). `undefined` only for the one case that has no
94
- * absolute form at all -- an INERT `/`-anchored pattern with no resolvable settings-source root,
95
- * unchanged from `resolveFileRuleAbsolutePath`'s own posture.
61
+ * The sibling of `resolveFileRuleAbsolutePath` that KEEPS a glob-shaped pattern: the same anchor and
62
+ * root, the same stripping of one redundant trailing `/**`, but the absolute text comes back with any
63
+ * remaining glob characters intact. `undefined` only for an INERT pattern.
96
64
  *
97
- * Exists because claude's own deny-rendering path (`dR`, dump byte 15365699 region) does NOT drop a
98
- * glob-shaped deny the way `Jm`'s write-ALLOW-only short-circuit does (round 10's own `Jm` finding,
99
- * `resolveFileRuleAbsolutePath`'s own header) -- a glob-shaped DENY instead becomes an SBPL `(regex
100
- * ...)` clause (claude's `Li` (dump byte 15365905) / `Rt` (dump byte 15282610)) rather than being silently
101
- * unenforced by the sandbox layer. Callers check `isGlobShapedFileRulePattern` on the result to
102
- * decide `subpath` vs a `globToSbplRegexSource`/`recursiveGlobToSbplRegexSource` conversion; ALLOW
103
- * entries keep using `resolveFileRuleAbsolutePath` (glob-shaped dropped), per the controller's own
104
- * explicit ruling: "Dropping glob-shaped ALLOW rules stays as it is, because that's stricter."
65
+ * Used for DENY entries: a glob-shaped deny is not dropped from the sandbox but rendered as an SBPL
66
+ * `(regex ...)` clause. Callers test the result with `isGlobShapedFileRulePattern` to choose between
67
+ * `subpath` and `globToSbplRegexSource`/`recursiveGlobToSbplRegexSource`. ALLOW entries keep using
68
+ * `resolveFileRuleAbsolutePath` (glob-shaped ones dropped), which is the stricter choice.
105
69
  */
106
70
  export declare function resolveFileRuleAbsoluteGlobText(pattern: string, opts: {
107
71
  cwd: string;
108
72
  home: string;
109
73
  sourceDir?: string;
110
74
  }): string | undefined;
75
+ /** True when `text` holds any of `* ? [ ]`. The sandbox deny split classifies with `scanDenyPathGlob` instead (below). */
76
+ export declare function isGlobShapedFileRulePattern(text: string): boolean;
111
77
  /**
112
- * claude's own `Rt` (dump byte 15282610: `e.includes("*")||e.includes("?")||e.includes("[")||e.includes("]")`),
113
- * confirmed byte-equivalent to this module's own pre-existing glob-char test. Kept as the pure claude
114
- * primitive; the sandbox DENY split (`splitDenyPathsByGlobShape`/`globDenyEntriesOf`) classifies with
115
- * `scanDenyPathGlob` below instead (fix round 17, R.3 C-1).
78
+ * Whether a realpath result for a glob's fixed prefix should be REJECTED (and the prefix kept as
79
+ * written). Both sides are first tidied with POSIX `normalize` (which keeps a trailing slash).
80
+ *
81
+ * The written path stands for one or two acceptable spellings: itself, and, when it lies under
82
+ * `/tmp/` or `/var/`, the same path under `/private` (macOS's real location for both). A resolution
83
+ * is trusted only when it is one of those spellings, or sits strictly BELOW one of them, and is not
84
+ * also shallow (the root, `.`, or a single top-level name) or an ancestor of one of them. Anything
85
+ * else -- climbing up, moving sideways, landing somewhere unrelated -- is rejected. Plain string
86
+ * comparisons only; no case folding, no filesystem access.
116
87
  */
117
- export declare function isGlobShapedFileRulePattern(text: string): boolean;
88
+ export declare function isSuspiciousRealpathResolution(original: string, resolved: string): boolean;
118
89
  /**
119
- * Fix round 12 ("Important" item, claude's own `ed`, dump byte 15367994, found in the SAME chunk as
120
- * `Ch`/`mR`/`pR` below): every ANCESTOR directory of `path`, walking up via `dirname` until reaching
121
- * `/` or a fixed point -- does NOT include `path` itself, nor `/`. Feeds the ancestor-rename-bypass
122
- * fix: claude's own write/read sandbox profiles additionally deny `file-write-unlink`/
123
- * `file-write-create` on every ancestor of a denied path (and of a glob deny's own fixed prefix), so
124
- * a sandboxed `mv <ancestor> <elsewhere> && <write inside where it used to be> && mv <elsewhere>
125
- * <ancestor>` cannot rename the ancestor out of the way and back to slip a write past the deny.
90
+ * Every ANCESTOR directory of `path`, nearest first: `path.dirname` applied repeatedly, starting from
91
+ * `path`'s parent and stopping before `/` or `.` (neither is included, nor is `path` itself), or when
92
+ * `dirname` stops changing the value.
93
+ *
94
+ * Feeds the sandbox's ancestor-rename fence: the profile also denies unlinking/creating every
95
+ * ancestor of a denied path (and of a glob deny's fixed prefix), so `mv <ancestor> <elsewhere>`,
96
+ * a write inside, and a rename back cannot slip a write past the deny.
126
97
  */
127
98
  export declare function ancestorDirectoriesOf(path: string): string[];
99
+ /**
100
+ * An absolute glob as a whole-path SBPL regex. The fixed prefix is canonicalised first
101
+ * (`canonicalizedGlobFixedPrefix`) and spliced back as ESCAPED LITERAL text (`escapeRegexLiteralPath`);
102
+ * only the rest goes through `globToAnchoredRegex`. The prefix may hold `[`/`]` (it runs through
103
+ * one-character classes, `scanDenyPathGlob`), which must not become a class again. A glob-free text
104
+ * (reachable only by a direct call; the deny split sends it to `(subpath …)`) renders as the whole
105
+ * escaped, canonicalised literal.
106
+ */
128
107
  export declare function globToSbplRegexSource(absoluteGlob: string): string;
129
108
  /**
130
- * claude's own `td` (dump byte 15365977: `Po(e).slice(0,-1)+"(/.*)?$"`) -- `globToSbplRegexSource`'s
131
- * own whole-string match, WIDENED to also match "the pattern's own match point, optionally followed
132
- * by `/` and anything deeper" -- the regex equivalent of `subpath`'s own implicit recursive semantics
133
- * (a plain, non-glob deny already renders as `subpath`, which covers a directory AND everything under
134
- * it with no extra syntax). claude's own `dR` (the deny-clause builder, same dump region) always uses
135
- * this recursive form for a glob-shaped deny's OWN base clause -- never the bare `Li`/
136
- * `globToSbplRegexSource` form, which claude reserves for an ALLOW-carve-out entry nested inside a
137
- * deny (a feature this port does not carry -- see `resolveFileRuleAbsoluteGlobText`'s own header).
109
+ * `globToSbplRegexSource`, widened to also match everything BELOW a match: its final `$` is replaced
110
+ * by `(/.*)?$`. The regex counterpart of `subpath`'s "and everything under it"; a glob-shaped deny's
111
+ * own clause always uses this form.
138
112
  */
139
113
  export declare function recursiveGlobToSbplRegexSource(absoluteGlob: string): string;
140
114
  /**
141
- * WS-21 fix round 11: the ONE place a plain `sandbox.filesystem.denyWrite`/`denyRead` string list
142
- * (settings.json's own, user-typed, and `deriveSandboxPathsFromRules`'s own rule-derived denies,
143
- * which now may ALSO contain glob-shaped text -- see `resolveFileRuleAbsoluteGlobText`'s own header)
144
- * gets split by glob-shape before reaching `SeatbeltProfileInput`/`RunCommandOptions`: a non-glob
145
- * entry stays a plain path (`subpath`, unchanged); a glob-shaped one is converted via
146
- * `recursiveGlobToSbplRegexSource` (the recursive form, matching `subpath`'s own implicit
147
- * "and everything under it" semantics and claude's own `dR`, which always uses the recursive form for
148
- * a deny's own base clause). Called once per caller (tools/impl/bash.ts, tools/impl/monitor.ts) --
149
- * kept as one shared, tested primitive rather than two hand-copies, per this codebase's own
150
- * "a second copy would be exactly the kind of drift risk this whole phase's review lens exists to
151
- * catch" precedent (evaluator.ts's `extractCandidateWritePaths`, verbatim).
115
+ * The ONE place a plain `sandbox.filesystem.denyWrite`/`denyRead` list (settings.json's own,
116
+ * user-typed, and `deriveSandboxPathsFromRules`' rule-derived denies, which may be glob-shaped -- see
117
+ * `resolveFileRuleAbsoluteGlobText`) is split by glob shape before reaching `SeatbeltProfileInput`/
118
+ * `RunCommandOptions`: a non-glob entry stays a plain path (`subpath`); a glob-shaped one becomes a
119
+ * `recursiveGlobToSbplRegexSource` regex. Shared by tools/impl/bash.ts and tools/impl/monitor.ts
120
+ * rather than copied into each.
152
121
  *
153
- * Fix round 12: also returns `globFixedPrefixes` -- for each glob-shaped entry, its OWN canonicalized
154
- * fixed-prefix directory (claude's own `Rh(u)`, `undefined`/dropped when it resolves to `/`, matching
155
- * `Ch`'s own `if(p==="/")continue`). Feeds the ancestor-rename-bypass port
156
- * (`SeatbeltProfileInput.denyWriteGlobFixedPrefixes`/`denyReadGlobFixedPrefixes`, sandbox/profile.ts):
157
- * the plain `paths` entries need only their OWN ancestors walked (`ed`, `ancestorDirectoriesOf`
158
- * above) to close the bypass; a glob-shaped deny ALSO needs its fixed prefix walked, and the prefix
159
- * itself added as a literal deny target (claude's own `Ch` adds both).
122
+ * Also returns `globFixedPrefixes`: for each glob-shaped entry, its canonicalised fixed-prefix
123
+ * directory, dropped when it is `/`. The ancestor-rename fence
124
+ * (`SeatbeltProfileInput.denyWriteGlobFixedPrefixes`/`denyReadGlobFixedPrefixes`) walks a plain
125
+ * entry's own ancestors (`ancestorDirectoriesOf`); a glob-shaped deny also needs its fixed prefix
126
+ * walked, and the prefix itself denied as a literal target.
160
127
  *
161
- * Fix round 17 (R.3 C-1): classified by `scanDenyPathGlob`, not by claude's `Rt` alone -- an entry
162
- * whose only glob syntax is one-character bracket classes (`/x/[[]wip] app/.winter/skills`, a literal
163
- * path spelled for the glob grammar) lands in `paths` UNESCAPED (`/x/[wip] app/.winter/skills`), and a
164
- * glob entry's fixed prefix runs through such classes. Winter-only hardening (R.3 C-1); claude's
165
- * `Rt`/`Li` stop at the first `[` -- see `scanDenyPathGlob`'s own header.
128
+ * Classified by `scanDenyPathGlob`: an entry whose only glob syntax is one-character bracket classes
129
+ * (`/x/[[]wip] app/.winter/skills`) lands in `paths` UNESCAPED (`/x/[wip] app/.winter/skills`).
166
130
  */
167
131
  export declare function splitDenyPathsByGlobShape(paths: readonly string[]): {
168
132
  paths: string[];
169
133
  regexes: string[];
170
134
  globFixedPrefixes: string[];
171
135
  };
172
- /** One glob-shaped deny entry, its recursive SBPL regex source PAIRED with its own fixed-prefix
173
- * directory -- `splitDenyPathsByGlobShape`'s own `regexes`/`globFixedPrefixes` are two independently
174
- * FILTERED flat arrays (the latter drops a "/" prefix entirely) with no positional correspondence
175
- * once any entry is dropped from one but not the other; `fixedPrefix` here is NEVER dropped -- it is
176
- * always the literal string `"/"` in that case (claude's own `Rh` returns `"/"` too, and `fR`'s own
177
- * skip/ancestor logic reads that value directly rather than treating "no prefix" as a distinct case). */
136
+ /** One glob-shaped deny entry: its recursive SBPL regex PAIRED with its own fixed-prefix directory.
137
+ * `splitDenyPathsByGlobShape`'s `regexes`/`globFixedPrefixes` are two independently filtered arrays
138
+ * (the latter drops a `/` prefix) with no positional correspondence; `fixedPrefix` here is never
139
+ * dropped -- it is the string `"/"` when there is no deeper prefix. */
178
140
  export interface GlobDenyEntry {
179
141
  regex: string;
180
142
  fixedPrefix: string;
181
143
  }
182
144
  /**
183
- * Fix round 13 ("Important" item 1, claude's own `fR`, dump byte 15367091): the PAIRED form
184
- * `buildReadDenyKeepInPlaceBlock` (sandbox/profile.ts) needs -- see `GlobDenyEntry`'s own header for
185
- * why `splitDenyPathsByGlobShape`'s own two flat arrays cannot answer this. Scoped to glob-shaped
186
- * entries only (a plain entry needs no pairing at all -- its own path IS both its recursive-clause
187
- * anchor and its ancestor-walk root, `buildReadDenyKeepInPlaceBlock` uses `paths` directly for that).
145
+ * The PAIRED form `buildReadDenyKeepInPlaceBlock` (sandbox/profile.ts) needs -- see `GlobDenyEntry`.
146
+ * Glob-shaped entries only (a plain entry's own path is both its clause anchor and its ancestor-walk
147
+ * root), classified by the same `scanDenyPathGlob` as `splitDenyPathsByGlobShape`.
188
148
  */
189
149
  export declare function globDenyEntriesOf(paths: readonly string[]): GlobDenyEntry[];
190
150
  /**
191
- * `xi` (dump-confirmed): collapses repeated slashes, and specially handles a LEADING BOM so it
192
- * cannot accidentally trigger gitignore's own `!`/`#` line-directive meaning (negation/comment). A
193
- * bare leading BOM with no `!`/`#` after it is DELETED outright (empirically verified: the first of
194
- * the two `.replace()` calls below always consumes a leading BOM, since `^` in its own regex
195
- * is unconditional and only the FOLLOWING `[!#]?` is optional -- the second `.replace(/^/,
196
- * "[]")` is therefore unreachable dead code in the pinned binary's own source whenever the
197
- * input genuinely starts with a BOM, ported here as harmless dead code too rather than "corrected"
198
- * into a change of behaviour this module was not asked to make). A leading BOM immediately followed
199
- * by `!` or `#` becomes an escaped literal `!`/`#` (so the directive character survives as TEXT to
200
- * match, not as a line-level instruction).
151
+ * Normalises a root-relative pattern before it reaches `ignore()`.
152
+ *
153
+ * Every run of slashes collapses to one. Then a leading byte-order mark (U+FEFF) is dealt with, one
154
+ * level deep, so it can neither vanish in a way that promotes the next character to a gitignore
155
+ * directive nor linger as an invisible first character:
156
+ * - a pattern that is only whitespace (JS `\s`, which includes the BOM), optionally ending in
157
+ * exactly `/**`, is left as it is;
158
+ * - a BOM followed by `!` or `#` becomes a backslash, so the directive character stays literal;
159
+ * - two BOMs become a one-character class holding a BOM (`[<BOM>]`), keeping the second literal;
160
+ * - a lone BOM before anything else is dropped.
161
+ * A pattern with no leading BOM is returned after the slash collapse alone.
201
162
  */
202
163
  export declare function normalizeFileRulePattern(relativePattern: string): string;
203
164
  /**
204
- * `ki` (dump-confirmed): a pattern ending in `/**` is rewritten before being fed to `ignore()`.
205
- * - For DENY/ASK (`isAllow: false`): the trailing `/**` is simply dropped (`x/**` -> `x`), and the
206
- * result is left UNANCHORED (no leading `/` added) whenever it already has an inner `/`, is not an
207
- * allow rule, or already starts with `!`/`#` -- i.e. almost always for deny/ask. This is C-1's own
208
- * "`ki` turns a deny `x/**` into an unanchored `x`" finding: the bare `x` then matches at ANY
209
- * depth under the `ignore` package's own un-anchoring rule, covering everything under a directory
210
- * named `x` anywhere, not merely the literal anchor-relative `x/`.
211
- * - For ALLOW, when the stripped form has NO inner `/` (a single segment) and does not start with
212
- * `!`/`#`: the result is instead explicitly re-anchored (`x/**` -> `/x`), so `allow x/**` stays
213
- * scoped to the anchor root rather than becoming an anywhere-match the way the deny/ask case does.
214
- * This asymmetry is real and claude's own (not the allow-exact-only asymmetry the controller
215
- * retired) -- it survives because it is measured, not invented.
216
- * - A pattern whose stripped form is empty or all-slashes (`/**`, `//**`) is left as `/**`.
217
- * - Any pattern that does NOT end in `/**` passes through unchanged.
165
+ * The trailing-`/**` rewrite, which differs by direction:
166
+ * - a pattern NOT ending in `/**` is returned unchanged;
167
+ * - if what precedes the `/**` is empty or only slashes, the result is `/**`;
168
+ * - otherwise the `/**` is dropped. For DENY/ASK (`isAllow: false`) the rest is returned as it is:
169
+ * a single segment `x` then matches a directory `x` at any depth and everything under it. For
170
+ * ALLOW, a rest that is a single segment (no `/`) not starting with `!` or `#` gets a leading `/`
171
+ * (`x/**` -> `/x`), so the allow stays scoped to the anchor root; a rest with a `/` in it, or
172
+ * starting with `!`/`#`, is returned as it is.
218
173
  */
219
174
  export declare function unanchorTrailingDoubleStar(pattern: string, isAllow: boolean): string;
220
175
  export interface FileRuleCandidate<TEntry> {
221
176
  entry: TEntry;
222
- /** The rule's own specifier text, exactly as authored (the `jOe`/`xi`/`ki` chain runs on this). */
177
+ /** The rule's own pattern text, exactly as written (anchor resolution, normalisation and the trailing-`/**` rewrite run on this). */
223
178
  pattern: string;
224
- /** `bl(source)`'s stand-in for a `/`-anchored rule -- see `resolveFileRuleAnchor`'s own header. Absent = every `/`-anchored candidate is inert. */
179
+ /** The settings-source directory for a `/`-anchored rule -- see `resolveFileRuleAnchor`. Absent = every `/`-anchored candidate is inert. */
225
180
  sourceDir?: string | undefined;
226
181
  }
227
182
  /**
228
- * Item 2 (fix round 9), REVISED by round 10's own item-3 ruling: a malformed pattern's own compile
229
- * failure is a THROW out of `matchFileRulesGrouped`, not a direction-aware return value. Round 9
230
- * tried to resolve the failure INSIDE this function (denyAsk -> the broken group's first entry,
231
- * allow -> null); round 10's controller ruling is that this is not what claude does -- claude's own
232
- * `Ma` has no per-group catch at all (only the per-TOOL-CALL one far above it, at `d8t`/`ome`'s own
233
- * boundary), so ONE throwing group aborts the WHOLE permission check for that call, exactly the same
234
- * way regardless of which direction (deny/ask/allow) was being evaluated when it happened. This
235
- * class is what makes that propagation typed rather than "any thrown Error" -- caught exactly once,
236
- * at `evaluator.ts`'s own `evaluate()` (the "decide this one call" boundary), and turned into the
237
- * generic fail-closed deny `d8t`'s own hardcoded fallback produces.
183
+ * A malformed pattern's compile failure is a THROW out of `matchFileRulesGrouped`, the same on every
184
+ * direction: one broken group aborts the whole permission check for that call. This class makes that
185
+ * propagation typed; it is caught exactly once, at `evaluator.ts`'s `evaluate()` (the "decide this one
186
+ * call" boundary), and turned into a generic fail-closed deny.
238
187
  */
239
188
  export declare class FileRuleCompileError extends Error {
240
189
  constructor(message: string, options?: {
@@ -242,140 +191,91 @@ export declare class FileRuleCompileError extends Error {
242
191
  });
243
192
  }
244
193
  /**
245
- * `ln`+`Ma`, combined into one grouped match: builds ONE `ignore()` instance per anchor ROOT from
246
- * every candidate (see this module's header for why grouping is load-bearing, not cosmetic), then
247
- * tests `path` against each root's group in turn, returning the FIRST candidate's own `entry` whose
248
- * group matched -- `null` when nothing matched anywhere. `behavior` is `"allow"` or `"denyAsk"`,
249
- * mirroring `matchFileRule`'s own existing direction vocabulary (never allow AND denyAsk on the
250
- * page a caller passed to `ki`, exactly as `ln`'s `r==="allow"` check is exactly `behavior==="allow"`
251
- * here too).
252
- *
253
- * THROWS `FileRuleCompileError` (fix round 10, item 3) when any consulted anchor root's own
254
- * candidate patterns fail to compile into a real `ignore()` instance -- never resolved to `null`
255
- * or a particular entry here; see that class's own header for why, and `evaluator.ts`'s `evaluate()`
256
- * for the one place it is caught.
194
+ * Matches `path` against `candidates`, grouped by anchor root:
195
+ * - each candidate's anchor is resolved (`resolveFileRuleAnchor`, with its own `sourceDir`); an
196
+ * INERT candidate is skipped; a `null` root means `opts.cwd`;
197
+ * - its pattern is normalised (`normalizeFileRulePattern`) and then rewritten
198
+ * (`unanchorTrailingDoubleStar`, `isAllow` = `behavior === "allow"`);
199
+ * - all candidates with the same root go into one case-insensitive `ignore()` instance, in candidate
200
+ * order; the roots are kept in the order they first appear;
201
+ * - for each root in that order, `path` is made relative to the root (`path.relative`). A relative
202
+ * path that is empty, starts with `/`, `./` or `../`, or is exactly `.` or `..` is outside the
203
+ * root, and that group is skipped -- the `ignore` package's own rule for an invalid path, so a
204
+ * name that merely starts with `..` (`..x/evil.sh`) is inside;
205
+ * - the first group that reports the path ignored decides: the result is the entry of the candidate
206
+ * whose compiled pattern text the package names as the matching rule (when two candidates in a
207
+ * group compiled to the same text, the LATER one); if that text maps to no entry, the next group
208
+ * is tried;
209
+ * - `null` when no group matches.
210
+ * THROWS `FileRuleCompileError` when the package fails while testing a group (a malformed pattern
211
+ * only fails then, on first use, never when it is added). The message names the group's root:
212
+ * `a file-rule pattern under <JSON-quoted root> failed to compile`, with the package's error as `cause`.
257
213
  */
258
214
  export declare function matchFileRulesGrouped<TEntry>(candidates: readonly FileRuleCandidate<TEntry>[], path: string, opts: {
259
215
  cwd: string;
260
216
  home: string;
261
217
  }, behavior: "allow" | "denyAsk"): TEntry | null;
262
218
  /**
263
- * `Smt`'s own job (round 5's `QCt`): rewrite a path through its REAL prefix (e.g. `/private/tmp/x`, what
264
- * `realpathSync` actually returns) back to the commonly-typed TRUSTED alias (`/tmp/x`) -- the
265
- * direction a real, resolved path needs to go to be compared against a rule an author wrote in the
266
- * short form. A path with no matching real prefix passes through unchanged.
219
+ * Rewrites a path that starts with one of macOS's real system directories back to the short symlink
220
+ * spelling users type: `/private/tmp` -> `/tmp`, `/private/var` -> `/var`, `/private/etc` -> `/etc`,
221
+ * `/usr/bin` -> `/bin`, `/usr/lib` -> `/lib`, `/usr/sbin` -> `/sbin`, checked in that order. A path
222
+ * matches a pair when it equals the real directory or starts with it followed by `/`. Each pair is
223
+ * VERIFIED once per process (`realpathSync(alias) === real`) rather than assumed, and a pair that
224
+ * does not resolve that way on this machine is never applied. Any other path is returned unchanged.
267
225
  */
268
226
  export declare function canonicalizeTrustedSymlinkPath(path: string): string;
269
227
  /**
270
- * SV-8 (the router same-view test on the 57e7fef binary): claude's own acceptEdits
271
- * working-directory boundary check is `sm` (dump-confirmed by content search) -- a plain RELATIVE-
272
- * PATH PREFIX test, never a compiled glob at all. Winter's own `isWithinBounds` (evaluator.ts) used
273
- * to reuse the general file-rule matcher with a `"**"` sentinel pattern -- harmless before SV-6/C-1,
274
- * but once the general matcher started interpreting `[`, `]`, `*` and `\` as glob metacharacters, a
275
- * cwd or additional-directory root containing any of them (e.g. `[wip] app`) made `"**"` fail to
276
- * compile the way the caller intended, and acceptEdits asked for every write inside that cwd instead
277
- * of auto-approving them.
278
- *
279
- * This function sidesteps the escaping question SV-8 raises entirely, the same way claude's own `sm`
280
- * does: a plain path-prefix test never interprets EITHER path as glob syntax, so a root containing a
281
- * glob-special character needs no escaping here at all -- unlike a real RULE pattern (I-G's own
282
- * concern), which does.
228
+ * The acceptEdits working-directory boundary: is `childPath` inside `rootPath`? A plain path-prefix
229
+ * test, never a glob, so a root containing `[`, `]`, `*` or `\` (a cwd named `[wip] app`) needs no
230
+ * escaping.
283
231
  *
284
- * Ported: `caseFold` (default `true`, matching `sm`'s own default and I-D's case-insensitivity
285
- * finding generally) folds BOTH paths before computing the relative path between them; the macOS
286
- * `/private/var` -> `/var` and `/private/tmp` -> `/tmp` aliasing is real-symlink-aware -- macOS
287
- * itself maintains both as symlinks to the `/private/...` originals, so a session cwd resolved
288
- * through one spelling and a root configured with the other name the SAME real directory (this
289
- * matters in practice: `os.tmpdir()` on macOS resolves through `/private/var/folders/...`, which is
290
- * exactly the shape every mkdtemp-based fixture in this codebase's own test suite produces). Not
291
- * ported: `sm`'s own `uncShapeParity` and `skipPrivateAlias` options (Windows-only concerns) and its
292
- * `Gn`/`Ha` UNC-path checks -- this codebase supports macOS only (CLAUDE.md's own "latest-OS
293
- * floors" rule).
294
- *
295
- * Fix round 6 (R5-2, the re-review against the pinned 2.1.250 dump): ONLY these TWO pairs -- round
296
- * 5 widened this to the full six-pair `ni()`/`Sl()` map (`/private/etc`, `/usr/bin`, `/usr/lib`,
297
- * `/usr/sbin` included), which was WRONG for `sm` specifically: content search against the pinned
298
- * 2.1.250 dump (not the 2.1.280 build round 5 was cited against) found `sm`'s own alias regexes
299
- * verbatim -- `g=r?/^\/private\/var\//i:/^\/private\/var\//,w=r?/^\/private\/tmp(\/|$)/i:/^\/private\/
300
- * tmp(\/|$)/` -- exactly these two, unconditionally, never the wider six-pair set. Reverted to match;
301
- * the six-pair map (`trustedSymlinkEquivalences`/`canonicalizeTrustedSymlinkPath`) stays, but is now
302
- * used ONLY by the allow-rule retry in evaluator.ts (`cqe`'s own scope, confirmed at the same dump
303
- * site), never by this function.
232
+ * 1. On each path (independently), a leading `/private/var/` becomes `/var/`, and a leading
233
+ * `/private/tmp` followed by `/` or the end of the path becomes `/tmp` (case-sensitive tests);
234
+ * macOS keeps both short spellings as symlinks, and `os.tmpdir()` resolves through
235
+ * `/private/var/folders/...`.
236
+ * 2. With `caseFold` (default `true`), both are then lower-cased.
237
+ * 3. The answer comes from `path.relative(root, child)`: `""` is inside; `..` or anything starting
238
+ * `../` is outside; otherwise inside unless the relative path is absolute.
304
239
  */
305
240
  export declare function isPathWithinRoot(childPath: string, rootPath: string, opts?: {
306
241
  caseFold?: boolean;
307
242
  }): boolean;
308
243
  /**
309
- * The traversal fence for a plugin MANIFEST's own declared component paths (`commands`/`agents`/
310
- * `skills`/`output-styles`/`workflows`/`hooks`).
311
- *
312
- * Fix round 6 (R5-1 + a promoted minor, the re-review against the PINNED 2.1.250 dump): claude's own
313
- * check here is `nV` (dump-confirmed by content search against the pinned dump directly, at the
314
- * scratchpad path the controller named -- superseding fix round 5's citation of `KGe`/`Aoe` against
315
- * the INSTALLED 2.1.280 binary, which this round's own ruling says is not the parity authority):
316
- * `nV(root,entry)` resolves `entry` against `root`, computes `u=path.relative(root,resolved)`, and
317
- * refuses (`return null`) when `u.startsWith("..")`. Three ways this DIFFERS from `isPathWithinRoot`/
318
- * `sm` above, all ported exactly rather than reused:
319
- * 1. CASE-SENSITIVE, always -- `nV`'s own body has no folding call anywhere (confirmed by reading
320
- * it in full), unlike `sm`'s own `r?/.../i:/.../ ` case-fold branching. Fix round 5's own
321
- * `resolvesWithinPluginRoot` wrongly delegated to `isPathWithinRoot`'s DEFAULT `caseFold:true`,
322
- * so on a case-sensitive volume a manifest entry like `../FOO/agents` under a root
323
- * `.../plugins/foo` was admitted (folded, `FOO` read as `foo`) where claude's own `nV` (and
324
- * this rewrite) refuses it.
325
- * 2. NAIVE STRING-PREFIX, not segment-aware -- `u.startsWith("..")` is a bare string test, unlike
326
- * `sm`'s own `uj` (`/(?:^|[\\/])\.\.(?:[\\/]|$)/`, confirmed by reading ITS full definition too),
327
- * which requires a `..` SEGMENT bounded by a separator or a string edge. This means a component
328
- * name that merely STARTS WITH the two characters `..` -- e.g. `"..x/agents"`, a real,
329
- * non-escaping subdirectory name -- is REFUSED by claude too, not only a genuine `"../"` escape.
330
- * Matched here rather than "fixed", per the ruling: claude's own inconsistency between its two
331
- * path-safety mechanisms is not this codebase's to resolve by choosing the more correct one.
332
- * 3. NO trusted-symlink alias mapping at all -- `nV`'s own body never calls anything resembling
333
- * `Smt`/`canonicalizeTrustedSymlinkPath`. Moot in practice here regardless, since both operands
334
- * below are ALREADY realpath'd before this comparison runs (a real, resolved path from
335
- * `/tmp`/`/var` already comes back in its long `/private/...` form either way).
336
- *
337
- * DISCLOSED DIVERGENCE FROM THE PINNED 2.1.250, kept as DELIBERATE HARDENING (the controller's own
338
- * explicit ruling): `nV` itself is PURELY LEXICAL -- 2.1.250 has no symlink-following/realpath step
339
- * for a plugin component path at all. This function still realpaths both the candidate and the
340
- * plugin root first (originally ported from the INSTALLED 2.1.280 binary's own `KGe`/`Aoe`, which DID
341
- * add this in a build newer than the pin), refusing a symlinked override that points outside the
342
- * plugin where 2.1.250 would load it -- the safe direction, and it matches claude's own newer
343
- * behaviour. `nV`'s own comparison shape (case-sensitive, naive-prefix, no alias) is then applied to
344
- * the REALPATH'D forms rather than to the raw ones `nV` itself compares. `resolveRealTarget` (paths.ts)
345
- * has the graceful "walk up to the nearest existing ancestor" fallback for a candidate that does not
346
- * exist YET, and rethrows a non-ENOENT failure (ELOOP on a symlink cycle, EACCES, ...), caught here
347
- * and treated as a refusal rather than letting a malformed manifest entry crash the whole
348
- * plugin-loading pass.
244
+ * Whether a path a plugin MANIFEST declares for a component (`commands`/`agents`/`skills`/
245
+ * `output-styles`/`workflows`/`hooks`) stays inside the plugin:
246
+ * 1. a candidate containing a backslash is refused;
247
+ * 2. both the candidate and the plugin root are resolved to their real targets
248
+ * (`resolveRealTarget`, which walks up to the nearest existing ancestor for a path that does not
249
+ * exist yet); any failure there (a symlink loop, a permission error) refuses;
250
+ * 3. `path.relative(realRoot, realCandidate)`: `""` is inside; ANY relative path starting with the
251
+ * two characters `..` is refused -- `..x/agents` included, a naive string test kept on purpose;
252
+ * otherwise inside unless the relative path is absolute.
253
+ * Case-sensitive, and with no `/tmp`-style alias mapping (both sides are already real paths).
254
+ * Resolving symlinks first refuses an override that points outside the plugin.
349
255
  */
350
256
  export declare function resolvesWithinPluginRoot(candidatePath: string, pluginRoot: string): boolean;
351
257
  /**
352
- * Fix round 4 (I-G): a real, resolved filesystem path (e.g. `resolve(winterHome)`) can legitimately
353
- * contain `[`, `]`, `*` or `\` -- none of which were glob-special under Winter's pre-fix-round-4
354
- * grammar, but all four are now, since `matchFileRulesGrouped` compiles every pattern through the
355
- * real `ignore` package. A caller building a rule PATTERN out of a real path (`buildBaselineDenyRules`,
356
- * engine.ts) must escape these four before interpolating the path into pattern text, or a home
357
- * directory literally named e.g. `/Users/name[wip]` would have its OWN floor's `[wip]` read back as
358
- * a character class instead of the four literal characters it names on disk.
258
+ * A real path (e.g. `resolve(winterHome)`) can contain `[`, `]`, `*` or `\`, all of which are glob
259
+ * syntax to `matchFileRulesGrouped`. A caller building a rule PATTERN from a real path
260
+ * (`buildBaselineDenyRules`, engine.ts) escapes it with this first, or a home named
261
+ * `/Users/name[wip]` would have its own floor's `[wip]` read back as a character class.
262
+ *
263
+ * 1. a backslash goes before every `[`, `]`, `*` and `\`;
264
+ * 2. then the trailing run of whitespace (`\s`), if any, gets a backslash before EACH of its
265
+ * characters -- the `ignore` package trims unescaped trailing whitespace off a pattern, so a path
266
+ * ending in a space would otherwise lose its own protection.
359
267
  *
360
- * `?` is DELIBERATELY LEFT RAW, per the controller's own ruling: claude's grammar has no working
361
- * escape for `?` at all (this module's own `unanchorTrailingDoubleStar`/`\?`-quirk sibling
362
- * documentation) -- an escaped `\?` would require a literal backslash the real path never has, so it
363
- * would never match the floor's own intended directory at all. A bare `?` in the pattern instead acts
364
- * as a single-character wildcard, which still MATCHES a real `?` in the path (a wildcard matches
365
- * anything, including the literal character) -- over-matching by one character class is the safe
366
- * direction for a DENY floor (WS-07 §3.1's own posture: a deny that reaches slightly too far is a
367
- * false-negative-avoiding cost, never a hole), where an escape that matches NOTHING would be a hole.
268
+ * `?` is deliberately left raw: the grammar has no working escape for it (`\?` would demand a literal
269
+ * backslash the real path never has, so it would match nothing). A raw `?` is a one-character
270
+ * wildcard, which still matches the real `?` -- over-matching by one character is the safe direction
271
+ * for a DENY floor, where an escape that matched nothing would be a hole.
368
272
  */
369
273
  export declare function escapeFileRulePathSegment(path: string): string;
370
274
  /**
371
- * A SINGLE pattern against a SINGLE path -- for a caller that is already iterating rule entries one
372
- * at a time for a reason unrelated to C-1/SV-7 (e.g. evaluator.ts's own cross-tool
373
- * `findFileDenyBlockingEdit`, a Winter-invented safety net with no claude analogue: claude's own
374
- * Write decision never consults `Read(...)` rules at all, so "a Read deny also blocks a Write" is
375
- * this codebase's own extension, not a ported behaviour). Grouping (this module's own header)
376
- * therefore does not apply across DIFFERENT callers' unrelated single-pattern checks the way it does
377
- * within one `matchFileRulesGrouped` call -- this is a thin, no-negation-support convenience, not a
378
- * second matching engine.
275
+ * A SINGLE pattern against a SINGLE path -- for a caller already iterating rule entries one at a time
276
+ * for its own reason (e.g. evaluator.ts's cross-tool `findFileDenyBlockingEdit`, a Winter-only safety
277
+ * net: a Read deny also blocks a Write). Grouping does not apply across such unrelated single checks;
278
+ * this is a thin convenience, not a second matching engine.
379
279
  */
380
280
  export declare function matchesSingleFileRulePattern(pattern: string, path: string, opts: {
381
281
  cwd: string;