@manyoars/oars 0.4.1 → 0.5.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.
Files changed (188) hide show
  1. package/README.md +79 -29
  2. package/dist/bin.js +3 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +207 -21
  5. package/dist/club.d.ts +19 -0
  6. package/dist/club.js +132 -0
  7. package/dist/gate/hook-fill-main.js +1 -1
  8. package/dist/gate/hook-fill.d.ts +1 -1
  9. package/dist/gate/hook-fill.js +1 -1
  10. package/dist/gate/hook-main.js +7 -5
  11. package/dist/gate/hook.d.ts +12 -1
  12. package/dist/gate/hook.js +38 -7
  13. package/dist/hook-bin.js +23 -4
  14. package/dist/main.js +5 -0
  15. package/dist/node-floor.d.ts +10 -0
  16. package/dist/node-floor.js +23 -0
  17. package/dist/oars-hook.d.ts +2 -0
  18. package/dist/oars-hook.js +7 -0
  19. package/dist/sessions/allow.d.ts +1 -0
  20. package/dist/sessions/allow.js +1 -1
  21. package/dist/sessions/call-shape.d.ts +111 -0
  22. package/dist/sessions/call-shape.js +464 -0
  23. package/dist/sessions/candidates.d.ts +65 -0
  24. package/dist/sessions/candidates.js +236 -0
  25. package/dist/sessions/card-command.d.ts +24 -0
  26. package/dist/sessions/card-command.js +71 -0
  27. package/dist/sessions/card-font.d.ts +6 -0
  28. package/dist/sessions/card-font.js +36 -0
  29. package/dist/sessions/card-line.d.ts +30 -0
  30. package/dist/sessions/card-line.js +105 -0
  31. package/dist/sessions/card-render.d.ts +15 -0
  32. package/dist/sessions/card-render.js +146 -0
  33. package/dist/sessions/card.d.ts +59 -0
  34. package/dist/sessions/card.js +107 -0
  35. package/dist/sessions/cell-increments.d.ts +14 -3
  36. package/dist/sessions/cell-increments.js +79 -12
  37. package/dist/sessions/cells.d.ts +42 -4
  38. package/dist/sessions/cells.js +72 -18
  39. package/dist/sessions/codex-hook-bin.d.ts +1 -0
  40. package/dist/sessions/codex-hook-bin.js +24 -0
  41. package/dist/sessions/codex-hook-install.d.ts +21 -0
  42. package/dist/sessions/codex-hook-install.js +47 -0
  43. package/dist/sessions/codex-hook-view.d.ts +20 -0
  44. package/dist/sessions/codex-hook-view.js +20 -0
  45. package/dist/sessions/codex-hook.d.ts +77 -0
  46. package/dist/sessions/codex-hook.js +255 -0
  47. package/dist/sessions/codex.d.ts +9 -0
  48. package/dist/sessions/codex.js +104 -18
  49. package/dist/sessions/command.d.ts +17 -2
  50. package/dist/sessions/command.js +145 -33
  51. package/dist/sessions/copilot.d.ts +3 -0
  52. package/dist/sessions/copilot.js +12 -4
  53. package/dist/sessions/derive.js +2 -1
  54. package/dist/sessions/empty-state.d.ts +17 -0
  55. package/dist/sessions/empty-state.js +38 -0
  56. package/dist/sessions/exclusion.js +48 -3
  57. package/dist/sessions/extract.d.ts +16 -3
  58. package/dist/sessions/extract.js +281 -24
  59. package/dist/sessions/gemini.d.ts +2 -0
  60. package/dist/sessions/gemini.js +29 -2
  61. package/dist/sessions/harness-files.d.ts +27 -0
  62. package/dist/sessions/harness-files.js +109 -0
  63. package/dist/sessions/head-only.d.ts +7 -0
  64. package/dist/sessions/head-only.js +21 -0
  65. package/dist/sessions/hook-calls.js +4 -3
  66. package/dist/sessions/hooks.d.ts +1 -1
  67. package/dist/sessions/hooks.js +6 -2
  68. package/dist/sessions/identity.d.ts +6 -0
  69. package/dist/sessions/injected.d.ts +13 -0
  70. package/dist/sessions/injected.js +260 -0
  71. package/dist/sessions/install.d.ts +31 -20
  72. package/dist/sessions/install.js +80 -82
  73. package/dist/sessions/load-status.d.ts +41 -0
  74. package/dist/sessions/load-status.js +156 -0
  75. package/dist/sessions/machine-child-deps.d.ts +5 -0
  76. package/dist/sessions/machine-child-deps.js +83 -0
  77. package/dist/sessions/machine-child.d.ts +43 -0
  78. package/dist/sessions/machine-child.js +120 -0
  79. package/dist/sessions/machine-credential.d.ts +10 -0
  80. package/dist/sessions/machine-credential.js +20 -0
  81. package/dist/sessions/machine-enroll.d.ts +47 -0
  82. package/dist/sessions/machine-enroll.js +230 -0
  83. package/dist/sessions/machine-hold.d.ts +49 -0
  84. package/dist/sessions/machine-hold.js +88 -0
  85. package/dist/sessions/machine-ids.d.ts +6 -0
  86. package/dist/sessions/machine-ids.js +9 -0
  87. package/dist/sessions/machine-notice.d.ts +21 -0
  88. package/dist/sessions/machine-notice.js +89 -0
  89. package/dist/sessions/machine-pull.d.ts +40 -0
  90. package/dist/sessions/machine-pull.js +220 -0
  91. package/dist/sessions/machine-repo.d.ts +7 -0
  92. package/dist/sessions/machine-repo.js +30 -0
  93. package/dist/sessions/machine-scan-cache.d.ts +12 -0
  94. package/dist/sessions/machine-scan-cache.js +47 -0
  95. package/dist/sessions/machine-send.d.ts +12 -0
  96. package/dist/sessions/machine-send.js +156 -0
  97. package/dist/sessions/machine-stop.d.ts +22 -0
  98. package/dist/sessions/machine-stop.js +53 -0
  99. package/dist/sessions/machine.d.ts +41 -0
  100. package/dist/sessions/machine.js +89 -0
  101. package/dist/sessions/opencode.d.ts +2 -0
  102. package/dist/sessions/opencode.js +6 -0
  103. package/dist/sessions/otel.d.ts +21 -0
  104. package/dist/sessions/otel.js +109 -38
  105. package/dist/sessions/owner-only.d.ts +7 -0
  106. package/dist/sessions/owner-only.js +34 -0
  107. package/dist/sessions/pi.d.ts +13 -1
  108. package/dist/sessions/pi.js +27 -13
  109. package/dist/sessions/pool-books.d.ts +1 -1
  110. package/dist/sessions/pool-books.js +1 -1
  111. package/dist/sessions/pool-choices.d.ts +4 -2
  112. package/dist/sessions/pool-choices.js +4 -2
  113. package/dist/sessions/pool-command.d.ts +46 -6
  114. package/dist/sessions/pool-command.js +613 -136
  115. package/dist/sessions/pool-delta.d.ts +7 -0
  116. package/dist/sessions/pool-delta.js +28 -5
  117. package/dist/sessions/pool-due.d.ts +43 -0
  118. package/dist/sessions/pool-due.js +113 -0
  119. package/dist/sessions/pool-nightly.d.ts +9 -1
  120. package/dist/sessions/pool-nightly.js +65 -21
  121. package/dist/sessions/pool-outbox.d.ts +51 -5
  122. package/dist/sessions/pool-outbox.js +217 -24
  123. package/dist/sessions/pool-print.d.ts +1 -1
  124. package/dist/sessions/pool-raw.js +4 -3
  125. package/dist/sessions/pool-replica.d.ts +4 -4
  126. package/dist/sessions/pool-replica.js +26 -7
  127. package/dist/sessions/pool-send-lock.js +1 -1
  128. package/dist/sessions/pool-sent.d.ts +2 -0
  129. package/dist/sessions/pool-sent.js +21 -4
  130. package/dist/sessions/pool-wire.d.ts +176 -15
  131. package/dist/sessions/pool-wire.js +297 -33
  132. package/dist/sessions/pool.js +4 -2
  133. package/dist/sessions/public-vocabulary.d.ts +5 -0
  134. package/dist/sessions/public-vocabulary.js +16 -0
  135. package/dist/sessions/read-call-shape.js +12 -6
  136. package/dist/sessions/read-fit.d.ts +5 -0
  137. package/dist/sessions/read-fit.js +23 -2
  138. package/dist/sessions/read-interaction.js +1 -1
  139. package/dist/sessions/read-lead-lag.js +1 -1
  140. package/dist/sessions/read-readable.js +1 -1
  141. package/dist/sessions/read.d.ts +1 -1
  142. package/dist/sessions/recall-consequence.d.ts +2 -2
  143. package/dist/sessions/recall-consequence.js +14 -9
  144. package/dist/sessions/recall-object.d.ts +9 -2
  145. package/dist/sessions/receipt.d.ts +8 -0
  146. package/dist/sessions/receipt.js +78 -0
  147. package/dist/sessions/recovery-calls.d.ts +3 -2
  148. package/dist/sessions/recovery-calls.js +9 -3
  149. package/dist/sessions/redact.d.ts +11 -2
  150. package/dist/sessions/redact.js +201 -23
  151. package/dist/sessions/repo-root.d.ts +7 -0
  152. package/dist/sessions/repo-root.js +17 -13
  153. package/dist/sessions/reversion.d.ts +8 -0
  154. package/dist/sessions/reversion.js +114 -0
  155. package/dist/sessions/schema.d.ts +78 -4
  156. package/dist/sessions/schema.js +31 -1
  157. package/dist/sessions/session-shape.d.ts +2 -1
  158. package/dist/sessions/session-shape.js +15 -0
  159. package/dist/sessions/sources.d.ts +9 -0
  160. package/dist/sessions/sources.js +16 -5
  161. package/dist/sessions/steps.js +3 -2
  162. package/dist/sessions/store.d.ts +41 -2
  163. package/dist/sessions/store.js +68 -16
  164. package/dist/sessions/try-demo.d.ts +8 -0
  165. package/dist/sessions/try-demo.js +90 -0
  166. package/dist/sessions/try.d.ts +13 -0
  167. package/dist/sessions/try.js +50 -0
  168. package/dist/sessions/vscode-copilot.d.ts +4 -0
  169. package/dist/sessions/vscode-copilot.js +25 -18
  170. package/dist/telemetry/command.js +12 -6
  171. package/dist/telemetry/consent.d.ts +9 -3
  172. package/dist/telemetry/consent.js +25 -9
  173. package/dist/telemetry/fields.d.ts +11 -0
  174. package/dist/telemetry/fields.js +56 -0
  175. package/dist/telemetry/measures.d.ts +13 -0
  176. package/dist/telemetry/measures.js +30 -0
  177. package/dist/telemetry/observe.d.ts +15 -2
  178. package/dist/telemetry/observe.js +34 -7
  179. package/dist/telemetry/region.d.ts +6 -0
  180. package/dist/telemetry/region.js +28 -0
  181. package/dist/telemetry/report.js +1 -1
  182. package/dist/telemetry/schema.d.ts +3 -0
  183. package/dist/telemetry/schema.js +11 -1
  184. package/dist/telemetry/send.d.ts +5 -2
  185. package/dist/telemetry/send.js +15 -7
  186. package/dist/telemetry/words.d.ts +6 -0
  187. package/dist/telemetry/words.js +6 -0
  188. package/package.json +24 -5
@@ -1,3 +1,6 @@
1
+ // What a call touched, in shape and never in value (t870): the class of its target and the classes of its arguments. Both are computed at capture, from
2
+ // the path and the input that only the extractor holds, and stored as closed-list members. The path, the argument values, and the base name never leave it.
3
+ import { PUBLIC_FLAGS, PUBLIC_SUBCOMMANDS } from './public-vocabulary.js';
1
4
  /** Where a path target sits in the project, as a closed list. `none` is a call with no path target (a shell call, a search, a fetch). */
2
5
  export const TARGET_CLASSES = ['source', 'test', 'config', 'doc', 'lockfile', 'generated', 'data', 'none', 'other'];
3
6
  /** What an argument holds, as a closed list: a call's arguments are the sorted set of these. `none` is a call with no arguments. */
@@ -83,3 +86,464 @@ export const argClassOf = (input) => {
83
86
  }
84
87
  return held.size === 0 ? ['none'] : ARG_CLASSES.filter((kind) => held.has(kind)).sort();
85
88
  };
89
+ /** The size of a value as written (its length as text, a number's digits included), as a closed list. A boolean and a null have none. */
90
+ export const VALUE_SIZES = ['0', '1-8', '9-32', '33-128', '129-512', '513-2k', '2k-8k', 'over-8k'];
91
+ const SIZE_EDGES = [1, 9, 33, 129, 513, 2049, 8193];
92
+ /** How many folders a path value sits below the repository or the working folder; `outside` is an absolute path under neither, so the folders above them are never counted. */
93
+ export const PATH_DEPTHS = ['0', '1', '2', '3-4', '5-8', 'over-8', 'outside'];
94
+ /** The class of a path value's extension: `none` is a name with none (a folder, a `Makefile`). */
95
+ export const EXTENSION_CLASSES = ['source', 'doc', 'config', 'data', 'image', 'none', 'other'];
96
+ /** The kind of the folders a path value names: `root` is a value with no folder part. */
97
+ export const DIRECTORY_KINDS = ['root', 'source', 'test', 'doc', 'generated', 'hidden', 'other'];
98
+ /** What a pattern value uses, as a sorted set: `literal` is a pattern that uses none of the rest. A regular expression and a glob share the list. */
99
+ export const PATTERN_FEATURES = ['alternation', 'anchor', 'class', 'escape', 'group', 'literal', 'quantifier', 'recursive', 'wildcard'];
100
+ const SOURCE_DIRS = words('src lib app apps packages pkg cmd internal server client components pages');
101
+ const IMAGE_EXTENSIONS = words('png jpg jpeg gif svg webp ico bmp tiff avif');
102
+ const GLOB_ARGS = words('glob include exclude');
103
+ const DRIVE = /^(?:[a-z]:|~)$/i;
104
+ const ABSOLUTE = /^(?:[a-z]:[\\/]|[\\/]|~)/i;
105
+ const sizeOf = (value) => {
106
+ const length = (typeof value === 'string' ? value : JSON.stringify(value)).length;
107
+ return VALUE_SIZES[SIZE_EDGES.filter((edge) => length >= edge).length] ?? 'over-8k';
108
+ };
109
+ const depthOf = (folders) => (folders <= 2 ? PATH_DEPTHS[folders] ?? '0' : folders <= 4 ? '3-4' : folders <= 8 ? '5-8' : 'over-8');
110
+ /** A path with its `.` and `..` parts resolved, so a path that climbs out of a folder reads as outside it. A Windows drive, a leading `~`, and the leading `//` of a UNC or WSL path stay, as the leading `/` of a posix path does. */
111
+ const resolved = (path) => {
112
+ const kept = [];
113
+ for (const part of path.split('/')) {
114
+ if (part === '..') {
115
+ if (kept.length !== 1 || !DRIVE.test(kept[0] ?? ''))
116
+ kept.pop();
117
+ }
118
+ else if (part !== '' && part !== '.')
119
+ kept.push(part);
120
+ }
121
+ return `${path.startsWith('//') ? '//' : path.startsWith('/') ? '/' : ''}${kept.join('/')}`;
122
+ };
123
+ /**
124
+ * A path value's folders and name below the base folder, or null for an absolute path outside it. A relative value is taken from the cwd before it is measured, so a relative and an absolute path to one file read the same
125
+ * when the cwd is a folder inside the repository. Only the counts and classes below leave this function.
126
+ */
127
+ const partsBelow = (value, root, cwd) => {
128
+ const base = root?.replace(/\\/g, '/').replace(/\/+$/, '');
129
+ const given = value.replace(/\\/g, '/');
130
+ const from = cwd?.replace(/\\/g, '/').replace(/\/+$/, '');
131
+ const path = ABSOLUTE.test(given) ? resolved(given) : from === undefined || from === '' ? given : resolved(`${from}/${given}`);
132
+ const inside = base !== undefined && base !== '';
133
+ // The root itself (a `.` resolved against a cwd at the root, or its absolute path) has no parts below it, as the same path with a trailing slash has none.
134
+ if (inside && path.toLowerCase() === base.toLowerCase())
135
+ return [];
136
+ const below = inside && path.toLowerCase().startsWith(`${base.toLowerCase()}/`) ? path.slice(base.length + 1) : ABSOLUTE.test(path) ? null : path;
137
+ return below === null ? null : below.split('/').filter((part) => part !== '' && part !== '.');
138
+ };
139
+ const extensionOf = (name) => {
140
+ const lower = name.toLowerCase();
141
+ const dot = lower.lastIndexOf('.');
142
+ if (dot <= 0)
143
+ return 'none';
144
+ const extension = lower.slice(dot + 1);
145
+ if (SOURCE_EXTENSIONS.has(extension))
146
+ return 'source';
147
+ if (DOC_EXTENSIONS.has(extension))
148
+ return 'doc';
149
+ if (CONFIG_EXTENSIONS.has(extension))
150
+ return 'config';
151
+ if (DATA_EXTENSIONS.has(extension))
152
+ return 'data';
153
+ if (IMAGE_EXTENSIONS.has(extension))
154
+ return 'image';
155
+ return 'other';
156
+ };
157
+ const dirKindOf = (folders) => {
158
+ const lower = folders.map((folder) => folder.toLowerCase());
159
+ if (lower.length === 0)
160
+ return 'root';
161
+ if (lower.some((folder) => GENERATED_DIRS.has(folder)))
162
+ return 'generated';
163
+ if (lower.some((folder) => TEST_DIRS.has(folder)))
164
+ return 'test';
165
+ if (lower.some((folder) => DOC_DIRS.has(folder)))
166
+ return 'doc';
167
+ if (lower.some((folder) => folder.startsWith('.')))
168
+ return 'hidden';
169
+ if (lower.some((folder) => SOURCE_DIRS.has(folder)))
170
+ return 'source';
171
+ return 'other';
172
+ };
173
+ const REGEX_TESTS = [
174
+ ['alternation', /(?<!\\)\|/],
175
+ ['anchor', /^\^|(?<!\\)\$$|\\[bAzZ]/],
176
+ ['class', /(?<!\\)\[[^\]]*\]|\\[dwsDWS]/],
177
+ ['escape', /\\[^dwsDWSbAzZ]/],
178
+ ['group', /(?<!\\)\(/],
179
+ ['quantifier', /(?<![\\(])[*+?]|(?<!\\)\{\d+(?:,\d*)?\}/],
180
+ ['wildcard', /(?<!\\)\./],
181
+ ];
182
+ const GLOB_TESTS = [
183
+ ['alternation', /\{[^}]*,[^}]*\}/],
184
+ ['class', /\[[^\]]*\]/],
185
+ ['recursive', /\*\*/],
186
+ ['wildcard', /[*?]/],
187
+ ];
188
+ const patternFeaturesOf = (pattern, glob) => {
189
+ const used = (glob ? GLOB_TESTS : REGEX_TESTS).filter(([, test]) => test.test(pattern)).map(([feature]) => feature);
190
+ return used.length === 0 ? ['literal'] : [...used].sort();
191
+ };
192
+ /**
193
+ * The features of each argument of a call (layer 1), sorted by key: its value's size, and for a path its depth below the repository root (else the working folder), its extension class, and its folder kind, and for a pattern what it uses. A Glob call's
194
+ * `pattern` and a `glob`, `include`, or `exclude` argument read as globs; any other pattern as a regular expression. A boolean and a null have no features (a boolean goes as a closed value, `argValuesOf`). The values are read
195
+ * here and go no further: every field is a member of a closed list.
196
+ */
197
+ export const argFeaturesOf = (native, input, root, cwd) => Object.keys(input)
198
+ .sort()
199
+ .flatMap((key) => {
200
+ const value = input[key];
201
+ if (value === null || value === undefined || typeof value === 'boolean')
202
+ return [];
203
+ const kind = argClassOfEntry(key, value);
204
+ const feature = { key, valueSize: sizeOf(value) };
205
+ if (typeof value === 'string' && kind === 'path') {
206
+ // An absolute path outside the repository root (else the working folder) names folders above it, so its depth is `outside` and its folder kind `other`: only its own name's extension is read.
207
+ const parts = partsBelow(value, root, cwd);
208
+ feature.extension = extensionOf(parts === null ? value.replace(/^.*[\\/]/, '') : (parts.at(-1) ?? ''));
209
+ feature.pathDepth = parts === null ? 'outside' : depthOf(parts.length - 1);
210
+ feature.dirKind = parts === null ? 'other' : dirKindOf(parts.slice(0, -1));
211
+ }
212
+ if (typeof value === 'string' && kind === 'pattern')
213
+ feature.patternFeatures = patternFeaturesOf(value, native === 'Glob' || GLOB_ARGS.has(key.toLowerCase()));
214
+ return [feature];
215
+ });
216
+ /** The closed values of a call's arguments, sorted by key: every boolean, and every string its tool's list for the key holds. A custom agent's name is off the list, so it is not here. */
217
+ export const argValuesOf = (native, input, closed) => Object.keys(input)
218
+ .sort()
219
+ .flatMap((key) => {
220
+ const value = input[key];
221
+ if (typeof value === 'boolean')
222
+ return [{ key, enumValue: String(value) }];
223
+ return typeof value === 'string' && closed.get(native)?.get(key)?.has(value) === true ? [{ key, enumValue: value }] : [];
224
+ });
225
+ /** What kind of name a token is: a file's name, a folder's name below the session root, an environment variable's name, a flag's value that is a name, or a custom agent's name. */
226
+ export const NAME_KINDS = ['file', 'dir', 'env', 'flag', 'agent'];
227
+ const DIR_ARGS = words('cwd workdir directory dir path');
228
+ const ENV_ARGS = words('env environment envs');
229
+ const VARIABLE_NAME = '[A-Za-z_][A-Za-z0-9_]*';
230
+ const ENV_NAME = new RegExp(`^${VARIABLE_NAME}$`);
231
+ const ENV_ASSIGNMENT = new RegExp(`^(${VARIABLE_NAME})=`);
232
+ // A PowerShell scope word before the name (`$env:NAME`, `$script:NAME`, `$global:NAME`) is not part of it, in any case. Only these three: any other word before a colon (`$local:x`, `$private:x`) is an ordinary POSIX variable and its text after the colon is not a name.
233
+ const WHOLE_REFERENCE = new RegExp(`^(?:\\$(?:(?:env|script|global):)?(${VARIABLE_NAME})|\\$\\{(?:(?:env|script|global):)?(${VARIABLE_NAME})\\})$`, 'i');
234
+ /**
235
+ * The one rule for a variable reference: a word names a variable only when its raw source, quotes included, is exactly one reference: `$NAME`, `${NAME}`, or a PowerShell `$env:NAME` or scope form (`$script:NAME`, `$global:NAME`),
236
+ * bare or in double quotes. Every other word is literal: a `$` inside a longer word, a quote, a command substitution, an escape, and `%NAME%` (no POSIX shell or PowerShell expands it) name nothing, which sends less and never more.
237
+ * The tokenizer reads each word through this.
238
+ */
239
+ export const variableOf = (raw) => {
240
+ const match = WHOLE_REFERENCE.exec(/^"(.*)"$/s.exec(raw)?.[1] ?? raw);
241
+ return match?.[1] ?? match?.[2];
242
+ };
243
+ // A path that is exactly a home folder, on any mount or distribution (`/home/jsmith`, `C:\Users\jsmith`, `/mnt/c/Users/jsmith`, `/c/Users/jsmith`, `/var/home/jsmith`, `/export/home/jsmith`, `\\wsl.localhost\Ubuntu\home\jsmith`): its last
244
+ // segment is the machine's account name, not a file. The test runs only on a path outside the session root, so the folders above `home` or `users` need no list.
245
+ const HOME_FOLDER = /(?:^|[\\/])(?:home|users)[\\/][^\\/]+[\\/]*$/i;
246
+ // A long flag with its value joined (`--region=eu`), or a flag word on its own: a long flag, or one short letter. A cluster (`-am`) or a short flag with its value attached holds no flag name of its own.
247
+ const JOINED_LONG_FLAG = /^(--[A-Za-z][A-Za-z0-9_-]*)=(.*)$/s;
248
+ const FLAG_TOKEN = /^(?:--[A-Za-z][A-Za-z0-9_-]*|-[A-Za-z])$/;
249
+ const FLAG_VALUE_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
250
+ // The flags whose value is text, a message or a query or an expression, and so no name even when it is one word.
251
+ const TEXT_FLAGS = words('-m --message --body --title --description --comment --text --query --data --eval -e --command --subject --notes');
252
+ // The flags whose value is a credential, so never a name however word-like it is: the redactor only knows the shapes of the credentials it has seen, and an opaque token is none of them.
253
+ const CREDENTIAL_FLAGS = words('--token --access-token --accesstoken --access_token --auth-token --authtoken --auth_token --api-token --apitoken --api_token --refresh-token --bearer --bearer-token --id-token ' +
254
+ '--password --passwd --pass --mysqlpass --pwd --passphrase --secret --client-secret --client_secret --secret-key --secret_key --secret-access-key ' +
255
+ '--api-key --apikey --api_key --private-key --auth --authorization --credential --credentials');
256
+ // A prefixed spelling is a credential by its last word: set off by a separator or a capital letter (`--db-password`, `--db-pass`, `--vendor_api_key`, `--clientSecret`, `--AWSSecret`), or joined to what precedes it for the words
257
+ // that are rarely the end of anything else (`--dbpassword`, `--githubtoken`, `--clientsecret`). `pass` needs the separator, so `--bypass` and `--compass` are not credentials, and neither are `--tokenizer`, `--customer`, `--sort-key`,
258
+ // and a bare `--key`. The list is finite: a credential under a name that fits none of these shapes still reads as a name.
259
+ const CREDENTIAL_SUFFIX = /(?:^|[-_])(?:token|password|passwd|pass|pwd|passphrase|secret|credentials?|cookie)$|(?:password|passwd|passphrase|secret|token)$|(?:api|private|secret|access|encryption|signing|master|license|shared|session)[-_]key$/;
260
+ const isCredentialFlag = (flag) => CREDENTIAL_FLAGS.has(flag.toLowerCase()) ||
261
+ CREDENTIAL_SUFFIX.test(flag
262
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
263
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
264
+ .toLowerCase());
265
+ // The one-letter flags whose next word is a name, and only on the program that gives them that meaning: `-k` selects tests on pytest, and is a passphrase on `openssl` and a switch on `curl` and `du`. Any other one-letter flag may be a
266
+ // switch followed by a positional (`-i needle`) or take a credential (`-p`), so its next word is not read.
267
+ const SHORT_NAME_FLAGS = words('-k');
268
+ const PYTEST_PROGRAMS = words('pytest py.test');
269
+ const runsPytest = (head, args) => PYTEST_PROGRAMS.has(head.text.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '').toLowerCase()) || args.some((word, at) => !word.quoted && word.text === '-m' && args[at + 1]?.text === 'pytest');
270
+ /**
271
+ * The values of a command's flags that are names: the next word of a long flag (or of `-k` on pytest), or the value joined to a long one, when it is a single identifier-like word, and the flag is not one that takes
272
+ * text or a credential. A long boolean flag followed by a positional is still read as if the positional were its value.
273
+ */
274
+ const flagValueNames = (head, args) => {
275
+ const pytest = runsPytest(head, args);
276
+ const found = [];
277
+ for (let at = 0; at < args.length; at += 1) {
278
+ const word = args[at];
279
+ // A word after the command's opaque point (`tokenize`) may be literal text the reader took for words, so neither its joined value nor the word after it is a name.
280
+ if (word === undefined || (word.quoted && word.joined !== true) || word.opaque === true)
281
+ continue;
282
+ if (word.text === '--')
283
+ break;
284
+ const joined = JOINED_LONG_FLAG.exec(word.text);
285
+ const flag = joined?.[1] ?? word.text;
286
+ if (TEXT_FLAGS.has(flag) || isCredentialFlag(flag) || !FLAG_TOKEN.test(flag))
287
+ continue;
288
+ const after = flag.startsWith('--') || (pytest && SHORT_NAME_FLAGS.has(flag)) ? args[at + 1] : undefined;
289
+ const next = after?.opaque === true ? undefined : after;
290
+ const value = joined === null ? (next !== undefined && (next.quoted || !next.text.startsWith('-')) ? next.text : undefined) : joined[2];
291
+ if (value !== undefined && FLAG_VALUE_NAME.test(value))
292
+ found.push(value);
293
+ }
294
+ return found;
295
+ };
296
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
297
+ /**
298
+ * The name tokens of a call (layer 2), read from the values layer 1 reads: the file basename of each path value and the directory names of its segments below the root (else the working folder), never a segment above it, so an account
299
+ * or home folder is not here; the variable names a shell command assigns (`VAR=` before its program), references (`$VAR`, `${VAR}`), or an `env` object holds; a flag value that is a name; and a custom agent's `subagent_type`.
300
+ * Never a value: an assignment's value, a message, a path, and a number are read for the name and go no further. Sorted by kind, then name, and single. The wire sends each through its one gate.
301
+ */
302
+ export const argNamesOf = (native, input, root, cwd, commands, closed, restored = []) => {
303
+ const held = new Map();
304
+ const add = (kind, name) => {
305
+ if (name !== '')
306
+ held.set(`${kind}\0${name}`, { kind, name });
307
+ };
308
+ const addPath = (value, folderArg) => {
309
+ // A value that starts with a variable (`$HOME/notes`, `%USERPROFILE%\notes`) is anchored where the variable points, not at the cwd, so it is read as outside the root.
310
+ const parts = /^[$%]/.test(value) ? null : partsBelow(value, root, cwd);
311
+ const inside = (parts ?? []).filter((part) => part !== '..');
312
+ // Above the root a segment is the machine's, not the project's: only a file's own name is read from such a path, and not even that when the path is a home folder, whose name is the account's.
313
+ if (parts === null) {
314
+ if (!folderArg && !HOME_FOLDER.test(value))
315
+ add('file', value.replace(/^.*[\\/]/, ''));
316
+ return;
317
+ }
318
+ for (const folder of folderArg ? inside : inside.slice(0, -1))
319
+ add('dir', folder);
320
+ if (!folderArg)
321
+ add('file', inside.at(-1) ?? '');
322
+ };
323
+ // The path a `git restore` names is read as the path an edit names is.
324
+ for (const path of restored)
325
+ addPath(path, false);
326
+ for (const [key, value] of Object.entries(input)) {
327
+ const lower = key.toLowerCase();
328
+ if (typeof value === 'string' && PATH_ARGS.has(lower))
329
+ addPath(value, DIR_ARGS.has(lower));
330
+ else if (ENV_ARGS.has(lower) && isPlainObject(value)) {
331
+ for (const name of Object.keys(value))
332
+ if (ENV_NAME.test(name))
333
+ add('env', name);
334
+ }
335
+ else if (lower === 'subagent_type' && typeof value === 'string' && closed.get(native)?.get(key)?.has(value) === false)
336
+ add('agent', value);
337
+ }
338
+ for (const { before, head, args } of commands) {
339
+ for (const word of before) {
340
+ const assigned = (word.quoted && word.joined !== true) || word.opaque === true ? null : ENV_ASSIGNMENT.exec(word.text)?.[1];
341
+ if (assigned !== undefined && assigned !== null)
342
+ add('env', assigned);
343
+ }
344
+ for (const word of [...before, head, ...args])
345
+ if (word.variable !== undefined)
346
+ add('env', word.variable);
347
+ for (const name of flagValueNames(head, args))
348
+ add('flag', name);
349
+ }
350
+ return [...held.values()].sort((a, b) => NAME_KINDS.indexOf(a.kind) - NAME_KINDS.indexOf(b.kind) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
351
+ };
352
+ /** What a literal of a command becomes in its template: a closed list, so a template holds no argument text. `<flag>` is a flag-like word whose name is not spelled (an attached value, a cluster off the public list). */
353
+ export const TEMPLATE_SLOTS = ['<text>', '<path>', '<ident>', '<num>', '<url>', '<var>', '<flag>'];
354
+ // A command is redacted before it is read, so an scp-style address (`git@host:org/repo`) reaches here as the redactor's email mask and is no URL word.
355
+ const URL_WORD = /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/|www\.)/i;
356
+ const NUMBER_WORD = /^[+-]?\d+(?:[.,]\d+)*$/;
357
+ // A separator, a glob character, a home or dot folder, or a file extension: the word names a place, whatever it is called.
358
+ const PATH_WORD = /[\\/*?]|^~|^\.{1,2}$|\.[A-Za-z][A-Za-z0-9]{0,9}$/;
359
+ const SHORT_FLAG_NAME = /^-[A-Za-z]$/;
360
+ /** The program as the step identity names it: the basename in lower case, without a Windows launcher suffix. */
361
+ const programName = (text) => text
362
+ .replace(/^.*[\\/]/, '')
363
+ .toLowerCase()
364
+ .replace(/\.(?:exe|cmd|bat)$/, '');
365
+ /** The slot a literal word has, by its shape alone, in this order: URL, number, path, identifier, then any other text. */
366
+ const slotOf = (text) => {
367
+ if (URL_WORD.test(text))
368
+ return '<url>';
369
+ if (NUMBER_WORD.test(text))
370
+ return '<num>';
371
+ if (PATH_WORD.test(text))
372
+ return '<path>';
373
+ return FLAG_VALUE_NAME.test(text) ? '<ident>' : '<text>';
374
+ };
375
+ /**
376
+ * A flag word as a template holds it, and whether the word after it is text. A number (`head -20`) is `<num>`. A flag keeps its name when it is on the public flag list or is one letter, the two cases the step identity names too; any other flag is `<flag>`
377
+ * (a private script's flag, a cluster, a value attached to a short flag), so a flag a program or a person made up is not spelled. The value joined to a kept flag is its slot. The next word is text after a text or credential flag, and after any
378
+ * one-letter flag but `-k` on pytest (`pytest`): the names reader refuses to read that word (`-p` is a password on `sshpass` and a count on `head`), so its type is not sent either.
379
+ */
380
+ const flagToken = (text, redact, opaque, pytest) => {
381
+ if (NUMBER_WORD.test(text))
382
+ return { token: '<num>', text: false };
383
+ const joined = JOINED_LONG_FLAG.exec(text);
384
+ const flag = joined?.[1] ?? text;
385
+ const takesText = TEXT_FLAGS.has(flag) || isCredentialFlag(flag) || (SHORT_FLAG_NAME.test(flag) && !(pytest && SHORT_NAME_FLAGS.has(flag)));
386
+ if (!PUBLIC_FLAGS.has(flag) && !SHORT_FLAG_NAME.test(flag))
387
+ return { token: '<flag>', text: joined === null && takesText };
388
+ if (joined !== null)
389
+ return { token: `${redact(flag)}=${opaque || takesText ? '<text>' : slotOf(joined[2] ?? '')}`, text: false };
390
+ return { token: redact(flag), text: takesText };
391
+ };
392
+ /**
393
+ * One command as a template: its program, its subcommand, and its flags, as written, and every other word as a slot (`git commit -m <text>`, `pytest <path> -k <ident>`). A program given as a path is `<path>`. The subcommand is the first word the
394
+ * program's own public table lists (`PUBLIC_SUBCOMMANDS`, the table the step identity reads), so no free word of an argument list is ever kept as one. The word after a credential flag, a text flag, or a one-letter flag (but `-k` on pytest), joined or apart, is
395
+ * `<text>` whatever its shape, so not even the type of a secret is sent. A word after `--`, a quoted word, and a word at or after the command's opaque point (`tokenize`) are never kept words: a flag-shaped one at that point keeps its name and no value. A variable reference is
396
+ * `<var>` and its name is the names layer's. Every kept word is masked by `redact`, so a credential-shaped word is not stored as it is; the wire gates each one again (`wireTemplate`).
397
+ */
398
+ const templateOf = ({ head, args }, redact) => {
399
+ const program = programName(head.text);
400
+ const table = PUBLIC_SUBCOMMANDS.get(program);
401
+ const tokens = [];
402
+ if (head.variable !== undefined)
403
+ tokens.push('<var>');
404
+ else if (head.opaque === true || (head.quoted && /\s/.test(head.text)))
405
+ tokens.push('<text>');
406
+ else
407
+ tokens.push(/[\\/]|^~|^\.{1,2}$/.test(head.text) ? '<path>' : redact(program));
408
+ const pytest = runsPytest(head, args);
409
+ let subcommand = false;
410
+ let textNext = false;
411
+ let ended = false;
412
+ for (const word of args) {
413
+ const text = textNext;
414
+ textNext = false;
415
+ const flagLike = (!word.quoted || word.joined === true) && !ended && word.text.startsWith('-') && word.text !== '--';
416
+ if (word.variable !== undefined)
417
+ tokens.push('<var>');
418
+ else if (text || (word.opaque === true && !flagLike))
419
+ tokens.push('<text>');
420
+ else if (ended || (word.quoted && !flagLike))
421
+ tokens.push(slotOf(word.text));
422
+ else if (word.text === '--') {
423
+ ended = true;
424
+ tokens.push('--');
425
+ }
426
+ else if (flagLike) {
427
+ const flag = flagToken(word.text, redact, word.opaque === true, pytest);
428
+ textNext = flag.text;
429
+ tokens.push(flag.token);
430
+ }
431
+ else if (!subcommand && table?.has(word.text) === true) {
432
+ subcommand = true;
433
+ tokens.push(redact(word.text));
434
+ }
435
+ else
436
+ tokens.push(slotOf(word.text));
437
+ }
438
+ return tokens.join(' ');
439
+ };
440
+ /** A call's command templates, one per command in the order they run (`templateOf`). Sent through the wire's one gate, which drops a template whose kept word fails it. */
441
+ export const templatesOf = (commands, redact) => commands.map((command) => templateOf(command, redact));
442
+ /** What an edit did, as a closed set: the shape of the change and never its text. Written in this order on the wire. */
443
+ export const DIFF_SHAPES = ['import-added', 'import-removed', 'identifier-renamed', 'lines-added', 'lines-removed', 'lines-changed', 'whitespace-only', 'file-written'];
444
+ // A line that brings a module in, in the forms the common languages share: an import, a from-import, a require, an include, a using directive, and a use declaration.
445
+ const IMPORT_LINE = /^(?:import\s|from\s+\S+\s+import\s|(?:const|let|var)\s.*=\s*require\(|#include\s|using\s+(?:static\s+)?[\w.]+\s*;?$|use\s+[\w:{}, *]+;?$)/;
446
+ const CODE_TOKEN = /[A-Za-z_$][\w$]*|\d+|\s+|[^\w\s$]/g;
447
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
448
+ const textLines = (text) => text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== '');
449
+ /** The lines of `from` that `against` does not hold, by count and in order, so a line moved is no change. */
450
+ const unmatched = (from, against) => {
451
+ const spare = new Map();
452
+ for (const line of against)
453
+ spare.set(line, (spare.get(line) ?? 0) + 1);
454
+ return from.filter((line) => {
455
+ const held = spare.get(line) ?? 0;
456
+ spare.set(line, held - 1);
457
+ return held <= 0;
458
+ });
459
+ };
460
+ /** Whether `after` is `before` with names replaced: the same tokens apart from identifiers, each old name replaced by one new name and no two by the same, and no replaced name left standing in the line. */
461
+ const isRename = (before, after) => {
462
+ const was = Array.from(before.matchAll(CODE_TOKEN), (token) => token[0]);
463
+ const now = Array.from(after.matchAll(CODE_TOKEN), (token) => token[0]);
464
+ if (was.length !== now.length)
465
+ return false;
466
+ const forward = new Map();
467
+ const backward = new Map();
468
+ for (const [at, old] of was.entries()) {
469
+ const next = now[at] ?? '';
470
+ if (old === next)
471
+ continue;
472
+ if (!IDENTIFIER.test(old) || !IDENTIFIER.test(next) || (forward.get(old) ?? next) !== next || (backward.get(next) ?? old) !== old)
473
+ return false;
474
+ forward.set(old, next);
475
+ backward.set(next, old);
476
+ }
477
+ return forward.size > 0 && !was.some((token, at) => token === now[at] && (forward.has(token) || backward.has(token)));
478
+ };
479
+ /** The shapes of one edit, from its old and new text. Whitespace alone is one shape; else the lines that differ are read as imports, renames (a removed and an added line that pair up), changed lines, and lines only added or removed. */
480
+ const shapesOfEdit = (before, after) => {
481
+ const held = new Set();
482
+ if (before === after)
483
+ return held;
484
+ if (before.replace(/\s+/g, '') === after.replace(/\s+/g, ''))
485
+ return held.add('whitespace-only');
486
+ const was = textLines(before);
487
+ const now = textLines(after);
488
+ const removed = unmatched(was, now);
489
+ const added = unmatched(now, was);
490
+ for (const line of removed)
491
+ if (IMPORT_LINE.test(line))
492
+ held.add('import-removed');
493
+ for (const line of added)
494
+ if (IMPORT_LINE.test(line))
495
+ held.add('import-added');
496
+ const gone = removed.filter((line) => !IMPORT_LINE.test(line));
497
+ const come = added.filter((line) => !IMPORT_LINE.test(line));
498
+ // Lines that changed pair up in the order they stand, so a rename is a pair that reads as one.
499
+ for (const [at, line] of gone.entries()) {
500
+ const partner = come[at];
501
+ if (partner !== undefined)
502
+ held.add(isRename(line, partner) ? 'identifier-renamed' : 'lines-changed');
503
+ }
504
+ if (come.length > gone.length)
505
+ held.add('lines-added');
506
+ if (gone.length > come.length)
507
+ held.add('lines-removed');
508
+ return held;
509
+ };
510
+ /**
511
+ * What a call that edits a file did, as a closed set (layer 3), never its text: a `Write` is `file-written`; an `Edit` or each entry of a `MultiEdit` is read from its old and new text (`shapesOfEdit`), and the call has the union, in the list's order.
512
+ * A call that is no edit, or an edit that changes nothing, has none.
513
+ */
514
+ export const diffShapeOf = (tool, input) => {
515
+ const text = (value) => (typeof value === 'string' ? value : '');
516
+ const edits = tool === 'MultiEdit' ? (Array.isArray(input.edits) ? input.edits : []) : tool === 'Edit' ? [input] : [];
517
+ const held = new Set(tool === 'Write' ? ['file-written'] : []);
518
+ for (const edit of edits) {
519
+ if (typeof edit !== 'object' || edit === null)
520
+ continue;
521
+ for (const shape of shapesOfEdit(text(Reflect.get(edit, 'old_string')), text(Reflect.get(edit, 'new_string'))))
522
+ held.add(shape);
523
+ }
524
+ return DIFF_SHAPES.filter((shape) => held.has(shape));
525
+ };
526
+ /**
527
+ * What changed from the previous call of the same tool in the same prompt, as a closed set (layer 1): `same` when nothing did; else any of a key added, a key removed, a kept key's value whose features or closed value
528
+ * changed (`feature-changed`), a kept key's value that changed while its features did not (`value-changed`), and the target (`target-changed`). The first call of a tool in a prompt has none.
529
+ */
530
+ export const CHANGE_KINDS = ['same', 'key-added', 'key-removed', 'feature-changed', 'value-changed', 'target-changed'];
531
+ export const changeKindOf = (before, after) => {
532
+ const was = new Set(Object.keys(before.input));
533
+ const now = new Set(Object.keys(after.input));
534
+ const facts = (call, key) => JSON.stringify([call.features.find((entry) => entry.key === key) ?? null, call.values.find((entry) => entry.key === key) ?? null]);
535
+ const kept = [...now].filter((key) => was.has(key));
536
+ const moved = kept.filter((key) => JSON.stringify(before.input[key]) !== JSON.stringify(after.input[key]));
537
+ const held = new Set();
538
+ if ([...now].some((key) => !was.has(key)))
539
+ held.add('key-added');
540
+ if ([...was].some((key) => !now.has(key)))
541
+ held.add('key-removed');
542
+ if (moved.some((key) => facts(before, key) !== facts(after, key)))
543
+ held.add('feature-changed');
544
+ if (moved.some((key) => facts(before, key) === facts(after, key)))
545
+ held.add('value-changed');
546
+ if (before.target !== after.target)
547
+ held.add('target-changed');
548
+ return held.size === 0 ? ['same'] : CHANGE_KINDS.filter((kind) => held.has(kind));
549
+ };
@@ -0,0 +1,65 @@
1
+ import { HOOK_HARNESS } from '../telemetry/fields.js';
2
+ export declare const CANDIDATES_FILE = "candidates.jsonl";
3
+ export declare const CANDIDATE_RECORD_VERSION = 2;
4
+ /** Where an instruction file sits relative to the session. `mainCheckout` is the span between a linked worktree and the main checkout that holds it, a span the harnesses treat apart. */
5
+ export declare const INSTRUCTION_POSITIONS: readonly ["working", "parent", "above", "mainCheckout", "user"];
6
+ export type InstructionPosition = (typeof INSTRUCTION_POSITIONS)[number];
7
+ /** Claude Code's hook events, the 33 in the installed harness's own list (2.1.319). A name not on this list is counted as `other`, so a settings file cannot put free text in a record. */
8
+ export declare const HOOK_EVENT_NAMES: readonly ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest", "PermissionDenied", "Notification", "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop", "TaskCreated", "TaskCompleted", "TeammateIdle", "InstructionsLoaded", "ConfigChange", "CwdChanged", "FileChanged", "WorktreeCreate", "WorktreeRemove", "PreCompact", "PostCompact", "Elicitation", "ElicitationResult", "Setup", "PostToolBatch", "UserPromptExpansion", "PreModelSwitch", "PostModelSwitch", "DirectoryAdded", "MessageDisplay"];
9
+ export type HookEventName = (typeof HOOK_EVENT_NAMES)[number] | 'other';
10
+ export type InstructionCounts = {
11
+ [position in InstructionPosition]: number;
12
+ };
13
+ export type HookEventCounts = {
14
+ [event in HookEventName]?: number;
15
+ };
16
+ /** Instruction files beside `CLAUDE.md`, counted in the working folder only: `CLAUDE.local.md`, `.claude/CLAUDE.md`, and the `.claude/rules/` files that name no `paths` condition. */
17
+ export declare const WORKING_KINDS: readonly ["local", "dotClaude", "rules"];
18
+ export type WorkingKind = (typeof WORKING_KINDS)[number];
19
+ export type WorkingKindCounts = {
20
+ [kind in WorkingKind]: number;
21
+ };
22
+ /** Where a settings file sits: the user's, the working folder's own `.claude/`, or a folder above it up to the repo root (and the main checkout root of a linked worktree). */
23
+ export declare const HOOK_SOURCES: readonly ["user", "working", "ancestors"];
24
+ export type HookSource = (typeof HOOK_SOURCES)[number];
25
+ export type HookSourceCounts = {
26
+ [source in HookSource]: HookEventCounts;
27
+ };
28
+ export interface CandidateCounts {
29
+ instructions: InstructionCounts;
30
+ workingKinds: WorkingKindCounts;
31
+ skills: {
32
+ project: number;
33
+ user: number;
34
+ };
35
+ hooks: HookSourceCounts;
36
+ /** Folders from the working folder up to the repo root; null when no repository holds it. */
37
+ repoLevels: number | null;
38
+ /** Folders from the working folder up to the main checkout of a linked worktree nested inside it; null otherwise. */
39
+ mainLevels: number | null;
40
+ }
41
+ export interface CandidateSnapshot extends CandidateCounts {
42
+ v: number;
43
+ session: string;
44
+ at: string;
45
+ harness: typeof HOOK_HARNESS;
46
+ }
47
+ /** Which position a folder `level` steps above the working folder holds; the derivation places a loaded file's level with this same rule. */
48
+ export declare const positionOfLevel: (level: number, { repoLevels, mainLevels }: Pick<CandidateCounts, "repoLevels" | "mainLevels">) => Exclude<InstructionPosition, "user">;
49
+ /** Counts what exists around `cwd`, under the harness's config folder `claudeDir`. It throws on anything but a missing file. */
50
+ export declare const snapshotCandidates: ({ cwd, claudeDir }: {
51
+ cwd: string;
52
+ claudeDir: string;
53
+ }) => Promise<CandidateCounts>;
54
+ /**
55
+ * Appends this session's snapshot. Fail-open: any error writes nothing and returns, because a hook must never fail or block a session, and a partial count would read as evidence
56
+ * that files were absent. The counts are finished before the store is touched, so nothing partial can reach the file.
57
+ */
58
+ export declare const recordCandidates: (storeDir: string, { session, cwd, claudeDir, at }: {
59
+ session: string;
60
+ cwd: string;
61
+ claudeDir: string;
62
+ at: string;
63
+ }) => Promise<void>;
64
+ /** The first snapshot of each session (a resumed session starts again and appends another), by session id. A line of another version or a torn line is skipped. */
65
+ export declare const readCandidates: (storeDir: string) => Promise<Map<string, CandidateSnapshot>>;