@manyoars/oars 0.2.0 → 0.4.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 (320) hide show
  1. package/README.md +147 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +10 -4
  5. package/dist/cli.js +440 -91
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.d.ts +1 -1
  9. package/dist/commands/login.js +3 -3
  10. package/dist/errors.d.ts +13 -13
  11. package/dist/errors.js +28 -11
  12. package/dist/gate/checks.d.ts +68 -0
  13. package/dist/gate/checks.js +584 -0
  14. package/dist/gate/command.d.ts +14 -0
  15. package/dist/gate/command.js +61 -0
  16. package/dist/gate/hook-fill-main.d.ts +1 -0
  17. package/dist/gate/hook-fill-main.js +16 -0
  18. package/dist/gate/hook-fill.d.ts +38 -0
  19. package/dist/gate/hook-fill.js +94 -0
  20. package/dist/gate/hook-main.d.ts +1 -0
  21. package/dist/gate/hook-main.js +47 -0
  22. package/dist/gate/hook-process.d.ts +24 -0
  23. package/dist/gate/hook-process.js +112 -0
  24. package/dist/gate/hook-record.d.ts +3 -0
  25. package/dist/gate/hook-record.js +8 -0
  26. package/dist/gate/hook.d.ts +48 -0
  27. package/dist/gate/hook.js +259 -0
  28. package/dist/gate/install.d.ts +25 -0
  29. package/dist/gate/install.js +126 -0
  30. package/dist/gate/local-bin.d.ts +8 -0
  31. package/dist/gate/local-bin.js +36 -0
  32. package/dist/gate/notes.d.ts +19 -0
  33. package/dist/gate/notes.js +27 -0
  34. package/dist/gate/novelty.d.ts +35 -0
  35. package/dist/gate/novelty.js +99 -0
  36. package/dist/gate/policy.d.ts +25 -0
  37. package/dist/gate/policy.js +73 -0
  38. package/dist/gate/record.d.ts +120 -0
  39. package/dist/gate/record.js +125 -0
  40. package/dist/gate/registry-state.d.ts +10 -0
  41. package/dist/gate/registry-state.js +30 -0
  42. package/dist/gate/registry.d.ts +56 -0
  43. package/dist/gate/registry.js +94 -0
  44. package/dist/gate/replay.d.ts +20 -0
  45. package/dist/gate/replay.js +254 -0
  46. package/dist/gate/report.d.ts +50 -0
  47. package/dist/gate/report.js +89 -0
  48. package/dist/gate/scope.d.ts +7 -0
  49. package/dist/gate/scope.js +27 -0
  50. package/dist/gate/transcripts.d.ts +68 -0
  51. package/dist/gate/transcripts.js +232 -0
  52. package/dist/hook-bin.d.ts +1 -0
  53. package/dist/hook-bin.js +50 -0
  54. package/dist/issuer.d.ts +10 -6
  55. package/dist/issuer.js +19 -24
  56. package/dist/main.d.ts +1 -0
  57. package/dist/main.js +67 -0
  58. package/dist/oidc.js +1 -1
  59. package/dist/sessions/actions.d.ts +35 -0
  60. package/dist/sessions/actions.js +429 -0
  61. package/dist/sessions/allow.d.ts +23 -0
  62. package/dist/sessions/allow.js +71 -0
  63. package/dist/sessions/archive.d.ts +11 -0
  64. package/dist/sessions/archive.js +38 -0
  65. package/dist/sessions/audit.d.ts +42 -0
  66. package/dist/sessions/audit.js +104 -0
  67. package/dist/sessions/baseline.d.ts +62 -0
  68. package/dist/sessions/baseline.js +190 -0
  69. package/dist/sessions/call-shape.d.ts +19 -0
  70. package/dist/sessions/call-shape.js +85 -0
  71. package/dist/sessions/capture-runtime.d.ts +10 -0
  72. package/dist/sessions/capture-runtime.js +12 -0
  73. package/dist/sessions/cell-increments.d.ts +47 -0
  74. package/dist/sessions/cell-increments.js +243 -0
  75. package/dist/sessions/cells.d.ts +109 -0
  76. package/dist/sessions/cells.js +261 -0
  77. package/dist/sessions/codex.d.ts +53 -0
  78. package/dist/sessions/codex.js +745 -0
  79. package/dist/sessions/command.d.ts +127 -0
  80. package/dist/sessions/command.js +419 -0
  81. package/dist/sessions/copilot.d.ts +40 -0
  82. package/dist/sessions/copilot.js +432 -0
  83. package/dist/sessions/coverage.d.ts +19 -0
  84. package/dist/sessions/coverage.js +56 -0
  85. package/dist/sessions/cursor.d.ts +24 -0
  86. package/dist/sessions/cursor.js +324 -0
  87. package/dist/sessions/derive.d.ts +57 -0
  88. package/dist/sessions/derive.js +386 -0
  89. package/dist/sessions/exclusion.d.ts +87 -0
  90. package/dist/sessions/exclusion.js +361 -0
  91. package/dist/sessions/extract.d.ts +181 -0
  92. package/dist/sessions/extract.js +1806 -0
  93. package/dist/sessions/fold.d.ts +17 -0
  94. package/dist/sessions/fold.js +71 -0
  95. package/dist/sessions/frame.d.ts +24 -0
  96. package/dist/sessions/frame.js +95 -0
  97. package/dist/sessions/gemini.d.ts +37 -0
  98. package/dist/sessions/gemini.js +613 -0
  99. package/dist/sessions/gold-arms.d.ts +67 -0
  100. package/dist/sessions/gold-arms.js +172 -0
  101. package/dist/sessions/gold.d.ts +197 -0
  102. package/dist/sessions/gold.js +463 -0
  103. package/dist/sessions/graph.d.ts +12 -0
  104. package/dist/sessions/graph.js +34 -0
  105. package/dist/sessions/grok.d.ts +36 -0
  106. package/dist/sessions/grok.js +421 -0
  107. package/dist/sessions/hook-calls.d.ts +3 -0
  108. package/dist/sessions/hook-calls.js +47 -0
  109. package/dist/sessions/hook-index.d.ts +66 -0
  110. package/dist/sessions/hook-index.js +164 -0
  111. package/dist/sessions/hooks.d.ts +17 -0
  112. package/dist/sessions/hooks.js +281 -0
  113. package/dist/sessions/identity.d.ts +27 -0
  114. package/dist/sessions/identity.js +131 -0
  115. package/dist/sessions/install.d.ts +70 -0
  116. package/dist/sessions/install.js +246 -0
  117. package/dist/sessions/lineage.d.ts +19 -0
  118. package/dist/sessions/lineage.js +118 -0
  119. package/dist/sessions/mcp.d.ts +7 -0
  120. package/dist/sessions/mcp.js +157 -0
  121. package/dist/sessions/merkle.d.ts +7 -0
  122. package/dist/sessions/merkle.js +28 -0
  123. package/dist/sessions/motif.d.ts +44 -0
  124. package/dist/sessions/motif.js +59 -0
  125. package/dist/sessions/opencode.d.ts +33 -0
  126. package/dist/sessions/opencode.js +319 -0
  127. package/dist/sessions/otel.d.ts +60 -0
  128. package/dist/sessions/otel.js +494 -0
  129. package/dist/sessions/package-books.d.ts +48 -0
  130. package/dist/sessions/package-books.js +120 -0
  131. package/dist/sessions/package-resolve.d.ts +32 -0
  132. package/dist/sessions/package-resolve.js +185 -0
  133. package/dist/sessions/pending.d.ts +34 -0
  134. package/dist/sessions/pending.js +142 -0
  135. package/dist/sessions/pi.d.ts +36 -0
  136. package/dist/sessions/pi.js +353 -0
  137. package/dist/sessions/pool-books.d.ts +21 -0
  138. package/dist/sessions/pool-books.js +75 -0
  139. package/dist/sessions/pool-choices.d.ts +5 -0
  140. package/dist/sessions/pool-choices.js +11 -0
  141. package/dist/sessions/pool-command.d.ts +115 -0
  142. package/dist/sessions/pool-command.js +669 -0
  143. package/dist/sessions/pool-delta.d.ts +54 -0
  144. package/dist/sessions/pool-delta.js +190 -0
  145. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  146. package/dist/sessions/pool-nightly-deps.js +11 -0
  147. package/dist/sessions/pool-nightly.d.ts +10 -0
  148. package/dist/sessions/pool-nightly.js +85 -0
  149. package/dist/sessions/pool-outbox.d.ts +76 -0
  150. package/dist/sessions/pool-outbox.js +313 -0
  151. package/dist/sessions/pool-print.d.ts +26 -0
  152. package/dist/sessions/pool-print.js +68 -0
  153. package/dist/sessions/pool-raw.d.ts +27 -0
  154. package/dist/sessions/pool-raw.js +68 -0
  155. package/dist/sessions/pool-replica.d.ts +35 -0
  156. package/dist/sessions/pool-replica.js +91 -0
  157. package/dist/sessions/pool-send-lock.d.ts +20 -0
  158. package/dist/sessions/pool-send-lock.js +87 -0
  159. package/dist/sessions/pool-sent.d.ts +4 -0
  160. package/dist/sessions/pool-sent.js +26 -0
  161. package/dist/sessions/pool-totals.d.ts +10 -0
  162. package/dist/sessions/pool-totals.js +21 -0
  163. package/dist/sessions/pool-wire.d.ts +435 -0
  164. package/dist/sessions/pool-wire.js +1100 -0
  165. package/dist/sessions/pool.d.ts +32 -0
  166. package/dist/sessions/pool.js +164 -0
  167. package/dist/sessions/projection.d.ts +75 -0
  168. package/dist/sessions/projection.js +165 -0
  169. package/dist/sessions/public-vocabulary.d.ts +72 -0
  170. package/dist/sessions/public-vocabulary.js +295 -0
  171. package/dist/sessions/query.d.ts +123 -0
  172. package/dist/sessions/query.js +268 -0
  173. package/dist/sessions/read-adoption.d.ts +2 -0
  174. package/dist/sessions/read-adoption.js +27 -0
  175. package/dist/sessions/read-call-shape.d.ts +2 -0
  176. package/dist/sessions/read-call-shape.js +25 -0
  177. package/dist/sessions/read-change.d.ts +2 -0
  178. package/dist/sessions/read-change.js +30 -0
  179. package/dist/sessions/read-cost.d.ts +4 -0
  180. package/dist/sessions/read-cost.js +48 -0
  181. package/dist/sessions/read-day-move.d.ts +3 -0
  182. package/dist/sessions/read-day-move.js +65 -0
  183. package/dist/sessions/read-fit.d.ts +7 -0
  184. package/dist/sessions/read-fit.js +41 -0
  185. package/dist/sessions/read-interaction.d.ts +5 -0
  186. package/dist/sessions/read-interaction.js +62 -0
  187. package/dist/sessions/read-lead-lag.d.ts +15 -0
  188. package/dist/sessions/read-lead-lag.js +81 -0
  189. package/dist/sessions/read-live.d.ts +6 -0
  190. package/dist/sessions/read-live.js +38 -0
  191. package/dist/sessions/read-method.d.ts +2 -0
  192. package/dist/sessions/read-method.js +29 -0
  193. package/dist/sessions/read-next-step.d.ts +2 -0
  194. package/dist/sessions/read-next-step.js +35 -0
  195. package/dist/sessions/read-order.d.ts +54 -0
  196. package/dist/sessions/read-order.js +62 -0
  197. package/dist/sessions/read-readable.d.ts +8 -0
  198. package/dist/sessions/read-readable.js +24 -0
  199. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  200. package/dist/sessions/read-recovery-edge.js +26 -0
  201. package/dist/sessions/read-recovery.d.ts +20 -0
  202. package/dist/sessions/read-recovery.js +97 -0
  203. package/dist/sessions/read-rows.d.ts +13 -0
  204. package/dist/sessions/read-rows.js +47 -0
  205. package/dist/sessions/read-substitution.d.ts +5 -0
  206. package/dist/sessions/read-substitution.js +51 -0
  207. package/dist/sessions/read-support.d.ts +48 -0
  208. package/dist/sessions/read-support.js +73 -0
  209. package/dist/sessions/read-tool-day.d.ts +2 -0
  210. package/dist/sessions/read-tool-day.js +26 -0
  211. package/dist/sessions/read-version-boundary.d.ts +31 -0
  212. package/dist/sessions/read-version-boundary.js +95 -0
  213. package/dist/sessions/read.d.ts +86 -0
  214. package/dist/sessions/read.js +87 -0
  215. package/dist/sessions/recall-bundle.d.ts +12 -0
  216. package/dist/sessions/recall-bundle.js +33 -0
  217. package/dist/sessions/recall-consequence.d.ts +45 -0
  218. package/dist/sessions/recall-consequence.js +179 -0
  219. package/dist/sessions/recall-frames.d.ts +18 -0
  220. package/dist/sessions/recall-frames.js +44 -0
  221. package/dist/sessions/recall-next.d.ts +145 -0
  222. package/dist/sessions/recall-next.js +241 -0
  223. package/dist/sessions/recall-object.d.ts +139 -0
  224. package/dist/sessions/recall-object.js +78 -0
  225. package/dist/sessions/recall-pool.d.ts +19 -0
  226. package/dist/sessions/recall-pool.js +96 -0
  227. package/dist/sessions/recall-push.d.ts +26 -0
  228. package/dist/sessions/recall-push.js +47 -0
  229. package/dist/sessions/recall-reads.d.ts +124 -0
  230. package/dist/sessions/recall-reads.js +417 -0
  231. package/dist/sessions/recall-render.d.ts +19 -0
  232. package/dist/sessions/recall-render.js +168 -0
  233. package/dist/sessions/recovery-calls.d.ts +21 -0
  234. package/dist/sessions/recovery-calls.js +25 -0
  235. package/dist/sessions/redact.d.ts +17 -0
  236. package/dist/sessions/redact.js +113 -0
  237. package/dist/sessions/repo-root.d.ts +10 -0
  238. package/dist/sessions/repo-root.js +101 -0
  239. package/dist/sessions/schema.d.ts +694 -0
  240. package/dist/sessions/schema.js +167 -0
  241. package/dist/sessions/session-shape.d.ts +76 -0
  242. package/dist/sessions/session-shape.js +355 -0
  243. package/dist/sessions/sources.d.ts +50 -0
  244. package/dist/sessions/sources.js +233 -0
  245. package/dist/sessions/steps.d.ts +105 -0
  246. package/dist/sessions/steps.js +250 -0
  247. package/dist/sessions/store.d.ts +309 -0
  248. package/dist/sessions/store.js +1498 -0
  249. package/dist/sessions/strata.d.ts +26 -0
  250. package/dist/sessions/strata.js +77 -0
  251. package/dist/sessions/survival.d.ts +27 -0
  252. package/dist/sessions/survival.js +230 -0
  253. package/dist/sessions/transcript-tail.d.ts +14 -0
  254. package/dist/sessions/transcript-tail.js +64 -0
  255. package/dist/sessions/verify.d.ts +53 -0
  256. package/dist/sessions/verify.js +286 -0
  257. package/dist/sessions/vocab-load.d.ts +26 -0
  258. package/dist/sessions/vocab-load.js +61 -0
  259. package/dist/sessions/vscode-copilot.d.ts +38 -0
  260. package/dist/sessions/vscode-copilot.js +494 -0
  261. package/dist/telemetry/command.d.ts +8 -0
  262. package/dist/telemetry/command.js +85 -0
  263. package/dist/telemetry/consent.d.ts +17 -0
  264. package/dist/telemetry/consent.js +63 -0
  265. package/dist/telemetry/crash.d.ts +8 -0
  266. package/dist/telemetry/crash.js +52 -0
  267. package/dist/telemetry/emit.d.ts +31 -0
  268. package/dist/telemetry/emit.js +42 -0
  269. package/dist/telemetry/fields.d.ts +16 -0
  270. package/dist/telemetry/fields.js +26 -0
  271. package/dist/telemetry/health.d.ts +21 -0
  272. package/dist/telemetry/health.js +108 -0
  273. package/dist/telemetry/hook-emit.d.ts +4 -0
  274. package/dist/telemetry/hook-emit.js +18 -0
  275. package/dist/telemetry/measures.d.ts +32 -0
  276. package/dist/telemetry/measures.js +128 -0
  277. package/dist/telemetry/observe.d.ts +13 -0
  278. package/dist/telemetry/observe.js +51 -0
  279. package/dist/telemetry/read-events.d.ts +9 -0
  280. package/dist/telemetry/read-events.js +17 -0
  281. package/dist/telemetry/record.d.ts +17 -0
  282. package/dist/telemetry/record.js +26 -0
  283. package/dist/telemetry/report.d.ts +77 -0
  284. package/dist/telemetry/report.js +188 -0
  285. package/dist/telemetry/schema.d.ts +40 -0
  286. package/dist/telemetry/schema.js +187 -0
  287. package/dist/telemetry/send-main.d.ts +1 -0
  288. package/dist/telemetry/send-main.js +11 -0
  289. package/dist/telemetry/send.d.ts +36 -0
  290. package/dist/telemetry/send.js +172 -0
  291. package/dist/telemetry/spool.d.ts +17 -0
  292. package/dist/telemetry/spool.js +70 -0
  293. package/dist/telemetry/wire.d.ts +25 -0
  294. package/dist/telemetry/wire.js +76 -0
  295. package/package.json +4 -5
  296. package/vocab/crates.json +1 -0
  297. package/vocab/go.json +1 -0
  298. package/vocab/harness.json +1 -0
  299. package/vocab/homebrew.json +1 -0
  300. package/vocab/manifest.json +1 -0
  301. package/vocab/maven.json +1 -0
  302. package/vocab/models.json +1 -0
  303. package/vocab/npm.json +1 -0
  304. package/vocab/programs.json +1 -0
  305. package/vocab/pypi.json +1 -0
  306. package/vocab/rubygems.json +1 -0
  307. package/dist/commands/deploy-input.d.ts +0 -38
  308. package/dist/commands/deploy-input.js +0 -150
  309. package/dist/commands/deploy.d.ts +0 -22
  310. package/dist/commands/deploy.js +0 -124
  311. package/dist/commands/status.d.ts +0 -7
  312. package/dist/commands/status.js +0 -18
  313. package/dist/commands/templates.d.ts +0 -11
  314. package/dist/commands/templates.js +0 -32
  315. package/dist/commands/test-charge.d.ts +0 -11
  316. package/dist/commands/test-charge.js +0 -34
  317. package/dist/journal.d.ts +0 -26
  318. package/dist/journal.js +0 -58
  319. package/parity/deploy-plans/paid-api.json +0 -34
  320. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,386 @@
1
+ // Derive (register A28): what version 2 computed while reading a transcript, now a pure function of the capture records. Nothing here
2
+ // reads raw, so a change to a rule below bumps EXTRACTOR_VERSION and re-derives the store from the log alone. The rules match the
3
+ // version-2 extractor line for line (error_signatures.py, prompt_graphs.py, cost_per_class.py), and the recovery episodes match
4
+ // recovery_episodes.py, so the numbers measured before the split reproduce after it.
5
+ import { captureSession, classify, compress, cut, EDITS, EXPLORE, ACT, normalize, SHELL, shortName } from './extract.js';
6
+ import { DECLINED_RE, INTERRUPT_MARKER, REJECTED_RE } from './session-shape.js';
7
+ import { ERROR_EXAMPLE_CHARS, EXTRACTOR_VERSION, } from './schema.js';
8
+ const byPosition = (a, b) => a.n - b.n;
9
+ /** The `<tool> | <normalized line>` key; the line was redacted at capture, and normalize can expose a shape the first pass missed. */
10
+ const signature = (tool, line, redact) => `${tool} | ${redact(normalize(line))}`;
11
+ const groupBy = (items, keyOf) => {
12
+ const groups = new Map();
13
+ for (const item of items) {
14
+ const key = keyOf(item);
15
+ const group = groups.get(key);
16
+ if (group === undefined)
17
+ groups.set(key, [item]);
18
+ else
19
+ group.push(item);
20
+ }
21
+ return groups;
22
+ };
23
+ // The prompt outcome signals. All of them read the main line with a resumed session's repeats dropped: a repeated call has status
24
+ // `none` under a later prompt, so it would read as a fresh edit of the same path. A subagent transcript's calls are not read here.
25
+ /**
26
+ * A later edit of a path within this many prompts is a redo. On the operator's store 58% of re-edits with no commit between them are one prompt
27
+ * apart and 82% within three; past that the tail is long and flat (88% at five, 99% at twenty), so a larger window adds unrelated later work.
28
+ */
29
+ export const REDO_WINDOW = 3;
30
+ // The rejection result and the interrupt marker live in session-shape.ts, which capture also reads; the marker is a user line capture keeps as a prompt of its own.
31
+ export { INTERRUPT_MARKER };
32
+ export const firstCopies = (records) => {
33
+ const seen = new Set();
34
+ return records.filter((record) => {
35
+ if (record.side)
36
+ return false;
37
+ if (record.toolUseId === null)
38
+ return true;
39
+ if (seen.has(record.toolUseId))
40
+ return false;
41
+ seen.add(record.toolUseId);
42
+ return true;
43
+ });
44
+ };
45
+ /** A failed edit wrote nothing. An edit with no result in the file may have written, so it counts. */
46
+ const isEdit = (tool) => EDITS.has(tool.tool) && tool.status !== 'error';
47
+ /** The commits a call made. A record captured before `segments` has only its first line's `target`, so it stays the floor it was. */
48
+ const commitsIn = (tool) => {
49
+ if (!SHELL.has(tool.tool) || tool.status !== 'ok')
50
+ return 0;
51
+ if (tool.segments === undefined)
52
+ return tool.target === 'git commit' ? 1 : 0;
53
+ return tool.commits?.length ?? 0;
54
+ };
55
+ /** The `edit-verify` rule's shell call, less a backgrounded one, whose `ok` says only that it started. */
56
+ const isVerifyCall = (tool) => SHELL.has(tool.tool) && !tool.keys.includes('run_in_background');
57
+ const ANY_ARGS_RUNNER = /^(?:vitest|jest|mocha|ava|pytest|tox|nox|rspec|phpunit|ctest|tsc|eslint|ruff|mypy|pyright|playwright|cypress|bazel|ninja|pre-commit|unittest)(?: |$)/;
58
+ /** A package manager that runs a tool by name: `pnpm vitest run`, `yarn tsc`, `npm exec vitest`. */
59
+ const PACKAGE_RUNNER = /^(?:npm exec|pnpm|yarn|bun) /;
60
+ /** Runners whose installing or opening subcommand verifies nothing. */
61
+ const NOT_A_RUN = /^(?:playwright install|pre-commit install|cypress open)(?: |$)/;
62
+ const VERB_RUNNER = /^(?:npm|pnpm|yarn|bun|mvn|mvnw|gradle|gradlew|just|make|cargo|go|dotnet|deno|swift|astro|nx|turbo) /;
63
+ const WORK_WORD = /^(?:[\w:-]*[-:])?(?:test|tests|check|checks|typecheck|verify|build|lint|compile|e2e|clippy|nextest|vet)(?:[-:][\w:-]*)?$/;
64
+ const OTHER_RUNNER = /^(?:make$|(?:npm|bun) t(?: |$)|(?:mvn|mvnw) (?:.* )?(?:package|install)(?: |$))/;
65
+ /**
66
+ * A subcommand whose later words are names, not work: `npm install lint-staged` and `go run check` verify nothing. One word may come before it, because
67
+ * a flag's value stays in `run`: `pnpm --filter web add test-utils` reads `pnpm web add test-utils`, and `yarn workspace web add x` has two. A first
68
+ * word that is itself work (`cargo test add`, `npm test install`) runs that work, and the name after it is its argument.
69
+ */
70
+ const NAMES_WORDS = /^(?:npm|pnpm|yarn|bun|cargo|go|dotnet) (?:(?!run )(?:workspace \S+|\S+) )?(?:add|i|install|uninstall|remove|rm|link|view|get)(?: |$)|^(?:cargo|go) run(?: |$)/;
71
+ const RUN_WRAPPER = /^(?:npx|bunx|uvx|pipx run|uv run|poetry run|pnpm exec|pnpm dlx|yarn dlx|bundle exec|python[\d.]*|py) /;
72
+ /** Whether a segment runs a build, a test suite, a type check, or a linter. A rule over the captured `run`, so a change here re-derives without a transcript. */
73
+ export const isRunner = (segment) => {
74
+ const run = segment.run.replace(RUN_WRAPPER, '');
75
+ const bare = run.replace(PACKAGE_RUNNER, '');
76
+ if (NOT_A_RUN.test(bare))
77
+ return false;
78
+ if (ANY_ARGS_RUNNER.test(run) || ANY_ARGS_RUNNER.test(bare) || OTHER_RUNNER.test(run))
79
+ return true;
80
+ if (NAMES_WORDS.test(run) && !WORK_WORD.test(run.split(' ')[1] ?? ''))
81
+ return false;
82
+ return VERB_RUNNER.test(run) && run.split(' ').slice(1).some((word) => WORK_WORD.test(word));
83
+ };
84
+ /**
85
+ * The last runner after the prompt's last edit decides, so a later `ls` or `git status` does not. Undefined when a shell call is reached
86
+ * that was captured before `segments`: it could be a runner, and saying `none` would claim it was not.
87
+ */
88
+ const verifyOf = (calls) => {
89
+ const lastEdit = calls.findLastIndex(isEdit);
90
+ if (lastEdit < 0)
91
+ return 'na';
92
+ for (const call of calls.slice(lastEdit + 1).filter(isVerifyCall).reverse()) {
93
+ if (call.segments === undefined)
94
+ return undefined;
95
+ const runner = call.segments.findLast(isRunner);
96
+ if (runner !== undefined) {
97
+ if (call.status === 'none')
98
+ return 'none';
99
+ if (!runner.read)
100
+ return 'masked';
101
+ return call.status === 'ok' ? 'passed' : 'failed';
102
+ }
103
+ }
104
+ return 'none';
105
+ };
106
+ /** A rejection names the call the operator declined, and the harness follows it with the interrupt marker, so it is read first. */
107
+ const stoppedOf = (errors, nextHead) => {
108
+ if (errors.some((error) => REJECTED_RE.test(error.line)))
109
+ return 'rejected';
110
+ return nextHead?.startsWith(INTERRUPT_MARKER) === true ? 'interrupt' : null;
111
+ };
112
+ /** A decline in the fixed text Codex or Gemini CLI writes (`DECLINED_RE`). It is read for `intervention` only: `stopped` stays as the books and the gold arms read it. */
113
+ const declinedIn = (errors) => errors.some((error) => DECLINED_RE.test(error.line));
114
+ /**
115
+ * The prompts whose edit a later prompt repeated, and of those the ones the very next prompt repeated (`immediate`, the `redo` bucket of `intervention`). One walk in call order: a commit settles every path
116
+ * edited so far, so it empties the map. `nextOf` gives a prompt's next record.
117
+ */
118
+ const redonePrompts = (calls, nextOf) => {
119
+ const redone = new Set();
120
+ const immediate = new Set();
121
+ const lastEditor = new Map();
122
+ for (const call of calls) {
123
+ if (commitsIn(call) > 0)
124
+ lastEditor.clear();
125
+ else if (call.prompt !== null && call.target !== null && isEdit(call)) {
126
+ const earlier = lastEditor.get(call.target);
127
+ if (earlier !== undefined && earlier !== call.prompt && call.prompt - earlier <= REDO_WINDOW)
128
+ redone.add(earlier);
129
+ if (earlier !== undefined && nextOf.get(earlier) === call.prompt)
130
+ immediate.add(earlier);
131
+ lastEditor.set(call.target, call.prompt);
132
+ }
133
+ }
134
+ return { redone, immediate };
135
+ };
136
+ /**
137
+ * A segment that undoes work in git: a revert that names a commit (a bare `git revert` is `--abort`, `--continue`, or `--quit`, whose flag the segment drops), a restore, or a bare checkout
138
+ * (the segment drops a path, a flag, and a word with a slash or quotes, so `git checkout -- <path>` and `git checkout feature/x` are the same bare word pair; `isGitUndo` narrows it by the step's flags).
139
+ */
140
+ const GIT_UNDO = /^git (?:revert .|restore(?: |$))|^git checkout(?: \.)?$/;
141
+ const BARE_CHECKOUT = /^git checkout$/;
142
+ /** Flags of a `git checkout` that move to or create a branch, or leave one with a literal `-`: none of them undoes work. `<literal>` is also what the step makes of any flag off the public list (`--theirs`, `--patch`), so those read as no undo too: a limit, pinned in `intervention.test.ts`. */
143
+ const CHECKOUT_MOVES = new Set(['<literal>', '-b', '-B', '--orphan', '--detach']);
144
+ /** A prompt that begins by asking for the undo, in English: `revert that`, `please undo`, `rollback`. */
145
+ const ASKS_UNDO = /^(?:please )?(?:revert|undo|roll ?back)\b/i;
146
+ /** The capture version that keeps a `git checkout` step's `target` class. */
147
+ const TARGET_CLASS_CAPTURE = 23;
148
+ /** Whether the segment at index `at` is a checkout that moves to a branch: the step with the same `seg` holds the flags that the segment drops (the stored step no longer holds `run`). */
149
+ const movesBranch = (tool, at) => {
150
+ const steps = tool.steps?.filter(({ seg }) => seg === at).map(({ identity }) => identity).filter((identity) => identity !== null && 'program' in identity) ?? [];
151
+ // From CAPTURE 23 the step names the class of what the checkout acts on: a proved path is an undo whatever the flags, and an unproved word is never one. Before it, only the flags tell.
152
+ if (Number(tool.cv) >= TARGET_CLASS_CAPTURE) {
153
+ if (steps.some((identity) => identity.target === 'path'))
154
+ return false;
155
+ if (steps.some((identity) => identity.target === 'word'))
156
+ return true;
157
+ }
158
+ return steps.some((identity) => identity.flags.some((flag) => CHECKOUT_MOVES.has(flag)));
159
+ };
160
+ /** A clean shell call that undid work in git. `git reset --hard` is read from the step, since a segment keeps no flag. */
161
+ const isGitUndo = (tool) => SHELL.has(tool.tool) &&
162
+ tool.status === 'ok' &&
163
+ (tool.segments?.some(({ run }, at) => GIT_UNDO.test(run) && !(BARE_CHECKOUT.test(run) && movesBranch(tool, at))) === true ||
164
+ tool.steps?.some(({ identity }) => identity !== null && 'program' in identity && identity.program === 'git' && identity.sub === 'reset' && identity.flags.includes('--hard')) === true);
165
+ /**
166
+ * What the next prompt `next` did to the work of a prompt (docs/sessions-store.md, "Prompt intervention"). First match: the last prompt or the marker's own record is `unknown`; then a stop (`stopped` set) is `user-stop`; then `revert`, `redo`,
167
+ * `after-error`; then `unknown` again when the next prompt ran a shell call captured before `segments`, which could have been an undo; else `none`.
168
+ */
169
+ const interventionOf = (prompt, stopped, next, settled, nextCalls, immediate) => {
170
+ if (next === undefined || prompt.head.startsWith(INTERRUPT_MARKER))
171
+ return 'unknown';
172
+ // A stop is read before the others: a rejected call is settled as an error, so `after-error` would otherwise claim every rejection.
173
+ if (stopped)
174
+ return 'user-stop';
175
+ if (settled.some(isEdit) && (nextCalls.some(isGitUndo) || ASKS_UNDO.test(next.head.trimStart())))
176
+ return 'revert';
177
+ if (immediate)
178
+ return 'redo';
179
+ if (settled.at(-1)?.status === 'error')
180
+ return 'after-error';
181
+ return nextCalls.some((tool) => SHELL.has(tool.tool) && tool.segments === undefined) ? 'unknown' : 'none';
182
+ };
183
+ /** The version-2 prompt and error records of one transcript, with the outcome signals. */
184
+ export const deriveSession = (capture, redact) => {
185
+ const tools = [...capture.tools].sort(byPosition);
186
+ const { session, rev } = capture.session;
187
+ // One pass groups the calls and the main-line errors by prompt, so a session costs O(prompts + tools), not their product.
188
+ const toolsOf = groupBy(tools, (tool) => tool.prompt);
189
+ const errorsOf = groupBy(capture.errorLines.filter((error) => !error.side), (error) => error.prompt);
190
+ // Capture pushes an error line for every failed result it reads, repeats included; `errors` and `recovery` keep them, as version 2 did.
191
+ const stopsOf = groupBy(firstCopies(capture.errorLines), (error) => error.prompt);
192
+ const calls = firstCopies(tools);
193
+ const settledOf = groupBy(calls, (tool) => tool.prompt);
194
+ const nextOf = new Map();
195
+ for (let at = 1; at < capture.prompts.length; at++) {
196
+ const before = capture.prompts[at - 1];
197
+ const after = capture.prompts[at];
198
+ if (before !== undefined && after !== undefined)
199
+ nextOf.set(before.index, after.index);
200
+ }
201
+ const { redone, immediate } = redonePrompts(calls, nextOf);
202
+ const prompts = capture.prompts.map((prompt, position) => {
203
+ const mine = toolsOf.get(prompt.index) ?? [];
204
+ const main = mine.filter((tool) => !tool.side);
205
+ const settled = settledOf.get(prompt.index) ?? [];
206
+ const sequence = main.map((tool) => shortName(tool.tool));
207
+ const errors = errorsOf.get(prompt.index) ?? [];
208
+ const stopped = stoppedOf(stopsOf.get(prompt.index) ?? [], capture.prompts[position + 1]?.head);
209
+ // The two main-line calls after the prompt's first main-line error (version 2 re-armed on each error and kept the first two).
210
+ const first = errors[0];
211
+ const recovery = first === undefined ? [] : main.filter((tool) => tool.n >= first.after).slice(0, 2).map((tool) => shortName(tool.tool));
212
+ const bigrams = {};
213
+ for (let index = 0; index + 1 < sequence.length; index += 1) {
214
+ const pair = `${sequence[index] ?? ''}>${sequence[index + 1] ?? ''}`;
215
+ bigrams[pair] = (bigrams[pair] ?? 0) + 1;
216
+ }
217
+ return {
218
+ kind: 'prompt',
219
+ v: EXTRACTOR_VERSION,
220
+ session,
221
+ rev,
222
+ index: prompt.index,
223
+ ts: prompt.ts,
224
+ wallSec: prompt.wallSec,
225
+ head: prompt.head,
226
+ cls: classify(sequence),
227
+ tools: sequence,
228
+ shape: compress(sequence),
229
+ errors: errors.length,
230
+ thinking: prompt.thinking,
231
+ sideTools: mine.length - main.length,
232
+ turns: prompt.turns,
233
+ model: prompt.model,
234
+ efforts: prompt.efforts,
235
+ skills: prompt.skills,
236
+ usage: prompt.usage,
237
+ ...(prompt.usageObserved === undefined ? {} : { usageObserved: prompt.usageObserved }),
238
+ ...(prompt.usageResponses === undefined ? {} : { usageResponses: prompt.usageResponses }),
239
+ recovery,
240
+ explore: sequence.filter((tool) => EXPLORE.has(tool)).length,
241
+ strata: prompt.strata,
242
+ act: sequence.filter((tool) => ACT.has(tool)).length,
243
+ bigrams,
244
+ contextFingerprint: prompt.promptFingerprint,
245
+ verify: verifyOf(settled),
246
+ stopped,
247
+ commits: settled.reduce((sum, tool) => sum + commitsIn(tool), 0),
248
+ redo: redone.has(prompt.index),
249
+ intervention: interventionOf(prompt, stopped !== null || declinedIn(stopsOf.get(prompt.index) ?? []), capture.prompts[position + 1], settled, settledOf.get(capture.prompts[position + 1]?.index ?? -1) ?? [], immediate.has(prompt.index)),
250
+ ...(prompt.frame === undefined ? {} : { frame: prompt.frame }),
251
+ };
252
+ });
253
+ const classOf = new Map(prompts.map((prompt) => [prompt.index, prompt.cls]));
254
+ const errors = capture.errorLines.map((error) => ({
255
+ kind: 'error',
256
+ v: EXTRACTOR_VERSION,
257
+ session,
258
+ rev,
259
+ ts: error.ts,
260
+ harnessVersion: error.harnessVersion,
261
+ tool: error.tool,
262
+ sig: signature(error.tool, error.line, redact),
263
+ example: cut(error.line, ERROR_EXAMPLE_CHARS),
264
+ toolUseId: error.toolUseId,
265
+ next: tools.slice(error.after, error.after + 2).map((tool) => tool.tool),
266
+ editFollowed: tools.slice(error.after, error.after + 6).some((tool) => EDITS.has(tool.tool)),
267
+ prompt: error.prompt,
268
+ cls: error.prompt === null ? null : (classOf.get(error.prompt) ?? null),
269
+ }));
270
+ return { prompts, errors };
271
+ };
272
+ export const extractSession = (input) => {
273
+ const capture = captureSession(input);
274
+ return { ...capture, ...deriveSession(capture, input.redact) };
275
+ };
276
+ /** An episode ends when the same call succeeds, the session ends, or this many calls pass. */
277
+ export const RECOVERY_WINDOW = 20;
278
+ const FAMILY = new Map([
279
+ ['Bash', 'shell'],
280
+ ['PowerShell', 'shell'],
281
+ ['BashOutput', 'shell'],
282
+ ['Edit', 'edit'],
283
+ ['Write', 'edit'],
284
+ ['MultiEdit', 'edit'],
285
+ ['NotebookEdit', 'edit'],
286
+ ['Read', 'read'],
287
+ ['Grep', 'read'],
288
+ ['Glob', 'read'],
289
+ ['LS', 'read'],
290
+ ['WebFetch', 'fetch'],
291
+ ['WebSearch', 'fetch'],
292
+ ]);
293
+ const READ_ONLY = new Set(['Read', 'Grep', 'Glob', 'LS', 'TodoWrite', 'ToolSearch', 'TaskList', 'TaskGet']);
294
+ /**
295
+ * The transcript's calls in order, each with its first result's status. A resumed session repeats earlier lines, so a repeated
296
+ * `toolUseId` is the same call and only the first copy counts; an error result whose call is not earlier in the file is a call of
297
+ * its own, named `?`.
298
+ */
299
+ const callsOf = (capture, redact) => {
300
+ const tools = [...capture.tools].sort(byPosition);
301
+ const firstError = new Map();
302
+ for (const error of capture.errorLines)
303
+ if (error.toolUseId !== null && !firstError.has(error.toolUseId))
304
+ firstError.set(error.toolUseId, error);
305
+ const seen = new Set();
306
+ const calls = [];
307
+ let pending = 0;
308
+ const flush = (before) => {
309
+ for (let error = capture.errorLines[pending]; error !== undefined && error.after <= before; error = capture.errorLines[pending]) {
310
+ pending += 1;
311
+ if (error.toolUseId === null || !seen.has(error.toolUseId))
312
+ calls.push({ tool: '?', target: '?', status: 'err', sig: signature('?', error.line, redact) });
313
+ }
314
+ };
315
+ for (const tool of tools) {
316
+ flush(tool.n);
317
+ if (tool.toolUseId !== null) {
318
+ if (seen.has(tool.toolUseId))
319
+ continue;
320
+ seen.add(tool.toolUseId);
321
+ }
322
+ const error = tool.toolUseId === null ? undefined : firstError.get(tool.toolUseId);
323
+ const status = tool.status === 'ok' ? 'ok' : tool.status === 'error' ? 'err' : null;
324
+ calls.push({ tool: tool.tool, target: tool.target ?? tool.tool, status, sig: status === 'err' && error !== undefined ? signature(tool.tool, error.line, redact) : null });
325
+ }
326
+ flush(Number.POSITIVE_INFINITY);
327
+ return calls;
328
+ };
329
+ const resolve = (calls, index, failed) => {
330
+ const window = calls.slice(index + 1, index + 1 + RECOVERY_WINDOW);
331
+ const fixed = window.findIndex((call) => call.status === 'ok' && call.tool === failed.tool && call.target === failed.target);
332
+ if (fixed >= 0)
333
+ return { outcome: 'fixed', calls: fixed + 1 };
334
+ const family = FAMILY.get(failed.tool) ?? failed.tool;
335
+ const around = window.findIndex((call) => {
336
+ if (call.status !== 'ok')
337
+ return false;
338
+ const callFamily = FAMILY.get(call.tool) ?? call.tool;
339
+ const sameTarget = call.target === failed.target && !READ_ONLY.has(call.tool);
340
+ const sameFamily = callFamily === family && (!READ_ONLY.has(call.tool) || family === 'read');
341
+ // A failed shell command that was writing a file (heredoc, quoting) is routinely replaced by Write.
342
+ return sameTarget || sameFamily || (family === 'shell' && callFamily === 'edit');
343
+ });
344
+ return around >= 0 ? { outcome: 'worked around', calls: around + 1 } : { outcome: 'abandoned', calls: window.length };
345
+ };
346
+ /** One episode per failed call of the transcript. */
347
+ export const episodesOf = (capture, redact) => {
348
+ const calls = callsOf(capture, redact);
349
+ const episodes = [];
350
+ for (const [index, call] of calls.entries()) {
351
+ if (call.status !== 'err' || call.sig === null)
352
+ continue;
353
+ episodes.push({ session: capture.session.session, sig: call.sig, ...resolve(calls, index, call) });
354
+ }
355
+ return episodes;
356
+ };
357
+ const median = (values) => {
358
+ const sorted = [...values].sort((a, b) => a - b);
359
+ const middle = Math.floor(sorted.length / 2);
360
+ return sorted.length % 2 === 1 ? (sorted[middle] ?? 0) : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
361
+ };
362
+ /** Signatures seen in two or more sessions, ranked by sessions times median recovery calls (recovery_episodes.py's order). */
363
+ export const rankRecovery = (episodes, top) => {
364
+ const bySig = new Map();
365
+ for (const episode of episodes)
366
+ bySig.set(episode.sig, [...(bySig.get(episode.sig) ?? []), episode]);
367
+ const ranked = [];
368
+ for (const [sig, list] of bySig) {
369
+ const sessions = new Set(list.map((episode) => episode.session)).size;
370
+ if (sessions < 2)
371
+ continue;
372
+ const medianCalls = median(list.map((episode) => episode.calls));
373
+ ranked.push({
374
+ sig,
375
+ sessions,
376
+ errors: list.length,
377
+ medianCalls,
378
+ score: sessions * medianCalls,
379
+ fixed: list.filter((episode) => episode.outcome === 'fixed').length,
380
+ workedAround: list.filter((episode) => episode.outcome === 'worked around').length,
381
+ abandoned: list.filter((episode) => episode.outcome === 'abandoned').length,
382
+ });
383
+ }
384
+ ranked.sort((a, b) => b.score - a.score || b.sessions - a.sessions || b.errors - a.errors || (a.sig < b.sig ? -1 : a.sig > b.sig ? 1 : 0));
385
+ return ranked.slice(0, top);
386
+ };
@@ -0,0 +1,87 @@
1
+ import type { Outcome } from '../cli.js';
2
+ import { type SessionsRuntime } from './store.js';
3
+ export interface ExclusionPaths {
4
+ /** One term per line, matched without regard to case. */
5
+ terms: string;
6
+ /** Where `oars sessions exclusions` writes the excluded sessions. */
7
+ list: string;
8
+ }
9
+ /** `OARS_EXCLUSION_TERMS` names the term file; the default sits beside the claude config's other local-only session-commons files. */
10
+ export declare const exclusionPaths: (projectsRoot: string, env: NodeJS.ProcessEnv, out?: string) => ExclusionPaths;
11
+ /** The terms, lowercased; a missing, unreadable, or undecodable file gives none, which excludes everything. */
12
+ export declare const loadTerms: (file: string) => Promise<string[]>;
13
+ type Source = 'projects' | 'raw';
14
+ export type Reason = 'clean' | 'term' | 'no-terms' | 'no-raw' | 'unreadable';
15
+ export interface Verdict {
16
+ excluded: boolean;
17
+ reason: Reason;
18
+ /** The files whose lines held a term. */
19
+ hits: {
20
+ key: string;
21
+ main: boolean;
22
+ source: Source;
23
+ }[];
24
+ }
25
+ /** The transcript files of one session, the main one first, one per path under the project folder (the harness's copy before oars' raw archive's): what a raw copy is built from, after the gate has cleared them. */
26
+ export declare const transcriptFilesOf: (runtime: SessionsRuntime, project: string, session: string) => Promise<{
27
+ key: string;
28
+ path: string;
29
+ main: boolean;
30
+ }[]>;
31
+ /**
32
+ * When the newest file of a session was last written, in epoch milliseconds, over the same files the gate scans (the main transcript, its subagents, and oars' raw copies), or 0 when it has none.
33
+ * A transcript that kept growing since the last capture is newer here than in the store, so the deposit command reads it to tell a session still in progress from one that has ended.
34
+ */
35
+ export declare const lastWritten: (runtime: SessionsRuntime, project: string, session: string) => Promise<number>;
36
+ /** The A38 rule for one session; `extra` adds terms to the file's, but never stands in for it, so a missing or empty term file still excludes everything. */
37
+ export declare const checkSession: (runtime: SessionsRuntime, termsFile: string, project: string, session: string, extra?: readonly string[]) => Promise<Verdict>;
38
+ interface SourceHits {
39
+ main: number;
40
+ subagent: number;
41
+ }
42
+ export interface FolderCounts {
43
+ folder: number;
44
+ sessions: number;
45
+ excluded: number;
46
+ /** Files in this folder that held a term, per source. */
47
+ hits: {
48
+ projects: SourceHits;
49
+ raw: SourceHits;
50
+ };
51
+ }
52
+ export interface ExclusionCounts {
53
+ sessions: number;
54
+ excluded: number;
55
+ kept: number;
56
+ reasons: {
57
+ term: number;
58
+ noRaw: number;
59
+ noTerms: number;
60
+ unreadable: number;
61
+ };
62
+ /** Directories in a project folder that are not sessions (no main transcript, and a name that neither is a session id nor ends in `.jsonl`); in neither the list nor the sessions. */
63
+ notSessions: number;
64
+ /** Distinct transcript files that held a term, across both sources. */
65
+ hitFiles: SourceHits;
66
+ /** The same count per source, because a file in both is counted in each, and the distinct sessions with a hit in that source. */
67
+ bySource: {
68
+ projects: SourceHits & {
69
+ sessions: number;
70
+ };
71
+ raw: SourceHits & {
72
+ sessions: number;
73
+ };
74
+ };
75
+ folders: FolderCounts[];
76
+ }
77
+ /** Scans every session in the harness's folders, the raw archive, and the store's own log. Returns counts and, apart from them, the excluded sessions. */
78
+ export declare const scanStore: (runtime: SessionsRuntime, termsFile: string) => Promise<{
79
+ counts: ExclusionCounts;
80
+ excluded: {
81
+ project: string;
82
+ session: string;
83
+ }[];
84
+ }>;
85
+ /** `oars sessions exclusions`: writes the excluded sessions to a local file and prints counts only. */
86
+ export declare const sessionsExclusions: (runtime: SessionsRuntime, where: ExclusionPaths) => Promise<Outcome>;
87
+ export {};