@manyoars/oars 0.3.0 → 0.4.1

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 (161) hide show
  1. package/README.md +44 -29
  2. package/dist/cli.d.ts +4 -1
  3. package/dist/cli.js +102 -32
  4. package/dist/commands/login.d.ts +1 -1
  5. package/dist/commands/login.js +1 -1
  6. package/dist/gate/hook-main.js +13 -1
  7. package/dist/gate/hook-process.d.ts +11 -1
  8. package/dist/gate/hook-process.js +50 -2
  9. package/dist/gate/hook-record.d.ts +3 -0
  10. package/dist/gate/hook-record.js +8 -0
  11. package/dist/gate/hook.d.ts +2 -0
  12. package/dist/gate/hook.js +3 -0
  13. package/dist/gate/replay.js +2 -5
  14. package/dist/gate/report.js +1 -1
  15. package/dist/gate/transcripts.js +1 -1
  16. package/dist/hook-bin.js +27 -2
  17. package/dist/issuer.d.ts +7 -9
  18. package/dist/issuer.js +10 -32
  19. package/dist/main.js +3 -0
  20. package/dist/sessions/actions.js +78 -0
  21. package/dist/sessions/allow.d.ts +10 -1
  22. package/dist/sessions/allow.js +29 -9
  23. package/dist/sessions/baseline.d.ts +1 -1
  24. package/dist/sessions/baseline.js +6 -4
  25. package/dist/sessions/call-shape.d.ts +19 -0
  26. package/dist/sessions/call-shape.js +85 -0
  27. package/dist/sessions/cell-increments.d.ts +5 -1
  28. package/dist/sessions/cell-increments.js +84 -50
  29. package/dist/sessions/cells.d.ts +33 -3
  30. package/dist/sessions/cells.js +89 -9
  31. package/dist/sessions/codex.js +93 -27
  32. package/dist/sessions/command.d.ts +11 -1
  33. package/dist/sessions/command.js +64 -18
  34. package/dist/sessions/coverage.d.ts +19 -0
  35. package/dist/sessions/coverage.js +56 -0
  36. package/dist/sessions/cursor.js +2 -2
  37. package/dist/sessions/derive.js +72 -6
  38. package/dist/sessions/exclusion.d.ts +6 -0
  39. package/dist/sessions/exclusion.js +8 -0
  40. package/dist/sessions/extract.d.ts +4 -2
  41. package/dist/sessions/extract.js +58 -26
  42. package/dist/sessions/hook-calls.js +1 -1
  43. package/dist/sessions/hook-index.d.ts +14 -1
  44. package/dist/sessions/hook-index.js +10 -3
  45. package/dist/sessions/hooks.d.ts +5 -1
  46. package/dist/sessions/hooks.js +133 -22
  47. package/dist/sessions/identity.d.ts +8 -3
  48. package/dist/sessions/identity.js +45 -15
  49. package/dist/sessions/install.d.ts +18 -2
  50. package/dist/sessions/install.js +53 -8
  51. package/dist/sessions/mcp.d.ts +3 -2
  52. package/dist/sessions/mcp.js +8 -4
  53. package/dist/sessions/motif.d.ts +2 -1
  54. package/dist/sessions/motif.js +2 -2
  55. package/dist/sessions/opencode.d.ts +33 -0
  56. package/dist/sessions/opencode.js +319 -0
  57. package/dist/sessions/otel.d.ts +60 -0
  58. package/dist/sessions/otel.js +494 -0
  59. package/dist/sessions/package-books.d.ts +48 -0
  60. package/dist/sessions/package-books.js +120 -0
  61. package/dist/sessions/package-resolve.d.ts +2 -0
  62. package/dist/sessions/package-resolve.js +3 -1
  63. package/dist/sessions/pi.d.ts +36 -0
  64. package/dist/sessions/pi.js +353 -0
  65. package/dist/sessions/pool-books.d.ts +21 -0
  66. package/dist/sessions/pool-books.js +75 -0
  67. package/dist/sessions/pool-choices.d.ts +5 -0
  68. package/dist/sessions/pool-choices.js +11 -0
  69. package/dist/sessions/pool-command.d.ts +76 -5
  70. package/dist/sessions/pool-command.js +263 -136
  71. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  72. package/dist/sessions/pool-nightly-deps.js +11 -0
  73. package/dist/sessions/pool-nightly.d.ts +10 -0
  74. package/dist/sessions/pool-nightly.js +85 -0
  75. package/dist/sessions/pool-outbox.d.ts +15 -2
  76. package/dist/sessions/pool-outbox.js +79 -26
  77. package/dist/sessions/pool-print.d.ts +1 -1
  78. package/dist/sessions/pool-raw.d.ts +27 -0
  79. package/dist/sessions/pool-raw.js +68 -0
  80. package/dist/sessions/pool-replica.d.ts +35 -0
  81. package/dist/sessions/pool-replica.js +91 -0
  82. package/dist/sessions/pool-send-lock.d.ts +20 -0
  83. package/dist/sessions/pool-send-lock.js +87 -0
  84. package/dist/sessions/pool-sent.d.ts +4 -0
  85. package/dist/sessions/pool-sent.js +26 -0
  86. package/dist/sessions/pool-totals.d.ts +10 -0
  87. package/dist/sessions/pool-totals.js +21 -0
  88. package/dist/sessions/pool-wire.d.ts +240 -30
  89. package/dist/sessions/pool-wire.js +695 -215
  90. package/dist/sessions/public-vocabulary.d.ts +2 -3
  91. package/dist/sessions/public-vocabulary.js +2 -9
  92. package/dist/sessions/query.d.ts +16 -4
  93. package/dist/sessions/query.js +32 -10
  94. package/dist/sessions/read-cost.js +14 -3
  95. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  96. package/dist/sessions/read-recovery-edge.js +26 -0
  97. package/dist/sessions/read.d.ts +8 -1
  98. package/dist/sessions/read.js +6 -3
  99. package/dist/sessions/recall-bundle.d.ts +12 -0
  100. package/dist/sessions/recall-bundle.js +33 -0
  101. package/dist/sessions/recall-frames.d.ts +18 -0
  102. package/dist/sessions/recall-frames.js +44 -0
  103. package/dist/sessions/recall-next.d.ts +145 -0
  104. package/dist/sessions/recall-next.js +241 -0
  105. package/dist/sessions/recall-object.d.ts +2 -0
  106. package/dist/sessions/recall-object.js +1 -0
  107. package/dist/sessions/recall-push.d.ts +2 -2
  108. package/dist/sessions/recall-push.js +3 -51
  109. package/dist/sessions/recall-reads.d.ts +7 -0
  110. package/dist/sessions/recall-reads.js +7 -0
  111. package/dist/sessions/recovery-calls.d.ts +21 -0
  112. package/dist/sessions/recovery-calls.js +25 -0
  113. package/dist/sessions/redact.d.ts +11 -0
  114. package/dist/sessions/redact.js +59 -0
  115. package/dist/sessions/schema.d.ts +59 -9
  116. package/dist/sessions/schema.js +22 -3
  117. package/dist/sessions/session-shape.d.ts +6 -3
  118. package/dist/sessions/session-shape.js +19 -6
  119. package/dist/sessions/sources.js +75 -1
  120. package/dist/sessions/store.d.ts +13 -1
  121. package/dist/sessions/store.js +36 -16
  122. package/dist/sessions/survival.d.ts +2 -0
  123. package/dist/sessions/survival.js +2 -2
  124. package/dist/sessions/transcript-tail.d.ts +14 -0
  125. package/dist/sessions/transcript-tail.js +64 -0
  126. package/dist/sessions/vscode-copilot.js +24 -7
  127. package/dist/telemetry/command.d.ts +8 -0
  128. package/dist/telemetry/command.js +85 -0
  129. package/dist/telemetry/consent.d.ts +17 -0
  130. package/dist/telemetry/consent.js +63 -0
  131. package/dist/telemetry/crash.d.ts +8 -0
  132. package/dist/telemetry/crash.js +52 -0
  133. package/dist/telemetry/emit.d.ts +31 -0
  134. package/dist/telemetry/emit.js +42 -0
  135. package/dist/telemetry/fields.d.ts +16 -0
  136. package/dist/telemetry/fields.js +26 -0
  137. package/dist/telemetry/health.d.ts +21 -0
  138. package/dist/telemetry/health.js +108 -0
  139. package/dist/telemetry/hook-emit.d.ts +4 -0
  140. package/dist/telemetry/hook-emit.js +18 -0
  141. package/dist/telemetry/measures.d.ts +32 -0
  142. package/dist/telemetry/measures.js +128 -0
  143. package/dist/telemetry/observe.d.ts +13 -0
  144. package/dist/telemetry/observe.js +51 -0
  145. package/dist/telemetry/read-events.d.ts +9 -0
  146. package/dist/telemetry/read-events.js +17 -0
  147. package/dist/telemetry/record.d.ts +17 -0
  148. package/dist/telemetry/record.js +26 -0
  149. package/dist/telemetry/report.d.ts +77 -0
  150. package/dist/telemetry/report.js +188 -0
  151. package/dist/telemetry/schema.d.ts +40 -0
  152. package/dist/telemetry/schema.js +187 -0
  153. package/dist/telemetry/send-main.d.ts +1 -0
  154. package/dist/telemetry/send-main.js +11 -0
  155. package/dist/telemetry/send.d.ts +36 -0
  156. package/dist/telemetry/send.js +172 -0
  157. package/dist/telemetry/spool.d.ts +17 -0
  158. package/dist/telemetry/spool.js +70 -0
  159. package/dist/telemetry/wire.d.ts +25 -0
  160. package/dist/telemetry/wire.js +76 -0
  161. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @manyoars/oars
2
2
 
3
- The `oars` command line for Many Oars: sign in, manage API keys, and keep a local memory of your own Claude Code sessions (`oars sessions`).
3
+ The `oars` command line for Many Oars: sign in, manage API keys, and keep a local memory of your own coding-agent sessions (`oars sessions`).
4
4
 
5
- Accounts are not open yet; `oars login` says so until launch.
5
+ Accounts are not open yet: `oars login` uses the Many Oars issuer by default, and until Many Oars sign-in at `login.manyoars.com` is deployed it exits 2 with an error that names `login.manyoars.com`.
6
6
 
7
7
  Node 24 or later. ESM only.
8
8
 
@@ -34,7 +34,7 @@ These flags work on every command:
34
34
  Signs in with your browser (RFC 8628 device flow) and stores the login.
35
35
 
36
36
  - `--no-browser` prints the link and code without opening a browser.
37
- - `--issuer-url <url>` is the Keycloak realm URL. The default is `OARS_ISSUER_URL`. With neither, `login` uses the built-in Many Oars issuer once Many Oars has launched, which it learns from `https://manyoars.com/.well-known/oars.json` (`{"launched": true}`). The flag only switches the built-in issuer on; it never supplies a URL. Before launch, or when the built-in issuer does not answer discovery, with no `--issuer-url` or `OARS_ISSUER_URL`, the command prints "Many Oars has not launched yet: pass --issuer-url or set OARS_ISSUER_URL." and exits 2.
37
+ - `--issuer-url <url>` is the Keycloak realm URL. The default is `OARS_ISSUER_URL`. With neither, `login` uses the built-in Many Oars issuer, `https://login.manyoars.com/realms/aor`; nothing has to be switched on first. When the built-in issuer does not answer discovery (it may not be deployed yet), with no `--issuer-url` or `OARS_ISSUER_URL`, the command prints "Cannot reach Many Oars sign-in at login.manyoars.com (it may not be deployed yet). Pass --issuer-url or set OARS_ISSUER_URL to use another issuer." and exits 2.
38
38
 
39
39
  ### logout
40
40
 
@@ -44,24 +44,37 @@ Revokes the stored login and deletes it.
44
44
 
45
45
  `oars keys` manages the API keys of your account.
46
46
 
47
- - `oars keys create --name <name>` creates a key and prints its secret once; the platform keeps no copy. The default authorities are `AGENT_OPERATE` and `AGENT_READ`; `--authority <name>` (repeatable) sets others, and `--expires-in-days <n>` sets an expiry. A key expires after 90 days unless `--expires-in-days <n>` (at most 365) says otherwise; replace it before then.
47
+ - `oars keys create --name <name>` creates a key and prints its secret once; the platform keeps no copy. The default authorities are `AGENT_OPERATE` and `AGENT_READ`; `--authority <name>` (repeatable) sets others, and `--expires-in-days <n>` sets an expiry. `--expires-in-days` takes 1 to 365; without it the platform sets the expiry, and `oars keys list` shows it. Replace a key before it expires.
48
48
  - `--workload <id>` limits the key to one workload you created. Such a key may carry only `AGENT_OPERATE` and `AGENT_READ` (more is a 400, `API_KEY_WORKLOAD_SCOPE_EXCEEDS_CAP`), and it works on that workload only.
49
49
  - A retry with the same `--idempotency-key` returns no secret: the command exits 5 and names the key, and you revoke it and create another.
50
50
  - Creating or revoking a key needs a signed-in person. With `OARS_API_KEY` set, any API key gets 403 `HUMAN_SESSION_REQUIRED` (exit 3) and logging in again does not help: unset it and use the `oars login` session.
51
51
  - `oars keys list` lists the keys with their prefix, scope, and expiry. A secret never comes back.
52
52
  - `oars keys revoke <keyId>` ends a key at once. There is no `rotate`: revoke the key and create another.
53
53
 
54
+ ### telemetry
55
+
56
+ oars records how it is used, on this machine, and says so: on the first run that is not a hook it prints a notice on stderr, once. Each command leaves an event in a local spool under `<config>/oars/telemetry/`: the command path, the names of the flags given (never their values), the duration, the exit code, a closed error class, the oars version, the OS, and the time. So does each fire of a hook (the bare `oars gate hook` included, written after its answer is printed: which hook, how long, hit, miss, held, or error, the bytes it printed, and the harness name and version and the model name when the hook saw them), each recall (how it was asked and which read families it served, as counts), each deposit pass or sent batch (sessions counted, batches, bytes, and how many sessions each gate kept out, never which), and each fetch (an ETag hit, a cap hit). Five more kinds of event are recorded, each only counts, sizes, and closed words: once a night, a `store-health` snapshot of the session store (counts and sizes, and the lists of harness versions and model names it holds) and a `run-cost` event (time and peak memory); a `crash` event when oars fails in a way it did not plan for (the error's class and a hash of its stack, never its message); and, for each `oars pool deposit` pass, one `wire` event per harness (which fields its batches held and how many bytes each took) and, with `--send`, a `wire-send` event (the HTTP status, the pool's refusal code, and the size). Nothing else is recorded: no session content, prompt text, file path, URL, or argument. Every field of every event, with an example, is in [docs/telemetry.md](https://github.com/manyoars/oars/blob/main/docs/telemetry.md). oars sends the events in batches to the pool server in `OARS_POOL_URL` if you set one, else to the Many Oars pool (`https://pool.manyoars.com`), which is live. A per-install random id (not derived from your machine or account) is in each event.
57
+
58
+ - `oars telemetry status` says whether it is on, which switch decided that, whether the notice was shown, and how large the spool is.
59
+ - `oars telemetry off` stops recording and sending; `oars telemetry on` resumes it and prints the notice again.
60
+ - `oars telemetry show` prints the spool exactly as stored, oldest first.
61
+ - `oars telemetry report` prints your own numbers from the spool (hook latency percentiles, hit rates, and store health over the last 7 and 30 days) with no server and no account, and the fleet's percentile beside each when `OARS_POOL_URL` names a server that serves it.
62
+ - `OARS_TELEMETRY=0` or `DO_NOT_TRACK=1` turns it off for the process, and wins over `oars telemetry on`.
63
+
64
+ Every event type, with an example, is in [docs/telemetry.md](https://github.com/manyoars/oars/blob/main/docs/telemetry.md).
65
+
54
66
  ### sessions
55
67
 
56
- `oars sessions` keeps a local, deterministic memory of your own Claude Code sessions. oars makes no model call and no network call, except `gold arms`, which sends redacted text through your own Claude subscription.
68
+ `oars sessions` keeps a local, deterministic memory of your own coding-agent sessions. The session commands make no model call and no network call, except `gold arms`, which sends redacted text through your own Claude subscription. oars also records telemetry events (a command's path, its flag names, how long it took, its exit code, and its error class, with a random install id, the oars version, the OS, and the time; never session content, prompts, paths, URLs, or argument values), which oars sends in batches to the pool server in `OARS_POOL_URL`, else to the Many Oars pool (live) ([what telemetry records and how to turn it off](https://github.com/manyoars/oars/blob/main/docs/telemetry.md)).
57
69
 
58
- What it captures: the Claude Code sessions in the folders you name with `--allow`, and, by name, the sessions of Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, and VS Code Copilot Chat (`--allow cursor`, `codex`, `copilot`, `gemini`, `grok`, `vscode-copilot`). Where it stores them: `<config>/oars/sessions/` on your machine (`<config>` is `%APPDATA%` on Windows, `$XDG_CONFIG_HOME` or `~/.config` elsewhere). The `raw/` folder in it is on by default and holds raw transcripts (a hard link or copy of each deposited one). Secret masking applies to the log, not to `raw/`: those files are unredacted and keep whatever the transcript held, secrets included. They stay on your machine and, apart from `gold arms`, nothing is uploaded. To remove them, see [Remove everything](#remove-everything).
70
+ What it captures: by default, the sessions of every Claude Code project folder and the sessions of Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, OpenCode, Pi, and VS Code Copilot Chat, plus OpenTelemetry GenAI agent traces (OTLP JSON files) from the folder `OARS_OTEL_TRACES` names (else `<config>/oars/otel-traces`), when there is one. `--allow` narrows that to the folders and stores you name (`--allow cursor`, `codex`, `copilot`, `gemini`, `grok`, `opencode`, `pi`, `vscode-copilot`, `otel` name the stores), and `--deny` keeps one out. Where it stores them: `<config>/oars/sessions/` on your machine (`<config>` is `%APPDATA%` on Windows, `$XDG_CONFIG_HOME` or `~/.config` elsewhere). The `raw/` folder in it is on by default and holds raw transcripts (a hard link or copy of each deposited one). Secret masking applies to the log, not to `raw/`: those files are unredacted and keep whatever the transcript held, secrets included. They stay on your machine: apart from `gold arms`, no session command uploads them, telemetry carries nothing from them, and after a yes to the raw prompt (`oars pool raw`) and covenant acceptance, the nightly task uploads a redacted copy of each (never the file itself), except from a folder set to skeleton only or off. To remove them, see [Remove everything](#remove-everything).
59
71
 
60
- Apart from that, oars itself sends nothing; the hooks and the MCP tool add recall text to the running session, and your harness sends that to its model like any other context.
72
+ Apart from that and telemetry (on by default, off with `oars telemetry off`, `OARS_TELEMETRY=0`, or `DO_NOT_TRACK=1`; it sends to the pool server in `OARS_POOL_URL`, else to the Many Oars pool at `pool.manyoars.com`, which is live), oars itself sends nothing, except what you opted into with a pool server: after you accept its covenant, the nightly task (registered on Windows only) sends closed-vocabulary session skeletons, and, only after a separate yes to the raw prompt, a locally redacted (best-effort) copy of each full transcript; `oars pool outbox` prints what is queued when you run it, but the nightly task builds new batches and sends them in the same run without printing them first, and a folder can be switched to skeleton only or off; the hooks and the MCP tool add recall text to the running session, and your harness sends that to its model like any other context.
61
73
 
62
74
  ```bash
63
- # Read the folders you name (a path, or its name under ~/.claude/projects). At least one --allow is required; there is no "all".
64
- oars sessions deposit --allow ~/work/app --allow ~/work/tools --redact-domain corp.example
75
+ # Read every folder; or only the ones you name (a path, or its name under ~/.claude/projects); --deny keeps one out.
76
+ oars sessions deposit --redact-domain corp.example
77
+ oars sessions deposit --allow ~/work/app --allow ~/work/tools --deny ~/work/tools/secret
65
78
 
66
79
  # Look up an error as printed, a file, or a prompt class.
67
80
  oars sessions recall --error "unexpected EOF while looking for matching \`''"
@@ -83,7 +96,7 @@ oars sessions read --family lead-lag # order 3: a labeled experiment beh
83
96
  oars sessions read --family fit --slice model=claude-opus-5-5 --json
84
97
  oars sessions books --by class --since 30 # tokens, wall time, turns, errors, verify-after-edit rate, and the outcome signals
85
98
  oars sessions books --by pull # the same per package, MCP server, plugin, or skill a prompt pulled
86
- oars sessions books --by harness # the same, grouped by Claude Code, Cursor, Codex, Copilot, Gemini, Grok Build, and VS Code Copilot Chat (deposit the others with --allow cursor, --allow codex, --allow copilot, --allow gemini, --allow grok, or --allow vscode-copilot)
99
+ oars sessions books --by harness # the same, grouped by Claude Code, Cursor, Codex, Copilot, Gemini, Grok Build, OpenCode, Pi, and VS Code Copilot Chat (deposit the others with --allow cursor, --allow codex, --allow copilot, --allow gemini, --allow grok, --allow opencode, --allow pi, --allow vscode-copilot, or --allow otel)
87
100
  oars sessions measure --last 50 # recurring errors per 100 sessions, cache-write per session, recall calls per session
88
101
  oars sessions measure --last 50 --repo <root> # the same, for one repository's sessions
89
102
  oars sessions verify # consistency invariants on the stored skeletons and capture records
@@ -99,7 +112,7 @@ before a line is written (redaction runs on the full text, then the text is cut)
99
112
  text, never raw content. It keeps these text fields and no others: the first 300 characters of each prompt, the first informative line of a
100
113
  tool error (cut to 1,000 characters in the error-line record, 160 characters in a derived example), TLDR lines (400) and Confidence lines (120) from assistant text, WebSearch queries, fetched URLs, file
101
114
  paths, and git branch names (the last four have no length limit), all redacted first; plus the project folder slug (not redacted, so it
102
- carries your username and directory names; the log stays local), skill names, effort values, model names, harness versions, tool names (including MCP server names), a shell call's program and subcommand, and tool-input key names. A tool call's file target is a salted hash, never the path. Apart from `gold arms`, nothing is uploaded.
115
+ carries your username and directory names; the log stays local), skill names, effort values, model names, harness versions, tool names (including MCP server names), a shell call's program and subcommand, and tool-input key names. A tool call's file target is a salted hash, never the path. Apart from `gold arms`, no `oars sessions` command uploads anything, and telemetry carries none of these fields; `oars pool` and the nightly task send skeletons that carry some of these names after redaction (skill, effort, model, harness version, tool, and MCP server names, a shell call's program, subcommand, and flag names, tool-input key names, a pulled package's name (and its version only when a public list holds it), and a web host (sent as it is after redaction; the pool hashes it on arrival), each after redaction; see `docs/pool.md`, "Wire field register").
103
116
  Subagent transcripts (`<project>/<session>/subagents/`) are deposited with their parent session and subagent type; `books --by class`
104
117
  shows their tokens as the `delegated` column, and their errors are in the signatures, `measure`, and recall under the session that
105
118
  spawned them, so per-session counts still count main sessions. Every deposited transcript is also
@@ -109,25 +122,26 @@ field, and a Merkle root over the raw lines) that `verify` checks; the skeleton
109
122
  per hundred sessions and, on a second line, the distinct recurring signatures in the window.
110
123
  #### Many folders: `--allow` prefixes, globs, and `--allow-file`
111
124
 
112
- An `--allow` entry is a folder (or its slug), a folder prefix with a trailing slash (`~/work/` reads that folder and every project under it), or a glob (`*` any run of characters, `?` one character, matched against the project-folder name Claude Code derives from the path). `--allow-file <list>` reads entries from a file, one per line (blank lines and `#` comments skipped), so hundreds of folders need no batching and no long command line. A leading `~` is expanded to your home folder, in a list file or quoted entry too. Both repeat and combine. Entries a deposit saves are what the SessionEnd hook and the nightly task reuse; a folder you have never named is never read.
125
+ An `--allow` entry is a folder (or its slug), a folder prefix with a trailing slash (`~/work/` reads that folder and every project under it), or a glob (`*` any run of characters, `?` one character, matched against the project-folder name Claude Code derives from the path). `--allow-file <list>` reads entries from a file, one per line (blank lines and `#` comments skipped), so hundreds of folders need no batching and no long command line. A leading `~` is expanded to your home folder, in a list file or quoted entry too. Both repeat and combine. With neither, a deposit reads every folder. `--deny <entry>` (or `!<entry>` in a list file) keeps the folders it names out, in any of those forms; it is saved, and it wins over every `--allow` until that exact entry is named with `--allow` again. Naming it again lifts the denial and nothing else when the saved list names no folder (every folder stays in scope); when the saved list names folders, the entry joins them. Entries a deposit saves are what the SessionEnd hook, the nightly task, `oars pool deposit`, and `oars gate replay` reuse; with none saved, they read every folder. An `--allow-file` that names nothing is refused.
113
126
 
114
127
  #### Recall in a running session: hooks
115
128
 
116
- `oars sessions hook install` writes four entries into one Claude Code settings file. Each is a deterministic, local, fail-open call into the store (no model, no network); an error inside a hook exits 0 with no output, so a hook never blocks or fails a session.
129
+ `oars sessions hook install` writes five entries into one Claude Code settings file. Each is a deterministic, local, fail-open call into the store (no model, no network); an error inside a hook exits 0 with no output, so a hook never blocks or fails a session.
117
130
 
118
- | Event | Does | Budget |
131
+ | Event | Does | Timeout written at install |
119
132
  |---|---|---|
120
- | `SessionStart` | injects `oars sessions recall --context` for the session's cwd as additional context: the top 5 errors by sessions within the latest 300 sessions of the repository (the `measure --last 300 --repo` window) and what usually followed | under 500 ms |
121
- | `PreToolUse` (no matcher: every tool) | for a call whose tool and input key names match a shape the store has seen fail often enough, and at least twice as often as its action does, injects its counts as additional context, once per session and shape (the check reads the last 64 KB of the recall log, about 44 rows, so a very long session can be told again, and the arm redraws when the nightly index moves), and records the recall. 10 percent of those sessions and shapes are held back: a held arm records what it would have shown and injects nothing. Starts node per call | under 500 ms |
122
- | `PostToolUseFailure` (matcher `Bash\|PowerShell`) | for a failed tool call whose error starts with `Exit code N` normalizes the first informative line as `deposit` does and, on a hit, adds "seen in N sessions; what followed: X". A shell pre-filter in the registered command reads the event first and starts node only when it carries `Exit code` | pre-filter: shell builtins only, no node start; whole path under 1 s |
123
- | `SessionEnd` | deposits that one transcript (idempotent by size and mtime), only from a folder an earlier `--allow` named. Once per session: `Stop` fires after every turn and would append a new revision of the growing transcript each time. A session the harness ends without `SessionEnd` is picked up by the nightly deposit | under 5 s |
133
+ | `SessionStart` | injects `oars sessions recall --context` for the session's cwd as additional context: the top 5 errors by sessions within the latest 300 sessions of the repository (the `measure --last 300 --repo` window) and what usually followed. Then, when any position qualifies, a table of the positions where earlier sessions split on the next step (a leading step at 20 to 80 percent of at least 20 observations; names and counts only, at most 600 bytes), from the kept pool bundle for the session's harness and version first and this store's pool second (the local half is derived over that harness and version bucket only, or over the harness's versions when the transcript states none). The table is one `recalls.jsonl` row and falls under the 10 percent holdback | under 500 ms |
134
+ | `UserPromptSubmit` | the same table again, only when it differs from the one this session was last sent (shown or held), so an unchanged table costs a prompt nothing (the check reads the last 64 KB of the recall log, shared by every session, so in a long or parallel session the last table row can fall out of the window and an unchanged table is sent again) | under 500 ms |
135
+ | `PreToolUse` (no matcher: every tool) | the gate hook's role: it fires after the call is decided, so it is a warning and does not claim to move the next step. For a call whose tool and input key names match a shape that fails at least half its calls (of at least 20 calls in at least 3 sessions), and at least twice as often as its action does, injects its counts as additional context, once per session and shape (the check reads the last 64 KB of the recall log, about 44 rows, so a very long session can be told again, and the arm redraws when the nightly index moves), and records the recall. 10 percent of those sessions and shapes are held back: a held arm records what it would have shown and injects nothing. Starts node per call | under 500 ms |
136
+ | `PostToolUseFailure` (matcher: every tool the action table maps, and `mcp__.*`) | the recovery edge: for a failed call it reads what followed this failure (same action and error class) in the kept pool bundle, else in this store, and adds "after a failed X (code), the next step was Y xN". It stays silent when the top next step is the failed action again (a retry), when a local edge has fewer than 5 followed steps, and when nothing followed. A call the cells do not cover keeps the older line, "seen in N sessions; what followed: X", under the same holdback, silent on a strict retry lead. Every served or held line is a `recalls.jsonl` row; 10 percent of sessions are held. For Bash and PowerShell a shell pre-filter in the registered command reads the event first and starts node only when it carries `Exit code` | pre-filter: shell builtins only, no node start; whole path under 1 s |
137
+ | `SessionEnd` | deposits that one transcript (idempotent by size and mtime), only from a folder the saved allowlist holds (every folder until a deposit names some, less any `--deny`). Once per session: `Stop` fires after every turn and would append a new revision of the growing transcript each time. A session the harness ends without `SessionEnd` is picked up by the nightly deposit | under 5 s |
124
138
 
125
- Claude Code fires `PostToolUse` on success only; a failed Bash call arrives as `PostToolUseFailure`, so that is the event registered. The pre-filter is a `read`, a `case`, and a `printf | node` in the shell the harness runs hooks in (Git Bash on Windows, where a fresh `node` alone costs 250 ms or more), so it needs no `findstr` and no extra dependency. Writers of the store (`SessionEnd`, the nightly run, `deposit`) take `store.lock`, so parallel sessions cannot lose each other's `state.json` entries; a lock older than ten minutes is broken.
139
+ Claude Code fires `PostToolUse` on success only; a failed Bash call arrives as `PostToolUseFailure`, so that is the event registered. The pre-filter is a `read`, a `case`, and a `printf | node` in the shell the harness runs hooks in (Git Bash on Windows), so it needs no `findstr` and no extra dependency. Writers of the store (`SessionEnd`, the nightly run, `deposit`) take `store.lock`, so parallel sessions cannot lose each other's `state.json` entries; a lock older than ten minutes is broken.
126
140
 
127
141
  ```bash
128
- # Nothing is written unless you name the file (default --scope file), or say --scope user|project.
142
+ # The default is the user settings file; --scope project or --file names another.
143
+ oars sessions hook install # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR)
129
144
  oars sessions hook install --scope file --file ./.claude/settings.local.json
130
- oars sessions hook install --scope user # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR)
131
145
  oars sessions hook install --scope project # ./.claude/settings.json
132
146
  oars sessions hook install --file my-settings.json --keep-retention
133
147
  oars sessions hook status # user, project, and the nightly task
@@ -138,7 +152,7 @@ Install merges: existing hooks (a Stop hook that rotates sessions, say) stay, ev
138
152
 
139
153
  #### Nightly schedule (Windows)
140
154
 
141
- `oars sessions hook install --schedule nightly` registers the Task Scheduler task `OarsSessionsNightly` (daily, 03:00) that runs the full deposit over the saved `--allow` and `--redact-domain` entries and refreshes the recurrence index the hooks read. `--schedule off` removes it; `hook status` shows whether it is registered. Alone, `--schedule` writes no settings file.
155
+ On Windows, `oars sessions hook install` registers the Task Scheduler task `OarsSessionsNightly` (daily, 03:00) unless it is already there; `--schedule nightly` registers it by itself, with no settings file. The task runs the full deposit over the saved `--allow` and `--redact-domain` entries and refreshes the recurrence index the hooks read. `--schedule off` removes it and records the choice in the store (`<config>/oars/sessions/schedule.json`), so a later plain install leaves it off until `--schedule nightly` registers it again and clears the record; `hook status` shows whether it is registered. A machine that refuses the task still gets the hooks, and the install says how to retry. Alone, `--schedule` writes no settings file.
142
156
 
143
157
  #### Reading `measure` before and after
144
158
 
@@ -146,30 +160,31 @@ Install merges: existing hooks (a Stop hook that rotates sessions, say) stay, ev
146
160
 
147
161
  #### Remove everything
148
162
 
149
- `oars sessions hook uninstall --scope user` (or the scope you installed); `oars sessions hook install --schedule off`; delete the store folder `<config>/oars/sessions/` (observations, skeletons, state, the hook index, `hook-calls.jsonl`, and `raw/`, which holds the unredacted transcripts). `gold sample` also writes its prompt file and sidecar to `~/.claude/session-commons/review/` (or `$CLAUDE_CONFIG_DIR/session-commons/review/`, or your `--out-dir`); delete those too. Nothing else is created. `cleanupPeriodDays` stays at the value install set unless you change it.
163
+ `oars sessions hook uninstall --scope user` (or the scope you installed); `oars sessions hook install --schedule off`; delete the store folder `<config>/oars/sessions/` (observations, skeletons, state, the hook index, `hook-calls.jsonl`, and `raw/`, which holds the unredacted transcripts). `gold sample` also writes its prompt file and sidecar to `~/.claude/session-commons/review/` (or `$CLAUDE_CONFIG_DIR/session-commons/review/`, or your `--out-dir`); delete those too. oars also writes `<config>/oars/telemetry/` (the telemetry spool and install id; delete it too) and, after `oars login`, `<config>/oars/credentials.json` (`oars logout` deletes it). `cleanupPeriodDays` stays at the value install set unless you change it.
150
164
 
151
165
  ### Gate: what your agents picked
152
166
 
153
167
  ```
154
- oars gate replay --allow ~/work/app # find installs, fetch-and-runs, MCP servers, and skills in logs on disk
155
- oars gate replay --registry-state # also ask npm or pip what the registry holds (off by default)
168
+ oars gate replay # find installs, fetch-and-runs, MCP servers, and skills in logs on disk, and ask npm or pip what the registry holds
169
+ oars gate replay --no-registry-state # the same, without asking the registry
156
170
  oars gate report # counts over crossings.jsonl, by who recorded each, and what replay cannot see
157
171
  oars gate install # add the hook to Claude Code at user scope, so crossings are recorded as they happen
158
172
  oars gate uninstall # remove only that hook entry
159
173
  oars gate policy # print the policy in force (warn, block, or off per class)
160
174
  ```
161
175
 
162
- Replay is local and read-only on your transcripts; it writes `crossings.jsonl` and the novelty index `gate-index.txt` into the sessions store and nothing else (with `--registry-state`, npm and pip also keep their own caches). The hook (`oars gate hook`, run by Claude Code, not by you) appends to `crossings.jsonl` as each crossing happens, plus `gate-index.txt`, `gate-registry.jsonl`, and the empty `gate-ask-*.lock` files in the same store (a registry ask the hook had to cut is finished by a helper process that ends on its own timeouts, and the package manager keeps its own cache outside the store), and always exits 0. With no policy file it adds a note about a name first published in the last 30 days or an npm name the registry does not hold; `gate-policy.json` changes that per class, and a block needs `block` written by you. A block is a labeled deny, and a crossing goes nowhere unless you opt in (no opt-in exists yet). `report` reads only `crossings.jsonl`. Both record picks and neither judges them; a note or label states a fact about the registry and nothing else. See `docs/gate.md` in the repository.
176
+ Replay is local and read-only on your transcripts; it writes `crossings.jsonl` and the novelty index `gate-index.txt` into the sessions store and nothing else (npm and pip also keep their own caches, unless `--no-registry-state`). The hook (`oars gate hook`, run by Claude Code, not by you) appends to `crossings.jsonl` as each crossing happens, plus `gate-index.txt`, `gate-registry.jsonl`, and the empty `gate-ask-*.lock` files in the same store (a registry ask the hook had to cut is finished by a helper process that ends on its own timeouts, and the package manager keeps its own cache outside the store), and always exits 0. With no policy file it adds a note about a name first published in the last 30 days or an npm name the registry does not hold; `gate-policy.json` changes that per class, and a block needs `block` written by you. A block is a labeled deny, and a crossing goes nowhere unless you opt in (no opt-in exists yet). `report` reads only `crossings.jsonl`. Both record picks and neither judges them; a note or label states a fact about the registry and nothing else. See `docs/gate.md` in the repository.
163
177
 
164
178
  ### pool
165
179
 
166
- `oars pool` talks to a pool server that you name with `--server <url>` or `OARS_POOL_URL`; there is no default server. Every other command works without it.
180
+ `oars pool` uses the Many Oars pool, `https://pool.manyoars.com`, which is live (deposits there are closed until its covenant is published), or a pool server you name with `--server <url>` or `OARS_POOL_URL`. Every other command works without it.
167
181
 
168
182
  - `oars pool login --provider github|google` signs in by the provider's device flow, run here, and prints every request before it sends it. `OARS_GITHUB_CLIENT_ID` (or `OARS_GOOGLE_CLIENT_ID` and `OARS_GOOGLE_CLIENT_SECRET`) must be set; an unset one stops the command before anything is sent and names the variable.
169
- - `oars pool deposit` writes a batch of closed-vocabulary session skeletons to an outbox for each session that passes your folder allowlist and the exclusion check, and stops. Nothing is sent: `--send <hash>` sends no request. A batch that was shown and is unchanged stops with `sending_disabled` (exit 5), and any other hash stops with its own refusal.
183
+ - `oars pool deposit` writes a batch of session skeletons (a closed list of fields, each value redacted first; a host is hashed by the pool on arrival) to an outbox for each session that passes your folder allowlist and the exclusion check, and stops: it sends nothing itself. `--send <hash>` sends one batch that `outbox` printed and that is unchanged on disk, once this account has accepted the pool covenant (`--accept-covenant <version>`, or an earlier acceptance); before that it reads the covenant, sends no batch, and exits 3 with `covenant_not_accepted`. A hash that was never shown, or a batch that changed, is refused before any request. The nightly task is the other sender: after the pool says your account accepted its covenant, it sends new batches, and raw copies only if you said yes to the raw prompt (`oars pool raw --consent yes`). With no sign-in, no acceptance, or an unreachable server it sends nothing.
170
184
  - `deposit`, `outbox`, and `fetch` need `oars pool login` first; without it they exit 3 and name the server.
185
+ - `oars pool coverage [--org-size <band>] [--repo-visibility public|private|unknown]` sets the two coverage bands only you can state, read locally and never from the network; an unset band goes on the wire as `unknown`.
171
186
  - `oars pool outbox` prints each batch exactly as it would be sent, with its SHA-256, and `--explain` adds each field's rule.
172
- - `oars pool fetch --harness-version <version>` fetches that version's signed bundle, verifies it against the key pinned at login, and keeps it locally.
187
+ - `oars pool fetch --harness-version <version>` fetches that version's signed bundle, verifies it against the key pinned at login, and keeps it locally. It prints the public and disclosure floors the bundle states (and any read family on a floor of its own) and holds no floor of its own: the pool sets them, so a change is a deploy and never a new release of oars.
173
188
 
174
189
  ## The `--json` contract
175
190
 
package/dist/cli.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { CredentialStore } from './credentials.js';
2
2
  import { ExitCode } from './errors.js';
3
3
  import type { InstallRuntime } from './sessions/install.js';
4
4
  import type { SessionsRuntime } from './sessions/store.js';
5
+ import type { TelemetryRuntime } from './telemetry/record.js';
5
6
  /** Everything the CLI takes from its process, so the tests can stand in for each. */
6
7
  export interface RunDeps {
7
8
  env: NodeJS.ProcessEnv;
@@ -22,11 +23,13 @@ export interface RunDeps {
22
23
  sessions?: SessionsRuntime;
23
24
  /** Where `oars sessions hook` finds the hook script, the settings files, and schtasks; absent in a run that has none. */
24
25
  install?: InstallRuntime;
26
+ /** Where telemetry keeps its id, settings, and spool; absent in a run that has none, which records nothing. */
27
+ telemetry?: TelemetryRuntime | undefined;
25
28
  }
26
29
  /** What a command produced: the `--json` document, and the line a human sees instead. */
27
30
  export interface Outcome {
28
31
  document: object;
29
32
  text: string;
30
33
  }
31
- /** Runs one invocation (`argv` without the node and script entries) and returns the exit code. */
34
+ /** Runs one invocation (`argv` without the node and script entries) and returns the exit code. Telemetry records it afterward and cannot change it. */
32
35
  export declare const run: (argv: string[], deps: RunDeps) => Promise<ExitCode>;
package/dist/cli.js CHANGED
@@ -12,7 +12,11 @@ import { gateInstall, gatePolicy, gateReplay, gateReport, gateUninstall } from '
12
12
  import { DEADLINE_MS, gateHook } from './gate/hook.js';
13
13
  import { managerReader } from './gate/registry.js';
14
14
  import { exclusionPaths, sessionsExclusions } from './sessions/exclusion.js';
15
- import { poolDeposit, poolFetch, poolLogin, poolOutbox } from './sessions/pool-command.js';
15
+ import { poolCoverage, poolDeposit, poolFetch, poolLogin, poolOutbox, poolRaw } from './sessions/pool-command.js';
16
+ import { telemetryOff, telemetryOn, telemetryReport, telemetryShow, telemetryStatus } from './telemetry/command.js';
17
+ import { recordCrash } from './telemetry/crash.js';
18
+ import { emitOf } from './telemetry/emit.js';
19
+ import { finishTelemetry, observeCommand } from './telemetry/observe.js';
16
20
  const flag = (options, name) => options[name] === true;
17
21
  const text = (options, name) => {
18
22
  const value = options[name];
@@ -55,21 +59,24 @@ const printFailure = (deps, json, error) => {
55
59
  const { title, detail, actionUrl } = error.problem;
56
60
  deps.io.stderr(`${[title ?? error.message, detail, actionUrl].filter((line) => line !== undefined && line !== '').join('\n')}\n`);
57
61
  };
58
- const sessionDeps = (deps) => ({ sessions: deps.sessions, install: deps.install, now: deps.now });
59
- const buildProgram = (deps) => {
62
+ const sessionDeps = (deps) => ({ sessions: deps.sessions, install: deps.install, now: deps.now, telemetry: emitOf(deps) });
63
+ const buildProgram = (deps, observed) => {
60
64
  const program = new Command('oars')
61
- .description('Many Oars: sign in, manage API keys, and keep a local, searchable record of your own Claude Code sessions')
65
+ .description('Many Oars: sign in, manage API keys, and keep a local, searchable record of your own coding-agent sessions')
62
66
  .version(deps.version)
63
67
  .exitOverride()
64
68
  .configureOutput({ writeOut: deps.io.stdout, writeErr: deps.io.stderr })
65
69
  .option('--json', 'print one JSON document on stdout instead of text')
66
70
  .option('--api-url <url>', 'the platform API (default: OARS_API_URL, else https://api.manyoars.com)')
67
- .option('--idempotency-key <key>', "the base of the idempotency key (default: a fresh UUID); rerun a failed command with the same key to replay it");
71
+ .option('--idempotency-key <key>', "the base of the idempotency key (default: a fresh UUID); rerun a failed command with the same key to replay it")
72
+ .hook('preAction', (_program, action) => {
73
+ observeCommand(observed, action);
74
+ });
68
75
  program
69
76
  .command('login')
70
77
  .description('sign in with your browser (RFC 8628 device flow)')
71
78
  .option('--no-browser', 'print the link and code without opening a browser')
72
- .option('--issuer-url <url>', 'the Keycloak realm URL (default: OARS_ISSUER_URL, else Many Oars once it has launched)')
79
+ .option('--issuer-url <url>', 'the Keycloak realm URL (default: OARS_ISSUER_URL, else the Many Oars issuer)')
73
80
  .action(async (_options, command) => {
74
81
  const options = command.optsWithGlobals();
75
82
  const outcome = await login(deps, {
@@ -122,16 +129,17 @@ const buildProgram = (deps) => {
122
129
  const options = command.optsWithGlobals();
123
130
  emit(deps, flag(options, 'json'), await keysRevoke(deps, { apiUrl: resolveApiUrl(deps, text(options, 'apiUrl')), keyId }));
124
131
  });
125
- const sessions = program.command('sessions').description("deposit and recall your own Claude Code session logs, locally (no model calls, no network, except `gold arms`, which asks your own Claude subscription)");
132
+ const sessions = program.command('sessions').description("deposit and recall your own coding-agent session logs, locally (these commands make no model call and no network call, except `gold arms`, which asks your own Claude subscription; telemetry is separate: oars telemetry)");
126
133
  sessions
127
134
  .command('deposit')
128
- .description('read session logs from the folders you name into the local observation log; idempotent. Subagent transcripts under each session (<project>/<session>/subagents/) are deposited with their parent, and each raw transcript is hard-linked into the oars archive beside the store')
129
- .option('--allow <folder>', 'a project folder to read: its path, or its name under ~/.claude/projects (repeatable); `cursor`, `codex`, `copilot`, `gemini`, `grok`, and `vscode-copilot` name the Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, and VS Code Copilot Chat stores. A trailing slash names everything under a folder; * and ? are wildcards', collect)
130
- .option('--allow-file <list>', 'a file of --allow entries, one per line (# starts a comment); repeatable, combines with --allow', collect)
135
+ .description('read session logs into the local observation log, from every folder unless --allow names some or --deny keeps some out; idempotent. Subagent transcripts under each session (<project>/<session>/subagents/) are deposited with their parent, and each raw transcript is hard-linked or copied into the oars archive beside the store')
136
+ .option('--allow <folder>', 'a project folder to read: its path, or its name under ~/.claude/projects (repeatable); `cursor`, `codex`, `copilot`, `gemini`, `grok`, `opencode`, `pi`, `vscode-copilot`, and `otel` name the Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, OpenCode, Pi, VS Code Copilot Chat, and OpenTelemetry GenAI trace stores. A trailing slash names everything under a folder; * and ? are wildcards', collect)
137
+ .option('--allow-file <list>', 'a file of --allow entries, one per line (# starts a comment, and !<entry> keeps a folder out); repeatable, combines with --allow', collect)
138
+ .option('--deny <folder>', 'a folder never to read, in any form --allow takes (repeatable); saved, and it wins over every --allow until that exact entry is named with --allow', collect)
131
139
  .option('--redact-domain <domain>', 'an organization domain to mask in everything written (repeatable)', collect)
132
140
  .action(async (_options, command) => {
133
141
  const options = command.optsWithGlobals();
134
- emit(deps, flag(options, 'json'), await sessionsDeposit(sessionDeps(deps), { allow: list(options, 'allow'), allowFiles: list(options, 'allowFile'), redactDomains: list(options, 'redactDomain') }));
142
+ emit(deps, flag(options, 'json'), await sessionsDeposit(sessionDeps(deps), { allow: list(options, 'allow'), allowFiles: list(options, 'allowFile'), deny: list(options, 'deny'), redactDomains: list(options, 'redactDomain') }));
135
143
  });
136
144
  sessions
137
145
  .command('recall')
@@ -212,10 +220,10 @@ const buildProgram = (deps) => {
212
220
  });
213
221
  sessions
214
222
  .command('books')
215
- .description('tokens, wall time, turns, errors, verify-after-edit rate, and the outcome signals (verify passed, stops, redos, commits, and commit survival) per prompt class, model, harness, or pull')
216
- .option('--by <key>', 'class, model, harness, or pull (default class)', (value) => {
217
- if (value !== 'class' && value !== 'model' && value !== 'harness' && value !== 'pull')
218
- throw new InvalidArgumentError('it must be class, model, harness, or pull.');
223
+ .description('tokens, wall time, turns, errors, verify-after-edit rate, and the outcome signals (verify passed, stops, redos, commits, and commit survival) per prompt class, model, harness, or pull; with --by package, the public-package counts a field report prints, exclusions applied')
224
+ .option('--by <key>', 'class, model, harness, pull, or package (default class)', (value) => {
225
+ if (value !== 'class' && value !== 'model' && value !== 'harness' && value !== 'pull' && value !== 'package')
226
+ throw new InvalidArgumentError('it must be class, model, harness, pull, or package.');
219
227
  return value;
220
228
  })
221
229
  .option('--since <days>', 'only the last N days', positiveInteger)
@@ -223,8 +231,9 @@ const buildProgram = (deps) => {
223
231
  .action(async (_options, command) => {
224
232
  const options = command.optsWithGlobals();
225
233
  const picked = text(options, 'by');
226
- const by = picked === 'model' || picked === 'harness' || picked === 'pull' ? picked : 'class';
227
- emit(deps, flag(options, 'json'), await sessionsBooks(sessionDeps(deps), { by, sinceDays: count(options, 'since'), repo: text(options, 'repo') }));
234
+ const by = picked === 'model' || picked === 'harness' || picked === 'pull' || picked === 'package' ? picked : 'class';
235
+ const termsFile = deps.sessions === undefined ? undefined : exclusionPaths(deps.sessions.projectsRoot, deps.env).terms;
236
+ emit(deps, flag(options, 'json'), await sessionsBooks(sessionDeps(deps), { by, sinceDays: count(options, 'since'), repo: text(options, 'repo'), termsFile }));
228
237
  });
229
238
  sessions
230
239
  .command('measure')
@@ -313,11 +322,11 @@ const buildProgram = (deps) => {
313
322
  };
314
323
  hook
315
324
  .command('install')
316
- .description('merge the SessionStart, PostToolUseFailure, and SessionEnd hooks into a settings file (existing hooks are kept) and raise cleanupPeriodDays to at least 365')
317
- .option('--scope <scope>', 'user (~/.claude/settings.json), project (./.claude/settings.json), or file (needs --file; the default)', scopeOption)
325
+ .description('merge the SessionStart, UserPromptSubmit, PreToolUse, PostToolUseFailure, and SessionEnd hooks into a settings file (existing hooks are kept), raise cleanupPeriodDays to at least 365, and on Windows register the nightly task (which sends telemetry while telemetry is on, and pool batches only after you accept a pool covenant)')
326
+ .option('--scope <scope>', 'user (~/.claude/settings.json; the default), project (./.claude/settings.json), or file (needs --file; implied by --file)', scopeOption)
318
327
  .option('--file <path>', 'the settings file for --scope file')
319
328
  .option('--keep-retention', 'leave cleanupPeriodDays as it is (the harness deletes raw transcripts older than it)')
320
- .option('--schedule <mode>', 'nightly: register a Windows task that runs the full deposit and the recurrence refresh daily; off: remove it', (value) => {
329
+ .option('--schedule <mode>', 'nightly: register a Windows task that runs the full deposit and the recurrence refresh daily (a plain install on Windows does this too); off: remove it', (value) => {
321
330
  if (value !== 'nightly' && value !== 'off')
322
331
  throw new InvalidArgumentError('it must be nightly or off.');
323
332
  return value;
@@ -335,7 +344,7 @@ const buildProgram = (deps) => {
335
344
  hook
336
345
  .command('uninstall')
337
346
  .description('remove only the hooks `install` wrote from a settings file (cleanupPeriodDays stays; `install --schedule off` removes the task)')
338
- .option('--scope <scope>', 'user, project, or file (needs --file; the default)', scopeOption)
347
+ .option('--scope <scope>', 'user (the default), project, or file (needs --file; implied by --file)', scopeOption)
339
348
  .option('--file <path>', 'the settings file for --scope file')
340
349
  .action(async (_options, command) => {
341
350
  const options = command.optsWithGlobals();
@@ -352,9 +361,9 @@ const buildProgram = (deps) => {
352
361
  const gate = program.command('gate').description('record the install, fetch-and-run, MCP, and skill picks your agents make, live through a hook or from logs already on disk, without judging them');
353
362
  gate
354
363
  .command('replay')
355
- .description('find the crossings in your session logs and write them to crossings.jsonl in the sessions store; reads nothing outside the folders you name, and no network unless --registry-state')
356
- .option('--allow <folder>', 'a project folder to read, as for `oars sessions deposit` (repeatable); default: the allowlist a deposit saved', collect)
357
- .option('--registry-state', 'ask the package manager what the registry holds for each package named (npm view, pip index versions); off by default, and the manager keeps its own cache')
364
+ .description('find the crossings in your session logs and write them to crossings.jsonl in the sessions store; reads nothing outside the allowlist, and asks the package manager what the registry holds unless --no-registry-state')
365
+ .option('--allow <folder>', 'a project folder to read, as for `oars sessions deposit` (repeatable); default: the allowlist a deposit saved, else every folder', collect)
366
+ .option('--no-registry-state', 'do not ask the package manager what the registry holds for each package named (npm view, pip index versions; asked by default, as the live hook does, and the manager keeps its own cache)')
358
367
  .action(async (_options, command) => {
359
368
  const options = command.optsWithGlobals();
360
369
  emit(deps, flag(options, 'json'), await gateReplay(sessionDeps(deps), { allow: list(options, 'allow'), ...(flag(options, 'registryState') ? { registry: managerReader() } : {}) }));
@@ -398,9 +407,9 @@ const buildProgram = (deps) => {
398
407
  .action(async (_options, command) => {
399
408
  emit(deps, flag(command.optsWithGlobals(), 'json'), await gateReport(sessionDeps(deps)));
400
409
  });
401
- const pool = program.command('pool').description('sign in to a pool server, deposit closed-vocabulary session skeletons, and fetch signed reads; local capture, recall, and reads need none of it');
410
+ const pool = program.command('pool').description('sign in to a pool server, deposit session skeletons (a closed list of fields, each value redacted first) and, after a yes to the raw prompt, redacted raw copies, and fetch signed reads; local capture, recall, and reads need none of it');
402
411
  const serverOption = '--server <url>';
403
- const serverHelp = 'the pool server, https or plain http to localhost (or OARS_POOL_URL; there is no default)';
412
+ const serverHelp = 'the pool server, https or plain http to localhost (default: OARS_POOL_URL, else https://pool.manyoars.com)';
404
413
  pool
405
414
  .command('login')
406
415
  .description('sign in by GitHub or Google device flow, run here against the provider; prints each request before sending it, and the pool keeps a pseudonym; no harness token is read')
@@ -412,14 +421,33 @@ const buildProgram = (deps) => {
412
421
  });
413
422
  pool
414
423
  .command('deposit')
415
- .description('write a batch to the outbox for each session that passes the folder allowlist and the exclusion check, and stop: nothing is sent, and only counts print. --send <hash> runs the checks that a batch was printed and is unchanged, then stops: sending is disabled in this release')
416
- .option('--accept-covenant <version>', 'accept this version of the pool covenant before sending (with --send, which is disabled in this release)')
424
+ .description('write a batch to the outbox for each session that passes the folder allowlist and the exclusion check, and stop: nothing is sent, and only counts print. --send <hash> sends one batch that oars pool outbox printed and that is unchanged, once this account has accepted the pool covenant')
425
+ .option('--accept-covenant <version>', 'accept this version of the pool covenant before sending (with --send)')
417
426
  .option('--send <hash>', 'the SHA-256 of a batch that oars pool outbox printed')
418
427
  .option(serverOption, serverHelp)
419
428
  .action(async (_options, command) => {
420
429
  const options = command.optsWithGlobals();
421
430
  emit(deps, flag(options, 'json'), await poolDeposit(deps, { server: text(options, 'server'), acceptCovenant: text(options, 'acceptCovenant'), send: text(options, 'send') }));
422
431
  });
432
+ pool
433
+ .command('raw')
434
+ .description('the raw prompt: whether a redacted copy of each full transcript may leave besides its skeleton (--consent yes|no), and what one folder may send (--folder <folder> --mode all|skeleton|off); with no option, shows the prompt and your answer')
435
+ .option('--consent <answer>', 'yes or no; a no, or no answer, sends skeletons only')
436
+ .option('--folder <entry>', 'a folder, folder prefix, or glob, as --allow takes it')
437
+ .option('--mode <mode>', 'all, skeleton, or off for that folder: on by default after you accept; skeleton sends no raw; off sends nothing from that folder to the pool from now on')
438
+ .action(async (_options, command) => {
439
+ const options = command.optsWithGlobals();
440
+ emit(deps, flag(options, 'json'), await poolRaw(deps, { consent: text(options, 'consent'), folder: text(options, 'folder'), mode: text(options, 'mode') }));
441
+ });
442
+ pool
443
+ .command('coverage')
444
+ .description('state whom your deposits cover, kept on this machine and never looked up: --org-size solo, 2-10, 11-100, 101-1000, over-1000, or unknown; --repo-visibility public, private, or unknown. With no flag it prints what is set')
445
+ .option('--org-size <band>', 'how many people your organization has')
446
+ .option('--repo-visibility <visibility>', 'whether your repositories are public or private')
447
+ .action(async (_options, command) => {
448
+ const options = command.optsWithGlobals();
449
+ emit(deps, flag(options, 'json'), await poolCoverage(deps, { orgSize: text(options, 'orgSize'), repoVisibility: text(options, 'repoVisibility') }));
450
+ });
423
451
  pool
424
452
  .command('outbox')
425
453
  .description('print every batch exactly as it would be sent, request line and headers included, with the credential as its size; prints each batch SHA-256 and records that it was shown')
@@ -433,12 +461,44 @@ const buildProgram = (deps) => {
433
461
  .command('fetch')
434
462
  .description('fetch the signed bundle for one harness version, verify it against the key pinned at login, and keep it locally')
435
463
  .option('--harness <name>', 'the harness whose bundle to fetch (default: the one your local store holds the most sessions of, claude-code when none leads)')
436
- .requiredOption('--harness-version <version>', 'the public harness version whose bundle to fetch')
464
+ .requiredOption('--harness-version <version>', 'the public harness version whose bundle to fetch, or unknown for a session that recorded none (Cursor, Gemini, Grok Build, Pi, or a VS Code Copilot Chat session whose requests name no agent version; there is no bundle for otel)')
437
465
  .option(serverOption, serverHelp)
438
466
  .action(async (_options, command) => {
439
467
  const options = command.optsWithGlobals();
440
468
  emit(deps, flag(options, 'json'), await poolFetch(deps, { server: text(options, 'server'), harness: text(options, 'harness'), harnessVersion: text(options, 'harnessVersion') ?? '' }));
441
469
  });
470
+ const telemetry = program.command('telemetry').description('what oars records about its own use, on this machine (docs/telemetry.md): status, on, off, show, and report');
471
+ const telemetryJson = (command) => flag(command.optsWithGlobals(), 'json');
472
+ telemetry
473
+ .command('status')
474
+ .description('whether telemetry is on, which switch decided it, whether the notice was shown, and the size of the spool')
475
+ .action((_options, command) => {
476
+ emit(deps, telemetryJson(command), telemetryStatus(deps));
477
+ });
478
+ telemetry
479
+ .command('on')
480
+ .description('turn telemetry on, and print what it records (OARS_TELEMETRY=0 and DO_NOT_TRACK=1 still win)')
481
+ .action((_options, command) => {
482
+ emit(deps, telemetryJson(command), telemetryOn(deps));
483
+ });
484
+ telemetry
485
+ .command('off')
486
+ .description('turn telemetry off: nothing more is recorded or sent')
487
+ .action((_options, command) => {
488
+ emit(deps, telemetryJson(command), telemetryOff(deps));
489
+ });
490
+ telemetry
491
+ .command('show')
492
+ .description('print the spool exactly as stored: every event telemetry has kept, oldest first')
493
+ .action((_options, command) => {
494
+ emit(deps, telemetryJson(command), telemetryShow(deps));
495
+ });
496
+ telemetry
497
+ .command('report')
498
+ .description('your own hook latency, hit rates, and store health over the last 7 and 30 days, from the spool, beside the fleet\'s when OARS_POOL_URL names a pool server that answers')
499
+ .action(async (_options, command) => {
500
+ emit(deps, telemetryJson(command), await telemetryReport(deps));
501
+ });
442
502
  return program;
443
503
  };
444
504
  const asCliError = (error) => {
@@ -448,16 +508,18 @@ const asCliError = (error) => {
448
508
  return new CliError(error.message.replace(/^error: /, ''), ExitCode.USAGE, 'usage');
449
509
  return new CliError(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`, ExitCode.UNEXPECTED, 'unexpected');
450
510
  };
451
- /** Runs one invocation (`argv` without the node and script entries) and returns the exit code. */
452
- export const run = async (argv, deps) => {
511
+ const execute = async (argv, deps, observed) => {
453
512
  try {
454
- await buildProgram(deps).parseAsync(argv, { from: 'user' });
513
+ await buildProgram(deps, observed).parseAsync(argv, { from: 'user' });
455
514
  return ExitCode.OK;
456
515
  }
457
516
  catch (error) {
458
517
  // --help and --version end the parse through exitOverride with exit code 0; commander has already printed them.
459
518
  if (error instanceof CommanderError && error.exitCode === 0)
460
519
  return ExitCode.OK;
520
+ // A failure the program did not mean (not a CliError, not commander's own): its class and a hash of its stack are recorded, then it is handled as before.
521
+ if (!(error instanceof CliError) && !(error instanceof CommanderError))
522
+ recordCrash(deps, error);
461
523
  const failure = asCliError(error);
462
524
  // Commander wrote its own usage message to stderr; a CliError is ours to print.
463
525
  if (!(error instanceof CommanderError) || argv.includes('--json'))
@@ -465,3 +527,11 @@ export const run = async (argv, deps) => {
465
527
  return failure.exitCode;
466
528
  }
467
529
  };
530
+ /** Runs one invocation (`argv` without the node and script entries) and returns the exit code. Telemetry records it afterward and cannot change it. */
531
+ export const run = async (argv, deps) => {
532
+ const observed = { path: [], flags: [] };
533
+ const startedAt = deps.now();
534
+ const exit = await execute(argv, deps, observed);
535
+ finishTelemetry(deps, argv, observed, startedAt, exit);
536
+ return exit;
537
+ };
@@ -1,7 +1,7 @@
1
1
  import type { Outcome, RunDeps } from '../cli.js';
2
2
  export interface LoginOptions {
3
3
  apiUrl: string;
4
- /** `--issuer-url`; `OARS_ISSUER_URL` when absent, then the built-in production issuer once Many Oars has launched (issuer.ts). */
4
+ /** `--issuer-url`; `OARS_ISSUER_URL` when absent, then the built-in production issuer (issuer.ts). */
5
5
  issuerUrl?: string;
6
6
  /** False for `--no-browser`. */
7
7
  browser: boolean;
@@ -5,7 +5,7 @@ import { exchangeKeycloakToken } from '../session.js';
5
5
  const STEP = 'login';
6
6
  /** RFC 8628 device flow on Keycloak, then the exchange; stores the credentials. */
7
7
  export const login = async (deps, options) => {
8
- const issuerUrl = await resolveIssuerUrl(deps, options.issuerUrl);
8
+ const issuerUrl = resolveIssuerUrl(deps, options.issuerUrl);
9
9
  const config = await discoverIssuer(deps, options.issuerUrl, () => discover(issuerUrl, { ...deps, step: STEP }));
10
10
  const device = await startDeviceAuthorization(config, STEP);
11
11
  const url = device.verification_uri_complete ?? device.verification_uri;
@@ -7,8 +7,10 @@ import { performance } from 'node:perf_hooks';
7
7
  import { defaultConfigDir } from '../credentials.js';
8
8
  import { DEADLINE_MS } from './hook.js';
9
9
  import { isLocked, startFill, startHelper } from './hook-fill.js';
10
+ import { fireRecorder } from './hook-record.js';
10
11
  import { runHookProcess } from './hook-process.js';
11
12
  import { managerReader } from './registry.js';
13
+ import { hookEmitter } from '../telemetry/hook-emit.js';
12
14
  /** The process itself ends this long after `DEADLINE_MS`. */
13
15
  const GRACE_MS = 100;
14
16
  const readStdin = async () => {
@@ -32,4 +34,14 @@ const depsFor = (stage) => {
32
34
  stage,
33
35
  };
34
36
  };
35
- await runHookProcess({ stdin: readStdin, deps: depsFor, exit: (code) => process.exit(code), write: (text) => new Promise((done) => process.stdout.write(text, () => { done(); })), deadlineMs: DEADLINE_MS + GRACE_MS - performance.now() });
37
+ // One event per fire, recorded after the answer is written (`runHookProcess`), so it is never in front of the tool call. The version is read from `dist/gate/../../package.json`.
38
+ const emit = hookEmitter(process.env, process.platform, homedir(), new URL('../../package.json', import.meta.url), Date.now);
39
+ await runHookProcess({
40
+ stdin: readStdin,
41
+ deps: depsFor,
42
+ exit: (code) => process.exit(code),
43
+ write: (text) => new Promise((done) => process.stdout.write(text, () => { done(); })),
44
+ deadlineMs: DEADLINE_MS + GRACE_MS - performance.now(),
45
+ clock: () => performance.now(),
46
+ record: fireRecorder(emit),
47
+ });
@@ -1,4 +1,10 @@
1
1
  import { type GateHookDeps } from './hook.js';
2
+ /** What telemetry is told of one fire of `oars gate hook`: how long the handler took, how it ended, and the bytes it printed. */
3
+ export interface HookFire {
4
+ latencyMs: number;
5
+ outcome: 'hit' | 'miss' | 'held' | 'error';
6
+ bytes: number;
7
+ }
2
8
  export interface HookProcess {
3
9
  /** Reads the whole of stdin. Called inside the guard, so a stream that cannot be set up is a failed read, not a crash. */
4
10
  stdin: () => Promise<string>;
@@ -9,6 +15,10 @@ export interface HookProcess {
9
15
  write?: (text: string) => Promise<void>;
10
16
  /** Milliseconds from now until the process ends itself. */
11
17
  deadlineMs: number;
18
+ /** Told once how the fire went, with the transcript path the harness named (to read the frame from, never to record), after its output is written so recording is never in front of the answer. A throw here is swallowed: the exit code and stdout do not depend on it. */
19
+ record?: (fire: HookFire, transcript: string | undefined) => void | Promise<void>;
20
+ /** Milliseconds, only compared with itself: the latency in a fire is the difference across the handler. Absent: `Date.now`. */
21
+ clock?: () => number;
12
22
  }
13
23
  /** Resolves once `exit(0)` has been called, or never when the deadline timer is what ended the process. */
14
- export declare const runHookProcess: ({ stdin, deps, exit, write, deadlineMs }: HookProcess) => Promise<void>;
24
+ export declare const runHookProcess: ({ stdin, deps, exit, write, deadlineMs, record, clock }: HookProcess) => Promise<void>;