@gotgenes/pi-permission-system 26.2.1 → 26.2.2

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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [26.2.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v26.2.1...pi-permission-system-v26.2.2) (2026-08-18)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **pi-permission-system:** derive session-approval patterns through the injected PathFlavor ([cf561de](https://github.com/gotgenes/pi-packages/commit/cf561de4c19895ed9495d82f22280fe3ba215aa5)), closes [#655](https://github.com/gotgenes/pi-packages/issues/655)
14
+
8
15
  ## [26.2.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v26.2.0...pi-permission-system-v26.2.1) (2026-08-17)
9
16
 
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "26.2.1",
3
+ "version": "26.2.2",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,8 +1,8 @@
1
1
  import type { BashProgram } from "#src/access-intent/bash/program";
2
+ import type { PathNormalizer } from "#src/path-normalizer";
2
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
4
  import { buildBashExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
4
5
  import { SessionApproval } from "#src/session-approval";
5
- import { deriveApprovalPattern } from "#src/session-rules";
6
6
  import type { GateResult } from "./descriptor";
7
7
  import { selectUncoveredExternalPaths } from "./external-directory-policy";
8
8
  import { accessFactsFromPath } from "./helpers";
@@ -25,6 +25,7 @@ export function describeBashExternalDirectoryGate(
25
25
  tcc: ToolCallContext,
26
26
  bashProgram: BashProgram | null,
27
27
  resolver: ScopedPermissionResolver,
28
+ normalizer: PathNormalizer,
28
29
  ): GateResult {
29
30
  if (!bashProgram) return null;
30
31
  const command = bashProgram.commandText();
@@ -94,7 +95,9 @@ export function describeBashExternalDirectoryGate(
94
95
  matchedPattern: preCheck.matchedPattern,
95
96
  });
96
97
 
97
- const patterns = uncoveredPaths.map((p) => deriveApprovalPattern(p));
98
+ const patterns = uncoveredEntries.map(({ path }) =>
99
+ normalizer.approvalPatternFor(path),
100
+ );
98
101
 
99
102
  return {
100
103
  surface: "external_directory",
@@ -1,9 +1,9 @@
1
1
  import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import type { BashProgram } from "#src/access-intent/bash/program";
3
+ import type { PathNormalizer } from "#src/path-normalizer";
3
4
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
5
  import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
5
6
  import { SessionApproval } from "#src/session-approval";
6
- import { deriveApprovalPattern } from "#src/session-rules";
7
7
  import type { PermissionCheckResult } from "#src/types";
8
8
  import { pickMostRestrictive } from "./candidate-check";
9
9
  import type { GateResult } from "./descriptor";
@@ -33,6 +33,7 @@ export function describeBashPathGate(
33
33
  tcc: ToolCallContext,
34
34
  bashProgram: BashProgram | null,
35
35
  resolver: ScopedPermissionResolver,
36
+ normalizer: PathNormalizer,
36
37
  ): GateResult {
37
38
  if (!bashProgram) return null;
38
39
  const command = bashProgram.commandText();
@@ -120,7 +121,7 @@ export function describeBashPathGate(
120
121
  // Derive the pattern from the lexical absolute form (the cd-aware resolved
121
122
  // path), so it matches the values a later call produces. For an unknown base
122
123
  // (`forLiteral`) `value()` is the raw token.
123
- const pattern = deriveApprovalPattern(worstEntry.path.value());
124
+ const pattern = normalizer.approvalPatternFor(worstEntry.path);
124
125
  const payload = buildPathAskPayload({
125
126
  toolName: tcc.toolName,
126
127
  pathValue: worstToken,
@@ -3,7 +3,6 @@ import type { PathNormalizer } from "#src/path-normalizer";
3
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
4
  import { buildExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
5
5
  import { SessionApproval } from "#src/session-approval";
6
- import { deriveApprovalPattern } from "#src/session-rules";
7
6
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
8
7
  import type { GateResult } from "./descriptor";
9
8
  import { resolveExternalDirectoryPolicy } from "./external-directory-policy";
@@ -78,7 +77,7 @@ export function describeExternalDirectoryGate(
78
77
  resolver,
79
78
  tcc.agentName ?? undefined,
80
79
  );
81
- const pattern = deriveApprovalPattern(accessPath.value());
80
+ const pattern = normalizer.approvalPatternFor(accessPath);
82
81
 
83
82
  const payload = buildExternalDirectoryAskPayload({
84
83
  toolName: tcc.toolName,
@@ -3,7 +3,6 @@ import type { PathNormalizer } from "#src/path-normalizer";
3
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
4
  import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
5
5
  import { SessionApproval } from "#src/session-approval";
6
- import { deriveApprovalPattern } from "#src/session-rules";
7
6
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
8
7
  import type { GateDescriptor, GateResult } from "./descriptor";
9
8
  import { accessFactsFromPath } from "./helpers";
@@ -46,7 +45,7 @@ export function describePathGate(
46
45
 
47
46
  // Derive the approval pattern from the lexical absolute form so it matches
48
47
  // the policy values a later call produces.
49
- const pattern = deriveApprovalPattern(accessPath.value());
48
+ const pattern = normalizer.approvalPatternFor(accessPath);
50
49
 
51
50
  const payload = buildPathAskPayload({
52
51
  toolName: tcc.toolName,
@@ -1,4 +1,3 @@
1
- import type { AccessPath } from "#src/access-intent/access-path";
2
1
  import { BashProgram } from "#src/access-intent/bash/program";
3
2
  import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
4
3
  import {
@@ -24,7 +23,7 @@ import { describeExternalDirectoryGate } from "./external-directory";
24
23
  import { describePathGate } from "./path";
25
24
  import type { GateRunner } from "./runner";
26
25
  import { describeSkillReadGate } from "./skill-read";
27
- import { describeToolGate } from "./tool";
26
+ import { describeToolGate, type ToolPathAccess } from "./tool";
28
27
  import type { GateOutcome, ToolCallContext } from "./types";
29
28
 
30
29
  /**
@@ -115,10 +114,16 @@ export class ToolCallGatePipeline {
115
114
  normalizer,
116
115
  this.customExtractors,
117
116
  ),
118
- () => describeBashExternalDirectoryGate(tcc, bashProgram, this.resolver),
119
- () => describeBashPathGate(tcc, bashProgram, this.resolver),
117
+ () =>
118
+ describeBashExternalDirectoryGate(
119
+ tcc,
120
+ bashProgram,
121
+ this.resolver,
122
+ normalizer,
123
+ ),
124
+ () => describeBashPathGate(tcc, bashProgram, this.resolver, normalizer),
120
125
  () => {
121
- const { toolCheck, accessPath } = this.resolvePerToolCheck(
126
+ const { toolCheck, pathAccess } = this.resolvePerToolCheck(
122
127
  tcc,
123
128
  shell,
124
129
  bashProgram,
@@ -128,7 +133,7 @@ export class ToolCallGatePipeline {
128
133
  tcc,
129
134
  toolCheck,
130
135
  formatter,
131
- accessPath,
136
+ pathAccess,
132
137
  shell,
133
138
  );
134
139
  toolDescriptor.preCheck = toolCheck;
@@ -153,15 +158,16 @@ export class ToolCallGatePipeline {
153
158
  * #502); every other tool (and a path-bearing tool with no path) keeps the
154
159
  * raw `tool` intent the manager normalizes.
155
160
  *
156
- * Returns the `AccessPath` alongside the check so `describeToolGate` derives
157
- * the session-approval value from `accessPath.value()`.
161
+ * Returns the resolved path alongside the check, already paired with the
162
+ * session scope approving it grants — derived here, where the normalizer
163
+ * lives, rather than inside the gate (#655).
158
164
  */
159
165
  private resolvePerToolCheck(
160
166
  tcc: ToolCallContext,
161
167
  shell: ShellInvocation | null,
162
168
  bashProgram: BashProgram | null,
163
169
  normalizer: PathNormalizer,
164
- ): { toolCheck: PermissionCheckResult; accessPath?: AccessPath } {
170
+ ): { toolCheck: PermissionCheckResult; pathAccess?: ToolPathAccess } {
165
171
  if (shell) {
166
172
  if (bashProgram) {
167
173
  return {
@@ -190,7 +196,10 @@ export class ToolCallGatePipeline {
190
196
  if (filePath !== null) {
191
197
  const accessPath = normalizer.forPath(filePath);
192
198
  return {
193
- accessPath,
199
+ pathAccess: {
200
+ path: accessPath,
201
+ approvalPattern: normalizer.approvalPatternFor(accessPath),
202
+ },
194
203
  toolCheck: this.resolver.resolve({
195
204
  kind: "access-path",
196
205
  surface: tcc.toolName,
@@ -5,7 +5,10 @@ import {
5
5
  classifyToolKind,
6
6
  type ShellInvocation,
7
7
  } from "#src/access-intent/tool-kind";
8
- import { suggestSessionPattern } from "#src/pattern-suggest";
8
+ import {
9
+ suggestPathSessionPattern,
10
+ suggestSessionPattern,
11
+ } from "#src/pattern-suggest";
9
12
  import { buildToolAskPayload } from "#src/presentation/tool-ask-payload";
10
13
  import { SessionApproval } from "#src/session-approval";
11
14
  import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
@@ -18,18 +21,30 @@ import {
18
21
  } from "./helpers";
19
22
  import type { ToolCallContext } from "./types";
20
23
 
24
+ /**
25
+ * A path-bearing tool call's resolved path, paired with the session scope
26
+ * approving it would grant.
27
+ *
28
+ * The pattern is derived by the pipeline's `PathNormalizer`, which owns the
29
+ * session's `PathFlavor`, rather than re-derived here from `path.value()` — so
30
+ * the gate carries the platform's separator semantics without holding them
31
+ * (#655).
32
+ */
33
+ export interface ToolPathAccess {
34
+ readonly path: AccessPath;
35
+ readonly approvalPattern: string;
36
+ }
37
+
21
38
  /**
22
39
  * Derive the value used for session-approval pattern suggestions.
23
40
  *
24
- * Bash → command string; MCP → qualified target;
25
- * path-bearing tools the `AccessPath`'s lexical absolute form (`value()`),
26
- * so the suggested pattern matches the policy values a later call produces;
27
- * others (or a path-bearing tool with no path) → catch-all wildcard.
41
+ * Bash → command string; MCP → qualified target; everything else → catch-all
42
+ * wildcard. A path-bearing tool that resolved a path never reaches here — its
43
+ * suggestion comes from the already-derived {@link ToolPathAccess} pattern.
28
44
  */
29
45
  function deriveSuggestionValue(
30
46
  toolName: string,
31
47
  check: PermissionCheckResult,
32
- accessPath?: AccessPath,
33
48
  ): string {
34
49
  switch (classifyToolKind(toolName)) {
35
50
  case "bash":
@@ -37,7 +52,7 @@ function deriveSuggestionValue(
37
52
  case "mcp":
38
53
  return check.target ?? "mcp";
39
54
  default:
40
- return accessPath ? accessPath.value() : "*";
55
+ return "*";
41
56
  }
42
57
  }
43
58
 
@@ -51,7 +66,7 @@ export function describeToolGate(
51
66
  tcc: ToolCallContext,
52
67
  check: PermissionCheckResult,
53
68
  formatter: ToolPreviewFormatter,
54
- accessPath?: AccessPath,
69
+ pathAccess?: ToolPathAccess,
55
70
  shell?: ShellInvocation | null,
56
71
  ): GateDescriptor {
57
72
  // A shell invocation (native `bash` or an aliased shell tool) is gated on the
@@ -67,10 +82,12 @@ export function describeToolGate(
67
82
  );
68
83
 
69
84
  // Compute session approval suggestion for the "for this session" option.
70
- const suggestion = suggestSessionPattern(
71
- gateSurface,
72
- deriveSuggestionValue(gateSurface, check, accessPath),
73
- );
85
+ const suggestion = pathAccess
86
+ ? suggestPathSessionPattern(gateSurface, pathAccess.approvalPattern)
87
+ : suggestSessionPattern(
88
+ gateSurface,
89
+ deriveSuggestionValue(gateSurface, check),
90
+ );
74
91
 
75
92
  const payload = buildToolAskPayload({
76
93
  check,
@@ -89,8 +106,8 @@ export function describeToolGate(
89
106
 
90
107
  // A path-bearing tool carries the AccessPath's alias set; every other surface
91
108
  // (bash command, MCP target, plain tool) carries its already-portable value.
92
- const accessIntent = accessPath
93
- ? accessFactsFromPath(gateSurface, accessPath)
109
+ const accessIntent = pathAccess
110
+ ? accessFactsFromPath(gateSurface, pathAccess.path)
94
111
  : accessFactsFromValue(gateSurface, decisionValue);
95
112
 
96
113
  return {
@@ -0,0 +1,27 @@
1
+ import type { PathFlavor } from "#src/path/path-flavor";
2
+
3
+ /**
4
+ * Derive the wildcard glob to record when a user approves an accessed path for
5
+ * the session: the path's directory scope, with `*` appended.
6
+ *
7
+ * The scope is the value up to and including its last path separator, so the
8
+ * pattern is spelled with the separator the value itself carries. That matters
9
+ * on a win32 host, where Git Bash tokens are POSIX-shaped while Node's own
10
+ * `sep` is a backslash: deriving `/tmp/logs\*` from `/tmp/logs/` widens the
11
+ * grant to the parent directory once the `windowsSeparators` fold (#653)
12
+ * normalizes both operands. A value carrying no separator falls back to the
13
+ * current directory, which is what callers see only if they skipped resolving
14
+ * the path to its absolute form first (#438).
15
+ *
16
+ * The platform's separator alphabet arrives as an injected {@link PathFlavor},
17
+ * never an ambient `node:path` read, so win32 derivation is decidable — and
18
+ * testable — on a POSIX host (#655).
19
+ */
20
+ export function deriveApprovalPattern(
21
+ pathValue: string,
22
+ flavor: PathFlavor,
23
+ ): string {
24
+ const lastSeparator = flavor.lastSeparatorIndex(pathValue);
25
+ if (lastSeparator < 0) return `.${flavor.impl.sep}*`;
26
+ return `${pathValue.slice(0, lastSeparator + 1)}*`;
27
+ }
@@ -17,7 +17,8 @@ import type { WildcardMatchOptions } from "#src/wildcard-matcher";
17
17
  * fold or separator fold is a silent bypass (the #382 / #508 class). `PathFlavor`
18
18
  * captures that mapping once so the leaves consume the resolved capability
19
19
  * instead of re-interpreting a raw `NodeJS.Platform` string. It owns platform
20
- * **semantics** — syntax ({@link hasPathSeparator}), token shape
20
+ * **semantics** — syntax ({@link hasPathSeparator} /
21
+ * {@link lastSeparatorIndex}), token shape
21
22
  * ({@link bashTokenShape}), and the equivalence relation ({@link fold} /
22
23
  * {@link comparable} / {@link isWithin} / {@link matchOptions}); domain policy
23
24
  * (lexical cleanup, alias generation, safe-system-path exclusions, rule
@@ -50,6 +51,15 @@ export interface PathFlavor {
50
51
  * POSIX; `/` or `\` on win32 (where a backslash is a separator, #520).
51
52
  */
52
53
  hasPathSeparator(token: string): boolean;
54
+ /**
55
+ * Index of the last path separator in `value`, or `-1` when it holds none.
56
+ *
57
+ * Reads the same separator alphabet as {@link hasPathSeparator}, so a caller
58
+ * that must split a path at its directory boundary uses the separator the
59
+ * value was written with rather than this platform's default `sep` — the two
60
+ * differ for a Git Bash token on a win32 host (`/dev/null`, `/tmp/logs/`).
61
+ */
62
+ lastSeparatorIndex(value: string): number;
53
63
  /**
54
64
  * The MSYS/Git-Bash interpretation of a bash-command token. On win32 this
55
65
  * carries device / drive-mount / posix-absolute / plain semantics; on POSIX
@@ -60,6 +70,8 @@ export interface PathFlavor {
60
70
 
61
71
  class PlatformPathFlavor implements PathFlavor {
62
72
  readonly matchOptions: WildcardMatchOptions | undefined;
73
+ /** Every separator spelling this platform recognizes, the one alphabet both separator answers read. */
74
+ private readonly separators: readonly string[];
63
75
 
64
76
  constructor(
65
77
  readonly impl: PlatformPath,
@@ -68,6 +80,7 @@ class PlatformPathFlavor implements PathFlavor {
68
80
  this.matchOptions = windows
69
81
  ? { caseInsensitive: true, windowsSeparators: true }
70
82
  : undefined;
83
+ this.separators = windows ? ["/", "\\"] : ["/"];
71
84
  }
72
85
 
73
86
  fold(value: string): string {
@@ -91,7 +104,14 @@ class PlatformPathFlavor implements PathFlavor {
91
104
  }
92
105
 
93
106
  hasPathSeparator(token: string): boolean {
94
- return token.includes("/") || (this.windows && token.includes("\\"));
107
+ return this.lastSeparatorIndex(token) >= 0;
108
+ }
109
+
110
+ lastSeparatorIndex(value: string): number {
111
+ return this.separators.reduce(
112
+ (last, separator) => Math.max(last, value.lastIndexOf(separator)),
113
+ -1,
114
+ );
95
115
  }
96
116
 
97
117
  bashTokenShape(token: string): BashTokenShape {
@@ -8,6 +8,7 @@ import {
8
8
  normalizePathForComparison,
9
9
  normalizePathPolicyLiteral,
10
10
  } from "./access-intent/path-normalization";
11
+ import { deriveApprovalPattern } from "./path/approval-pattern";
11
12
  import { isPathOutsideWorkingDirectory } from "./path/path-containment";
12
13
  import { isPiInfrastructureRead } from "./path/pi-infrastructure-read";
13
14
 
@@ -99,6 +100,20 @@ export class PathNormalizer {
99
100
  }
100
101
  }
101
102
 
103
+ /**
104
+ * The session-approval glob for an accessed path: its directory scope plus
105
+ * `*`, derived through the baked flavor.
106
+ *
107
+ * Takes the already-built {@link AccessPath} — the lexical form is what a
108
+ * later tool call is matched on, so the pattern must be derived from the
109
+ * same representation the decision displayed (#438). Deriving it here rather
110
+ * than at each gate keeps the platform's separator alphabet with the object
111
+ * that owns the flavor, instead of an ambient `node:path` read (#655).
112
+ */
113
+ approvalPatternFor(accessPath: AccessPath): string {
114
+ return deriveApprovalPattern(accessPath.value(), this.flavor);
115
+ }
116
+
102
117
  /** Platform-aware absoluteness (`win32` vs `posix` rules). */
103
118
  isAbsolute(pathValue: string): boolean {
104
119
  return this.flavor.impl.isAbsolute(pathValue);
@@ -1,6 +1,5 @@
1
1
  import { PATH_BEARING_TOOLS } from "./access-intent/path-surfaces";
2
2
  import { prefix, stripBashCommentLines } from "./bash-arity";
3
- import { deriveApprovalPattern } from "./session-rules";
4
3
 
5
4
  /** The suggestion returned for a "Yes, for this session" dialog option. */
6
5
  export interface SessionApprovalSuggestion {
@@ -115,14 +114,16 @@ function buildLabel(pattern: string, surface: string): string {
115
114
  }
116
115
 
117
116
  /**
118
- * Suggest a session-approval pattern for the given permission surface and value.
117
+ * Suggest a session-approval pattern from a surface's own value vocabulary
118
+ * a bash command, an MCP target, a skill name.
119
119
  *
120
120
  * Returns a `SessionApprovalSuggestion` with the surface, the wildcard pattern
121
- * to store in `SessionRules`, and a human-readable dialog label.
121
+ * to store in `SessionRules`, and a human-readable dialog label. Any surface
122
+ * with no vocabulary of its own falls back to the catch-all wildcard, which is
123
+ * also what a path-bearing tool invoked without a path resolves to.
122
124
  *
123
- * `value` is expected to be the canonical (cwd-resolved, absolute) path for
124
- * path surfaces callers resolve it before suggesting, so the derived pattern
125
- * matches the policy values a later tool call produces.
125
+ * A path surface goes through {@link suggestPathSessionPattern} instead: its
126
+ * pattern is a path-language product, and this module holds no path semantics.
126
127
  */
127
128
  export function suggestSessionPattern(
128
129
  surface: string,
@@ -140,22 +141,30 @@ export function suggestSessionPattern(
140
141
  case "skill":
141
142
  pattern = value;
142
143
  break;
143
- case "external_directory":
144
- pattern = deriveApprovalPattern(value);
145
- break;
146
- case "path":
147
- pattern = deriveApprovalPattern(value);
148
- break;
149
144
  default:
150
- // Path-bearing tools: derive a directory-scoped pattern from the path.
151
- if (PATH_BEARING_TOOLS.has(surface) && value !== "*") {
152
- pattern = deriveApprovalPattern(value);
153
- break;
154
- }
155
- // Extension tools / fallback.
145
+ // Extension tools, and path-bearing tools invoked without a path.
156
146
  pattern = "*";
157
147
  break;
158
148
  }
159
149
 
160
150
  return { surface, pattern, label: buildLabel(pattern, surface) };
161
151
  }
152
+
153
+ /**
154
+ * Build the suggestion for a path surface from a pattern the caller already
155
+ * derived through its `PathNormalizer` (#655).
156
+ *
157
+ * The derivation belongs to the normalizer, which owns the session's
158
+ * `PathFlavor`; this module labels the result and must not re-interpret the
159
+ * separators it is handed.
160
+ */
161
+ export function suggestPathSessionPattern(
162
+ surface: string,
163
+ approvalPattern: string,
164
+ ): SessionApprovalSuggestion {
165
+ return {
166
+ surface,
167
+ pattern: approvalPattern,
168
+ label: buildLabel(approvalPattern, surface),
169
+ };
170
+ }
@@ -1,5 +1,3 @@
1
- import { dirname, sep } from "node:path";
2
-
3
1
  import type { Ruleset } from "./rule";
4
2
  import type { SessionApproval } from "./session-approval";
5
3
  import type { SessionApprovalRecorder } from "./session-approval-recorder";
@@ -48,32 +46,3 @@ export class SessionRules implements SessionApprovalRecorder {
48
46
  this.rules = [];
49
47
  }
50
48
  }
51
-
52
- /**
53
- * Derive the wildcard glob pattern to approve from a normalized path.
54
- *
55
- * Returns `<parent-dir>/*` so that `evaluate()` / `wildcardMatch()` matches
56
- * all paths under the approved directory — identical semantics to the former
57
- * `SessionApprovalCache` prefix matching, using the unified wildcard engine.
58
- *
59
- * For paths that already end with a separator (directories), the separator
60
- * is treated as the directory boundary and `*` is appended directly.
61
- *
62
- * The path is expected to be the canonical (cwd-resolved, absolute) form used
63
- * for policy matching, so the derived pattern matches the same policy values a
64
- * later tool call produces. Callers that hold a working directory resolve the
65
- * path to that form first; the function itself stays free of cwd state.
66
- */
67
- export function deriveApprovalPattern(normalizedPath: string): string {
68
- // If the path already ends with a separator, it's a directory — glob its contents.
69
- if (normalizedPath.endsWith(sep)) {
70
- return `${normalizedPath}*`;
71
- }
72
- const dir = dirname(normalizedPath);
73
- if (dir === normalizedPath) {
74
- // Root path — dirname('/') === '/'
75
- return `${dir}*`;
76
- }
77
- const prefix = dir.endsWith(sep) ? dir : `${dir}${sep}`;
78
- return `${prefix}*`;
79
- }