@cortexkit/aft-bridge 0.58.2 → 0.60.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.
Files changed (114) hide show
  1. package/dist/bash-format.d.ts.map +1 -1
  2. package/dist/bash-format.js +5 -0
  3. package/dist/bash-format.js.map +1 -1
  4. package/dist/bash-hints.d.ts +76 -30
  5. package/dist/bash-hints.d.ts.map +1 -1
  6. package/dist/bash-hints.js +225 -47
  7. package/dist/bash-hints.js.map +1 -1
  8. package/dist/bash-host-fallback.d.ts +27 -0
  9. package/dist/bash-host-fallback.d.ts.map +1 -1
  10. package/dist/bash-host-fallback.js +55 -16
  11. package/dist/bash-host-fallback.js.map +1 -1
  12. package/dist/binary-identity.d.ts +13 -0
  13. package/dist/binary-identity.d.ts.map +1 -1
  14. package/dist/binary-identity.js +91 -2
  15. package/dist/binary-identity.js.map +1 -1
  16. package/dist/bridge.d.ts +45 -3
  17. package/dist/bridge.d.ts.map +1 -1
  18. package/dist/bridge.js +161 -42
  19. package/dist/bridge.js.map +1 -1
  20. package/dist/callgraph-format.d.ts.map +1 -1
  21. package/dist/callgraph-format.js +7 -7
  22. package/dist/callgraph-format.js.map +1 -1
  23. package/dist/child-process.d.ts.map +1 -1
  24. package/dist/child-process.js +10 -1
  25. package/dist/child-process.js.map +1 -1
  26. package/dist/config-error.d.ts +2 -2
  27. package/dist/config-error.js +2 -2
  28. package/dist/config-watch.d.ts +10 -6
  29. package/dist/config-watch.d.ts.map +1 -1
  30. package/dist/config-watch.js +46 -12
  31. package/dist/config-watch.js.map +1 -1
  32. package/dist/durable-log.d.ts +11 -0
  33. package/dist/durable-log.d.ts.map +1 -1
  34. package/dist/durable-log.js +67 -24
  35. package/dist/durable-log.js.map +1 -1
  36. package/dist/error-contract.d.ts +29 -5
  37. package/dist/error-contract.d.ts.map +1 -1
  38. package/dist/error-contract.js +68 -9
  39. package/dist/error-contract.js.map +1 -1
  40. package/dist/feature-config.d.ts +35 -18
  41. package/dist/feature-config.d.ts.map +1 -1
  42. package/dist/feature-config.js +236 -94
  43. package/dist/feature-config.js.map +1 -1
  44. package/dist/index.d.ts +11 -9
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +11 -9
  47. package/dist/index.js.map +1 -1
  48. package/dist/location-lifecycle.d.ts.map +1 -1
  49. package/dist/location-lifecycle.js +4 -2
  50. package/dist/location-lifecycle.js.map +1 -1
  51. package/dist/message-detach.d.ts +8 -0
  52. package/dist/message-detach.d.ts.map +1 -1
  53. package/dist/message-detach.js +32 -0
  54. package/dist/message-detach.js.map +1 -1
  55. package/dist/migration-notices.d.ts.map +1 -1
  56. package/dist/migration-notices.js +12 -5
  57. package/dist/migration-notices.js.map +1 -1
  58. package/dist/migration.d.ts +1 -0
  59. package/dist/migration.d.ts.map +1 -1
  60. package/dist/migration.js +39 -12
  61. package/dist/migration.js.map +1 -1
  62. package/dist/onnx-runtime.d.ts.map +1 -1
  63. package/dist/onnx-runtime.js +37 -32
  64. package/dist/onnx-runtime.js.map +1 -1
  65. package/dist/path-display.d.ts +5 -0
  66. package/dist/path-display.d.ts.map +1 -0
  67. package/dist/path-display.js +21 -0
  68. package/dist/path-display.js.map +1 -0
  69. package/dist/paths.d.ts +3 -0
  70. package/dist/paths.d.ts.map +1 -1
  71. package/dist/paths.js +47 -11
  72. package/dist/paths.js.map +1 -1
  73. package/dist/pool.d.ts +2 -0
  74. package/dist/pool.d.ts.map +1 -1
  75. package/dist/pool.js +4 -1
  76. package/dist/pool.js.map +1 -1
  77. package/dist/private-storage.d.ts +9 -0
  78. package/dist/private-storage.d.ts.map +1 -0
  79. package/dist/private-storage.js +75 -0
  80. package/dist/private-storage.js.map +1 -0
  81. package/dist/protocol.d.ts +4 -0
  82. package/dist/protocol.d.ts.map +1 -1
  83. package/dist/revivable-transport.d.ts +8 -2
  84. package/dist/revivable-transport.d.ts.map +1 -1
  85. package/dist/revivable-transport.js +63 -23
  86. package/dist/revivable-transport.js.map +1 -1
  87. package/dist/storage-paths.d.ts +3 -4
  88. package/dist/storage-paths.d.ts.map +1 -1
  89. package/dist/storage-paths.js +4 -5
  90. package/dist/storage-paths.js.map +1 -1
  91. package/dist/subc-transport.d.ts +104 -7
  92. package/dist/subc-transport.d.ts.map +1 -1
  93. package/dist/subc-transport.js +955 -61
  94. package/dist/subc-transport.js.map +1 -1
  95. package/dist/tar-executable.d.ts +1 -1
  96. package/dist/tar-executable.d.ts.map +1 -1
  97. package/dist/tar-executable.js +16 -2
  98. package/dist/tar-executable.js.map +1 -1
  99. package/dist/test-child-environment.d.ts +8 -0
  100. package/dist/test-child-environment.d.ts.map +1 -0
  101. package/dist/test-child-environment.js +105 -0
  102. package/dist/test-child-environment.js.map +1 -0
  103. package/dist/transport-factory.d.ts.map +1 -1
  104. package/dist/transport-factory.js +5 -4
  105. package/dist/transport-factory.js.map +1 -1
  106. package/dist/transport.d.ts +39 -0
  107. package/dist/transport.d.ts.map +1 -1
  108. package/dist/transport.js +26 -0
  109. package/dist/transport.js.map +1 -1
  110. package/dist/zoom-format.d.ts +1 -0
  111. package/dist/zoom-format.d.ts.map +1 -1
  112. package/dist/zoom-format.js +5 -1
  113. package/dist/zoom-format.js.map +1 -1
  114. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"bash-format.d.ts","sourceRoot":"","sources":["../src/bash-format.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiB5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAQzD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
1
+ {"version":3,"file":"bash-format.d.ts","sourceRoot":"","sources":["../src/bash-format.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAsB5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAQzD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
@@ -15,6 +15,11 @@ export function formatForegroundResult(data) {
15
15
  }
16
16
  if (status === "timed_out") {
17
17
  rendered += `\n[command timed out]`;
18
+ // Name the limit that killed it, so AFT's own default limit is not
19
+ // mistaken for the command failing.
20
+ if (typeof data.status_reason === "string" && data.status_reason !== "") {
21
+ rendered += ` ${data.status_reason}`;
22
+ }
18
23
  }
19
24
  if (typeof exit === "number" && exit !== 0) {
20
25
  rendered += `\n[exit code: ${exit}]`;
@@ -1 +1 @@
1
- {"version":3,"file":"bash-format.js","sourceRoot":"","sources":["../src/bash-format.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,gGAAgG;AAChG,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,OAAO,GAAG,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAA6B;IAClE,MAAM,MAAM,GAAI,IAAI,CAAC,cAAqC,IAAI,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,IAAI,CAAC,WAAiC,CAAC;IAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,KAAK,IAAI,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,CAAC,MAA4B,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,SAA+B,CAAC;IAClD,IAAI,QAAQ,GAAG,MAAM,CAAC;IACtB,IAAI,SAAS,IAAI,UAAU,EAAE,CAAC;QAC5B,QAAQ,IAAI,uCAAuC,UAAU,GAAG,CAAC;IACnE,CAAC;IACD,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;QAC3B,QAAQ,IAAI,uBAAuB,CAAC;IACtC,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QAC3C,QAAQ,IAAI,iBAAiB,IAAI,GAAG,CAAC;IACvC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,OAAO,CACL,MAAM,KAAK,WAAW;QACtB,MAAM,KAAK,QAAQ;QACnB,MAAM,KAAK,QAAQ;QACnB,MAAM,KAAK,WAAW;QACtB,MAAM,KAAK,cAAc,CAC1B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,WAAW,CAAC,GAAG,EAAE,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"bash-format.js","sourceRoot":"","sources":["../src/bash-format.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,gGAAgG;AAChG,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,OAAO,GAAG,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAA6B;IAClE,MAAM,MAAM,GAAI,IAAI,CAAC,cAAqC,IAAI,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,IAAI,CAAC,WAAiC,CAAC;IAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,KAAK,IAAI,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,CAAC,MAA4B,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,SAA+B,CAAC;IAClD,IAAI,QAAQ,GAAG,MAAM,CAAC;IACtB,IAAI,SAAS,IAAI,UAAU,EAAE,CAAC;QAC5B,QAAQ,IAAI,uCAAuC,UAAU,GAAG,CAAC;IACnE,CAAC;IACD,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;QAC3B,QAAQ,IAAI,uBAAuB,CAAC;QACpC,mEAAmE;QACnE,oCAAoC;QACpC,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,IAAI,IAAI,CAAC,aAAa,KAAK,EAAE,EAAE,CAAC;YACxE,QAAQ,IAAI,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvC,CAAC;IACH,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QAC3C,QAAQ,IAAI,iBAAiB,IAAI,GAAG,CAAC;IACvC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,OAAO,CACL,MAAM,KAAK,WAAW;QACtB,MAAM,KAAK,QAAQ;QACnB,MAAM,KAAK,QAAQ;QACnB,MAAM,KAAK,WAAW;QACtB,MAAM,KAAK,cAAc,CAC1B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,WAAW,CAAC,GAAG,EAAE,CAAC;AAC3B,CAAC"}
@@ -4,38 +4,54 @@ export type WatchCallerRole = "worker" | "primary";
4
4
  export declare const DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
5
5
  /** Largest bash_watch timeout accepted by the OpenCode and Pi plugins' tool schemas. */
6
6
  export declare const MAX_WATCH_TIMEOUT_MS = 1800000;
7
+ /**
8
+ * Default `bash.worker_wait_max_ms`: the longest a delegated worker's wait on
9
+ * one command blocks before control returns to it. Mirrors
10
+ * `DEFAULT_BASH_WORKER_WAIT_MAX_MS` in the engine's `config.rs`. A worker that
11
+ * blocked with no limit once sat behind a stuck test run for fifteen hours;
12
+ * with the limit it gets control back, sees the command is still running, and
13
+ * decides whether to wait again or kill it.
14
+ */
15
+ export declare const DEFAULT_WORKER_WAIT_MAX_MS = 1800000;
16
+ /** Smallest accepted `bash.worker_wait_max_ms`; mirrors `MIN_BASH_WORKER_WAIT_MAX_MS`. */
17
+ export declare const MIN_WORKER_WAIT_MAX_MS = 60000;
7
18
  /**
8
19
  * Longest delay a JavaScript timer accepts (2^31 - 1 ms, about 24.8 days); a
9
- * larger one fires almost at once. A bridge request that must wait for a
10
- * command with no hard-kill timeout uses it as its transport timeout, which
11
- * in practice means no transport timeout.
20
+ * larger one fires almost at once. Bridge transport timeouts derived from a
21
+ * configured limit are clamped to it.
12
22
  */
13
23
  export declare const LONGEST_TIMER_DELAY_MS = 2147483647;
24
+ /**
25
+ * How a delegated worker's waits are bounded, worded once for every tool
26
+ * description. It names the setting, not its value, so the prompt prefix is
27
+ * the same for every user.
28
+ */
29
+ export declare const WORKER_WAIT_LIMIT_PHRASE = "the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default)";
14
30
  /**
15
31
  * Description of the bash_watch tool's sync-wait defaults, embedded in both
16
32
  * plugins' tool descriptions. Every caller sees the same description, so it
17
33
  * has to state both roles' real behaviour.
18
34
  */
19
- export declare const WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits until the command finishes";
35
+ export declare const WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. An explicit timeout never exceeds that worker limit";
20
36
  /**
21
37
  * Description of the bash_watch timeout parameter. Every caller sees the same
22
38
  * schema, so it has to be true for both roles.
23
39
  */
24
- export declare const WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait until the command finishes; a value you pass is used as given.";
40
+ export declare const WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), after which the watch reports the command is still running; a value you pass is clamped to that worker limit.";
25
41
  /**
26
- * Effective sync bash_watch deadline in milliseconds, or `undefined` for a
27
- * wait with no deadline, which lasts until the task exits, a pattern matches,
28
- * a new message arrives, or the call is aborted.
42
+ * Effective sync bash_watch deadline in milliseconds.
29
43
  *
30
44
  * A delegated worker cannot be woken once its turn ends, so it has nothing
31
- * useful to do while its command runs except wait. Any deadline only makes it
32
- * call bash_watch again, and every such call re-reads its whole context. It
33
- * therefore waits with no deadline by default, and a timeout it passes is
34
- * honoured as given rather than clamped to the configured cap. A primary keeps
35
- * the short default and the cap because it can do other work or end its turn
36
- * and be woken by the completion reminder.
45
+ * useful to do while its command runs except wait, and a short deadline only
46
+ * makes it call bash_watch again, re-reading its whole context each time. It
47
+ * therefore waits up to the worker wait limit (`workerWaitMaxMs`,
48
+ * `bash.worker_wait_max_ms`) by default: long enough that re-watching is
49
+ * rare, short enough that a stuck command cannot hold it forever. A timeout
50
+ * it passes is clamped to the worker limit, not to the primary cap. A
51
+ * primary keeps the short default and the cap because it can do other work
52
+ * or end its turn and be woken by the completion reminder.
37
53
  */
38
- export declare function resolveWatchTimeoutMs(requestedMs: number | undefined, role: WatchCallerRole, capMs: number): number | undefined;
54
+ export declare function resolveWatchTimeoutMs(requestedMs: number | undefined, role: WatchCallerRole, capMs: number, workerWaitMaxMs: number): number;
39
55
  /**
40
56
  * Largest bash_watch timeout a caller of this role may pass. A primary is
41
57
  * bounded by the configured cap; a worker only by the schema maximum.
@@ -73,18 +89,48 @@ export declare function abortableSleep(ms: number, signal?: AbortSignal): Promis
73
89
  */
74
90
  export declare function formatWatchWaited(elapsedMs: number, limitMs: number | undefined, capMs: number | undefined): string;
75
91
  /**
76
- * Appended to a bash_watch reply whose sync deadline passed without a match.
77
- * The deadline is a property of the watch, not of the command: a delegated
78
- * worker that read the bare "timeout reached" line as its own execution being
79
- * interrupted declared a failed result while its long-running command was
80
- * still going. Each role gets only the move that applies to it: a worker must
81
- * keep watching and not report, while a primary may end its turn because the
82
- * completion reminder wakes it. A worker only reaches this with a timeout it
83
- * passed itself, so it is told that leaving the timeout out waits until the
84
- * command finishes. `timeoutParam` is the host's spelling of the bash_watch
85
- * timeout argument, so the worker is told a name it can pass.
86
- */
87
- export declare function watchTimeoutSteer(role: WatchCallerRole, timeoutParam?: string): string;
92
+ * Appended to a primary's bash_watch reply whose sync deadline passed without
93
+ * a match. The deadline is a property of the watch, not of the command, so the
94
+ * caller is told the command is still running and that the completion
95
+ * reminder wakes it. A worker gets {@link workerWatchStillRunning} instead.
96
+ */
97
+ export declare function watchTimeoutSteer(): string;
98
+ /** A duration in words: whole minutes as minutes, anything else in seconds. */
99
+ export declare function formatWaitDuration(ms: number): string;
100
+ /**
101
+ * The sentence naming a task's own kill deadline, from the `hard_kill` field
102
+ * and `started_at` of the engine's task status (`{ limit_ms, source }`), or how
103
+ * the task was killed when its hard kill fired (`status_reason`). It reports an
104
+ * absolute UTC deadline measured from command start and the approximate time
105
+ * remaining. Mirrors `kill_deadline_sentence` in the engine.
106
+ */
107
+ export declare function taskKillDeadlineText(data: Record<string, unknown>, role: WatchCallerRole, nowMs?: number): string;
108
+ /**
109
+ * A wait should not hand back a still-running task just before its own kill
110
+ * deadline. Five seconds covers the slowest status poll and kill publication.
111
+ */
112
+ export declare function taskKillDeadlineWithinHandoffMargin(data: Record<string, unknown>): boolean;
113
+ /** The last lines of `output`, so a worker can judge whether a command is stuck. */
114
+ export declare function outputTail(output: string | undefined): string;
115
+ /**
116
+ * Appended to a delegated worker's bash_watch reply whose deadline passed
117
+ * while the command still runs (by default the worker wait limit). The
118
+ * deadline is a property of the watch, not of the command: a worker that read
119
+ * a bare "timeout reached" line as its own execution failing declared a
120
+ * failed result while its command was still going. So it is told plainly that
121
+ * the command is still running, how long it has run, and its latest output,
122
+ * then given both moves by name: watch again to keep waiting, or kill a
123
+ * command that should have finished. `taskIdArg` and `timeoutParam` are the
124
+ * host's spellings of the bash_watch arguments.
125
+ */
126
+ export declare function workerWatchStillRunning(options: {
127
+ taskId: string;
128
+ waitedMs: number;
129
+ ranMs: number | undefined;
130
+ output: string | undefined;
131
+ taskIdArg?: string;
132
+ timeoutParam?: string;
133
+ }): string;
88
134
  /**
89
135
  * How long a sync bash_watch with no deadline keeps retrying while every
90
136
  * status poll times out because the bridge is busy. Without this bound a wait
@@ -110,9 +156,9 @@ export declare function interruptedWatchTail(role: WatchCallerRole, primaryTail:
110
156
  * command now runs in the background. AFT's own reply already says the task
111
157
  * won't wake the worker; this names the plugin-owned bash_watch tool, which
112
158
  * AFT cannot assume a host has. The suggested call passes no timeout on
113
- * purpose: a worker's watch without one waits until the command finishes, so
114
- * any number would only make it wake and watch again. `taskIdArg` is the
115
- * host's spelling of the task id argument.
159
+ * purpose: a worker's watch without one waits up to the worker wait limit,
160
+ * the longest it may, so any number would only make it wake sooner and watch
161
+ * again. `taskIdArg` is the host's spelling of the task id argument.
116
162
  */
117
163
  export declare function workerBackgroundTaskNote(taskId: string, taskIdArg?: string): string;
118
164
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"bash-hints.d.ts","sourceRoot":"","sources":["../src/bash-hints.ts"],"names":[],"mappings":"AAWA,8EAA8E;AAC9E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEnD,iFAAiF;AACjF,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,aAAgB,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,uLAC0I,CAAC;AAEvL;;;GAGG;AACH,eAAO,MAAM,+BAA+B,kOACqL,CAAC;AAElO;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CAGpB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAK1D;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE;IACvB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAI5D,CAAC;AAEF,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAW9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,MAAM,CAOR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,YAAY,SAAc,GAAG,MAAM,CAK3F;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,6FAC4D,CAAC;AAE7F;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,SAAW,GAAG,MAAM,CAErF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAInE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAInE;AAqBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAYjE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,OAAO,EAC5B,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAQR"}
1
+ {"version":3,"file":"bash-hints.d.ts","sourceRoot":"","sources":["../src/bash-hints.ts"],"names":[],"mappings":"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"}
@@ -1,44 +1,61 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import { relativePathEscapesRoot } from "./path-display.js";
4
5
  /** Sync bash_watch deadline used by a primary session that passes no timeout. */
5
6
  export const DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30_000;
6
7
  /** Largest bash_watch timeout accepted by the OpenCode and Pi plugins' tool schemas. */
7
8
  export const MAX_WATCH_TIMEOUT_MS = 1_800_000;
9
+ /**
10
+ * Default `bash.worker_wait_max_ms`: the longest a delegated worker's wait on
11
+ * one command blocks before control returns to it. Mirrors
12
+ * `DEFAULT_BASH_WORKER_WAIT_MAX_MS` in the engine's `config.rs`. A worker that
13
+ * blocked with no limit once sat behind a stuck test run for fifteen hours;
14
+ * with the limit it gets control back, sees the command is still running, and
15
+ * decides whether to wait again or kill it.
16
+ */
17
+ export const DEFAULT_WORKER_WAIT_MAX_MS = 1_800_000;
18
+ /** Smallest accepted `bash.worker_wait_max_ms`; mirrors `MIN_BASH_WORKER_WAIT_MAX_MS`. */
19
+ export const MIN_WORKER_WAIT_MAX_MS = 60_000;
8
20
  /**
9
21
  * Longest delay a JavaScript timer accepts (2^31 - 1 ms, about 24.8 days); a
10
- * larger one fires almost at once. A bridge request that must wait for a
11
- * command with no hard-kill timeout uses it as its transport timeout, which
12
- * in practice means no transport timeout.
22
+ * larger one fires almost at once. Bridge transport timeouts derived from a
23
+ * configured limit are clamped to it.
13
24
  */
14
25
  export const LONGEST_TIMER_DELAY_MS = 2_147_483_647;
26
+ /**
27
+ * How a delegated worker's waits are bounded, worded once for every tool
28
+ * description. It names the setting, not its value, so the prompt prefix is
29
+ * the same for every user.
30
+ */
31
+ export const WORKER_WAIT_LIMIT_PHRASE = "the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default)";
15
32
  /**
16
33
  * Description of the bash_watch tool's sync-wait defaults, embedded in both
17
34
  * plugins' tool descriptions. Every caller sees the same description, so it
18
35
  * has to state both roles' real behaviour.
19
36
  */
20
- export const WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits until the command finishes";
37
+ export const WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. An explicit timeout never exceeds that worker limit";
21
38
  /**
22
39
  * Description of the bash_watch timeout parameter. Every caller sees the same
23
40
  * schema, so it has to be true for both roles.
24
41
  */
25
- export const WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait until the command finishes; a value you pass is used as given.";
42
+ export const WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), after which the watch reports the command is still running; a value you pass is clamped to that worker limit.";
26
43
  /**
27
- * Effective sync bash_watch deadline in milliseconds, or `undefined` for a
28
- * wait with no deadline, which lasts until the task exits, a pattern matches,
29
- * a new message arrives, or the call is aborted.
44
+ * Effective sync bash_watch deadline in milliseconds.
30
45
  *
31
46
  * A delegated worker cannot be woken once its turn ends, so it has nothing
32
- * useful to do while its command runs except wait. Any deadline only makes it
33
- * call bash_watch again, and every such call re-reads its whole context. It
34
- * therefore waits with no deadline by default, and a timeout it passes is
35
- * honoured as given rather than clamped to the configured cap. A primary keeps
36
- * the short default and the cap because it can do other work or end its turn
37
- * and be woken by the completion reminder.
47
+ * useful to do while its command runs except wait, and a short deadline only
48
+ * makes it call bash_watch again, re-reading its whole context each time. It
49
+ * therefore waits up to the worker wait limit (`workerWaitMaxMs`,
50
+ * `bash.worker_wait_max_ms`) by default: long enough that re-watching is
51
+ * rare, short enough that a stuck command cannot hold it forever. A timeout
52
+ * it passes is clamped to the worker limit, not to the primary cap. A
53
+ * primary keeps the short default and the cap because it can do other work
54
+ * or end its turn and be woken by the completion reminder.
38
55
  */
39
- export function resolveWatchTimeoutMs(requestedMs, role, capMs) {
56
+ export function resolveWatchTimeoutMs(requestedMs, role, capMs, workerWaitMaxMs) {
40
57
  if (role === "worker")
41
- return requestedMs;
58
+ return Math.min(requestedMs ?? workerWaitMaxMs, workerWaitMaxMs);
42
59
  return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
43
60
  }
44
61
  /**
@@ -101,7 +118,7 @@ export function abortableSleep(ms, signal) {
101
118
  export function formatWatchWaited(elapsedMs, limitMs, capMs) {
102
119
  let limit;
103
120
  if (limitMs === undefined)
104
- limit = "no limit: waits until the command finishes";
121
+ limit = "no wait limit";
105
122
  else if (capMs !== undefined && limitMs >= capMs)
106
123
  limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
107
124
  else
@@ -109,23 +126,149 @@ export function formatWatchWaited(elapsedMs, limitMs, capMs) {
109
126
  return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
110
127
  }
111
128
  /**
112
- * Appended to a bash_watch reply whose sync deadline passed without a match.
113
- * The deadline is a property of the watch, not of the command: a delegated
114
- * worker that read the bare "timeout reached" line as its own execution being
115
- * interrupted declared a failed result while its long-running command was
116
- * still going. Each role gets only the move that applies to it: a worker must
117
- * keep watching and not report, while a primary may end its turn because the
118
- * completion reminder wakes it. A worker only reaches this with a timeout it
119
- * passed itself, so it is told that leaving the timeout out waits until the
120
- * command finishes. `timeoutParam` is the host's spelling of the bash_watch
121
- * timeout argument, so the worker is told a name it can pass.
129
+ * Appended to a primary's bash_watch reply whose sync deadline passed without
130
+ * a match. The deadline is a property of the watch, not of the command, so the
131
+ * caller is told the command is still running and that the completion
132
+ * reminder wakes it. A worker gets {@link workerWatchStillRunning} instead.
122
133
  */
123
- export function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
124
- if (role === "worker") {
125
- return `The command is still running; this is not a failure. Watch again and don't report a result until it finishes; a bash_watch without ${timeoutParam} waits until the command finishes.`;
126
- }
134
+ export function watchTimeoutSteer() {
127
135
  return "The command is still running; this is not a failure. Watch again, do other work, or end your turn: the completion reminder wakes you.";
128
136
  }
137
+ /** A duration in words: whole minutes as minutes, anything else in seconds. */
138
+ export function formatWaitDuration(ms) {
139
+ const rounded = Math.max(0, Math.round(ms));
140
+ if (rounded === 60_000)
141
+ return "1 minute";
142
+ if (rounded > 0 && rounded % 60_000 === 0)
143
+ return `${rounded / 60_000} minutes`;
144
+ const seconds = (rounded / 1000).toFixed(1).replace(/\.0$/, "");
145
+ return `${seconds}s`;
146
+ }
147
+ /**
148
+ * The sentence naming a task's own kill deadline, from the `hard_kill` field
149
+ * and `started_at` of the engine's task status (`{ limit_ms, source }`), or how
150
+ * the task was killed when its hard kill fired (`status_reason`). It reports an
151
+ * absolute UTC deadline measured from command start and the approximate time
152
+ * remaining. Mirrors `kill_deadline_sentence` in the engine.
153
+ */
154
+ export function taskKillDeadlineText(data, role, nowMs = Date.now()) {
155
+ if (data.status === "timed_out") {
156
+ const reason = typeof data.status_reason === "string" ? data.status_reason : "";
157
+ return reason === ""
158
+ ? "The task was killed by its time limit (exit 124)."
159
+ : `The task was ${reason}.`;
160
+ }
161
+ // A terminal task's deadline no longer matters; an unknown state (the
162
+ // bridge stayed busy) has none to report.
163
+ if (isTerminalTaskStatus(data.status) || typeof data.status !== "string")
164
+ return "";
165
+ if (data.status === "unknown")
166
+ return "";
167
+ // A remote task has no AFT hard-kill deadline (`hard_kill`); report its
168
+ // build-server status note instead (still queued, or when the runner will
169
+ // stop it). Mirrors `kill_deadline_text` in crates/aft/src/commands/bash_watch.rs.
170
+ if (typeof data.remote_phase_note === "string" && data.remote_phase_note !== "") {
171
+ const note = data.remote_phase_note;
172
+ return `${note.charAt(0).toUpperCase()}${note.slice(1)}.`;
173
+ }
174
+ const hardKill = data.hard_kill;
175
+ if (!hardKill || typeof hardKill.limit_ms !== "number") {
176
+ return "This task has no kill deadline.";
177
+ }
178
+ const startedAtMs = data.started_at;
179
+ if (typeof startedAtMs !== "number" || !Number.isSafeInteger(startedAtMs)) {
180
+ return "This task has no kill deadline.";
181
+ }
182
+ const limitMs = hardKill.limit_ms;
183
+ const limit = formatWaitDuration(limitMs);
184
+ const deadlineAt = startedAtMs + limitMs;
185
+ const when = `at ${formatKillDeadlineUtc(deadlineAt)}, when it has run ${limit}`;
186
+ let source;
187
+ if (hardKill.source === "timeout") {
188
+ source = "(the `timeout` you passed)";
189
+ }
190
+ else if (role === "worker") {
191
+ source =
192
+ "(its default background limit), but each wait you make on it moves that kill to at least the worker wait limit (`bash.worker_wait_max_ms`) after the wait, so it is not killed while you keep waiting; pass a `timeout` to set your own limit";
193
+ }
194
+ else {
195
+ source = "(its default background limit) unless you pass a longer `timeout`";
196
+ }
197
+ const remaining = deadlineAt <= nowMs
198
+ ? "; the kill deadline has passed"
199
+ : `; about ${formatApproximateRemaining(deadlineAt - nowMs)} remain`;
200
+ return `AFT kills this task ${when} ${source}${remaining}.`;
201
+ }
202
+ /**
203
+ * A wait should not hand back a still-running task just before its own kill
204
+ * deadline. Five seconds covers the slowest status poll and kill publication.
205
+ */
206
+ export function taskKillDeadlineWithinHandoffMargin(data) {
207
+ if (typeof data.status !== "string" || isTerminalTaskStatus(data.status))
208
+ return false;
209
+ const hardKill = data.hard_kill;
210
+ const limitMs = hardKill?.limit_ms;
211
+ const elapsedMs = data.elapsed_ms;
212
+ return (typeof limitMs === "number" &&
213
+ Number.isSafeInteger(limitMs) &&
214
+ typeof elapsedMs === "number" &&
215
+ Number.isSafeInteger(elapsedMs) &&
216
+ limitMs - elapsedMs <= 5_000);
217
+ }
218
+ function formatKillDeadlineUtc(unixMs) {
219
+ const date = new Date(unixMs);
220
+ if (!Number.isFinite(date.getTime()))
221
+ throw new Error("invalid bash task start time");
222
+ const year = String(date.getUTCFullYear()).padStart(4, "0");
223
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
224
+ const day = String(date.getUTCDate()).padStart(2, "0");
225
+ const hour = String(date.getUTCHours()).padStart(2, "0");
226
+ const minute = String(date.getUTCMinutes()).padStart(2, "0");
227
+ const second = String(date.getUTCSeconds()).padStart(2, "0");
228
+ const millis = date.getUTCMilliseconds();
229
+ const fraction = millis === 0 ? "" : `.${String(millis).padStart(3, "0")}`;
230
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}${fraction}Z`;
231
+ }
232
+ function formatApproximateRemaining(ms) {
233
+ if (ms >= 60_000) {
234
+ const minutes = Math.max(1, Math.floor((ms + 30_000) / 60_000));
235
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
236
+ }
237
+ return formatWaitDuration(ms);
238
+ }
239
+ function isTerminalTaskStatus(status) {
240
+ return (status === "completed" ||
241
+ status === "failed" ||
242
+ status === "killed" ||
243
+ status === "timed_out" ||
244
+ status === "fate_unknown");
245
+ }
246
+ /** Most lines of a still-running command's output shown to a worker. */
247
+ const WORKER_OUTPUT_TAIL_LINES = 20;
248
+ /** The last lines of `output`, so a worker can judge whether a command is stuck. */
249
+ export function outputTail(output) {
250
+ const lines = (output ?? "").replace(/\s+$/, "").split("\n");
251
+ return lines.slice(-WORKER_OUTPUT_TAIL_LINES).join("\n");
252
+ }
253
+ /**
254
+ * Appended to a delegated worker's bash_watch reply whose deadline passed
255
+ * while the command still runs (by default the worker wait limit). The
256
+ * deadline is a property of the watch, not of the command: a worker that read
257
+ * a bare "timeout reached" line as its own execution failing declared a
258
+ * failed result while its command was still going. So it is told plainly that
259
+ * the command is still running, how long it has run, and its latest output,
260
+ * then given both moves by name: watch again to keep waiting, or kill a
261
+ * command that should have finished. `taskIdArg` and `timeoutParam` are the
262
+ * host's spellings of the bash_watch arguments.
263
+ */
264
+ export function workerWatchStillRunning(options) {
265
+ const taskIdArg = options.taskIdArg ?? "taskId";
266
+ const timeoutParam = options.timeoutParam ?? "timeoutMs";
267
+ const ran = options.ranMs === undefined ? "" : ` It has run for ${formatWaitDuration(options.ranMs)}.`;
268
+ const tail = outputTail(options.output);
269
+ const output = tail === "" ? "No output yet." : `Recent output:\n${tail}`;
270
+ return `The command is still running after ${formatWaitDuration(options.waitedMs)} of watching; this is not a failure.${ran} Call bash_watch({ ${taskIdArg}: "${options.taskId}" }) again to keep waiting (without ${timeoutParam} a watch waits up to the worker wait limit, then reports it is still running), or bash_kill({ ${taskIdArg}: "${options.taskId}" }) if it should have finished by now. Don't report a result until it finishes.\n${output}`;
271
+ }
129
272
  /**
130
273
  * How long a sync bash_watch with no deadline keeps retrying while every
131
274
  * status poll times out because the bridge is busy. Without this bound a wait
@@ -153,12 +296,12 @@ export function interruptedWatchTail(role, primaryTail) {
153
296
  * command now runs in the background. AFT's own reply already says the task
154
297
  * won't wake the worker; this names the plugin-owned bash_watch tool, which
155
298
  * AFT cannot assume a host has. The suggested call passes no timeout on
156
- * purpose: a worker's watch without one waits until the command finishes, so
157
- * any number would only make it wake and watch again. `taskIdArg` is the
158
- * host's spelling of the task id argument.
299
+ * purpose: a worker's watch without one waits up to the worker wait limit,
300
+ * the longest it may, so any number would only make it wake sooner and watch
301
+ * again. `taskIdArg` is the host's spelling of the task id argument.
159
302
  */
160
303
  export function workerBackgroundTaskNote(taskId, taskIdArg = "taskId") {
161
- return `\n\nNOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ ${taskIdArg}: "${taskId}" }) to wait for completion before returning to the parent; without a timeout it waits until the command finishes. Subagents don't survive turn-end and won't be woken when the command finishes.`;
304
+ return `\n\nNOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ ${taskIdArg}: "${taskId}" }) to wait for completion before returning to the parent; without a timeout it waits up to ${WORKER_WAIT_LIMIT_PHRASE}, then reports it is still running; watch again to keep waiting, or call bash_kill({ ${taskIdArg}: "${taskId}" }) if it should have finished by now. Subagents don't survive turn-end and won't be woken when the command finishes.`;
162
305
  }
163
306
  /**
164
307
  * Line a bash_status snapshot of a still-running task ends with. A primary is
@@ -369,7 +512,7 @@ function nextCwdAfterCd(currentCwd, firstStage, afterCd) {
369
512
  }
370
513
  function isDirInsideProject(resolvedRoot, dir) {
371
514
  const rel = path.relative(resolvedRoot, path.resolve(dir));
372
- return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
515
+ return !relativePathEscapesRoot(rel);
373
516
  }
374
517
  function collectPathOperands(firstStage, startAfterCommand) {
375
518
  const operands = [];
@@ -380,6 +523,8 @@ function collectPathOperands(firstStage, startAfterCommand) {
380
523
  // the grep read as path-less ("in-project cwd") and nudged toward aft_search
381
524
  // for a target it could not see - the logs directory, in the field.
382
525
  let sawPattern = false;
526
+ let optionsEnded = false;
527
+ let optionValue;
383
528
  while (index < firstStage.length) {
384
529
  const tokenResult = readShellToken(firstStage, index);
385
530
  if (tokenResult === null)
@@ -393,23 +538,56 @@ function collectPathOperands(firstStage, startAfterCommand) {
393
538
  if (end <= index)
394
539
  break;
395
540
  index = skipSpaces(firstStage, end);
396
- if (token.startsWith("-"))
541
+ // A descriptor before a redirection is shell syntax, not a search path.
542
+ if (/^\d+$/.test(token) && /[<>]/.test(firstStage[end] ?? ""))
543
+ break;
544
+ if (optionValue) {
545
+ if (optionValue === "pattern" || optionValue === "file")
546
+ sawPattern = true;
547
+ optionValue = undefined;
548
+ continue;
549
+ }
550
+ if (!optionsEnded && token === "--") {
551
+ optionsEnded = true;
397
552
  continue;
398
- if (!sawPattern && !looksLikePathOperand(token)) {
553
+ }
554
+ if (!optionsEnded && token.startsWith("-")) {
555
+ if (token === "-e" || token === "--regexp")
556
+ optionValue = "pattern";
557
+ else if (token === "-f" || token === "--file")
558
+ optionValue = "file";
559
+ else if (/^(?:--regexp=|--file=|-e.+|-f.+)/.test(token))
560
+ sawPattern = true;
561
+ else if ([
562
+ "-A",
563
+ "-B",
564
+ "-C",
565
+ "-m",
566
+ "--after-context",
567
+ "--before-context",
568
+ "--context",
569
+ "--max-count",
570
+ "--include",
571
+ "--exclude",
572
+ "--exclude-dir",
573
+ "-g",
574
+ "--glob",
575
+ "-t",
576
+ "--type",
577
+ "-T",
578
+ "--type-not",
579
+ ].includes(token))
580
+ optionValue = "other";
581
+ continue;
582
+ }
583
+ if (!sawPattern) {
399
584
  sawPattern = true;
400
585
  continue;
401
586
  }
402
- if (looksLikePathOperand(token) || isDynamicPathOperand(token))
403
- operands.push(token);
587
+ operands.push(token);
404
588
  }
405
589
  return operands;
406
590
  }
407
- function looksLikePathOperand(token) {
408
- return (token.includes("/") ||
409
- token.startsWith("~") ||
410
- token.startsWith("./") ||
411
- token.startsWith("../"));
412
- }
413
591
  function isDynamicPathOperand(token) {
414
592
  return token.startsWith("~") || token.includes("$") || token.includes("`");
415
593
  }
@@ -430,7 +608,7 @@ function isPathInsideProject(resolvedRoot, baseCwd, operand) {
430
608
  // from the project root after a `cd`), absolute/`~` operands against the FS.
431
609
  const resolved = resolvePathOperand(baseCwd, operand);
432
610
  const rel = path.relative(resolvedRoot, resolved);
433
- return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
611
+ return !relativePathEscapesRoot(rel);
434
612
  }
435
613
  function shouldSuppressResolvedPath(resolved) {
436
614
  try {