@manyoars/oars 0.1.1 → 0.3.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 (240) hide show
  1. package/README.md +134 -26
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +6 -3
  5. package/dist/cli.js +370 -72
  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 -5
  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 +35 -0
  22. package/dist/gate/hook-process.d.ts +14 -0
  23. package/dist/gate/hook-process.js +64 -0
  24. package/dist/gate/hook.d.ts +46 -0
  25. package/dist/gate/hook.js +256 -0
  26. package/dist/gate/install.d.ts +25 -0
  27. package/dist/gate/install.js +126 -0
  28. package/dist/gate/local-bin.d.ts +8 -0
  29. package/dist/gate/local-bin.js +36 -0
  30. package/dist/gate/notes.d.ts +19 -0
  31. package/dist/gate/notes.js +27 -0
  32. package/dist/gate/novelty.d.ts +35 -0
  33. package/dist/gate/novelty.js +99 -0
  34. package/dist/gate/policy.d.ts +25 -0
  35. package/dist/gate/policy.js +73 -0
  36. package/dist/gate/record.d.ts +120 -0
  37. package/dist/gate/record.js +125 -0
  38. package/dist/gate/registry-state.d.ts +10 -0
  39. package/dist/gate/registry-state.js +30 -0
  40. package/dist/gate/registry.d.ts +56 -0
  41. package/dist/gate/registry.js +94 -0
  42. package/dist/gate/replay.d.ts +20 -0
  43. package/dist/gate/replay.js +257 -0
  44. package/dist/gate/report.d.ts +50 -0
  45. package/dist/gate/report.js +89 -0
  46. package/dist/gate/scope.d.ts +7 -0
  47. package/dist/gate/scope.js +27 -0
  48. package/dist/gate/transcripts.d.ts +68 -0
  49. package/dist/gate/transcripts.js +232 -0
  50. package/dist/hook-bin.d.ts +1 -0
  51. package/dist/hook-bin.js +25 -0
  52. package/dist/issuer.d.ts +16 -0
  53. package/dist/issuer.js +55 -0
  54. package/dist/main.d.ts +1 -0
  55. package/dist/main.js +64 -0
  56. package/dist/oidc.js +1 -1
  57. package/dist/sessions/actions.d.ts +35 -0
  58. package/dist/sessions/actions.js +351 -0
  59. package/dist/sessions/allow.d.ts +14 -0
  60. package/dist/sessions/allow.js +51 -0
  61. package/dist/sessions/archive.d.ts +11 -0
  62. package/dist/sessions/archive.js +38 -0
  63. package/dist/sessions/audit.d.ts +42 -0
  64. package/dist/sessions/audit.js +104 -0
  65. package/dist/sessions/baseline.d.ts +62 -0
  66. package/dist/sessions/baseline.js +188 -0
  67. package/dist/sessions/capture-runtime.d.ts +10 -0
  68. package/dist/sessions/capture-runtime.js +12 -0
  69. package/dist/sessions/cell-increments.d.ts +43 -0
  70. package/dist/sessions/cell-increments.js +209 -0
  71. package/dist/sessions/cells.d.ts +79 -0
  72. package/dist/sessions/cells.js +181 -0
  73. package/dist/sessions/codex.d.ts +53 -0
  74. package/dist/sessions/codex.js +679 -0
  75. package/dist/sessions/command.d.ts +117 -0
  76. package/dist/sessions/command.js +373 -0
  77. package/dist/sessions/copilot.d.ts +40 -0
  78. package/dist/sessions/copilot.js +432 -0
  79. package/dist/sessions/cursor.d.ts +24 -0
  80. package/dist/sessions/cursor.js +324 -0
  81. package/dist/sessions/derive.d.ts +57 -0
  82. package/dist/sessions/derive.js +320 -0
  83. package/dist/sessions/exclusion.d.ts +81 -0
  84. package/dist/sessions/exclusion.js +353 -0
  85. package/dist/sessions/extract.d.ts +179 -0
  86. package/dist/sessions/extract.js +1774 -0
  87. package/dist/sessions/fold.d.ts +17 -0
  88. package/dist/sessions/fold.js +71 -0
  89. package/dist/sessions/frame.d.ts +24 -0
  90. package/dist/sessions/frame.js +95 -0
  91. package/dist/sessions/gemini.d.ts +37 -0
  92. package/dist/sessions/gemini.js +613 -0
  93. package/dist/sessions/gold-arms.d.ts +67 -0
  94. package/dist/sessions/gold-arms.js +172 -0
  95. package/dist/sessions/gold.d.ts +197 -0
  96. package/dist/sessions/gold.js +463 -0
  97. package/dist/sessions/graph.d.ts +12 -0
  98. package/dist/sessions/graph.js +34 -0
  99. package/dist/sessions/grok.d.ts +36 -0
  100. package/dist/sessions/grok.js +421 -0
  101. package/dist/sessions/hook-calls.d.ts +3 -0
  102. package/dist/sessions/hook-calls.js +47 -0
  103. package/dist/sessions/hook-index.d.ts +53 -0
  104. package/dist/sessions/hook-index.js +157 -0
  105. package/dist/sessions/hooks.d.ts +13 -0
  106. package/dist/sessions/hooks.js +170 -0
  107. package/dist/sessions/identity.d.ts +22 -0
  108. package/dist/sessions/identity.js +101 -0
  109. package/dist/sessions/install.d.ts +54 -0
  110. package/dist/sessions/install.js +201 -0
  111. package/dist/sessions/lineage.d.ts +19 -0
  112. package/dist/sessions/lineage.js +118 -0
  113. package/dist/sessions/mcp.d.ts +6 -0
  114. package/dist/sessions/mcp.js +153 -0
  115. package/dist/sessions/merkle.d.ts +7 -0
  116. package/dist/sessions/merkle.js +28 -0
  117. package/dist/sessions/motif.d.ts +43 -0
  118. package/dist/sessions/motif.js +59 -0
  119. package/dist/sessions/package-resolve.d.ts +30 -0
  120. package/dist/sessions/package-resolve.js +183 -0
  121. package/dist/sessions/pending.d.ts +34 -0
  122. package/dist/sessions/pending.js +142 -0
  123. package/dist/sessions/pool-command.d.ts +44 -0
  124. package/dist/sessions/pool-command.js +542 -0
  125. package/dist/sessions/pool-delta.d.ts +54 -0
  126. package/dist/sessions/pool-delta.js +190 -0
  127. package/dist/sessions/pool-outbox.d.ts +63 -0
  128. package/dist/sessions/pool-outbox.js +260 -0
  129. package/dist/sessions/pool-print.d.ts +26 -0
  130. package/dist/sessions/pool-print.js +68 -0
  131. package/dist/sessions/pool-wire.d.ts +225 -0
  132. package/dist/sessions/pool-wire.js +620 -0
  133. package/dist/sessions/pool.d.ts +32 -0
  134. package/dist/sessions/pool.js +164 -0
  135. package/dist/sessions/projection.d.ts +75 -0
  136. package/dist/sessions/projection.js +165 -0
  137. package/dist/sessions/public-vocabulary.d.ts +73 -0
  138. package/dist/sessions/public-vocabulary.js +302 -0
  139. package/dist/sessions/query.d.ts +111 -0
  140. package/dist/sessions/query.js +246 -0
  141. package/dist/sessions/read-adoption.d.ts +2 -0
  142. package/dist/sessions/read-adoption.js +27 -0
  143. package/dist/sessions/read-call-shape.d.ts +2 -0
  144. package/dist/sessions/read-call-shape.js +25 -0
  145. package/dist/sessions/read-change.d.ts +2 -0
  146. package/dist/sessions/read-change.js +30 -0
  147. package/dist/sessions/read-cost.d.ts +4 -0
  148. package/dist/sessions/read-cost.js +37 -0
  149. package/dist/sessions/read-day-move.d.ts +3 -0
  150. package/dist/sessions/read-day-move.js +65 -0
  151. package/dist/sessions/read-fit.d.ts +7 -0
  152. package/dist/sessions/read-fit.js +41 -0
  153. package/dist/sessions/read-interaction.d.ts +5 -0
  154. package/dist/sessions/read-interaction.js +62 -0
  155. package/dist/sessions/read-lead-lag.d.ts +15 -0
  156. package/dist/sessions/read-lead-lag.js +81 -0
  157. package/dist/sessions/read-live.d.ts +6 -0
  158. package/dist/sessions/read-live.js +38 -0
  159. package/dist/sessions/read-method.d.ts +2 -0
  160. package/dist/sessions/read-method.js +29 -0
  161. package/dist/sessions/read-next-step.d.ts +2 -0
  162. package/dist/sessions/read-next-step.js +35 -0
  163. package/dist/sessions/read-order.d.ts +54 -0
  164. package/dist/sessions/read-order.js +62 -0
  165. package/dist/sessions/read-readable.d.ts +8 -0
  166. package/dist/sessions/read-readable.js +24 -0
  167. package/dist/sessions/read-recovery.d.ts +20 -0
  168. package/dist/sessions/read-recovery.js +97 -0
  169. package/dist/sessions/read-rows.d.ts +13 -0
  170. package/dist/sessions/read-rows.js +47 -0
  171. package/dist/sessions/read-substitution.d.ts +5 -0
  172. package/dist/sessions/read-substitution.js +51 -0
  173. package/dist/sessions/read-support.d.ts +48 -0
  174. package/dist/sessions/read-support.js +73 -0
  175. package/dist/sessions/read-tool-day.d.ts +2 -0
  176. package/dist/sessions/read-tool-day.js +26 -0
  177. package/dist/sessions/read-version-boundary.d.ts +31 -0
  178. package/dist/sessions/read-version-boundary.js +95 -0
  179. package/dist/sessions/read.d.ts +79 -0
  180. package/dist/sessions/read.js +84 -0
  181. package/dist/sessions/recall-consequence.d.ts +45 -0
  182. package/dist/sessions/recall-consequence.js +179 -0
  183. package/dist/sessions/recall-object.d.ts +137 -0
  184. package/dist/sessions/recall-object.js +77 -0
  185. package/dist/sessions/recall-pool.d.ts +19 -0
  186. package/dist/sessions/recall-pool.js +96 -0
  187. package/dist/sessions/recall-push.d.ts +26 -0
  188. package/dist/sessions/recall-push.js +95 -0
  189. package/dist/sessions/recall-reads.d.ts +117 -0
  190. package/dist/sessions/recall-reads.js +410 -0
  191. package/dist/sessions/recall-render.d.ts +19 -0
  192. package/dist/sessions/recall-render.js +168 -0
  193. package/dist/sessions/redact.d.ts +6 -0
  194. package/dist/sessions/redact.js +54 -0
  195. package/dist/sessions/repo-root.d.ts +10 -0
  196. package/dist/sessions/repo-root.js +101 -0
  197. package/dist/sessions/schema.d.ts +644 -0
  198. package/dist/sessions/schema.js +148 -0
  199. package/dist/sessions/session-shape.d.ts +73 -0
  200. package/dist/sessions/session-shape.js +342 -0
  201. package/dist/sessions/sources.d.ts +50 -0
  202. package/dist/sessions/sources.js +159 -0
  203. package/dist/sessions/steps.d.ts +105 -0
  204. package/dist/sessions/steps.js +250 -0
  205. package/dist/sessions/store.d.ts +297 -0
  206. package/dist/sessions/store.js +1478 -0
  207. package/dist/sessions/strata.d.ts +26 -0
  208. package/dist/sessions/strata.js +77 -0
  209. package/dist/sessions/survival.d.ts +25 -0
  210. package/dist/sessions/survival.js +230 -0
  211. package/dist/sessions/verify.d.ts +53 -0
  212. package/dist/sessions/verify.js +286 -0
  213. package/dist/sessions/vocab-load.d.ts +26 -0
  214. package/dist/sessions/vocab-load.js +61 -0
  215. package/dist/sessions/vscode-copilot.d.ts +38 -0
  216. package/dist/sessions/vscode-copilot.js +477 -0
  217. package/package.json +4 -5
  218. package/vocab/crates.json +1 -0
  219. package/vocab/go.json +1 -0
  220. package/vocab/harness.json +1 -0
  221. package/vocab/homebrew.json +1 -0
  222. package/vocab/manifest.json +1 -0
  223. package/vocab/maven.json +1 -0
  224. package/vocab/models.json +1 -0
  225. package/vocab/npm.json +1 -0
  226. package/vocab/programs.json +1 -0
  227. package/vocab/pypi.json +1 -0
  228. package/vocab/rubygems.json +1 -0
  229. package/dist/commands/deploy-input.d.ts +0 -35
  230. package/dist/commands/deploy-input.js +0 -107
  231. package/dist/commands/deploy.d.ts +0 -22
  232. package/dist/commands/deploy.js +0 -120
  233. package/dist/commands/status.d.ts +0 -7
  234. package/dist/commands/status.js +0 -18
  235. package/dist/commands/test-charge.d.ts +0 -11
  236. package/dist/commands/test-charge.js +0 -34
  237. package/dist/journal.d.ts +0 -26
  238. package/dist/journal.js +0 -58
  239. package/parity/deploy-plans/paid-api.json +0 -34
  240. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -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, or VS Code Copilot Chat, which replay does not read (`--allow codex`, `--allow cursor`, `--allow copilot`, `--allow gemini`, `--allow grok`, and `--allow vscode-copilot` 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 {};
@@ -0,0 +1,232 @@
1
+ // Where a replay reads: the raw transcripts of the allowed folders, the harness's copy first and then what only the archive still holds. Reads only.
2
+ import { join } from 'node:path';
3
+ import { INJECTED_TAGS, isErrorResult, isObj, LEADING_TAG, resultText } from '../sessions/extract.js';
4
+ import { SOURCES } from '../sessions/sources.js';
5
+ import { listDir, logLines, sessionFolders, subagentFiles } from '../sessions/store.js';
6
+ import { checkCall } from './checks.js';
7
+ import { permissionOf } from './record.js';
8
+ const projectTranscripts = async (folder, project, seen) => {
9
+ const found = [];
10
+ const add = (file, relative) => {
11
+ if (seen.has(`${project}/${relative}`))
12
+ return;
13
+ seen.add(`${project}/${relative}`);
14
+ found.push(file);
15
+ };
16
+ for (const entry of await listDir(folder)) {
17
+ if (entry.isFile() && entry.name.endsWith('.jsonl'))
18
+ add({ file: join(folder, entry.name), project, session: entry.name.slice(0, -'.jsonl'.length), agent: null }, entry.name);
19
+ }
20
+ for (const { dir, path, parent, name } of await subagentFiles(folder, await sessionFolders(folder))) {
21
+ add({ file: join(dir, name), project, session: parent, agent: name.slice(0, -'.jsonl'.length) }, [parent, 'subagents', ...path, name].join('/'));
22
+ }
23
+ return found;
24
+ };
25
+ /** 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. */
26
+ export const discover = async (roots, allow) => {
27
+ const seen = new Set();
28
+ const files = [];
29
+ let unreadable = 0;
30
+ for (const root of roots) {
31
+ // Replay reads only the harness's transcript format: a source-table folder (Cursor, Codex, Copilot, Gemini CLI, Grok Build, VS Code Copilot Chat) is not that format, and a project folder is never named for one.
32
+ const projects = (await listDir(root)).filter((entry) => entry.isDirectory() && !SOURCES.some((source) => source.name === entry.name) && allow.allows(entry.name));
33
+ for (const project of projects) {
34
+ try {
35
+ files.push(...(await projectTranscripts(join(root, project.name), project.name, seen)));
36
+ }
37
+ catch {
38
+ unreadable += 1;
39
+ }
40
+ }
41
+ }
42
+ return { files, unreadable };
43
+ };
44
+ /** What a call that named nothing leaves in `CallsSeen`: shared, since nothing is ever added to it. */
45
+ const NO_ENTRY = { found: [], removals: [] };
46
+ const stringOf = (value) => (typeof value === 'string' && value !== '' ? value : null);
47
+ const blocksOf = (line) => {
48
+ const content = isObj(line.message) ? line.message.content : undefined;
49
+ return Array.isArray(content) ? content.filter(isObj) : [];
50
+ };
51
+ const REFUSED = /^The user doesn't want to (?:proceed|take)/;
52
+ /** What a failed result says when the thing named does not exist. Read here and never kept: the record holds only the closed value. */
53
+ const NOT_FOUND = [
54
+ /\bE404\b|404 Not Found|is not in this registry|could not determine executable/i,
55
+ /No matching distribution|Could not find a version|404 Client Error|Package .{0,200} not found/i,
56
+ // The harness says "No such tool available". A server's own "unknown tool" names one of its actions (a batch step), so the server exists.
57
+ /No such tool available|server .{0,200} not (?:found|connected)/i,
58
+ /Unknown skill|skill .{0,200} not found/i,
59
+ ];
60
+ const statusOf = (block, text) => {
61
+ if (!isErrorResult(block, text))
62
+ return 'ok';
63
+ return REFUSED.test(text) ? 'refused' : 'error';
64
+ };
65
+ const settle = (entry, block) => {
66
+ if (entry.found.every((one) => one.status !== 'none') && entry.removals.every((one) => one.status !== 'none'))
67
+ return;
68
+ const text = resultText(block);
69
+ const status = statusOf(block, text);
70
+ const miss = status === 'error' && NOT_FOUND.some((pattern) => pattern.test(text)) ? 'not-found' : 'none';
71
+ for (const found of entry.found) {
72
+ found.status = status;
73
+ found.miss = miss;
74
+ }
75
+ for (const removal of entry.removals)
76
+ removal.status = status;
77
+ };
78
+ const parseLine = (raw) => {
79
+ if (raw === '')
80
+ return { line: null, bad: false };
81
+ try {
82
+ const parsed = JSON.parse(raw);
83
+ return { line: isObj(parsed) && parsed.type !== 'api-request-blob' ? parsed : null, bad: false };
84
+ }
85
+ catch {
86
+ return { line: null, bad: true };
87
+ }
88
+ };
89
+ /** The first pass: every call and its answer. Text is read only to settle a status; nothing of it leaves this function. */
90
+ export const readCalls = async (file, agent, redact, seen) => {
91
+ const read = { found: [], removals: [], unread: [], pipes: [], calls: 0, fresh: 0, badLines: 0 };
92
+ let position = -1;
93
+ let permission = 'unknown';
94
+ const readUse = (line, block) => {
95
+ read.calls += 1;
96
+ const id = stringOf(block.id);
97
+ if (id !== null && seen.has(id))
98
+ return;
99
+ read.fresh += 1;
100
+ const check = checkCall({ tool: stringOf(block.name) ?? '', input: block.input }, redact);
101
+ if (check.crossings.length === 0 && check.unread.length === 0 && check.removals.length === 0) {
102
+ // Seen all the same, so a transcript that replays this call does not count it twice.
103
+ if (id !== null)
104
+ seen.set(id, NO_ENTRY);
105
+ return;
106
+ }
107
+ const ts = stringOf(line.timestamp);
108
+ const side = agent !== null || line.isSidechain === true;
109
+ const found = check.crossings.map((crossing) => ({ crossing, ts, cwd: stringOf(line.cwd), toolUseId: id, permission, call: read.calls, line: position, side, status: 'none', miss: 'none' }));
110
+ const removals = check.removals.map((removal) => ({ removal, ts, status: 'none' }));
111
+ read.found.push(...found);
112
+ read.removals.push(...removals);
113
+ read.unread.push(...check.unread);
114
+ if (check.unread.includes('pipe-to-shell'))
115
+ read.pipes.push(read.calls);
116
+ if (id !== null)
117
+ seen.set(id, { found, removals });
118
+ };
119
+ for await (const raw of logLines(file)) {
120
+ position += 1;
121
+ const { line, bad } = parseLine(raw);
122
+ if (bad)
123
+ read.badLines += 1;
124
+ if (line === null)
125
+ continue;
126
+ const mode = stringOf(line.permissionMode);
127
+ if (mode !== null)
128
+ permission = permissionOf(mode);
129
+ for (const block of blocksOf(line)) {
130
+ if (block.type === 'tool_use')
131
+ readUse(line, block);
132
+ else if (block.type === 'tool_result') {
133
+ const entry = seen.get(stringOf(block.tool_use_id) ?? '');
134
+ if (entry !== undefined)
135
+ settle(entry, block);
136
+ }
137
+ }
138
+ }
139
+ return read;
140
+ };
141
+ const REPO_TOOLS = new Set(['Read', 'NotebookRead', 'Grep', 'Glob', 'LS']);
142
+ const WEB_TOOLS = new Set(['WebFetch', 'WebSearch']);
143
+ const resultSource = (tool) => {
144
+ if (tool === undefined)
145
+ return 'tool-output';
146
+ if (REPO_TOOLS.has(tool))
147
+ return 'repo-file';
148
+ if (WEB_TOOLS.has(tool))
149
+ return 'web';
150
+ return tool === 'Skill' ? 'skill' : 'tool-output';
151
+ };
152
+ /** A meta line is a skill's or a command's body. A leading tag the harness injects is the harness speaking, not the operator. */
153
+ const textSource = (line, text) => {
154
+ if (line.isMeta === true)
155
+ return 'skill';
156
+ const tag = LEADING_TAG.exec(text)?.[1];
157
+ return tag !== undefined && INJECTED_TAGS.has(tag) ? 'harness' : 'prompt';
158
+ };
159
+ const sourcedTexts = (line, tools) => {
160
+ if (line.type === 'attachment')
161
+ return isObj(line.attachment) ? [['harness', JSON.stringify(line.attachment)]] : [];
162
+ if (line.type === 'assistant') {
163
+ for (const block of blocksOf(line)) {
164
+ const id = stringOf(block.id);
165
+ const name = stringOf(block.name);
166
+ if (block.type === 'tool_use' && id !== null && name !== null)
167
+ tools.set(id, name);
168
+ }
169
+ return [];
170
+ }
171
+ if (line.type !== 'user')
172
+ return [];
173
+ const content = isObj(line.message) ? line.message.content : undefined;
174
+ if (typeof content === 'string')
175
+ return [[textSource(line, content), content]];
176
+ return blocksOf(line).flatMap((block) => {
177
+ if (block.type === 'tool_result')
178
+ return [[resultSource(tools.get(stringOf(block.tool_use_id) ?? '')), resultText(block)]];
179
+ const text = block.type === 'text' ? stringOf(block.text) : null;
180
+ return text === null ? [] : [[textSource(line, text), text]];
181
+ });
182
+ };
183
+ const canon = (name) => name.toLowerCase().replace(/[-_.]+/g, '-');
184
+ const patternOf = (name) => name
185
+ .split(/[-_.]+/)
186
+ .map((part) => part.replace(/[\\^$.*+?()[\]{}|/]/g, '\\$&'))
187
+ .join('[-_.]+');
188
+ export const readOrigins = async (file, firstLines, servers) => {
189
+ const origins = new Map();
190
+ const prompted = new Set();
191
+ const byCanon = new Map();
192
+ for (const name of firstLines.keys())
193
+ byCanon.set(canon(name), [...(byCanon.get(canon(name)) ?? []), name]);
194
+ const alternatives = [...firstLines.keys()]
195
+ .sort((one, two) => two.length - one.length)
196
+ .map(patternOf)
197
+ .join('|');
198
+ // Not inside a longer word or a path: `lib` is neither `lib-extra` nor `other/lib`, while `/lib` is how a skill is typed.
199
+ // An MCP server reaches the agent only as `mcp__<server>__<tool>`, which that rule refuses (`_` joins words), so that spelling is a mention too,
200
+ // but only of a name that crossed as a server, so a listed server that was never called lends its name to nothing; origins are keyed by name, so a package that also crossed as that server in the same transcript shares its origin. With no server the branch is
201
+ // left out, since an empty alternation matches everywhere.
202
+ const serverNames = [...servers]
203
+ .sort((one, two) => two.length - one.length)
204
+ .map(patternOf)
205
+ .join('|');
206
+ const serverBranch = serverNames === '' ? '' : `|(?<=(?<!\\w)mcp__)(?:${serverNames})(?=__)`;
207
+ const words = new RegExp(`(?<![\\w@.-])(?<![\\w.-]/)(?:${alternatives})(?![\\w-]|\\.\\w)${serverBranch}`, 'gi');
208
+ const last = Math.max(...firstLines.values());
209
+ const tools = new Map();
210
+ let position = -1;
211
+ for await (const raw of logLines(file)) {
212
+ position += 1;
213
+ if (position >= last)
214
+ break;
215
+ const { line } = parseLine(raw);
216
+ if (line === null)
217
+ continue;
218
+ for (const [source, text] of sourcedTexts(line, tools)) {
219
+ for (const match of text.matchAll(words)) {
220
+ for (const name of byCanon.get(canon(match[0])) ?? []) {
221
+ if (position >= (firstLines.get(name) ?? 0))
222
+ continue;
223
+ if (!origins.has(name))
224
+ origins.set(name, source);
225
+ if (source === 'prompt')
226
+ prompted.add(name);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ return { origins, prompted };
232
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ // The one entry file the installed hooks and the nightly task run: `node hook-bin.js <session-start|pre-tool|post-tool|stop|nightly>`, with the
2
+ // hook event JSON on stdin. It imports only the store modules (not the CLI), so a call costs little more than the node start, and it
3
+ // always exits 0 with no output on any failure: a hook must never fail or block a session.
4
+ import { homedir } from 'node:os';
5
+ import { HOOK_EVENTS, hookRuntime, runHook } from './sessions/hooks.js';
6
+ const readStdin = async () => {
7
+ process.stdin.setEncoding('utf8');
8
+ let text = '';
9
+ for await (const chunk of process.stdin)
10
+ text += String(chunk);
11
+ return text;
12
+ };
13
+ const eventOf = (name) => HOOK_EVENTS.find((event) => event === name);
14
+ try {
15
+ const event = eventOf(process.argv[2]);
16
+ if (event !== undefined) {
17
+ const output = await runHook(event, event === 'nightly' ? '' : await readStdin(), hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), Date.now);
18
+ if (output !== undefined)
19
+ process.stdout.write(output);
20
+ }
21
+ }
22
+ catch {
23
+ // Fail-open.
24
+ }
25
+ process.exitCode = 0;
@@ -0,0 +1,16 @@
1
+ import type { RunDeps } from './cli.js';
2
+ /**
3
+ * The production Keycloak realm (the platform's k8s/configmap.yaml: KEYCLOAK_ISSUER_URI). It ships in the CLI, reviewed with it:
4
+ * the launch flag below only switches it on, so a compromised site cannot send logins anywhere else.
5
+ */
6
+ export declare const PRODUCTION_ISSUER_URL = "https://login.manyoars.com/realms/aor";
7
+ /** The marketing site serves `{"launched": true}` here from launch day. */
8
+ export declare const LAUNCH_FLAG_URL = "https://manyoars.com/.well-known/oars.json";
9
+ /**
10
+ * The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If the flag is on but that
11
+ * issuer does not answer discovery (no answer, an error status, or a body that is not discovery JSON), the platform is not
12
+ * really open yet, so say what `login` says before launch (exit 2). An issuer the developer named keeps its own error.
13
+ */
14
+ export declare const discoverIssuer: <T>(deps: RunDeps, flag: string | undefined, discover: () => Promise<T>) => Promise<T>;
15
+ /** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer once the launch flag is on. */
16
+ export declare const resolveIssuerUrl: (deps: RunDeps, flag?: string) => Promise<string>;
package/dist/issuer.js ADDED
@@ -0,0 +1,55 @@
1
+ import { CliError, ExitCode } from './errors.js';
2
+ /**
3
+ * The production Keycloak realm (the platform's k8s/configmap.yaml: KEYCLOAK_ISSUER_URI). It ships in the CLI, reviewed with it:
4
+ * the launch flag below only switches it on, so a compromised site cannot send logins anywhere else.
5
+ */
6
+ export const PRODUCTION_ISSUER_URL = 'https://login.manyoars.com/realms/aor';
7
+ /** The marketing site serves `{"launched": true}` here from launch day. */
8
+ export const LAUNCH_FLAG_URL = 'https://manyoars.com/.well-known/oars.json';
9
+ const FLAG_TIMEOUT_MS = 5000;
10
+ const NOT_LAUNCHED = 'Many Oars has not launched yet: pass --issuer-url or set OARS_ISSUER_URL.';
11
+ /** True only for a 200 answer whose JSON is an object with `launched: true`; every other answer, or none, means not launched. */
12
+ const hasLaunched = async (deps) => {
13
+ try {
14
+ const response = await fetch(LAUNCH_FLAG_URL, {
15
+ headers: { 'User-Agent': `oars/${deps.version}`, Accept: 'application/json' },
16
+ redirect: 'error',
17
+ signal: AbortSignal.timeout(FLAG_TIMEOUT_MS),
18
+ });
19
+ if (!response.ok)
20
+ return false;
21
+ const body = await response.json();
22
+ return typeof body === 'object' && body !== null && Reflect.get(body, 'launched') === true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ };
28
+ /**
29
+ * The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If the flag is on but that
30
+ * issuer does not answer discovery (no answer, an error status, or a body that is not discovery JSON), the platform is not
31
+ * really open yet, so say what `login` says before launch (exit 2). An issuer the developer named keeps its own error.
32
+ */
33
+ export const discoverIssuer = async (deps, flag, discover) => {
34
+ try {
35
+ return await discover();
36
+ }
37
+ catch (error) {
38
+ const named = flag !== undefined || (deps.env.OARS_ISSUER_URL !== undefined && deps.env.OARS_ISSUER_URL !== '');
39
+ const unanswered = error instanceof CliError && (error.exitCode === ExitCode.UNREACHABLE || error.exitCode === ExitCode.UNEXPECTED);
40
+ if (!named && unanswered)
41
+ throw new CliError(NOT_LAUNCHED, ExitCode.USAGE, 'usage');
42
+ throw error;
43
+ }
44
+ };
45
+ /** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer once the launch flag is on. */
46
+ export const resolveIssuerUrl = async (deps, flag) => {
47
+ if (flag !== undefined)
48
+ return flag;
49
+ const fromEnv = deps.env.OARS_ISSUER_URL;
50
+ if (fromEnv !== undefined && fromEnv !== '')
51
+ return fromEnv;
52
+ if (await hasLaunched(deps))
53
+ return PRODUCTION_ISSUER_URL;
54
+ throw new CliError(NOT_LAUNCHED, ExitCode.USAGE, 'usage');
55
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { spawn } from 'node:child_process';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { setTimeout as sleep } from 'node:timers/promises';
7
+ import { openBrowser } from './browser.js';
8
+ import { run } from './cli.js';
9
+ import { defaultConfigDir, fileCredentialStore } from './credentials.js';
10
+ import { cursorRoot } from './sessions/cursor.js';
11
+ import { claudeEvaluator, runClaude } from './sessions/gold-arms.js';
12
+ import { SOURCES } from './sessions/sources.js';
13
+ /** The version in this package's package.json (one folder up from dist/). */
14
+ const readVersion = () => {
15
+ const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
16
+ const version = typeof manifest === 'object' && manifest !== null ? Reflect.get(manifest, 'version') : undefined;
17
+ return typeof version === 'string' ? version : '0.0.0';
18
+ };
19
+ const configDir = defaultConfigDir(process.env, process.platform, homedir());
20
+ // exitCode rather than process.exit(), so a piped stdout is flushed before the process ends.
21
+ process.exitCode = await run(process.argv.slice(2), {
22
+ env: process.env,
23
+ io: {
24
+ stdout: (text) => process.stdout.write(text),
25
+ stderr: (text) => process.stderr.write(text),
26
+ },
27
+ store: fileCredentialStore(configDir),
28
+ version: readVersion(),
29
+ sleep: (ms) => sleep(ms),
30
+ random: Math.random,
31
+ now: Date.now,
32
+ openBrowser,
33
+ install: {
34
+ nodePath: process.execPath,
35
+ hookBin: fileURLToPath(new URL('./hook-bin.js', import.meta.url)),
36
+ userSettings: join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'settings.json'),
37
+ cwd: process.cwd(),
38
+ schtasks: (args) => new Promise((resolve) => {
39
+ let output = '';
40
+ const child = spawn('schtasks', args, { windowsHide: true });
41
+ child.stdout.on('data', (chunk) => {
42
+ output += chunk.toString();
43
+ });
44
+ child.stderr.on('data', (chunk) => {
45
+ output += chunk.toString();
46
+ });
47
+ child.on('error', (error) => {
48
+ resolve({ code: 1, output: error.message });
49
+ });
50
+ child.on('close', (code) => {
51
+ resolve({ code: code ?? 1, output });
52
+ });
53
+ }),
54
+ },
55
+ sessions: {
56
+ storeDir: join(configDir, 'oars', 'sessions'),
57
+ projectsRoot: join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'projects'),
58
+ cursorRoot: cursorRoot(process.platform, process.env),
59
+ evaluator: claudeEvaluator(runClaude, process.env),
60
+ sourceRoots: Object.fromEntries(SOURCES.map((source) => [source.name, source.root(process.platform, process.env, homedir())])),
61
+ stdin: process.stdin,
62
+ stdout: process.stdout,
63
+ },
64
+ });
package/dist/oidc.js CHANGED
@@ -21,7 +21,7 @@ const keycloakFetch = ({ version, step, sleep, random }, isTokenEndpoint) => {
21
21
  response = await (isTokenEndpoint(url) ? fetch(request) : retrying(request));
22
22
  }
23
23
  catch (error) {
24
- throw unreachableError(step, error);
24
+ throw unreachableError(step, error, new URL(url).origin);
25
25
  }
26
26
  const { status } = response;
27
27
  if (status === 429 || status === 502 || status === 503 || status === 504) {
@@ -0,0 +1,35 @@
1
+ import type { Harness } from './schema.js';
2
+ export declare const CANONICAL_ACTIONS: readonly ["file.read", "file.edit", "file.write", "file.search", "shell.exec", "web.search", "web.fetch", "mcp.call", "skill.run", "agent.spawn", "plan.update", "ask.user", "recall.pull", "recall.push", "other"];
3
+ export type CanonicalAction = (typeof CANONICAL_ACTIONS)[number];
4
+ export interface ActionTable {
5
+ /** Native name to action. A row may say `other`: that is a known public name with no canonical action, so it keeps its name. */
6
+ rows: {
7
+ readonly [native: string]: CanonicalAction;
8
+ };
9
+ /** How this harness spells an MCP call, with named groups `server` and `tool`; null when it has no such calls. */
10
+ mcp: RegExp | null;
11
+ /** Whether that spelling marks where the server name ends. One that cannot may have read a private server's name as the tool, so the tool stays out of class P. */
12
+ toolDelimited: boolean;
13
+ /** Native shell tool to the shell it runs, for the frame; a tool not listed does not say which shell it is. */
14
+ shells: {
15
+ readonly [native: string]: string;
16
+ };
17
+ }
18
+ export declare const ACTION_TABLES: {
19
+ readonly [harness in Harness]: ActionTable;
20
+ };
21
+ /** The MCP call a native name spells, in full, as the transcript wrote it; undefined for any other name. */
22
+ export declare const mcpOf: (table: ActionTable, native: string) => {
23
+ server: string;
24
+ tool: string;
25
+ } | undefined;
26
+ /**
27
+ * What a native name becomes on a step. Rows win over the MCP pattern, so a harness can name a recall that travels over MCP. `native` is the
28
+ * name itself only when a row lists it; an MCP name holds a server name, so `callIdentity` decides whether it is shown.
29
+ */
30
+ export declare const actionOf: (table: ActionTable, native: string) => {
31
+ action: CanonicalAction;
32
+ native: string;
33
+ };
34
+ /** The shell a native tool runs, or null when the table does not say. */
35
+ export declare const shellOf: (table: ActionTable, native: string) => string | null;