@xhulz/nina 0.55.1 → 0.56.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 (138) hide show
  1. package/package.json +1 -1
  2. package/releases/0.56.0/core/GAPS.md +125 -0
  3. package/releases/0.56.0/core/answered.json +68 -0
  4. package/releases/0.56.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  5. package/releases/0.56.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  6. package/releases/0.56.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  7. package/releases/0.56.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  8. package/releases/0.56.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  9. package/releases/0.56.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  10. package/releases/0.56.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  11. package/releases/0.56.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  12. package/releases/0.56.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  13. package/releases/0.56.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  14. package/releases/0.56.0/core/tree/.claude/agents/architect.md +130 -0
  15. package/releases/0.56.0/core/tree/.claude/agents/dba.md +119 -0
  16. package/releases/0.56.0/core/tree/.claude/agents/design-reviewer.md +75 -0
  17. package/releases/0.56.0/core/tree/.claude/agents/devops.md +118 -0
  18. package/releases/0.56.0/core/tree/.claude/agents/implementer.md +163 -0
  19. package/releases/0.56.0/core/tree/.claude/agents/integration-tester.md +170 -0
  20. package/releases/0.56.0/core/tree/.claude/agents/planner.md +132 -0
  21. package/releases/0.56.0/core/tree/.claude/agents/qa.md +150 -0
  22. package/releases/0.56.0/core/tree/.claude/agents/reviewer.md +187 -0
  23. package/releases/0.56.0/core/tree/.claude/agents/secops.md +120 -0
  24. package/releases/0.56.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  25. package/releases/0.56.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  26. package/releases/0.56.0/core/tree/.claude/agents-overview.md +65 -0
  27. package/releases/0.56.0/core/tree/.claude/frontend.md +10 -0
  28. package/releases/0.56.0/core/tree/.claude/graph.md +79 -0
  29. package/releases/0.56.0/core/tree/.claude/patterns.md +251 -0
  30. package/releases/0.56.0/core/tree/.claude/pills/README.md +150 -0
  31. package/releases/0.56.0/core/tree/.claude/pipeline.md +101 -0
  32. package/releases/0.56.0/core/tree/.claude/retrieval.md +103 -0
  33. package/releases/0.56.0/core/tree/.claude/router.md +205 -0
  34. package/releases/0.56.0/core/tree/.claude/templates/integration.md +48 -0
  35. package/releases/0.56.0/core/tree/CLAUDE.md +166 -0
  36. package/releases/0.56.0/core/tree/scripts/cost-watch.mjs +25 -0
  37. package/releases/0.56.0/core/tree/scripts/edit-guard.mjs +24 -0
  38. package/releases/0.56.0/core/tree/scripts/harness-check.mjs +71 -0
  39. package/releases/0.56.0/core/tree/scripts/loop-gate.mjs +19 -0
  40. package/releases/0.56.0/core/vocabulary.json +14 -0
  41. package/releases/0.56.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  42. package/releases/0.56.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  43. package/releases/0.56.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  44. package/releases/0.56.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  45. package/releases/0.56.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  46. package/releases/0.56.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  47. package/releases/0.56.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  48. package/releases/0.56.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  49. package/releases/0.56.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  50. package/releases/0.56.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  51. package/releases/0.56.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  52. package/releases/0.56.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  53. package/releases/0.56.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  54. package/releases/0.56.0/surfaces/d1/tree/CLAUDE.md +2 -0
  55. package/releases/0.56.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  56. package/releases/0.56.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  57. package/releases/0.56.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  58. package/releases/0.56.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  59. package/releases/0.56.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  60. package/releases/0.56.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  61. package/releases/0.56.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  62. package/releases/0.56.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  63. package/releases/0.56.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  64. package/releases/0.56.0/surfaces/db/tree/.claude/graph.md +8 -0
  65. package/releases/0.56.0/surfaces/db/tree/.claude/patterns.md +22 -0
  66. package/releases/0.56.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  67. package/releases/0.56.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  68. package/releases/0.56.0/surfaces/db/tree/.claude/router.md +4 -0
  69. package/releases/0.56.0/surfaces/db/tree/CLAUDE.md +17 -0
  70. package/releases/0.56.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  71. package/releases/0.56.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  72. package/releases/0.56.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  73. package/releases/0.56.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  74. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  75. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  76. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  77. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  78. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  79. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  80. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  81. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  82. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  83. package/releases/0.56.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  84. package/releases/0.56.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  85. package/releases/0.56.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  86. package/releases/0.56.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  87. package/releases/0.56.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  88. package/releases/0.56.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  89. package/releases/0.56.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  90. package/releases/0.56.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  91. package/releases/0.56.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  92. package/releases/0.56.0/surfaces/frontend/tree/.claude/router.md +14 -0
  93. package/releases/0.56.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  94. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  95. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  96. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  97. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  98. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  99. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  100. package/releases/0.56.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  101. package/releases/0.56.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  102. package/releases/0.56.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  103. package/releases/0.56.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  104. package/releases/0.56.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  105. package/releases/0.56.0/surfaces/integrations/tree/.claude/router.md +6 -0
  106. package/releases/0.56.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  107. package/releases/0.56.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  108. package/releases/0.56.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  109. package/releases/0.56.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  110. package/releases/0.56.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  111. package/releases/0.56.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  112. package/releases/0.56.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  113. package/releases/0.56.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  114. package/releases/0.56.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  115. package/releases/0.56.0/surfaces/money/tree/.claude/graph.md +2 -0
  116. package/releases/0.56.0/surfaces/money/tree/.claude/patterns.md +49 -0
  117. package/releases/0.56.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  118. package/releases/0.56.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  119. package/releases/0.56.0/surfaces/money/tree/CLAUDE.md +11 -0
  120. package/releases/0.56.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  121. package/releases/0.56.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  122. package/releases/0.56.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  123. package/releases/0.56.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  124. package/releases/0.56.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  125. package/releases/0.56.0/surfaces/pii/tree/CLAUDE.md +5 -0
  126. package/releases/0.56.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  127. package/releases/0.56.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  128. package/releases/0.56.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  129. package/releases/0.56.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  130. package/releases/0.56.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  131. package/releases/0.56.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  132. package/releases/0.56.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  133. package/releases/0.56.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  134. package/src/commands/export.mjs +5 -5
  135. package/src/commands/langfuse.mjs +30 -1
  136. package/src/commands/stats.mjs +7 -0
  137. package/src/dashboard.html +2 -1
  138. package/src/transcripts.mjs +227 -1
@@ -124,6 +124,220 @@ const WRITES = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path',
124
124
  /** How many lines a text written through an edit tool holds: none for an empty one, a last newline ending one. */
125
125
  const linesIn = (text) => (typeof text === 'string' && text.length > 0 ? text.replace(/\n$/, '').split('\n').length : 0);
126
126
 
127
+ /** A temporary directory named in a command: what it writes there is scratch, not the tree. */
128
+ const TEMP = /(?:^|[^\w.-])(?:\/private)?\/tmp\/|\/var\/folders\/|\$\{?TMPDIR\b|\bmktemp\b|\/scratchpad\b/;
129
+
130
+ /** A script, in a heredoc or a quoted `-e`, that opens a file to write it. */
131
+ const SCRIPT_WRITES = /\bopen\([^)\n]*,\s*(?:mode\s*=\s*)?['"](?:w|a|x|r\+)[bt+]?['"]|\.write_(?:text|bytes)\(|\b(?:writeFileSync|appendFileSync|writeFile|appendFile|createWriteStream)\(/;
132
+
133
+ /** A script's write to a file it names as a literal: the path, judged as the shell's own writes are. */
134
+ const LITERAL_WRITES = [
135
+ /\bopen\(\s*(['"])([^'"\n]+)\1\s*,\s*(?:mode\s*=\s*)?['"](?:w|a|x|r\+)/g,
136
+ /\bPath\(\s*(['"])([^'"\n]+)\1\s*\)\.write_(?:text|bytes)\(/g,
137
+ /\b(?:writeFileSync|appendFileSync|writeFile|appendFile|createWriteStream)\(\s*(['"`])([^'"`\n]+)\1/g,
138
+ ];
139
+
140
+ /** A redirect's own words, which are not a command's last argument. */
141
+ const REDIRECTS = /(?:^|\s)(?:\d?[<>]{1,2}\|?|&>>?)(?:&\d+|\s*\S+)/g;
142
+
143
+ /** What may stand before a simple command's own name: a function it opens, `do`, `then`, a group, `nohup`. */
144
+ const LEADS = String.raw`^\s*(?:[A-Za-z_][\w-]*\(\)\s*)?(?:(?:do|then|else|time|nohup|\{|\(|!)\s*)*`;
145
+
146
+ /** A simple command that opens a function's body. */
147
+ const FUNCTION = /^\s*(?:function\s+[A-Za-z_][\w-]*\s*(?:\(\))?|[A-Za-z_][\w-]*\s*\(\))\s*\{/;
148
+
149
+ /** A `cd` or `pushd`, and where it goes. */
150
+ const CD = new RegExp(String.raw`${LEADS}(?:cd|pushd)\b(?:\s+(\S+))?`);
151
+
152
+ /**
153
+ * The shell's own ways to write a file, read in a simple command's words, each with where it writes: the redirect's
154
+ * or `tee`'s file, the last word of an in-place `sed` or `perl`, of a `cp` or a `mv`; a formatter's `--write` or
155
+ * `--fix`, `git apply` and `patch` write where the command stands.
156
+ */
157
+ const SHELL_WRITES = [
158
+ [/(?:^|[\s;&|(){}])\d?>>?\|?\s*(?!&)(?!\/dev\/(?:null|stdout|stderr)\b)([^\s;&|<>()]+)/, 1],
159
+ [/(?:^|[\s;&|(])tee\s+(?:-a\s+)?(?!\/dev\/null\b)([^\s;&|<>()-][^\s;&|<>()]*)/, 1],
160
+ [new RegExp(String.raw`${LEADS}(?:sed|perl)\s+(?:-\S*\s+)*(?:-[a-zA-Z]*i\b|--in-place)`), 'last'],
161
+ [new RegExp(String.raw`${LEADS}(?:cp|mv)\s`), 'last'],
162
+ [/\s--(?:write|fix)(?=\s|=|$)/, null],
163
+ [new RegExp(String.raw`${LEADS}(?:git\s+apply|patch)\b`), null],
164
+ ];
165
+
166
+ /** A simple command that runs an interpreter, whose script — a heredoc, a `-e` or `-c` — may open a file to write. */
167
+ const INTERPRETER = new RegExp(String.raw`${LEADS}(?:[A-Za-z_]\w*=\S*\s+)*(?:(?:pnpm|npx)\s+(?:exec\s+)?)?(?:python3?|node|deno|bun|perl|ruby|tsx)\b`);
168
+
169
+ /** A quoted word that is only a path or a variable, kept as a word rather than emptied as a string. */
170
+ const PLAIN = /^[\w$./{}~@:+,=-]*$/;
171
+
172
+ /**
173
+ * A command's simple commands, in order, as the shell splits them at `&&`, `||`, `;`, `|`, `&` and line ends outside
174
+ * quotes: each with its text as written, a heredoc's body joined to the command that opened it, and its words — a
175
+ * quoted path kept, any other quoted string emptied, no body — so that `a > b` in a script, or an arrow function in
176
+ * `node -e "…"`, is no redirect.
177
+ *
178
+ * @param {string} command
179
+ * @returns {{raw: string, words: string}[]}
180
+ */
181
+ function simpleCommands(command) {
182
+ const out = [];
183
+ let raw = '';
184
+ let words = '';
185
+ let quote = null;
186
+ let opened = 0;
187
+ const bodies = [];
188
+ const end = () => {
189
+ if (raw.trim()) out.push({ raw, words });
190
+ raw = '';
191
+ words = '';
192
+ };
193
+ for (let i = 0; i < command.length; i += 1) {
194
+ const c = command[i];
195
+ const prev = command[i - 1];
196
+ const next = command[i + 1];
197
+ if (quote) {
198
+ raw += c;
199
+ if (c === '\\' && quote === '"' && next !== undefined) {
200
+ raw += next;
201
+ i += 1;
202
+ } else if (c === quote) {
203
+ const inside = raw.slice(opened, -1);
204
+ words += PLAIN.test(inside) ? inside : '""';
205
+ quote = null;
206
+ }
207
+ continue;
208
+ }
209
+ if (c === '\\' && next !== undefined) {
210
+ // A backslash before a line end joins the lines; before anything else, it keeps that character as it is.
211
+ raw += next === '\n' ? ' ' : c + next;
212
+ words += next === '\n' ? ' ' : c + next;
213
+ i += 1;
214
+ continue;
215
+ }
216
+ if (c === '$' && next === '(' && command[i + 2] === '(') {
217
+ // Arithmetic, `$((1 << n))` or `$((a > b))`: neither a heredoc nor a redirect.
218
+ let depth = 0;
219
+ let j = i + 1;
220
+ for (; j < command.length; j += 1) {
221
+ if (command[j] === '(') depth += 1;
222
+ else if (command[j] === ')' && (depth -= 1) === 0) break;
223
+ }
224
+ raw += command.slice(i, j + 1);
225
+ words += '0';
226
+ i = j;
227
+ continue;
228
+ }
229
+ if (c === "'" || c === '"') {
230
+ quote = c;
231
+ raw += c;
232
+ opened = raw.length;
233
+ continue;
234
+ }
235
+ // A comment runs to the line's end: what it says is not run.
236
+ if (c === '#' && (prev === undefined || /[\s;&|(]/.test(prev))) {
237
+ while (i + 1 < command.length && command[i + 1] !== '\n') i += 1;
238
+ continue;
239
+ }
240
+ const heredoc = c === '<' && prev !== '<' ? /^<<(-?)\s*\\?(['"]?)([A-Za-z_][\w.-]*)\2/.exec(command.slice(i)) : null;
241
+ if (heredoc) {
242
+ bodies.push({ delimiter: heredoc[3], tabs: heredoc[1] === '-' });
243
+ raw += heredoc[0];
244
+ words += '<<';
245
+ i += heredoc[0].length - 1;
246
+ continue;
247
+ }
248
+ if (c === '\n') {
249
+ // Each heredoc opened on the line: the lines up to its delimiter, kept with the command that opened it.
250
+ let at = i + 1;
251
+ for (const { delimiter, tabs } of bodies.splice(0)) {
252
+ while (at < command.length) {
253
+ const stop = command.indexOf('\n', at);
254
+ const line = command.slice(at, stop === -1 ? command.length : stop);
255
+ raw += `\n${line}`;
256
+ at = stop === -1 ? command.length : stop + 1;
257
+ if ((tabs ? line.replace(/^\t+/, '') : line) === delimiter) break;
258
+ }
259
+ }
260
+ i = at - 1;
261
+ end();
262
+ continue;
263
+ }
264
+ const pair = (c === '&' || c === '|') && next === c;
265
+ const pipe = c === '|' && prev !== '>';
266
+ const background = c === '&' && prev !== '>' && prev !== '<' && next !== '>';
267
+ if (c === ';' || pair || pipe || background) {
268
+ end();
269
+ if (pair || (c === '|' && next === '&')) i += 1;
270
+ continue;
271
+ }
272
+ raw += c;
273
+ words += c;
274
+ }
275
+ end();
276
+ return out;
277
+ }
278
+
279
+ /**
280
+ * Whether a shell command writes a file outside a temporary directory: a redirect into a file, `tee`, `sed -i` or
281
+ * `perl -i`, `cp` or `mv`, a formatter's `--write` or `--fix`, `git apply` or `patch`, or an interpreter's script that
282
+ * opens a file to write it. Each write is placed where it lands: a path, a variable the command bound — to a temporary
283
+ * directory or not — or, for a relative one, where the command stands after its `cd`s, which starts in the project
284
+ * and keeps its place through a relative `cd`. A variable the command did not bind, a function's `$1`, is nowhere
285
+ * known, and not counted. One project's implementers wrote their files so, through a script that kept an escape the
286
+ * edit tools decode, and the count of the edit tools read those rounds as having written nothing. Read from the
287
+ * command alone, it is a sign, not a count: a script run from a file is missed, and so is one that writes the tree
288
+ * through a variable while it names a temporary directory.
289
+ *
290
+ * @param {string} command - A Bash tool call's command.
291
+ * @returns {boolean}
292
+ */
293
+ export function writesThroughShell(command) {
294
+ if (typeof command !== 'string') return false;
295
+ const vars = new Map();
296
+ let here = 'tree';
297
+ // Where each open subshell's `(` found the command standing: its `cd`s end with it.
298
+ const outside = [];
299
+ // A function's body runs where it is called, on what it is given: nowhere this command says.
300
+ let functions = 0;
301
+ let standing = here;
302
+ const place = (target) => {
303
+ if (typeof target !== 'string' || target === '') return standing;
304
+ const name = /^\$\{?(\w+|[@*#?$!-])/.exec(target);
305
+ if (name) return /^\$\{?TMPDIR\b/.test(target) ? 'scratch' : (vars.get(name[1]) ?? 'unknown');
306
+ if (TEMP.test(target) || target.startsWith('/dev/')) return 'scratch';
307
+ if (target.startsWith('~') || target.includes('""')) return 'unknown';
308
+ return target.startsWith('/') ? 'tree' : standing;
309
+ };
310
+ for (const { raw, words } of simpleCommands(command)) {
311
+ const groups = words.replace(/\$\([^()]*\)/g, '');
312
+ for (let n = /^\s*(\(*)/.exec(groups)[1].length; n > 0; n -= 1) outside.push(here);
313
+ for (let n = (groups.match(/\)/g) ?? []).length - (groups.match(/\(/g) ?? []).length; n > 0 && outside.length; n -= 1) here = outside.pop();
314
+ if (FUNCTION.test(words)) functions += 1;
315
+ standing = functions > 0 ? 'unknown' : here;
316
+ for (const [, name, value] of words.matchAll(/(?:^|\s)(?:export\s+)?([A-Za-z_]\w*)=(\S*)/g)) vars.set(name, place(value));
317
+ const cd = CD.exec(words);
318
+ if (cd) {
319
+ if (functions === 0) here = cd[1] === undefined ? 'unknown' : place(cd[1]);
320
+ } else {
321
+ for (const [rule, at] of SHELL_WRITES) {
322
+ const found = rule.exec(words);
323
+ if (!found) continue;
324
+ const target = at === 'last' ? words.replace(REDIRECTS, ' ').trim().split(/\s+/).at(-1) : at === null ? undefined : found[at];
325
+ if (place(target) === 'tree') return true;
326
+ }
327
+ if (INTERPRETER.test(raw) && SCRIPT_WRITES.test(raw)) {
328
+ // A script names its files inside it: a literal one is placed; with none, a script that names a temporary
329
+ // directory is taken to write there.
330
+ const literal = LITERAL_WRITES.flatMap((re) => [...raw.matchAll(re)].map((m) => m[2]));
331
+ if (literal.some((path) => place(path) === 'tree')) return true;
332
+ const names = TEMP.test(raw) || [...vars].some(([v, p]) => p !== 'tree' && new RegExp(`\\$\\{?${v}\\b`).test(raw));
333
+ if (literal.length === 0 && standing === 'tree' && !names) return true;
334
+ }
335
+ }
336
+ if (functions > 0 && /(?:^|[\s;])\}\s*$/.test(words)) functions -= 1;
337
+ }
338
+ return false;
339
+ }
340
+
127
341
  /** The line under a declared verdict that names what the report sends back. */
128
342
  const ISSUES = /^\s*ISSUES:(.*)$/;
129
343
 
@@ -646,7 +860,7 @@ async function attachAgentDetail(projectDir, dispatches) {
646
860
  if (
647
861
  run.agent_read_bytes === size &&
648
862
  !('resumes' in run) &&
649
- [...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'lines_written' in r && 'effort' in r && 'turns' in r && 'prompt_chars' in r)
863
+ [...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'lines_written' in r && 'shell_writes' in r && 'effort' in r && 'turns' in r && 'prompt_chars' in r)
650
864
  ) {
651
865
  continue;
652
866
  }
@@ -679,6 +893,8 @@ async function attachAgentDetail(projectDir, dispatches) {
679
893
  * run wrote, not its diff: an edit made twice counts twice, and a file a shell command changed not at all.
680
894
  */
681
895
  lines: new Map(),
896
+ /** Its shell commands that wrote a file outside a temporary directory, which no count of lines reaches. */
897
+ shellWrites: 0,
682
898
  /** The tool calls the round made, by id: over its turns, how many calls each turn sent at once. */
683
899
  calls: new Set(),
684
900
  /** How long the prompt that opened the round was, in characters; never the prompt. */
@@ -726,6 +942,15 @@ async function attachAgentDetail(projectDir, dispatches) {
726
942
  // A torn line loses one edit, not the run's.
727
943
  }
728
944
  }
945
+ if (line.includes('"name":"Bash"')) {
946
+ try {
947
+ for (const block of JSON.parse(line)?.message?.content ?? []) {
948
+ if (block?.type === 'tool_use' && block.name === 'Bash' && writesThroughShell(block.input?.command)) round.shellWrites += 1;
949
+ }
950
+ } catch {
951
+ // A torn line loses one command, not the run's.
952
+ }
953
+ }
729
954
  if (line.includes('"usage"')) {
730
955
  try {
731
956
  const row = JSON.parse(line);
@@ -808,6 +1033,7 @@ async function attachAgentDetail(projectDir, dispatches) {
808
1033
  record.effort = spent.effort;
809
1034
  record.files_touched = round.written.size;
810
1035
  record.lines_written = [...round.lines.values()].reduce((a, b) => a + b, 0);
1036
+ record.shell_writes = round.shellWrites;
811
1037
  record.skills = [...round.skills];
812
1038
  record.lessons_read = round.lessonsRead.size;
813
1039
  record.lessons_listed = round.lessonsListed;