@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,164 @@
1
+ // The hook index: a small file written at the end of every deposit so a hook never parses the whole observation log.
2
+ // SessionStart reads `projects` (the recurring signatures per project folder); PostToolUse reads `signatures` (sessions and
3
+ // what followed, by exact signature). Both are pure functions of the store.
4
+ import { readFile } from 'node:fs/promises';
5
+ import { join, resolve } from 'node:path';
6
+ import { resolveReads } from './recall-pool.js';
7
+ import { aboveBaseRate, fitReads, PUSH_FLOORS, toolShapeId } from './recall-reads.js';
8
+ import { expandHome } from './allow.js';
9
+ import { slugOf } from './store.js';
10
+ const INDEX_FILE = 'hook-index.json';
11
+ /** A signature is "recurring" once it has been seen in this many sessions. */
12
+ export const RECURRING_SESSIONS = 2;
13
+ const PER_PROJECT = 12;
14
+ const FOLLOWED = 3;
15
+ /** SessionStart ranks by sessions within this many latest sessions of its repository: the `measure --last 300 --repo` window the G1 reading uses. */
16
+ export const RECENT_SESSIONS = 300;
17
+ const topFollowed = (following) => Object.entries(following)
18
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
19
+ .slice(0, FOLLOWED);
20
+ const tally = () => ({ sessions: new Set(), following: {}, harness: new Set() });
21
+ // The same order `measure` uses, so the window here is the window `measure --last 300 --repo` counts.
22
+ const byStart = (a, b) => (a.tsFirst ?? '').localeCompare(b.tsFirst ?? '') || a.session.localeCompare(b.session);
23
+ const WORKTREE_SLUG = '--claude-worktrees-';
24
+ /**
25
+ * Groups project folder slugs into repositories without knowing any root: a worktree slug folds into its repository's, and a slug that
26
+ * extends another stored slug (a subfolder) joins the shortest one, the same extension rule `belongs` applies at recall time.
27
+ */
28
+ const repositoryOf = (projects) => {
29
+ const baseOf = (project) => project.split(WORKTREE_SLUG)[0] ?? project;
30
+ const bases = [...new Set(projects.map(baseOf))].sort((a, b) => a.length - b.length);
31
+ return (project) => {
32
+ const base = baseOf(project);
33
+ return bases.find((candidate) => base === candidate || base.startsWith(`${candidate}-`)) ?? base;
34
+ };
35
+ };
36
+ export const buildIndex = (store, redactDomains, at, reads = [], next) => {
37
+ const projectOf = new Map(store.sessions.map((session) => [session.session, session.project]));
38
+ // The latest 300 of each repository, not of the whole store: another repository's deposits must not move this one's ranking.
39
+ const family = repositoryOf([...new Set(store.sessions.map((session) => session.project))]);
40
+ const byRepository = new Map();
41
+ for (const session of store.sessions) {
42
+ const key = family(session.project);
43
+ const group = byRepository.get(key) ?? [];
44
+ byRepository.set(key, group);
45
+ group.push(session);
46
+ }
47
+ const recentNames = new Set([...byRepository.values()].flatMap((group) => group.sort(byStart).slice(-RECENT_SESSIONS).map((session) => session.session)));
48
+ const overall = new Map();
49
+ const byProject = new Map();
50
+ for (const error of store.errors) {
51
+ const project = projectOf.get(error.session);
52
+ const targets = [overall];
53
+ if (project !== undefined) {
54
+ const own = byProject.get(project) ?? new Map();
55
+ byProject.set(project, own);
56
+ targets.push(own);
57
+ }
58
+ for (const target of targets) {
59
+ const entry = target.get(error.sig) ?? tally();
60
+ target.set(error.sig, entry);
61
+ entry.sessions.add(error.session);
62
+ if (error.harnessVersion !== null)
63
+ entry.harness.add(error.harnessVersion);
64
+ for (const tool of error.next)
65
+ entry.following[tool] = (entry.following[tool] ?? 0) + 1;
66
+ }
67
+ }
68
+ const signatures = {};
69
+ for (const [sig, entry] of overall) {
70
+ if (entry.sessions.size >= RECURRING_SESSIONS)
71
+ signatures[sig] = { sessions: entry.sessions.size, followed: topFollowed(entry.following) };
72
+ }
73
+ const projects = {};
74
+ for (const [project, sigs] of byProject) {
75
+ const rows = [...sigs.entries()]
76
+ .filter(([, entry]) => entry.sessions.size >= RECURRING_SESSIONS)
77
+ .map(([sig, entry]) => ({
78
+ sig,
79
+ sessions: entry.sessions.size,
80
+ recent: [...entry.sessions].filter((name) => recentNames.has(name)).length,
81
+ followed: topFollowed(entry.following),
82
+ harness: [...entry.harness].sort(),
83
+ }))
84
+ .filter((row) => row.recent > 0)
85
+ .sort((a, b) => b.recent - a.recent || b.sessions - a.sessions || a.sig.localeCompare(b.sig))
86
+ .slice(0, PER_PROJECT);
87
+ if (rows.length > 0)
88
+ projects[project] = rows;
89
+ }
90
+ return {
91
+ at,
92
+ redactDomains: [...redactDomains],
93
+ projects,
94
+ signatures,
95
+ fit: Object.fromEntries(resolveReads(aboveBaseRate(store, fitReads(store, PUSH_FLOORS)), reads).map((read) => [toolShapeId(read.tool, read.keys), read])),
96
+ ...(next === undefined ? {} : { recoveryByFrame: next.recovery, positionsByFrame: next.positions }),
97
+ };
98
+ };
99
+ export const indexPath = (storeDir) => join(storeDir, INDEX_FILE);
100
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
101
+ // The file is ours; these checks guard a torn or foreign file, and the inner rows are trusted as written (as the store's are).
102
+ const isIndex = (value) => isRecord(value) && isRecord(value.projects) && isRecord(value.signatures) && Array.isArray(value.redactDomains) && typeof value.at === 'string';
103
+ /** The index as written, or undefined when there is none (or it is torn): a hook then says nothing. */
104
+ export const readIndex = async (storeDir) => {
105
+ try {
106
+ const parsed = JSON.parse(await readFile(indexPath(storeDir), 'utf8'));
107
+ return isIndex(parsed) ? parsed : undefined;
108
+ }
109
+ catch {
110
+ // A missing or torn index is "no index".
111
+ return undefined;
112
+ }
113
+ };
114
+ /** `<root>/.claude/worktrees/<name>` belongs to `<root>`, so a worktree session sees its repository's history. */
115
+ export const repositoryRoot = (cwd) => {
116
+ const marker = /[\\/]\.claude[\\/]worktrees[\\/]/.exec(cwd);
117
+ return marker === null ? cwd : cwd.slice(0, marker.index);
118
+ };
119
+ /** A project folder belongs to the repository when its slug is the root's slug or extends it (a subfolder or a worktree). */
120
+ const belongs = (project, rootSlug) => project === rootSlug || project.startsWith(`${rootSlug}-`);
121
+ /** True when a project folder slug belongs to the repository `root` sits in (a worktree path counts as its repository). */
122
+ export const inRepository = (project, root) => belongs(project, slugOf(repositoryRoot(resolve(expandHome(root)))));
123
+ const clip = (text, length) => (text.length <= length ? text : `${text.slice(0, length - 1)}…`);
124
+ /** The top recurring signatures for the repository, as a short block for injection; undefined when there is nothing to say. */
125
+ export const contextBlock = (index, options) => {
126
+ const rootSlug = slugOf(repositoryRoot(options.cwd));
127
+ const merged = new Map();
128
+ for (const [project, rows] of Object.entries(index.projects)) {
129
+ if (!belongs(project, rootSlug))
130
+ continue;
131
+ for (const row of rows) {
132
+ if (options.harness !== undefined && !row.harness.includes(options.harness))
133
+ continue;
134
+ const have = merged.get(row.sig);
135
+ if (have === undefined) {
136
+ merged.set(row.sig, { ...row });
137
+ continue;
138
+ }
139
+ have.sessions += row.sessions;
140
+ have.recent += row.recent;
141
+ const following = {};
142
+ for (const [tool, count] of [...have.followed, ...row.followed])
143
+ following[tool] = (following[tool] ?? 0) + count;
144
+ have.followed = topFollowed(following);
145
+ }
146
+ }
147
+ const rows = [...merged.values()].sort((a, b) => b.recent - a.recent || b.sessions - a.sessions || a.sig.localeCompare(b.sig)).slice(0, options.top);
148
+ if (rows.length === 0)
149
+ return undefined;
150
+ const header = `oars recall: errors that recur in this repository's earlier sessions${options.harness === undefined ? '' : ` (Claude Code ${options.harness})`}. Check these before retrying:`;
151
+ const lines = rows.map((row) => {
152
+ const followed = row.followed.map(([tool]) => tool).join(', ');
153
+ return `- ${clip(row.sig, 120)} (${String(row.recent)} sessions in the last ${String(RECENT_SESSIONS)}${followed === '' ? '' : `; next: ${followed}`})`;
154
+ });
155
+ const kept = [];
156
+ let used = header.length;
157
+ for (const line of lines) {
158
+ if (used + 1 + line.length > options.maxChars)
159
+ break;
160
+ kept.push(line);
161
+ used += 1 + line.length;
162
+ }
163
+ return kept.length === 0 ? undefined : [header, ...kept].join('\n');
164
+ };
@@ -0,0 +1,17 @@
1
+ import { type Emit } from '../telemetry/emit.js';
2
+ import type { HookCall } from './schema.js';
3
+ import { type SessionsRuntime } from './store.js';
4
+ import type { NightlyTelemetry } from '../telemetry/health.js';
5
+ export type HookEvent = HookCall['event'];
6
+ export declare const HOOK_EVENTS: readonly HookEvent[];
7
+ /** The runtime a hook process runs with. Only `nightly` reads the non-Claude stores (a saved `--allow cursor` or `--allow codex`); the Stop path stays free of it. */
8
+ export declare const hookRuntime: (event: HookEvent, env: {
9
+ [name: string]: string | undefined;
10
+ }, platform: NodeJS.Platform, home: string, io: Pick<SessionsRuntime, "stdin" | "stdout">) => SessionsRuntime;
11
+ /** What the nightly run records with, and nothing for any other hook: the telemetry folder sits beside the session store under the config directory, so `oars telemetry` and the nightly run read and write the same one. The version is read only for the nightly run, so the per-tool hooks never touch the manifest. */
12
+ export declare const nightlyTelemetry: (event: HookEvent, env: NodeJS.ProcessEnv, platform: NodeJS.Platform, home: string, version: () => string, now: () => number) => NightlyTelemetry | undefined;
13
+ /**
14
+ * Runs one hook. It never fails or blocks the session it runs in: whatever happens, it resolves (the process exits 0), and the
15
+ * hook-call line records the outcome, with the failure's first line when there was one, so the record is evidence, not a claim.
16
+ */
17
+ export declare const runHook: (event: HookEvent, stdin: string, runtime: SessionsRuntime, now: () => number, telemetry?: NightlyTelemetry, emit?: Emit) => Promise<string | undefined>;
@@ -0,0 +1,281 @@
1
+ // The hook entry points. Each takes the hook's stdin and returns the text for stdout (or undefined for none). They are fail-open:
2
+ // any error inside one is swallowed, so a hook can never fail or block a Claude Code session. Each call is counted in
3
+ // `hook-calls.jsonl` with its own duration, so `measure` can count recall calls per session and the timings can be checked.
4
+ import { join } from 'node:path';
5
+ import { performance } from 'node:perf_hooks';
6
+ import { defaultConfigDir } from '../credentials.js';
7
+ import { noEmit } from '../telemetry/emit.js';
8
+ import { frameWords, HOOK_HARNESS, recallFields } from '../telemetry/fields.js';
9
+ import { cursorRoot } from './cursor.js';
10
+ import { SOURCES, sourceLabel } from './sources.js';
11
+ import { ACTION_TABLES, actionOf } from './actions.js';
12
+ import { errorCodeOf, signatureOf } from './extract.js';
13
+ import { appendHookCall } from './hook-calls.js';
14
+ import { contextBlock, readIndex } from './hook-index.js';
15
+ import { bundleNext, frameKeyOf, lastTable, mergePositions, positionsRecall, recoveryDecision, recoveryRecall, signatureRecall, isSignatureRetry } from './recall-next.js';
16
+ import { pushRecall, tailOf } from './recall-push.js';
17
+ import { statedAt, statedOf } from './transcript-tail.js';
18
+ import { toolShapeId } from './recall-reads.js';
19
+ import { makeRedactor } from './redact.js';
20
+ import { errorLine, inputKey, logStep, newContext, runDeposit, stampKey, tryLog } from './steps.js';
21
+ import { deposit, depositSession, readSaved } from './store.js';
22
+ import { recordSurvival } from './survival.js';
23
+ export const HOOK_EVENTS = ['session-start', 'prompt', 'pre-tool', 'post-tool', 'stop', 'nightly'];
24
+ const TOP = 5;
25
+ const MAX_CHARS = 1200;
26
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ const parse = (stdin) => {
28
+ try {
29
+ const parsed = JSON.parse(stdin);
30
+ return isRecord(parsed) ? parsed : {};
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ };
36
+ const str = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
37
+ const additional = (eventName, context) => `${JSON.stringify({ hookSpecificOutput: { hookEventName: eventName, additionalContext: context } })}\n`;
38
+ /** What a next-node read needs of the hook's input: the session, the transcript's tail (version, model), and the clock; undefined when the harness gave no session, since the holdback arm is the session's. */
39
+ const nextInputOf = async (runtime, input, now) => {
40
+ const sid = str(input.session_id);
41
+ if (sid === undefined)
42
+ return undefined;
43
+ const transcript = str(input.transcript_path);
44
+ const tail = transcript === undefined ? '' : await tailOf(transcript);
45
+ const { harnessVersion } = statedOf(tail);
46
+ return { next: { storeDir: runtime.storeDir, sid, tail, platform: process.platform, at: new Date(now()).toISOString() }, asker: { harness: HOOK_HARNESS, harnessVersion } };
47
+ };
48
+ /**
49
+ * The split-positions table (Item 3): the kept bundle's positions first, the local pool's second. A `repeat` read (UserPromptSubmit) goes out only when the table differs from
50
+ * the one this session was last sent, shown or held, so a held session and a shown one hear the same number of times.
51
+ */
52
+ const positionsTable = async (runtime, input, now, index, repeat) => {
53
+ const prepared = await nextInputOf(runtime, input, now);
54
+ if (prepared === undefined)
55
+ return undefined;
56
+ const bundle = await bundleNext(runtime.storeDir, prepared.asker);
57
+ const positions = mergePositions(bundle?.positions ?? [], index?.positionsByFrame?.[frameKeyOf(prepared.asker)] ?? []);
58
+ if (positions.length === 0)
59
+ return undefined;
60
+ const previous = repeat ? await lastTable(runtime.storeDir, prepared.next.sid) : undefined;
61
+ return positionsRecall(prepared.next, prepared.asker, positions, bundle?.poolVersion ?? index?.at ?? '', previous);
62
+ };
63
+ const sessionStart = async (runtime, input, now) => {
64
+ const cwd = str(input.cwd);
65
+ const index = await readIndex(runtime.storeDir);
66
+ const block = index === undefined || cwd === undefined ? undefined : contextBlock(index, { cwd, top: TOP, maxChars: MAX_CHARS });
67
+ const recall = await positionsTable(runtime, input, now, index, false);
68
+ const model = str(input.model);
69
+ const shown = recall?.arm === 'shown' ? recall.answer.rendered : undefined;
70
+ const text = [block, shown].filter((part) => part !== undefined).join('\n\n');
71
+ return {
72
+ hit: text !== '' || recall !== undefined,
73
+ queried: index !== undefined || recall !== undefined,
74
+ ...(text === '' ? {} : { output: additional('SessionStart', text) }),
75
+ ...(recall === undefined ? {} : { recall }),
76
+ ...(model === undefined ? {} : { model }),
77
+ };
78
+ };
79
+ /** UserPromptSubmit: the positions table again, only when it changed since this session's last one. */
80
+ const prompt = async (runtime, input, now) => {
81
+ const recall = await positionsTable(runtime, input, now, await readIndex(runtime.storeDir), true);
82
+ if (recall === undefined)
83
+ return { hit: false, queried: false };
84
+ return recall.arm === 'shown' ? { output: additional('UserPromptSubmit', recall.answer.rendered), hit: true, queried: true, recall } : { hit: true, queried: true, recall };
85
+ };
86
+ const inputKeys = (input) => (isRecord(input.tool_input) ? Object.keys(input.tool_input) : []);
87
+ /**
88
+ * The fit push. The only output this hook writes is `additionalContext`: it holds no permission decision and changes no input, so whatever it does, the call runs as asked.
89
+ * A call with no read says nothing and logs no recall step: that is nearly every call, and nobody asked. A held push is a hit, for what it would have shown, with no output.
90
+ */
91
+ const preTool = async (runtime, input, now) => {
92
+ const sid = str(input.session_id);
93
+ const tool = str(input.tool_name);
94
+ if (sid === undefined || tool === undefined)
95
+ return { hit: false, queried: false };
96
+ const index = await readIndex(runtime.storeDir);
97
+ if (index === undefined)
98
+ return { hit: false, queried: false };
99
+ const recall = await pushRecall({ storeDir: runtime.storeDir, sid, tool, keys: inputKeys(input), transcript: str(input.transcript_path), platform: process.platform, at: new Date(now()).toISOString(), index });
100
+ if (recall === undefined)
101
+ return { hit: false, queried: false };
102
+ return recall.arm === 'shown' ? { output: additional('PreToolUse', recall.answer.rendered), hit: true, queried: true, recall } : { hit: true, queried: true, recall };
103
+ };
104
+ /** A failed tool arrives as PostToolUseFailure with the text in `error` (Bash and PowerShell start it with `Exit code N`); a PostToolUse response that starts that way is read too. */
105
+ const failureText = (input) => {
106
+ const failed = str(input.error);
107
+ if (failed !== undefined)
108
+ return failed;
109
+ const response = str(input.tool_response);
110
+ return response !== undefined && /^Exit code \d+/.test(response) ? response : undefined;
111
+ };
112
+ /**
113
+ * The recovery edge (Item 2): what followed a failure of this action and error class, from the kept bundle for the session's setup and then the local pool, held to A33's
114
+ * holdback like every next-node read. A same-step retry or a local edge under the floor says nothing, signature line included; the signature line answers only a failure no cell covers.
115
+ */
116
+ const postTool = async (runtime, input, now) => {
117
+ const tool = str(input.tool_name);
118
+ const text = failureText(input);
119
+ const prepared = tool === undefined || text === undefined ? undefined : await nextInputOf(runtime, input, now);
120
+ if (tool === undefined || text === undefined || prepared === undefined)
121
+ return { hit: false, queried: false };
122
+ const index = await readIndex(runtime.storeDir);
123
+ const bundle = await bundleNext(runtime.storeDir, prepared.asker);
124
+ if (index === undefined && bundle === undefined)
125
+ return { hit: false, queried: false };
126
+ const action = actionOf(ACTION_TABLES['claude-code'], tool).action;
127
+ const code = errorCodeOf(text);
128
+ const decision = recoveryDecision(bundle, index === undefined ? undefined : { recovery: index.recoveryByFrame?.[frameKeyOf(prepared.asker)] ?? {}, poolVersion: index.at }, action, code);
129
+ const event = str(input.hook_event_name) ?? 'PostToolUseFailure';
130
+ if (decision.kind === 'serve') {
131
+ const recall = await recoveryRecall(prepared.next, prepared.asker, action, code, decision);
132
+ return recall.arm === 'shown' ? { output: additional(event, recall.answer.rendered), hit: true, queried: true, recall } : { hit: true, queried: true, recall };
133
+ }
134
+ // A same-step retry, a local cell too thin to serve, and a cell whose top is that nothing followed all end the lookup: a cell that exists but says nothing is not a reason to fall back to a signature count.
135
+ if (decision.kind === 'retry' || decision.kind === 'floor' || decision.kind === 'ended')
136
+ return { hit: false, queried: true };
137
+ const sig = signatureOf(tool, text, makeRedactor(index?.redactDomains ?? []));
138
+ const found = index?.signatures[sig];
139
+ if (index === undefined || found === undefined || isSignatureRetry(tool, found))
140
+ return { hit: false, queried: true };
141
+ const recall = await signatureRecall(prepared.next, prepared.asker, tool, sig, found, index.at);
142
+ return recall.arm === 'shown' ? { output: additional(event, recall.answer.rendered), hit: true, queried: true, recall } : { hit: true, queried: true, recall };
143
+ };
144
+ const stop = async (runtime, input, now) => {
145
+ const transcript = str(input.transcript_path);
146
+ if (transcript === undefined)
147
+ return { hit: false, queried: false };
148
+ // The SessionEnd hook is the `stop` trigger (spec 4.1); derive and index follow at the nightly run, so only a deposit row is logged.
149
+ const deposited = await runDeposit(newContext(runtime.storeDir, now, 'stop'), false, inputKey('stop', [transcript], []), async (trace) => ({ value: await depositSession(runtime, transcript, now, trace), in: { transcript: 1 } }));
150
+ return { hit: deposited === 'deposited', queried: false };
151
+ };
152
+ const nightlyRun = async (runtime, now, timing) => {
153
+ // A state.json that cannot be read is a failed deposit run like any other: its error row and the skipped derive row (spec 4.4) are logged.
154
+ const saved = await readSaved(runtime.storeDir).catch((failure) => runDeposit(newContext(runtime.storeDir, now, 'nightly'), true, inputKey('nightly', [], []), () => Promise.reject(failure instanceof Error ? failure : new Error(String(failure)))));
155
+ const result = await runDeposit(newContext(runtime.storeDir, now, 'nightly'), true, inputKey('nightly', saved.allow, saved.redactDomains), async (trace) => {
156
+ const parseStart = timing.clock();
157
+ const done = await deposit(runtime, { allow: saved.allow, redactDomains: saved.redactDomains, now }, trace);
158
+ timing.parseMs = timing.clock() - parseStart;
159
+ return { value: done, in: { transcript: done.sessions + done.subagents + done.unchanged } };
160
+ });
161
+ const survival = await recordSurvival(runtime, saved.allow, now);
162
+ // A source store that could not be read does not fail the run, so the record carries it or nobody sees it.
163
+ const counts = Object.entries(result.sources);
164
+ const unreadable = counts.flatMap(([, tally]) => tally.unreadableLogged)[0];
165
+ if (unreadable !== undefined)
166
+ return { hit: true, queried: false, error: await errorLine(runtime.storeDir, unreadable) };
167
+ // A skip for an unsafe session id is likewise reported, never silent (the `rejected` doc in sources.ts); an unreadable store, which names its own reason, takes precedence.
168
+ const skipped = counts.find(([, tally]) => tally.rejected > 0);
169
+ if (skipped !== undefined)
170
+ return { hit: true, queried: false, error: `Skipped ${String(skipped[1].rejected)} ${sourceLabel(skipped[0], runtime.sources ?? SOURCES)} sessions whose id is not a safe file name` };
171
+ // A survival read that fails every night is otherwise invisible, for the same reason; it comes last because the run itself did not lose a session.
172
+ return survival === null ? { hit: true, queried: false, error: await errorLine(runtime.storeDir, 'Commit survival not read this run.') } : { hit: true, queried: false };
173
+ };
174
+ /** The nightly run, then (when telemetry is given) the store-health snapshot and the run's cost. Recording happens after the run and cannot change its outcome or its error. */
175
+ const nightly = async (runtime, now, telemetry) => {
176
+ const timing = { clock: telemetry?.now ?? now, parseMs: 0 };
177
+ const started = timing.clock();
178
+ let outcome;
179
+ let failure;
180
+ let failed = false;
181
+ try {
182
+ outcome = await nightlyRun(runtime, now, timing);
183
+ }
184
+ catch (thrown) {
185
+ failure = thrown;
186
+ failed = true;
187
+ }
188
+ if (telemetry !== undefined) {
189
+ // Loaded here so the hooks that run before every tool call never pay for the telemetry modules.
190
+ const { recordRunCost, recordStoreHealth } = await import('../telemetry/health.js');
191
+ await recordStoreHealth(telemetry, runtime);
192
+ recordRunCost(telemetry, { totalMs: timing.clock() - started, parseMs: timing.parseMs, failed: failed || outcome?.error !== undefined });
193
+ }
194
+ if (failed)
195
+ throw failure;
196
+ return outcome ?? { hit: false, queried: false };
197
+ };
198
+ /** What a lookup hook looks up, as its recall key (spec 4.2 names none for a lookup): the cwd for session-start, the failure's signature for post-tool, the call's shape for pre-tool; null when it had nothing to look up. */
199
+ const lookupKey = (event, input) => {
200
+ if (event === 'session-start') {
201
+ const cwd = str(input.cwd);
202
+ return cwd === undefined ? null : stampKey(event, cwd);
203
+ }
204
+ if (event === 'prompt')
205
+ return stampKey(event, 'positions');
206
+ const tool = str(input.tool_name);
207
+ if (event === 'pre-tool')
208
+ return tool === undefined ? null : stampKey(event, toolShapeId(tool, inputKeys(input)));
209
+ const text = failureText(input);
210
+ return event === 'post-tool' && tool !== undefined && text !== undefined ? stampKey(event, signatureOf(tool, text, makeRedactor([]))) : null;
211
+ };
212
+ /** The runtime a hook process runs with. Only `nightly` reads the non-Claude stores (a saved `--allow cursor` or `--allow codex`); the Stop path stays free of it. */
213
+ export const hookRuntime = (event, env, platform, home, io) => ({
214
+ storeDir: join(defaultConfigDir(env, platform, home), 'oars', 'sessions'),
215
+ projectsRoot: join(env.CLAUDE_CONFIG_DIR ?? join(home, '.claude'), 'projects'),
216
+ ...(event === 'nightly' ? { cursorRoot: cursorRoot(platform, env, home), sourceRoots: Object.fromEntries(SOURCES.map((source) => [source.name, source.root(platform, env, home)])) } : {}),
217
+ ...io,
218
+ });
219
+ /** What the nightly run records with, and nothing for any other hook: the telemetry folder sits beside the session store under the config directory, so `oars telemetry` and the nightly run read and write the same one. The version is read only for the nightly run, so the per-tool hooks never touch the manifest. */
220
+ export const nightlyTelemetry = (event, env, platform, home, version, now) => event === 'nightly' ? { env, telemetry: { dir: join(defaultConfigDir(env, platform, home), 'oars', 'telemetry'), platform }, version: version(), now } : undefined;
221
+ /**
222
+ * Runs one hook. It never fails or blocks the session it runs in: whatever happens, it resolves (the process exits 0), and the
223
+ * hook-call line records the outcome, with the failure's first line when there was one, so the record is evidence, not a claim.
224
+ */
225
+ export const runHook = async (event, stdin, runtime, now, telemetry, emit = noEmit) => {
226
+ const started = performance.now();
227
+ let session = null;
228
+ let outcome = { hit: false, queried: false };
229
+ let failed = false;
230
+ let failure;
231
+ let key = null;
232
+ let transcript;
233
+ try {
234
+ const input = parse(stdin);
235
+ transcript = str(input.transcript_path);
236
+ key = lookupKey(event, input);
237
+ session = str(input.session_id) ?? null;
238
+ if (event === 'session-start')
239
+ outcome = await sessionStart(runtime, input, now);
240
+ else if (event === 'prompt')
241
+ outcome = await prompt(runtime, input, now);
242
+ else if (event === 'pre-tool')
243
+ outcome = await preTool(runtime, input, now);
244
+ else if (event === 'post-tool')
245
+ outcome = await postTool(runtime, input, now);
246
+ else if (event === 'stop')
247
+ outcome = await stop(runtime, input, now);
248
+ else
249
+ outcome = await nightly(runtime, now, telemetry);
250
+ }
251
+ catch (thrown) {
252
+ failure = thrown;
253
+ failed = true;
254
+ }
255
+ const ms = Math.round(performance.now() - started);
256
+ // The failure line is masked once, the same for the hook-calls line and the step row beside it; both writes are guarded, so a counter
257
+ // that cannot be written never costs the session its context.
258
+ const line = failed ? await errorLine(runtime.storeDir, failure).catch(() => 'unknown error') : outcome.error ?? null;
259
+ await tryLog(() => appendHookCall(runtime.storeDir, { at: new Date(now()).toISOString(), event, session, hit: outcome.hit, ms, startMs: Math.round(started), error: line }));
260
+ // A lookup hook that ran a query (or failed trying) is a `recall` step started by its own trigger; the deposit hooks logged their rows as they ran.
261
+ if (key !== null && (failed || outcome.queried)) {
262
+ const report = { in: { query: 1 }, out: failed ? {} : { matches: outcome.hit ? 1 : 0 }, key };
263
+ await tryLog(() => logStep(newContext(runtime.storeDir, now, event), 'recall', failed ? 'error' : 'ok', ms, report, line));
264
+ }
265
+ // After the answer is settled and the local rows are written; it runs before hook-bin prints, at the cost measured in t876 item 5. `emit` never throws. A push states its own frame;
266
+ // every other fire reads the transcript's tail the way a push does, so the version and model are there on a miss, a post-tool, and a stop. (A nightly run has no hook input, so it has no transcript to read.)
267
+ const { recall } = outcome;
268
+ const stated = recall?.frame !== undefined || transcript === undefined ? undefined : await statedAt(transcript);
269
+ emit('hook', {
270
+ name: event,
271
+ latencyMs: ms,
272
+ outcome: failed ? 'error' : recall?.arm === 'held' ? 'held' : outcome.hit ? 'hit' : 'miss',
273
+ bytes: Buffer.byteLength(outcome.output ?? ''),
274
+ // The nightly is the scheduled task, not a harness's hook: it names no harness.
275
+ ...frameWords(event === 'nightly' ? undefined : recall?.frame?.harness ?? HOOK_HARNESS, recall?.frame?.harnessVersion ?? stated?.harnessVersion, recall?.frame?.model ?? outcome.model ?? stated?.model),
276
+ });
277
+ // A push is a recall: its read is the family its query names (the `fit` push names none), and its arm is the exposure record, as a count of one.
278
+ if (recall !== undefined)
279
+ emit('recall', recallFields(recall, [recall.query.family ?? 'fit']));
280
+ return outcome.output;
281
+ };
@@ -0,0 +1,27 @@
1
+ import { type ActionTable, type CanonicalAction } from './actions.js';
2
+ import type { Redactor } from './redact.js';
3
+ import type { PullIdentity, ShellIdentity, StepIdentity, ToolPull } from './schema.js';
4
+ /** A word of a shell command, as the tokenizer read it. */
5
+ export interface WordOfCommand {
6
+ text: string;
7
+ quoted: boolean;
8
+ }
9
+ export declare const NUMBER_FLAG = "-N";
10
+ export declare const isPositional: ({ text, quoted }: WordOfCommand) => boolean;
11
+ /**
12
+ * A pull with its package name as written (the command was redacted before it was read, and the name is checked once more part by part, as a host and a skill-run name are). A package off the public list can be identified by its version, so the version is masked beside it; a stated version of a listed package stays stated.
13
+ * A name that is not one (a URL, a path, a mask) is `<literal>`.
14
+ */
15
+ export declare const pullIdentity: (pull: ToolPull, redact: Redactor) => PullIdentity;
16
+ /** A shell step whose program is private or unreadable: its flags are its own, so none are kept. */
17
+ export declare const UNREAD_SHELL: ShellIdentity;
18
+ export declare const shellIdentity: (program: string, args: readonly WordOfCommand[], pulls: readonly ToolPull[], redact: Redactor) => ShellIdentity;
19
+ /** What a non-shell call's step reads as. `native` comes back masked unless every name it holds is public, since an MCP name spells out its server. */
20
+ export declare const callIdentity: (table: ActionTable, action: CanonicalAction, native: string, shown: string, input: {
21
+ skill: string | undefined;
22
+ url: string | undefined;
23
+ redact: Redactor;
24
+ }) => {
25
+ native: string;
26
+ identity: StepIdentity;
27
+ };
@@ -0,0 +1,131 @@
1
+ // A step's `identity` (recall spec Section 2.2): what the step acted on, by action. A program, subcommand, flag, MCP server and tool pass through
2
+ // `public-vocabulary.ts` and become `<literal>` when it does not list them; a skill-run name, a host, and a pull name are kept as they are after the
3
+ // operator's redaction (CAPTURE 24), and what leaves the machine is decided at the deposit's one gate and by the pool.
4
+ import { mcpOf } from './actions.js';
5
+ import { isWireName, isWirePullName, wireName } from './pool-wire.js';
6
+ import { BUNDLING_PROGRAMS, isPublicMcpTool, isRegistryVersion, LITERAL, PRIVATE, PUBLIC_FLAGS, PUBLIC_MCP_SERVERS, PUBLIC_PACKAGES, PUBLIC_PROGRAMS, PUBLIC_SUBCOMMANDS } from './public-vocabulary.js';
7
+ const orLiteral = (name, allowed) => (allowed.has(name) ? name : LITERAL);
8
+ /**
9
+ * A long-tail name (a skill run, a host, a pull) as the store keeps it: the name itself, or `<literal>` when it is not a name or the operator's redaction would change it. The test is the wire's own (`wireName`), so a name stored here is a
10
+ * name the deposit sends, and a list never decides it: the pool does, by deploy. The operator's exclusion terms are the deposit's, since they change after the capture.
11
+ */
12
+ const keptName = (name, redact, hasShape = isWireName) => {
13
+ const kept = wireName({ redact, terms: [] }, name, hasShape);
14
+ return kept === PRIVATE ? LITERAL : kept;
15
+ };
16
+ const SHORT_CLUSTER = /^-[a-z]{1,4}$/;
17
+ const SHORT_LETTER = /^-[A-Za-z]$/;
18
+ // A short flag with a value joined after a non-letter, as in `-c1-200`: the first capture is the name and the rest is the value, which is dropped.
19
+ const JOINED_VALUE = /^(-[A-Za-z])[^A-Za-z]/;
20
+ // `head -20`: the digits are the value and the flag has no name of its own, so every such flag reads alike.
21
+ const BARE_NUMBER = /^-\d+$/;
22
+ export const NUMBER_FLAG = '-N';
23
+ /**
24
+ * The names one flag word holds, without its value. On a program that bundles, a short cluster reads as its letters, so `-rf` and `-fr` agree;
25
+ * a value joined to a flag there can still read as letters, one each and never the word. On any other program the cluster is the literal.
26
+ */
27
+ const flagNames = (text, bundles) => {
28
+ const name = text.split('=')[0] ?? text;
29
+ if (name.startsWith('--') || PUBLIC_FLAGS.has(name))
30
+ return [orLiteral(name, PUBLIC_FLAGS)];
31
+ if (SHORT_LETTER.test(name))
32
+ return [name];
33
+ if (BARE_NUMBER.test(name))
34
+ return [NUMBER_FLAG];
35
+ const joined = JOINED_VALUE.exec(name)?.[1];
36
+ if (joined !== undefined)
37
+ return [joined];
38
+ return bundles && SHORT_CLUSTER.test(name) ? Array.from(name.slice(1), (letter) => `-${letter}`) : [LITERAL];
39
+ };
40
+ const flagsOf = (program, args) => {
41
+ const bundles = BUNDLING_PROGRAMS.has(program);
42
+ const flags = new Set();
43
+ for (const { text, quoted } of args) {
44
+ if (text === '--')
45
+ break;
46
+ if (quoted || !text.startsWith('-') || text.includes('$'))
47
+ continue;
48
+ for (const name of flagNames(text, bundles))
49
+ flags.add(name);
50
+ }
51
+ return [...flags].sort();
52
+ };
53
+ export const isPositional = ({ text, quoted }) => !quoted && text !== '' && !text.startsWith('-') && !/[$\\/]/.test(text);
54
+ /** A subcommand from the program's own table, wherever a global flag's value left it; a positional word outside the table is the literal. */
55
+ const subOf = (program, args) => {
56
+ const table = PUBLIC_SUBCOMMANDS.get(program);
57
+ if (table === undefined)
58
+ return null;
59
+ const positional = args.filter(isPositional);
60
+ const known = positional.find(({ text }) => table.has(text));
61
+ if (known !== undefined)
62
+ return known.text;
63
+ return positional.length === 0 ? null : LITERAL;
64
+ };
65
+ const REGISTRY_OF = {
66
+ npm: 'npm',
67
+ pypi: 'pypi',
68
+ cargo: 'crates',
69
+ go: 'go',
70
+ gem: 'rubygems',
71
+ brew: 'homebrew',
72
+ skill: 'skill',
73
+ plugin: 'plugin',
74
+ mcp: 'mcp',
75
+ };
76
+ /**
77
+ * A pull with its package name as written (the command was redacted before it was read, and the name is checked once more part by part, as a host and a skill-run name are). A package off the public list can be identified by its version, so the version is masked beside it; a stated version of a listed package stays stated.
78
+ * A name that is not one (a URL, a path, a mask) is `<literal>`.
79
+ */
80
+ export const pullIdentity = (pull, redact) => {
81
+ const name = keptName(pull.name, redact, isWirePullName);
82
+ const showsVersion = PUBLIC_PACKAGES.has(name) && pull.version !== null && isRegistryVersion(pull.version);
83
+ return { registry: REGISTRY_OF[pull.ecosystem], name, version: pull.version === null ? null : showsVersion ? pull.version : LITERAL };
84
+ };
85
+ /** A shell step whose program is private or unreadable: its flags are its own, so none are kept. */
86
+ export const UNREAD_SHELL = { program: LITERAL, sub: null, flags: [] };
87
+ /** Flags of `git checkout` that take only paths: a checkout with one of them acts on a path, never on a branch. */
88
+ const PATH_ONLY_FLAGS = new Set(['--ours', '--theirs', '--patch', '-p', '--pathspec-from-file']);
89
+ /**
90
+ * The class of what a `git checkout` acts on, from the line alone and never the target's text. A `--` marker or a path-only flag proves a path; a target word the segment drops (the same
91
+ * words `isPositional` refuses: quoted, or holding a slash, a backslash, or `$`) with neither proof could be a branch (`feature/x`) or a path (`src/a.ts`), so it is `word`; no such word is absent.
92
+ */
93
+ const checkoutTarget = (all) => {
94
+ // Words before `checkout` are the global options of `git` (`-C <dir>`), not what the checkout acts on.
95
+ const args = all.slice(all.findIndex(({ text, quoted }) => !quoted && text === 'checkout') + 1);
96
+ const dashes = args.findIndex(({ text, quoted }) => !quoted && text === '--');
97
+ const flagWords = (dashes < 0 ? args : args.slice(0, dashes)).filter(({ text, quoted }) => !quoted && text.startsWith('-')).map(({ text }) => text.split('=')[0] ?? text);
98
+ if (dashes >= 0 || flagWords.some((flag) => PATH_ONLY_FLAGS.has(flag)))
99
+ return 'path';
100
+ return args.some((word) => (word.quoted || !word.text.startsWith('-')) && !isPositional(word)) ? 'word' : undefined;
101
+ };
102
+ export const shellIdentity = (program, args, pulls, redact) => {
103
+ if (!PUBLIC_PROGRAMS.has(program))
104
+ return UNREAD_SHELL;
105
+ const sub = subOf(program, args);
106
+ const target = program === 'git' && sub === 'checkout' ? checkoutTarget(args) : undefined;
107
+ const identity = { program, sub, flags: flagsOf(program, args), ...(target === undefined ? {} : { target }) };
108
+ return pulls.length === 0 ? identity : { ...identity, pulls: pulls.map((pull) => pullIdentity(pull, redact)) };
109
+ };
110
+ const hostOf = (url, redact) => {
111
+ if (url === undefined || url === '')
112
+ return { host: null };
113
+ // The parser drops credentials, the path, and the query, and lower-cases the host; the redactor and the name shape then decide, and the pool hashes it on arrival.
114
+ const parsed = URL.canParse(url) ? new URL(url).hostname : '';
115
+ return { host: keptName(parsed, redact) };
116
+ };
117
+ /** What a non-shell call's step reads as. `native` comes back masked unless every name it holds is public, since an MCP name spells out its server. */
118
+ export const callIdentity = (table, action, native, shown, input) => {
119
+ if (action === 'web.fetch' || action === 'web.search')
120
+ return { native: shown, identity: hostOf(input.url, input.redact) };
121
+ if (action === 'skill.run')
122
+ return { native: shown, identity: { skill: keptName(input.skill ?? '', input.redact) } };
123
+ const mcp = action === 'mcp.call' ? mcpOf(table, native) : undefined;
124
+ if (mcp === undefined)
125
+ return { native: shown, identity: null };
126
+ if (!PUBLIC_MCP_SERVERS.has(mcp.server))
127
+ return { native: LITERAL, identity: { server: LITERAL, tool: LITERAL } };
128
+ if (!table.toolDelimited || !isPublicMcpTool(mcp.server, mcp.tool))
129
+ return { native: LITERAL, identity: { server: mcp.server, tool: LITERAL } };
130
+ return { native, identity: { server: mcp.server, tool: mcp.tool } };
131
+ };