@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,254 @@
1
+ // `oars gate replay`: the crossing checks over logs already on disk. It reads raw transcripts under the allowlist and writes one file, `crossings.jsonl`.
2
+ // It never calls `localSalt` or the step log, both of which write, and it holds no store lock, since a replay touches nothing a deposit reads.
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { compileAllow, runAllow } from '../sessions/allow.js';
6
+ import { makeRedactor } from '../sessions/redact.js';
7
+ import { paths, readState, writeAtomic } from '../sessions/store.js';
8
+ import { GATE_VERSION, liveCrossings, mergeCrossings, parseRows } from './record.js';
9
+ import { askable } from './registry.js';
10
+ import { withLocalBin } from './local-bin.js';
11
+ import { ageOf, againstLatestOf, askedOf, DAY_MS } from './registry-state.js';
12
+ import { scopesOf, seedIndex } from './novelty.js';
13
+ import { repositoryOf } from './scope.js';
14
+ import { discover, readCalls, readOrigins } from './transcripts.js';
15
+ export const crossingsFile = (storeDir) => join(storeDir, 'crossings.jsonl');
16
+ export const REGISTRY_NOT_READ = 'registry state not read';
17
+ /** A failed install is looked at for this many calls after it. */
18
+ const NEXT_WINDOW = 10;
19
+ const REGISTRY_WORKERS = 4;
20
+ /** A kept registry answer older than this many days is asked again: a `latest` goes stale, and a row would otherwise read "as of this run" forever. */
21
+ export const REGISTRY_MAX_DAYS = 30;
22
+ const keyOf = (crossing) => `${crossing.class}\0${crossing.name}`;
23
+ const PACKAGE_CLASSES = new Set(['npm-package', 'python-package']);
24
+ const nextOf = (found, read) => {
25
+ if (found.status === 'ok')
26
+ return 'proceeded';
27
+ if (found.status === 'none')
28
+ return 'no-result';
29
+ const end = found.call + NEXT_WINDOW;
30
+ const { crossing } = found;
31
+ const later = read.found.find((one) => one.call > found.call && one.call <= end && one.crossing.class === crossing.class && (one.crossing.name === crossing.name || PACKAGE_CLASSES.has(crossing.class)));
32
+ if (read.pipes.some((call) => call > found.call && call <= end && (later === undefined || call < later.call)))
33
+ return 'routed-around';
34
+ if (later === undefined)
35
+ return 'stopped';
36
+ if (later.crossing.name !== crossing.name)
37
+ return 'changed-name';
38
+ return later.crossing.program === crossing.program && later.crossing.form === crossing.form ? 'retried' : 'routed-around';
39
+ };
40
+ const originsOf = async (transcript, read) => {
41
+ const firstLines = new Map();
42
+ const servers = new Set();
43
+ for (const { crossing, line } of read.found) {
44
+ if (!firstLines.has(crossing.name))
45
+ firstLines.set(crossing.name, line);
46
+ if (crossing.class === 'mcp-server')
47
+ servers.add(crossing.name);
48
+ }
49
+ return firstLines.size === 0 ? { origins: new Map(), prompted: new Set() } : readOrigins(transcript.file, firstLines, servers);
50
+ };
51
+ const byTime = (one, two) => {
52
+ if (one.ts === two.ts)
53
+ return 0;
54
+ if (one.ts === null)
55
+ return 1;
56
+ if (two.ts === null)
57
+ return -1;
58
+ return one.ts < two.ts ? -1 : 1;
59
+ };
60
+ /** The widest scope in which this is the first use, read in time order so a run's result does not depend on the order folders list in. */
61
+ const noveltyWalker = () => {
62
+ const fleet = new Set();
63
+ const repositories = new Set();
64
+ const sessions = new Set();
65
+ return (held) => {
66
+ const key = keyOf(held.found.crossing);
67
+ const inRepository = `${repositoryOf(held.transcript.project)}\0${key}`;
68
+ const inSession = `${held.transcript.session}\0${key}`;
69
+ let novelty = 'none';
70
+ if (!fleet.has(key))
71
+ novelty = 'fleet';
72
+ else if (!repositories.has(inRepository))
73
+ novelty = 'repository';
74
+ else if (!sessions.has(inSession))
75
+ novelty = 'session';
76
+ fleet.add(key);
77
+ repositories.add(inRepository);
78
+ sessions.add(inSession);
79
+ return novelty;
80
+ };
81
+ };
82
+ const lastRemovals = (removals) => {
83
+ const last = new Map();
84
+ for (const { removal, transcript } of removals) {
85
+ if (removal.status === 'error' || removal.status === 'refused' || removal.ts === null)
86
+ continue;
87
+ const key = `${repositoryOf(transcript.project)}\0${keyOf(removal.removal)}`;
88
+ if ((last.get(key) ?? '') < removal.ts)
89
+ last.set(key, removal.ts);
90
+ }
91
+ return last;
92
+ };
93
+ const asAnswer = (row) => ({ state: row.state, latest: row.latest, created: row.created });
94
+ const isFolder = (path) => stat(path).then((found) => found.isDirectory(), () => false);
95
+ /** A command, its environment, or an earlier `cd` chose that call's registry or config: the gate cannot read it from here, so the name is not asked, kept answers included. */
96
+ const unaskedKeys = (rows) => new Set(rows.filter(({ found }) => !found.crossing.registryAsked).map(({ found }) => keyOf(found.crossing)));
97
+ /** One question per name, from the first call whose folder still exists: the manager reads its registry from there, and a guess at another folder could ask the wrong one. */
98
+ const askRegistry = async (rows, reader, cached) => {
99
+ const unasked = unaskedKeys(rows);
100
+ const answers = new Map([...cached].filter(([key]) => !unasked.has(key)).map(([key, row]) => [key, asAnswer(row)]));
101
+ const folders = new Map();
102
+ const asks = new Map();
103
+ for (const { found } of rows) {
104
+ const { crossing, cwd } = found;
105
+ const key = keyOf(crossing);
106
+ if (cwd === null || unasked.has(key) || answers.has(key) || asks.has(key) || crossing.versionForm === 'url' || !askedOf(crossing) || !askable(crossing.class, crossing.name))
107
+ continue;
108
+ const exists = folders.get(cwd) ?? (await isFolder(cwd));
109
+ folders.set(cwd, exists);
110
+ if (exists)
111
+ asks.set(key, { class: crossing.class, name: crossing.name, cwd });
112
+ }
113
+ const queue = [...asks];
114
+ const worker = async () => {
115
+ for (let item = queue.shift(); item !== undefined; item = queue.shift()) {
116
+ const [key, ask] = item;
117
+ answers.set(key, await reader(ask).catch(() => ({ state: 'unreachable', latest: null, created: null })));
118
+ }
119
+ };
120
+ await Promise.all(Array.from({ length: REGISTRY_WORKERS }, worker));
121
+ return answers;
122
+ };
123
+ const recordOf = (held, novelty, removedAt, answers) => {
124
+ const { found, transcript, origin, prompted, next } = held;
125
+ const { crossing, ts, side } = found;
126
+ // An answer reaches a row only when that row's own program was asked and its command left the registry to the default: the same name can cross twice.
127
+ const answer = askedOf(crossing) ? answers?.get(keyOf(crossing)) : undefined;
128
+ return {
129
+ kind: 'crossing',
130
+ v: GATE_VERSION,
131
+ via: 'replay',
132
+ day: ts === null ? null : ts.slice(0, 10),
133
+ class: crossing.class,
134
+ form: crossing.form,
135
+ program: crossing.program,
136
+ versionForm: crossing.versionForm,
137
+ statusReports: crossing.statusReports,
138
+ origin,
139
+ novelty,
140
+ // The operator named it only on the main line: a subagent's prompt is another model's words.
141
+ decidedBy: prompted && !side ? 'operator' : 'agent',
142
+ permission: found.permission,
143
+ status: found.status,
144
+ next,
145
+ removedLater: removedAt !== undefined && ts !== null && ts < removedAt,
146
+ depth: side ? 'subagent' : 'main',
147
+ miss: found.miss,
148
+ againstLatest: answer === undefined ? 'not-read' : againstLatestOf(crossing, answer),
149
+ registryState: answer?.state ?? 'not-read',
150
+ registryAge: answer === undefined ? 'not-read' : ageOf(answer, ts),
151
+ arm: 'none',
152
+ lineShown: 'none',
153
+ lineHeld: 'none',
154
+ session: transcript.session,
155
+ agent: transcript.agent,
156
+ toolUseId: found.toolUseId,
157
+ ts,
158
+ name: crossing.name,
159
+ version: crossing.version,
160
+ };
161
+ };
162
+ const cachedAnswers = async (file, now) => {
163
+ const text = await readFile(file, 'utf8').catch(() => '');
164
+ return new Map(parseRows(text)
165
+ .filter((row) => row.kind === 'registry' && (now - Date.parse(row.day)) / DAY_MS <= REGISTRY_MAX_DAYS)
166
+ .map((row) => [keyOf(row), row]));
167
+ };
168
+ const registryRows = (answers, rows, cached, day) => {
169
+ const added = new Map();
170
+ const unasked = unaskedKeys(rows);
171
+ for (const { found } of rows) {
172
+ const key = keyOf(found.crossing);
173
+ const answer = answers.get(key);
174
+ // An unreachable registry says nothing about the name, so a later run asks again.
175
+ if (cached.has(key) || added.has(key) || answer === undefined || answer.state === 'unreachable')
176
+ continue;
177
+ added.set(key, { kind: 'registry', v: GATE_VERSION, day, class: found.crossing.class, state: answer.state, created: answer.created, name: found.crossing.name, latest: answer.latest });
178
+ }
179
+ return [...[...cached].filter(([key]) => !unasked.has(key)).map(([, row]) => row), ...added.values()];
180
+ };
181
+ const summaryText = (run, file) => [
182
+ `Read ${String(run.transcripts)} transcripts (${String(run.unreadable)} unreadable, ${String(run.badLines)} torn lines) and ${String(run.calls)} tool calls.`,
183
+ `Crossings: ${String(run.crossings)}. Written to ${file}.`,
184
+ run.registryRead ? 'Registry state read through the package manager, as of this run, not as of each crossing.' : REGISTRY_NOT_READ,
185
+ 'Counts: oars gate report',
186
+ ].join('\n');
187
+ const readAll = async (runtime, entries, domains) => {
188
+ const redact = makeRedactor(domains);
189
+ const { files, unreadable: unlisted } = await discover([runtime.projectsRoot, paths(runtime.storeDir).raw], compileAllow(entries));
190
+ const seen = new Map();
191
+ const reads = [];
192
+ let unreadable = unlisted;
193
+ for (const transcript of files) {
194
+ try {
195
+ reads.push({ transcript, read: await readCalls(transcript.file, transcript.agent, redact, seen) });
196
+ }
197
+ catch {
198
+ unreadable += 1;
199
+ }
200
+ }
201
+ // A folder's own bin is read once the whole record is in, so `nextOf` below sees the form the row will carry.
202
+ const memo = new Map();
203
+ for (const { read } of reads) {
204
+ for (const [at, found] of read.found.entries())
205
+ read.found[at] = { ...found, crossing: await withLocalBin(found.crossing, found.cwd, memo) };
206
+ }
207
+ return { reads, transcripts: files.length, unreadable };
208
+ };
209
+ export const replay = async (runtime, options) => {
210
+ const state = await readState(runtime.storeDir);
211
+ const entries = runAllow(state.allow, options.allow);
212
+ const { reads, transcripts, unreadable } = await readAll(runtime, entries, state.redactDomains);
213
+ const held = [];
214
+ for (const { transcript, read } of reads) {
215
+ const { origins, prompted } = await originsOf(transcript, read).catch(() => ({ origins: new Map(), prompted: new Set() }));
216
+ for (const found of read.found)
217
+ held.push({ found, transcript, origin: origins.get(found.crossing.name) ?? 'none-seen', prompted: prompted.has(found.crossing.name), next: nextOf(found, read) });
218
+ }
219
+ held.sort((one, two) => byTime(one.found, two.found));
220
+ const removedAt = lastRemovals(reads.flatMap(({ transcript, read }) => read.removals.map((removal) => ({ removal, transcript }))));
221
+ const file = crossingsFile(runtime.storeDir);
222
+ const cached = await cachedAnswers(file, options.now());
223
+ const answers = options.registry === undefined ? undefined : await askRegistry(held, options.registry, cached);
224
+ const novelty = noveltyWalker();
225
+ const records = held.map((one) => recordOf(one, novelty(one), removedAt.get(`${repositoryOf(one.transcript.project)}\0${keyOf(one.found.crossing)}`), answers));
226
+ const unread = {};
227
+ for (const { read } of reads) {
228
+ for (const shape of read.unread)
229
+ unread[shape] = (unread[shape] ?? 0) + 1;
230
+ }
231
+ const day = new Date(options.now()).toISOString().slice(0, 10);
232
+ const run = {
233
+ kind: 'run',
234
+ v: GATE_VERSION,
235
+ day,
236
+ transcripts,
237
+ unreadable,
238
+ badLines: reads.reduce((sum, { read }) => sum + read.badLines, 0),
239
+ calls: reads.reduce((sum, { read }) => sum + read.fresh, 0),
240
+ crossings: records.length,
241
+ unread,
242
+ registryRead: options.registry !== undefined,
243
+ };
244
+ const registry = registryRows(answers ?? new Map(), held, cached, day);
245
+ // The file is read again here, as late as it can be, so a row the hook appended while the transcripts were read is kept, not overwritten.
246
+ const live = liveCrossings(parseRows(await readFile(file, 'utf8').catch(() => '')));
247
+ await writeAtomic(file, `${[run, ...registry, ...mergeCrossings(records, live)].map((row) => JSON.stringify(row)).join('\n')}\n`);
248
+ // The hook reads novelty from a kept index, not from this file; every name a transcript showed is in it, in the scopes the hook names them in.
249
+ await seedIndex(runtime.storeDir, file, held.flatMap((one) => {
250
+ const scopes = scopesOf(keyOf(one.found.crossing), repositoryOf(one.transcript.project), one.transcript.session);
251
+ return [scopes.fleet, scopes.repository, scopes.session];
252
+ }));
253
+ return { run, file, text: summaryText(run, file) };
254
+ };
@@ -0,0 +1,50 @@
1
+ import { type Row } from './record.js';
2
+ export interface Tally {
3
+ value: string;
4
+ count: number;
5
+ }
6
+ export interface ClassCount {
7
+ class: string;
8
+ form: string;
9
+ count: number;
10
+ distinct: number;
11
+ }
12
+ export interface Report {
13
+ ok: true;
14
+ transcripts: number;
15
+ unreadable: number;
16
+ badLines: number;
17
+ calls: number;
18
+ crossings: number;
19
+ /** Whether a replay has run: when it has not, only the hook's rows are here and the transcript fields read `not-read`. */
20
+ replayed: boolean;
21
+ registryRead: boolean;
22
+ via: Tally[];
23
+ classes: ClassCount[];
24
+ origin: Tally[];
25
+ novelty: Tally[];
26
+ decidedBy: Tally[];
27
+ permission: Tally[];
28
+ status: Tally[];
29
+ next: Tally[];
30
+ miss: Tally[];
31
+ depth: Tally[];
32
+ againstLatest: Tally[];
33
+ registryState: Tally[];
34
+ registryAge: Tally[];
35
+ /** Whether a note that would show did (`shown`) or was held back (`held`); `none` is every row with no note to show, and every block. */
36
+ arm: Tally[];
37
+ /** The fixed id of the line a row showed. */
38
+ lineShown: Tally[];
39
+ /** The fixed id of the line a held note withheld. */
40
+ lineHeld: Tally[];
41
+ /** Which policy stood for the rows the hook wrote: no file, a file that fit, or one that did not (the defaults stood). Rows with no policy read (replay) are not counted. */
42
+ policy: Tally[];
43
+ removedLater: number;
44
+ unseen: Tally[];
45
+ limits: string[];
46
+ }
47
+ /** What a replay of logs on disk cannot see, said every time so a count is never read as the whole. */
48
+ export declare const LIMITS: readonly string[];
49
+ export declare const reportOf: (rows: readonly Row[]) => Report;
50
+ export declare const formatReport: (report: Report) => string;
@@ -0,0 +1,89 @@
1
+ // `oars gate report`: counts over the rows the hook and a replay wrote, each crossing once. No judgment word appears here; a count says what was seen, and `LIMITS` says what was not.
2
+ import { AGAINST_LATEST, ARMS, LINE_IDS, CROSSING_CLASSES, CROSSING_FORMS, DEPTHS, DECIDERS, MISSES, NEXTS, NOVELTIES, ORIGINS, PERMISSIONS, REGISTRY_AGES, REGISTRY_STATES, STATUSES, VIAS, crossingsOf, } from './record.js';
3
+ import { POLICY_SOURCES } from './policy.js';
4
+ import { REGISTRY_NOT_READ } from './replay.js';
5
+ /** What a replay of logs on disk cannot see, said every time so a count is never read as the whole. */
6
+ export const LIMITS = [
7
+ 'a command run from a script, an alias, or a Makefile target, because only the call the agent made is in the log',
8
+ 'a change to a manifest or lockfile that a later bare install picked up',
9
+ 'what a person saw or approved on screen; the permission mode is what the log kept',
10
+ 'an MCP server that was started and never called',
11
+ 'folders off the allowlist, and logs the harness has already cleaned up',
12
+ 'a session in Codex, Cursor, Copilot, Gemini CLI, Grok Build, OpenCode, Pi, VS Code Copilot Chat, or an OpenTelemetry GenAI trace, which replay does not read (`--allow codex`, `--allow cursor`, `--allow copilot`, `--allow gemini`, `--allow grok`, `--allow opencode`, `--allow pi`, `--allow vscode-copilot`, and `--allow otel` add nothing to it): they keep sessions in their own formats, not as transcript files',
13
+ 'pool reads, sign-in, and deposit prompts; this release reads none of them',
14
+ ];
15
+ const tally = (vocabulary, rows, pick) => vocabulary.map((value) => ({ value, count: rows.filter((row) => pick(row) === value).length }));
16
+ const classCounts = (rows) => CROSSING_CLASSES.flatMap((cls) => CROSSING_FORMS.flatMap((form) => {
17
+ const these = rows.filter((row) => row.class === cls && row.form === form);
18
+ return these.length === 0 ? [] : [{ class: cls, form, count: these.length, distinct: new Set(these.map((row) => row.name)).size }];
19
+ }));
20
+ export const reportOf = (rows) => {
21
+ const run = rows.find((row) => row.kind === 'run');
22
+ const crossings = crossingsOf(rows);
23
+ return {
24
+ ok: true,
25
+ transcripts: run?.transcripts ?? 0,
26
+ unreadable: run?.unreadable ?? 0,
27
+ badLines: run?.badLines ?? 0,
28
+ calls: run?.calls ?? 0,
29
+ crossings: crossings.length,
30
+ replayed: run !== undefined,
31
+ // A hook row carries the registry as it stood at its crossing, so a file with such a row has registry state to show even when no replay asked for it.
32
+ registryRead: (run?.registryRead ?? false) || crossings.some((row) => row.registryState !== 'not-read'),
33
+ via: tally(VIAS, crossings, (row) => row.via ?? 'replay'),
34
+ classes: classCounts(crossings),
35
+ origin: tally(ORIGINS, crossings, (row) => row.origin),
36
+ novelty: tally(NOVELTIES, crossings, (row) => row.novelty),
37
+ decidedBy: tally(DECIDERS, crossings, (row) => row.decidedBy),
38
+ permission: tally(PERMISSIONS, crossings, (row) => row.permission),
39
+ status: tally(STATUSES, crossings, (row) => row.status),
40
+ next: tally(NEXTS, crossings, (row) => row.next),
41
+ miss: tally(MISSES, crossings, (row) => row.miss),
42
+ depth: tally(DEPTHS, crossings, (row) => row.depth),
43
+ againstLatest: tally(AGAINST_LATEST, crossings, (row) => row.againstLatest),
44
+ registryState: tally(REGISTRY_STATES, crossings, (row) => row.registryState),
45
+ registryAge: tally(REGISTRY_AGES, crossings, (row) => row.registryAge),
46
+ arm: tally(ARMS, crossings, (row) => row.arm),
47
+ lineShown: tally(LINE_IDS, crossings, (row) => row.lineShown),
48
+ lineHeld: tally(LINE_IDS, crossings, (row) => row.lineHeld),
49
+ policy: tally(POLICY_SOURCES, crossings.filter((row) => row.policy !== undefined), (row) => row.policy ?? 'default'),
50
+ removedLater: crossings.filter((row) => row.removedLater === true).length,
51
+ unseen: Object.entries(run?.unread ?? {}).map(([value, count]) => ({ value, count })),
52
+ limits: [...LIMITS],
53
+ };
54
+ };
55
+ const line = (label, counts) => {
56
+ const shown = counts.filter((one) => one.count > 0).map((one) => `${one.value} ${String(one.count)}`);
57
+ return `${label}: ${shown.length === 0 ? 'none' : shown.join(', ')}`;
58
+ };
59
+ export const formatReport = (report) => {
60
+ const registry = report.registryRead
61
+ ? [line('Against latest', report.againstLatest), line('Registry state', report.registryState), line('Registry age', report.registryAge)]
62
+ : [REGISTRY_NOT_READ];
63
+ const books = report.replayed
64
+ ? [`Read ${String(report.transcripts)} transcripts (${String(report.unreadable)} unreadable, ${String(report.badLines)} torn lines) and ${String(report.calls)} tool calls.`]
65
+ : ['No replay has run: these rows come from the hook, and what a transcript gives reads not-read until `oars gate replay` fills it in.'];
66
+ return [
67
+ ...books,
68
+ `Crossings: ${String(report.crossings)}`,
69
+ line('Recorded by', report.via),
70
+ ...report.classes.map((one) => ` ${one.class} ${one.form}: ${String(one.count)} (${String(one.distinct)} distinct)`),
71
+ line('Name came from', report.origin),
72
+ line('New to', report.novelty),
73
+ line('Decided by', report.decidedBy),
74
+ line('Permission mode', report.permission),
75
+ line('Result', report.status),
76
+ line('What happened next', report.next),
77
+ line('Name not found', report.miss),
78
+ line('Line', report.depth),
79
+ `Removed later: ${String(report.removedLater)}`,
80
+ line('Notes', report.arm),
81
+ line('Line shown', report.lineShown),
82
+ line('Line held', report.lineHeld),
83
+ ...(report.policy.some((one) => one.count > 0) ? [line('Policy', report.policy)] : []),
84
+ ...registry,
85
+ 'Replay cannot see:',
86
+ ...report.unseen.map((one) => `- ${String(one.count)} ${one.value} commands that state no name to record`),
87
+ ...report.limits.map((limit) => `- ${limit}`),
88
+ ].join('\n');
89
+ };
@@ -0,0 +1,7 @@
1
+ /** Claude Code names a worktree's folder `<repository>--claude-worktrees-<name>`, and a worktree is its repository. */
2
+ export declare const repositoryOf: (project: string) => string;
3
+ /**
4
+ * The repository a working directory is in: its nearest ancestor holding `.git` (a file in a worktree), after a worktree folder is taken off. The hook is handed
5
+ * where the session is now, which can be a subfolder; a few `stat` calls are all it costs.
6
+ */
7
+ export declare const repositoryOfCwd: (cwd: string) => Promise<string>;
@@ -0,0 +1,27 @@
1
+ // The scopes a name is new in: the whole record (fleet), one repository, and one session. A repository is named by the slug Claude Code gives its project folder, with a
2
+ // worktree counted as its repository, so a replay (which reads project folders) and a hook (which is handed a working directory) name the same repository the same way.
3
+ import { stat } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import { slugOf } from '../sessions/allow.js';
6
+ /** Claude Code names a worktree's folder `<repository>--claude-worktrees-<name>`, and a worktree is its repository. */
7
+ export const repositoryOf = (project) => project.replace(/--claude-worktrees-.*$/, '');
8
+ const WORKTREE_FOLDER = /[\\/]\.claude[\\/]worktrees[\\/].*$/;
9
+ const LEVELS = 12;
10
+ const exists = (path) => stat(path).then(() => true, () => false);
11
+ /**
12
+ * The repository a working directory is in: its nearest ancestor holding `.git` (a file in a worktree), after a worktree folder is taken off. The hook is handed
13
+ * where the session is now, which can be a subfolder; a few `stat` calls are all it costs.
14
+ */
15
+ export const repositoryOfCwd = async (cwd) => {
16
+ const base = cwd.replace(WORKTREE_FOLDER, '');
17
+ let folder = base;
18
+ for (let level = 0; level < LEVELS; level += 1) {
19
+ if (await exists(join(folder, '.git')))
20
+ return repositoryOf(slugOf(folder));
21
+ const parent = dirname(folder);
22
+ if (parent === folder)
23
+ break;
24
+ folder = parent;
25
+ }
26
+ return repositoryOf(slugOf(base));
27
+ };
@@ -0,0 +1,68 @@
1
+ import type { Allow } from '../sessions/allow.js';
2
+ import type { Redactor } from '../sessions/redact.js';
3
+ import { type Crossing, type Removal, type UnreadShape } from './checks.js';
4
+ import { type Miss, type Origin, type Permission, type Status } from './record.js';
5
+ export interface TranscriptFile {
6
+ file: string;
7
+ project: string;
8
+ /** The main session: a subagent's is its parent's. */
9
+ session: string;
10
+ /** The subagent transcript's own name; null on a main transcript. */
11
+ agent: string | null;
12
+ }
13
+ /** One copy per project and relative path. `raw/` hard-links the same file, and the harness's copy, when it still has one, is at least as long. */
14
+ export declare const discover: (roots: readonly string[], allow: Allow) => Promise<{
15
+ files: TranscriptFile[];
16
+ unreadable: number;
17
+ }>;
18
+ export interface Found {
19
+ crossing: Crossing;
20
+ ts: string | null;
21
+ /** In memory only: where a registry option would ask. Never written. */
22
+ cwd: string | null;
23
+ toolUseId: string | null;
24
+ permission: Permission;
25
+ /** The call's position in its transcript, and the line it sits on. */
26
+ call: number;
27
+ line: number;
28
+ side: boolean;
29
+ status: Status;
30
+ miss: Miss;
31
+ }
32
+ export interface FoundRemoval {
33
+ removal: Removal;
34
+ ts: string | null;
35
+ status: Status;
36
+ }
37
+ /** A call's answer can sit in another file when a resumed transcript holds it, so the entries are shared across a run. */
38
+ interface CallEntry {
39
+ found: Found[];
40
+ removals: FoundRemoval[];
41
+ }
42
+ export type CallsSeen = Map<string, CallEntry>;
43
+ export interface TranscriptRead {
44
+ found: Found[];
45
+ removals: FoundRemoval[];
46
+ unread: UnreadShape[];
47
+ /** The positions of the calls that pipe a download into a shell: a way around a stopped install. */
48
+ pipes: number[];
49
+ /** Every tool call line in this transcript, a replay of a resumed transcript's included: the position `Found.call` and `pipes` count in. */
50
+ calls: number;
51
+ /** The calls this transcript added to the run: those a transcript read earlier had not already shown. The run row's `calls`. */
52
+ fresh: number;
53
+ badLines: number;
54
+ }
55
+ /** The first pass: every call and its answer. Text is read only to settle a status; nothing of it leaves this function. */
56
+ export declare const readCalls: (file: string, agent: string | null, redact: Redactor, seen: CallsSeen) => Promise<TranscriptRead>;
57
+ /**
58
+ * The second pass, only for a transcript with a crossing, so the text of the rest is not read twice. A name's origin is what first showed it before
59
+ * its first crossing in this transcript, because after that the crossing's own output names it. Nothing shown, and it is `none-seen`, which says the
60
+ * transcript shows no source; the agent's own words do not count, since they are the thing being asked about.
61
+ */
62
+ export interface Origins {
63
+ origins: Map<string, Origin>;
64
+ /** Names a main-line or subagent prompt showed before their first crossing, whatever showed them first: the operator typing a name is a decision whoever saw it first. */
65
+ prompted: Set<string>;
66
+ }
67
+ export declare const readOrigins: (file: string, firstLines: ReadonlyMap<string, number>, servers: ReadonlySet<string>) => Promise<Origins>;
68
+ export {};