@manyoars/oars 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +82 -32
  2. package/dist/bin.js +3 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +206 -20
  5. package/dist/club.d.ts +19 -0
  6. package/dist/club.js +132 -0
  7. package/dist/gate/hook-fill-main.js +1 -1
  8. package/dist/gate/hook-fill.d.ts +1 -1
  9. package/dist/gate/hook-fill.js +1 -1
  10. package/dist/gate/hook-main.js +7 -5
  11. package/dist/gate/hook.d.ts +12 -1
  12. package/dist/gate/hook.js +38 -7
  13. package/dist/hook-bin.js +23 -4
  14. package/dist/issuer.d.ts +1 -1
  15. package/dist/issuer.js +2 -2
  16. package/dist/main.js +5 -0
  17. package/dist/node-floor.d.ts +10 -0
  18. package/dist/node-floor.js +23 -0
  19. package/dist/oars-hook.d.ts +2 -0
  20. package/dist/oars-hook.js +7 -0
  21. package/dist/sessions/allow.d.ts +1 -0
  22. package/dist/sessions/allow.js +1 -1
  23. package/dist/sessions/call-shape.d.ts +111 -0
  24. package/dist/sessions/call-shape.js +464 -0
  25. package/dist/sessions/candidates.d.ts +65 -0
  26. package/dist/sessions/candidates.js +236 -0
  27. package/dist/sessions/card-command.d.ts +24 -0
  28. package/dist/sessions/card-command.js +71 -0
  29. package/dist/sessions/card-font.d.ts +6 -0
  30. package/dist/sessions/card-font.js +36 -0
  31. package/dist/sessions/card-line.d.ts +30 -0
  32. package/dist/sessions/card-line.js +105 -0
  33. package/dist/sessions/card-render.d.ts +15 -0
  34. package/dist/sessions/card-render.js +146 -0
  35. package/dist/sessions/card.d.ts +59 -0
  36. package/dist/sessions/card.js +107 -0
  37. package/dist/sessions/cell-increments.d.ts +14 -3
  38. package/dist/sessions/cell-increments.js +79 -12
  39. package/dist/sessions/cells.d.ts +42 -4
  40. package/dist/sessions/cells.js +72 -18
  41. package/dist/sessions/codex-hook-bin.d.ts +1 -0
  42. package/dist/sessions/codex-hook-bin.js +24 -0
  43. package/dist/sessions/codex-hook-install.d.ts +21 -0
  44. package/dist/sessions/codex-hook-install.js +47 -0
  45. package/dist/sessions/codex-hook-view.d.ts +20 -0
  46. package/dist/sessions/codex-hook-view.js +20 -0
  47. package/dist/sessions/codex-hook.d.ts +77 -0
  48. package/dist/sessions/codex-hook.js +255 -0
  49. package/dist/sessions/codex.d.ts +9 -0
  50. package/dist/sessions/codex.js +104 -18
  51. package/dist/sessions/command.d.ts +17 -2
  52. package/dist/sessions/command.js +145 -33
  53. package/dist/sessions/copilot.d.ts +3 -0
  54. package/dist/sessions/copilot.js +12 -4
  55. package/dist/sessions/derive.js +2 -1
  56. package/dist/sessions/empty-state.d.ts +17 -0
  57. package/dist/sessions/empty-state.js +38 -0
  58. package/dist/sessions/exclusion.js +48 -3
  59. package/dist/sessions/extract.d.ts +16 -3
  60. package/dist/sessions/extract.js +281 -24
  61. package/dist/sessions/gemini.d.ts +2 -0
  62. package/dist/sessions/gemini.js +29 -2
  63. package/dist/sessions/harness-files.d.ts +27 -0
  64. package/dist/sessions/harness-files.js +109 -0
  65. package/dist/sessions/head-only.d.ts +7 -0
  66. package/dist/sessions/head-only.js +21 -0
  67. package/dist/sessions/hook-calls.js +4 -3
  68. package/dist/sessions/hooks.d.ts +1 -1
  69. package/dist/sessions/hooks.js +6 -2
  70. package/dist/sessions/identity.d.ts +6 -0
  71. package/dist/sessions/injected.d.ts +13 -0
  72. package/dist/sessions/injected.js +212 -0
  73. package/dist/sessions/install.d.ts +31 -20
  74. package/dist/sessions/install.js +80 -82
  75. package/dist/sessions/load-status.d.ts +41 -0
  76. package/dist/sessions/load-status.js +156 -0
  77. package/dist/sessions/machine-child-deps.d.ts +5 -0
  78. package/dist/sessions/machine-child-deps.js +83 -0
  79. package/dist/sessions/machine-child.d.ts +43 -0
  80. package/dist/sessions/machine-child.js +120 -0
  81. package/dist/sessions/machine-credential.d.ts +10 -0
  82. package/dist/sessions/machine-credential.js +20 -0
  83. package/dist/sessions/machine-enroll.d.ts +47 -0
  84. package/dist/sessions/machine-enroll.js +230 -0
  85. package/dist/sessions/machine-hold.d.ts +49 -0
  86. package/dist/sessions/machine-hold.js +88 -0
  87. package/dist/sessions/machine-ids.d.ts +6 -0
  88. package/dist/sessions/machine-ids.js +9 -0
  89. package/dist/sessions/machine-notice.d.ts +21 -0
  90. package/dist/sessions/machine-notice.js +89 -0
  91. package/dist/sessions/machine-pull.d.ts +40 -0
  92. package/dist/sessions/machine-pull.js +220 -0
  93. package/dist/sessions/machine-repo.d.ts +7 -0
  94. package/dist/sessions/machine-repo.js +30 -0
  95. package/dist/sessions/machine-scan-cache.d.ts +12 -0
  96. package/dist/sessions/machine-scan-cache.js +47 -0
  97. package/dist/sessions/machine-send.d.ts +12 -0
  98. package/dist/sessions/machine-send.js +156 -0
  99. package/dist/sessions/machine-stop.d.ts +22 -0
  100. package/dist/sessions/machine-stop.js +53 -0
  101. package/dist/sessions/machine.d.ts +41 -0
  102. package/dist/sessions/machine.js +89 -0
  103. package/dist/sessions/opencode.d.ts +2 -0
  104. package/dist/sessions/opencode.js +6 -0
  105. package/dist/sessions/otel.d.ts +21 -0
  106. package/dist/sessions/otel.js +109 -38
  107. package/dist/sessions/owner-only.d.ts +7 -0
  108. package/dist/sessions/owner-only.js +34 -0
  109. package/dist/sessions/pi.d.ts +13 -1
  110. package/dist/sessions/pi.js +27 -13
  111. package/dist/sessions/pool-books.d.ts +1 -1
  112. package/dist/sessions/pool-books.js +1 -1
  113. package/dist/sessions/pool-choices.d.ts +4 -2
  114. package/dist/sessions/pool-choices.js +4 -2
  115. package/dist/sessions/pool-command.d.ts +46 -6
  116. package/dist/sessions/pool-command.js +577 -133
  117. package/dist/sessions/pool-delta.d.ts +7 -0
  118. package/dist/sessions/pool-delta.js +28 -5
  119. package/dist/sessions/pool-due.d.ts +43 -0
  120. package/dist/sessions/pool-due.js +113 -0
  121. package/dist/sessions/pool-nightly.d.ts +9 -1
  122. package/dist/sessions/pool-nightly.js +65 -21
  123. package/dist/sessions/pool-outbox.d.ts +51 -5
  124. package/dist/sessions/pool-outbox.js +217 -24
  125. package/dist/sessions/pool-print.d.ts +1 -1
  126. package/dist/sessions/pool-raw.js +4 -3
  127. package/dist/sessions/pool-replica.d.ts +4 -4
  128. package/dist/sessions/pool-replica.js +26 -7
  129. package/dist/sessions/pool-send-lock.js +1 -1
  130. package/dist/sessions/pool-sent.d.ts +2 -0
  131. package/dist/sessions/pool-sent.js +21 -4
  132. package/dist/sessions/pool-wire.d.ts +172 -12
  133. package/dist/sessions/pool-wire.js +284 -30
  134. package/dist/sessions/pool.js +4 -2
  135. package/dist/sessions/public-vocabulary.d.ts +5 -0
  136. package/dist/sessions/public-vocabulary.js +16 -0
  137. package/dist/sessions/read-call-shape.js +12 -6
  138. package/dist/sessions/read-fit.d.ts +5 -0
  139. package/dist/sessions/read-fit.js +23 -2
  140. package/dist/sessions/read.d.ts +1 -1
  141. package/dist/sessions/recall-consequence.d.ts +2 -2
  142. package/dist/sessions/recall-consequence.js +14 -9
  143. package/dist/sessions/recall-object.d.ts +9 -2
  144. package/dist/sessions/receipt.d.ts +8 -0
  145. package/dist/sessions/receipt.js +78 -0
  146. package/dist/sessions/recovery-calls.d.ts +3 -2
  147. package/dist/sessions/recovery-calls.js +9 -3
  148. package/dist/sessions/redact.d.ts +2 -2
  149. package/dist/sessions/redact.js +116 -14
  150. package/dist/sessions/reversion.d.ts +8 -0
  151. package/dist/sessions/reversion.js +114 -0
  152. package/dist/sessions/schema.d.ts +78 -4
  153. package/dist/sessions/schema.js +31 -1
  154. package/dist/sessions/session-shape.d.ts +2 -1
  155. package/dist/sessions/session-shape.js +15 -0
  156. package/dist/sessions/sources.d.ts +9 -0
  157. package/dist/sessions/sources.js +16 -5
  158. package/dist/sessions/steps.js +3 -2
  159. package/dist/sessions/store.d.ts +41 -2
  160. package/dist/sessions/store.js +68 -16
  161. package/dist/sessions/try-demo.d.ts +8 -0
  162. package/dist/sessions/try-demo.js +90 -0
  163. package/dist/sessions/try.d.ts +13 -0
  164. package/dist/sessions/try.js +50 -0
  165. package/dist/sessions/vscode-copilot.d.ts +4 -0
  166. package/dist/sessions/vscode-copilot.js +25 -18
  167. package/dist/telemetry/command.js +12 -6
  168. package/dist/telemetry/consent.d.ts +9 -3
  169. package/dist/telemetry/consent.js +25 -9
  170. package/dist/telemetry/fields.d.ts +11 -0
  171. package/dist/telemetry/fields.js +56 -0
  172. package/dist/telemetry/measures.d.ts +13 -0
  173. package/dist/telemetry/measures.js +30 -0
  174. package/dist/telemetry/observe.d.ts +15 -2
  175. package/dist/telemetry/observe.js +34 -7
  176. package/dist/telemetry/region.d.ts +6 -0
  177. package/dist/telemetry/region.js +28 -0
  178. package/dist/telemetry/report.js +1 -1
  179. package/dist/telemetry/schema.d.ts +3 -0
  180. package/dist/telemetry/schema.js +11 -1
  181. package/dist/telemetry/send.d.ts +5 -2
  182. package/dist/telemetry/send.js +15 -7
  183. package/dist/telemetry/words.d.ts +6 -0
  184. package/dist/telemetry/words.js +6 -0
  185. package/package.json +23 -4
@@ -6,10 +6,19 @@ export declare const crossingsPath: (storeDir: string) => string;
6
6
  export declare const registryCachePath: (storeDir: string) => string;
7
7
  /** The registry ask is cut at this many ms on the process clock, less a write reserve; `hook-main.ts` ends the process a short grace after it, so the whole call is under a second. */
8
8
  export declare const DEADLINE_MS = 800;
9
+ /** The environment variable a test sets to give a built hook more than `DEADLINE_MS`, so a loaded machine that spends the budget starting node still reaches the code under test. */
10
+ export declare const DEADLINE_ENV = "OARS_GATE_HOOK_DEADLINE_MS";
11
+ /** `DEADLINE_MS`, or the value of `DEADLINE_ENV` when it is a whole number of ms above it; nothing else (a lower, empty, or malformed value) moves the deadline. */
12
+ export declare const deadlineOf: (env: NodeJS.ProcessEnv) => number;
9
13
  /** A registry answer the hook kept is used for this many days. A `latest` is read "as of the crossing", so an older one is asked again. */
10
14
  export declare const HOOK_REGISTRY_DAYS = 1;
11
15
  /** A not-found answer is trusted this long: a name can be published any minute, and a block rests on the answer. */
12
16
  export declare const NOT_FOUND_MS = 600000;
17
+ /** The environment variable a test sets to start the hand-off earlier than `HANDOFF_BUDGET_MS` before the deadline, so a case with a long deadline reaches the hand-off at a time the test names. */
18
+ export declare const HANDOFF_MARK_ENV = "OARS_GATE_HOOK_HANDOFF_MARK_MS";
19
+ /** When a registry ask still open starts the hand-off to a helper, on the process clock: `HANDOFF_BUDGET_MS` before the deadline, so the lock and the helper's blocking spawn fit even when the machine is slow.
20
+ * The value of `HANDOFF_MARK_ENV` moves it earlier when it is a whole number of ms above 0 and below that; nothing else (empty, malformed, zero, or later) moves it. */
21
+ export declare const handOffMarkOf: (env: NodeJS.ProcessEnv, deadlineMs: number) => number;
13
22
  export interface GateHookDeps {
14
23
  storeDir: string;
15
24
  now: () => number;
@@ -17,13 +26,15 @@ export interface GateHookDeps {
17
26
  clock: () => number;
18
27
  /** The whole hook ends by this many ms on `clock`. */
19
28
  deadlineMs: number;
29
+ /** When an ask still open starts the hand-off to a helper, in ms on `clock`. Absent: `HANDOFF_BUDGET_MS` before `deadlineMs`. */
30
+ handOffMarkMs?: number;
20
31
  /** The draw for the holdback, in [0, 1). Absent: `Math.random`. */
21
32
  random?: () => number;
22
33
  /** Asks the package manager. Absent: no question is ever asked. */
23
34
  registry?: RegistryReader;
24
35
  /** Appends text to a file. Replaceable so a full disk can be tried. */
25
36
  append?: (file: string, text: string) => Promise<void>;
26
- /** Called with the name whose ask was cut or had no time to start, so a helper can finish it and fill the cache for the next sight. Absent: nothing is finished. */
37
+ /** Called with the name whose ask is still open at the hand-off mark, or had no time to start, so a helper can finish it and fill the cache for the next sight. Absent: nothing is finished. */
27
38
  fill?: (ask: FillAsk) => Promise<void>;
28
39
  /** Whether a helper already holds the name, which keeps the hook from asking the manager a second time. Absent: never locked. */
29
40
  locked?: (ask: FillAsk) => Promise<boolean>;
package/dist/gate/hook.js CHANGED
@@ -20,6 +20,13 @@ export const crossingsPath = (storeDir) => join(storeDir, 'crossings.jsonl');
20
20
  export const registryCachePath = (storeDir) => join(storeDir, 'gate-registry.jsonl');
21
21
  /** The registry ask is cut at this many ms on the process clock, less a write reserve; `hook-main.ts` ends the process a short grace after it, so the whole call is under a second. */
22
22
  export const DEADLINE_MS = 800;
23
+ /** The environment variable a test sets to give a built hook more than `DEADLINE_MS`, so a loaded machine that spends the budget starting node still reaches the code under test. */
24
+ export const DEADLINE_ENV = 'OARS_GATE_HOOK_DEADLINE_MS';
25
+ /** `DEADLINE_MS`, or the value of `DEADLINE_ENV` when it is a whole number of ms above it; nothing else (a lower, empty, or malformed value) moves the deadline. */
26
+ export const deadlineOf = (env) => {
27
+ const given = Number(env[DEADLINE_ENV]);
28
+ return Number.isInteger(given) && given > DEADLINE_MS ? given : DEADLINE_MS;
29
+ };
23
30
  /** A registry answer the hook kept is used for this many days. A `latest` is read "as of the crossing", so an older one is asked again. */
24
31
  export const HOOK_REGISTRY_DAYS = 1;
25
32
  /** A not-found answer is trusted this long: a name can be published any minute, and a block rests on the answer. */
@@ -28,6 +35,17 @@ export const NOT_FOUND_MS = 600_000;
28
35
  const WRITE_RESERVE_MS = 150;
29
36
  /** One call hands at most this many names to helpers; the rest wait for a later sight, so a call naming dozens cannot start dozens of processes. */
30
37
  const MAX_HELPERS = 3;
38
+ /** What the hand-off to a helper is given before the deadline: the lock, then a blocking spawn of the helper. A loaded machine took up to about 400 ms for it. */
39
+ const HANDOFF_BUDGET_MS = 400;
40
+ /** The environment variable a test sets to start the hand-off earlier than `HANDOFF_BUDGET_MS` before the deadline, so a case with a long deadline reaches the hand-off at a time the test names. */
41
+ export const HANDOFF_MARK_ENV = 'OARS_GATE_HOOK_HANDOFF_MARK_MS';
42
+ /** When a registry ask still open starts the hand-off to a helper, on the process clock: `HANDOFF_BUDGET_MS` before the deadline, so the lock and the helper's blocking spawn fit even when the machine is slow.
43
+ * The value of `HANDOFF_MARK_ENV` moves it earlier when it is a whole number of ms above 0 and below that; nothing else (empty, malformed, zero, or later) moves it. */
44
+ export const handOffMarkOf = (env, deadlineMs) => {
45
+ const latest = deadlineMs - HANDOFF_BUDGET_MS;
46
+ const given = Number(env[HANDOFF_MARK_ENV]);
47
+ return Number.isInteger(given) && given > 0 && given < latest ? given : latest;
48
+ };
31
49
  /** An ask with less than this left is not started: it could not finish. */
32
50
  const MIN_ASK_MS = 100;
33
51
  const isInput = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -95,22 +113,35 @@ const registryAnswer = async (deps, crossing, cwd, kept, helpers, hold) => {
95
113
  return undefined;
96
114
  const stop = new AbortController();
97
115
  let answer;
116
+ const hand = { off: false };
117
+ const handOff = async () => {
118
+ if (helpers.started >= MAX_HELPERS)
119
+ return;
120
+ helpers.started += 1;
121
+ hand.off = true;
122
+ await deps.fill?.(ask).catch(() => undefined);
123
+ };
98
124
  if (budgetOf(deps) >= MIN_ASK_MS) {
99
125
  // The first sight on a fresh store has no folder yet, and the deadline's synchronous write needs one: made here, before the ask, so a cut-off hook can still leave its row.
100
126
  await mkdir(deps.storeDir, { recursive: true }).catch(() => undefined);
101
127
  hold(() => { stop.abort(); });
102
128
  const pending = deps.registry({ ...ask, signal: stop.signal });
103
- // Starting the manager is a blocking spawn that can take a good part of the budget, so the cut counts what is left now, not what was left before the call: read before it, the cut
129
+ // Starting the manager is a blocking spawn that can take a good part of the budget, so the marks count what is left now, not what was left before the call: read before it, the cut
104
130
  // could land behind the process's own deadline, which ends the process with the ask still running.
105
- answer = await within(pending, budgetOf(deps));
131
+ // An answer before the mark never pays for a helper; an ask that starts after the mark hands off at once. An ask still open at the mark has the hand-off started beside it (the lock and the helper's spawn, which also block), early enough that a
132
+ // loaded machine still finishes it inside the deadline; the cut after that drops nothing, because the helper already holds the name.
133
+ answer = await within(pending, (deps.handOffMarkMs ?? deps.deadlineMs - HANDOFF_BUDGET_MS) - deps.clock());
134
+ if (answer === undefined) {
135
+ await handOff();
136
+ answer = await within(pending, budgetOf(deps));
137
+ }
106
138
  }
107
139
  if (answer === undefined) {
108
- // Not answered in time: the row reads not-read, and a helper finishes the ask so the next sight reads it from the cache. This ask is stopped first, so the manager is never asked twice at once.
140
+ // Not answered in time: the row reads not-read, and the helper (started at the mark, or here when there was no time to reach it) finishes the ask so the next sight reads it from the cache.
141
+ // This ask is stopped first, so the hook's manager does not outlive the hook.
109
142
  stop.abort();
110
- if (helpers.started < MAX_HELPERS) {
111
- helpers.started += 1;
112
- await deps.fill?.(ask).catch(() => undefined);
113
- }
143
+ if (!hand.off)
144
+ await handOff();
114
145
  return undefined;
115
146
  }
116
147
  return { answer, asked: true };
package/dist/hook-bin.js CHANGED
@@ -1,8 +1,9 @@
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
1
+ // The one entry file the installed hooks and the detached sender run: `node hook-bin.js <session-start|pre-tool|post-tool|stop|nightly>`, with the
2
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
3
  // always exits 0 with no output on any failure: a hook must never fail or block a session.
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
6
7
  import { hookEmitter } from './telemetry/hook-emit.js';
7
8
  import { sendNightly } from './telemetry/send.js';
8
9
  import { HOOK_EVENTS, hookRuntime, nightlyTelemetry, runHook } from './sessions/hooks.js';
@@ -24,20 +25,38 @@ const versionOf = () => {
24
25
  return '0.0.0';
25
26
  }
26
27
  };
27
- /** After the covenant is accepted, the nightly task sends what the pool outbox holds; with no server, sign-in, or acceptance it sends nothing. Loaded only here so the per-tool hooks never pay for it. It never throws. */
28
+ /** After the covenant is accepted, the detached sender (hook-bin.js nightly) sends what the pool outbox holds; with no server, sign-in, or acceptance it sends nothing. Loaded only here so the per-tool hooks never pay for it. It never throws. */
28
29
  const nightlyPool = async (event) => {
29
30
  const { nightlyPoolDeps } = await import('./sessions/pool-nightly-deps.js');
30
31
  const { nightlyPoolSend } = await import('./sessions/pool-nightly.js');
31
32
  await nightlyPoolSend(nightlyPoolDeps(process.env, hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), versionOf(), Date.now));
32
33
  };
33
34
  const eventOf = (name) => HOOK_EVENTS.find((event) => event === name);
35
+ /** The machine modes: `stop --machine` is the Stop of a cloud VM, a VPS, or a runner (a detached child does the work), and `machine-child` is that child. Neither is a local hook event. */
36
+ const machineModeOf = (argv) => (argv[2] === 'machine-child' ? 'child' : argv[2] === 'stop' && argv.includes('--machine') ? 'stop' : undefined);
34
37
  try {
35
- const event = eventOf(process.argv[2]);
38
+ const machineMode = machineModeOf(process.argv);
39
+ const event = machineMode === undefined ? eventOf(process.argv[2]) : undefined;
40
+ if (machineMode === 'stop') {
41
+ const { machineStop } = await import('./sessions/machine-stop.js');
42
+ const { spawn } = await import('node:child_process');
43
+ const { storeDir } = hookRuntime('stop', process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout });
44
+ machineStop({ env: process.env, stdin: await readStdin(), hookBin: fileURLToPath(import.meta.url), home: homedir(), storeDir, write: (text) => process.stdout.write(text), spawn });
45
+ }
46
+ if (machineMode === 'child') {
47
+ const { runMachineChild } = await import('./sessions/machine-child-deps.js');
48
+ await runMachineChild(process.env, hookRuntime('stop', process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), versionOf(), Date.now, process.argv[3] ?? '');
49
+ }
36
50
  if (event !== undefined) {
37
51
  const output = await runHook(event, event === 'nightly' ? '' : await readStdin(), hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), Date.now, nightlyTelemetry(event, process.env, process.platform, homedir(), versionOf, Date.now), hookEmitter(process.env, process.platform, homedir(), new URL('../package.json', import.meta.url), Date.now));
38
52
  if (output !== undefined)
39
53
  process.stdout.write(output);
40
- // The nightly task is where telemetry is sent from; it never throws, and it sends nothing unless telemetry is on and the notice was shown (the server is OARS_POOL_URL, else the Many Oars pool).
54
+ // The session just ended and its deposit is written: start a due sweep and send, detached, and do not wait for it (the hook stays sub-second and prints nothing more). Loaded only here so the per-tool hooks never pay for it.
55
+ if (event === 'stop') {
56
+ const { afterStop } = await import('./sessions/pool-due.js');
57
+ await afterStop(hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }).storeDir, fileURLToPath(import.meta.url));
58
+ }
59
+ // The detached sender is where telemetry is sent from; it never throws, and it sends nothing unless telemetry is on and the notice was shown (the server is OARS_POOL_URL, else the Many Oars pool).
41
60
  if (event === 'nightly') {
42
61
  await sendNightly(process.env, process.platform, homedir(), Date.now);
43
62
  await nightlyPool(event);
package/dist/issuer.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare const PRODUCTION_ISSUER_URL = "https://login.manyoars.com/realms/
7
7
  /**
8
8
  * The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If that issuer does not answer
9
9
  * discovery (no answer, an error status, or a body that is not discovery JSON), the platform is not up yet, so say so (exit 2) and
10
- * name the pool. An issuer the developer named keeps its own error.
10
+ * name the sign-in host. An issuer the developer named keeps its own error.
11
11
  */
12
12
  export declare const discoverIssuer: <T>(deps: RunDeps, flag: string | undefined, discover: () => Promise<T>) => Promise<T>;
13
13
  /** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer. */
package/dist/issuer.js CHANGED
@@ -4,11 +4,11 @@ import { CliError, ExitCode } from './errors.js';
4
4
  * and is the default for `oars login`: no site or file can send a login anywhere else.
5
5
  */
6
6
  export const PRODUCTION_ISSUER_URL = 'https://login.manyoars.com/realms/aor';
7
- const UNAVAILABLE = 'Cannot reach Many Oars sign-in at login.manyoars.com (Many Oars and its pool at pool.manyoars.com may not be deployed yet). Pass --issuer-url or set OARS_ISSUER_URL to use another issuer.';
7
+ const UNAVAILABLE = 'Cannot reach Many Oars sign-in at login.manyoars.com (it may not be deployed yet). Pass --issuer-url or set OARS_ISSUER_URL to use another issuer.';
8
8
  /**
9
9
  * The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If that issuer does not answer
10
10
  * discovery (no answer, an error status, or a body that is not discovery JSON), the platform is not up yet, so say so (exit 2) and
11
- * name the pool. An issuer the developer named keeps its own error.
11
+ * name the sign-in host. An issuer the developer named keeps its own error.
12
12
  */
13
13
  export const discoverIssuer = async (deps, flag, discover) => {
14
14
  try {
package/dist/main.js CHANGED
@@ -7,8 +7,10 @@ import { setTimeout as sleep } from 'node:timers/promises';
7
7
  import { openBrowser } from './browser.js';
8
8
  import { run } from './cli.js';
9
9
  import { defaultConfigDir, fileCredentialStore } from './credentials.js';
10
+ import { codexRoot } from './sessions/codex.js';
10
11
  import { cursorRoot } from './sessions/cursor.js';
11
12
  import { claudeEvaluator, runClaude } from './sessions/gold-arms.js';
13
+ import { catchUpOnStore } from './sessions/pool-due.js';
12
14
  import { SOURCES } from './sessions/sources.js';
13
15
  import { spawnDetachedSend } from './telemetry/send.js';
14
16
  /** The version in this package's package.json (one folder up from dist/). */
@@ -28,6 +30,7 @@ process.exitCode = await run(process.argv.slice(2), {
28
30
  store: fileCredentialStore(configDir),
29
31
  telemetry: { dir: join(configDir, 'oars', 'telemetry'), platform: process.platform, spawnSend: spawnDetachedSend },
30
32
  version: readVersion(),
33
+ catchUp: () => catchUpOnStore(join(configDir, 'oars', 'sessions'), fileURLToPath(new URL('./hook-bin.js', import.meta.url))),
31
34
  sleep: (ms) => sleep(ms),
32
35
  random: Math.random,
33
36
  now: Date.now,
@@ -38,6 +41,7 @@ process.exitCode = await run(process.argv.slice(2), {
38
41
  hookBin: fileURLToPath(new URL('./hook-bin.js', import.meta.url)),
39
42
  userSettings: join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'settings.json'),
40
43
  cwd: process.cwd(),
44
+ codex: { home: codexRoot(process.env), nodePath: process.execPath, hookBin: fileURLToPath(new URL('./sessions/codex-hook-bin.js', import.meta.url)) },
41
45
  schtasks: (args) => new Promise((resolve) => {
42
46
  let output = '';
43
47
  const child = spawn('schtasks', args, { windowsHide: true });
@@ -63,5 +67,6 @@ process.exitCode = await run(process.argv.slice(2), {
63
67
  sourceRoots: Object.fromEntries(SOURCES.map((source) => [source.name, source.root(process.platform, process.env, homedir())])),
64
68
  stdin: process.stdin,
65
69
  stdout: process.stdout,
70
+ stderr: process.stderr,
66
71
  },
67
72
  });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The oldest Node the CLI supports (`engines.node` in package.json): the latest 22.x when the floor leg of `.github/workflows/ci.yml`
3
+ * (`cli-node-floor`) passed the whole CLI suite on it (run 42783202609, `cli on Node 22.23.4`). The one proven cause below it is `node:sqlite`
4
+ * URI filenames (`sessions/opencode.ts:91`), which Node 22.13 rejected (t1087 run 42776737858).
5
+ */
6
+ export declare const MIN_NODE = "22.23.4";
7
+ /** One line for a Node older than the floor, or undefined when `version` (as in `process.versions.node`) is new enough. */
8
+ export declare const nodeFloorMessage: (version: string) => string | undefined;
9
+ /** Writes the one line plus a newline and exits 1 on a Node under the floor; does neither otherwise. `write` and `exit` are injected so a test can see both. */
10
+ export declare const exitIfTooOld: (version: string, write: (text: string) => unknown, exit: (code: number) => unknown) => void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The oldest Node the CLI supports (`engines.node` in package.json): the latest 22.x when the floor leg of `.github/workflows/ci.yml`
3
+ * (`cli-node-floor`) passed the whole CLI suite on it (run 42783202609, `cli on Node 22.23.4`). The one proven cause below it is `node:sqlite`
4
+ * URI filenames (`sessions/opencode.ts:91`), which Node 22.13 rejected (t1087 run 42776737858).
5
+ */
6
+ export const MIN_NODE = '22.23.4';
7
+ const parts = (version) => version.split('.').map((part) => Number.parseInt(part, 10));
8
+ const atLeast = (version, floor) => {
9
+ const have = parts(version);
10
+ const need = parts(floor);
11
+ const at = need.findIndex((value, index) => (have[index] ?? 0) !== value);
12
+ return at === -1 || (have[at] ?? 0) > (need[at] ?? 0);
13
+ };
14
+ /** One line for a Node older than the floor, or undefined when `version` (as in `process.versions.node`) is new enough. */
15
+ export const nodeFloorMessage = (version) => atLeast(version, MIN_NODE) ? undefined : `oars needs Node ${MIN_NODE} or newer; this is Node ${version}.`;
16
+ /** Writes the one line plus a newline and exits 1 on a Node under the floor; does neither otherwise. `write` and `exit` are injected so a test can see both. */
17
+ export const exitIfTooOld = (version, write, exit) => {
18
+ const tooOld = nodeFloorMessage(version);
19
+ if (tooOld !== undefined) {
20
+ write(`${tooOld}\n`);
21
+ exit(1);
22
+ }
23
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { exitIfTooOld } from './node-floor.js';
3
+ // The same one line and exit 1 as `oars` gives on a Node under the floor, before the hook program loads.
4
+ exitIfTooOld(process.versions.node, (text) => process.stderr.write(text), (code) => process.exit(code));
5
+ // The `oars-hook` executable: the host-independent command a machine's Stop and SessionEnd hooks run (`oars-hook stop --machine`, written by `oars sessions hook install --machine`). It is the same program as
6
+ // `hook-bin.js`, behind a name on PATH, so a settings file committed from one machine names no install path and still runs on a VM whose setup script installed the package globally.
7
+ await import('./hook-bin.js');
@@ -10,6 +10,7 @@ export interface Allow {
10
10
  }
11
11
  /** The prefix `--deny` saves an entry under. */
12
12
  export declare const DENY = "!";
13
+ export declare const isDenial: (entry: string) => boolean;
13
14
  export declare const denialOf: (entry: string) => string;
14
15
  export declare const compileAllow: (entries: readonly string[]) => Allow;
15
16
  /**
@@ -34,7 +34,7 @@ const matcherOf = (entry) => {
34
34
  };
35
35
  /** The prefix `--deny` saves an entry under. */
36
36
  export const DENY = '!';
37
- const isDenial = (entry) => entry.startsWith(DENY);
37
+ export const isDenial = (entry) => entry.startsWith(DENY);
38
38
  export const denialOf = (entry) => `${DENY}${entry}`;
39
39
  export const compileAllow = (entries) => {
40
40
  const compiled = entries.filter((entry) => !isDenial(entry)).map((entry) => ({ entry, matches: matcherOf(expandHome(entry)) }));
@@ -17,3 +17,114 @@ export declare const targetClassOf: (path: string | undefined, base: TargetBase
17
17
  export declare const argClassOf: (input: {
18
18
  readonly [key: string]: unknown;
19
19
  }) => ArgClass[];
20
+ /** The size of a value as written (its length as text, a number's digits included), as a closed list. A boolean and a null have none. */
21
+ export declare const VALUE_SIZES: readonly ["0", "1-8", "9-32", "33-128", "129-512", "513-2k", "2k-8k", "over-8k"];
22
+ export type ValueSize = (typeof VALUE_SIZES)[number];
23
+ /** How many folders a path value sits below the repository or the working folder; `outside` is an absolute path under neither, so the folders above them are never counted. */
24
+ export declare const PATH_DEPTHS: readonly ["0", "1", "2", "3-4", "5-8", "over-8", "outside"];
25
+ export type PathDepth = (typeof PATH_DEPTHS)[number];
26
+ /** The class of a path value's extension: `none` is a name with none (a folder, a `Makefile`). */
27
+ export declare const EXTENSION_CLASSES: readonly ["source", "doc", "config", "data", "image", "none", "other"];
28
+ export type ExtensionClass = (typeof EXTENSION_CLASSES)[number];
29
+ /** The kind of the folders a path value names: `root` is a value with no folder part. */
30
+ export declare const DIRECTORY_KINDS: readonly ["root", "source", "test", "doc", "generated", "hidden", "other"];
31
+ export type DirectoryKind = (typeof DIRECTORY_KINDS)[number];
32
+ /** What a pattern value uses, as a sorted set: `literal` is a pattern that uses none of the rest. A regular expression and a glob share the list. */
33
+ export declare const PATTERN_FEATURES: readonly ["alternation", "anchor", "class", "escape", "group", "literal", "quantifier", "recursive", "wildcard"];
34
+ export type PatternFeature = (typeof PATTERN_FEATURES)[number];
35
+ /** One argument's features (layer 1): its key and the closed-list facts about its value, never the value. `pathDepth`, `extension`, and `dirKind` are on a path argument only, `patternFeatures` on a pattern only. */
36
+ export interface ArgFeature {
37
+ key: string;
38
+ valueSize: ValueSize;
39
+ pathDepth?: PathDepth;
40
+ extension?: ExtensionClass;
41
+ dirKind?: DirectoryKind;
42
+ patternFeatures?: PatternFeature[];
43
+ }
44
+ /**
45
+ * The features of each argument of a call (layer 1), sorted by key: its value's size, and for a path its depth below the repository root (else the working folder), its extension class, and its folder kind, and for a pattern what it uses. A Glob call's
46
+ * `pattern` and a `glob`, `include`, or `exclude` argument read as globs; any other pattern as a regular expression. A boolean and a null have no features (a boolean goes as a closed value, `argValuesOf`). The values are read
47
+ * here and go no further: every field is a member of a closed list.
48
+ */
49
+ export declare const argFeaturesOf: (native: string, input: {
50
+ readonly [key: string]: unknown;
51
+ }, root: string | undefined, cwd?: string) => ArgFeature[];
52
+ /** One argument's closed value (layer 1): a boolean as `true` or `false`, or a member of the closed list its tool pins for the key (`CLOSED_ARG_VALUES`). A value off the list is not one, so it is never sent. */
53
+ export interface ArgValue {
54
+ key: string;
55
+ enumValue: string;
56
+ }
57
+ /** The closed values of a call's arguments, sorted by key: every boolean, and every string its tool's list for the key holds. A custom agent's name is off the list, so it is not here. */
58
+ export declare const argValuesOf: (native: string, input: {
59
+ readonly [key: string]: unknown;
60
+ }, closed: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>) => ArgValue[];
61
+ /** What kind of name a token is: a file's name, a folder's name below the session root, an environment variable's name, a flag's value that is a name, or a custom agent's name. */
62
+ export declare const NAME_KINDS: readonly ["file", "dir", "env", "flag", "agent"];
63
+ export type NameKind = (typeof NAME_KINDS)[number];
64
+ /** One name token (layer 2): the kind and the name as it was written. The wire sends it through its one gate (`wireName`). */
65
+ export interface ArgName {
66
+ kind: NameKind;
67
+ name: string;
68
+ }
69
+ /** A word of a shell command as the tokenizer read it. */
70
+ export interface CommandWord {
71
+ text: string;
72
+ quoted: boolean;
73
+ /** The variable the whole word references: set only when the word's raw source, quotes included, is exactly one reference (`variableOf`). Every other word holds none, whatever `$` it contains. */
74
+ variable?: string;
75
+ /** The word starts at or after the command's opaque point (`tokenize`): no variable, assignment, or flag value is read from it. */
76
+ opaque?: true;
77
+ /** The word's raw source starts with an unquoted `NAME=` or `--flag=` and holds a double quote after it (`tokenize`): `quoted` is true, but the name and the joined value are read as for an unquoted word. */
78
+ joined?: true;
79
+ }
80
+ /** A shell command's words as the name read takes them: those before the program (assignments, wrappers), the program's own word, and its arguments. */
81
+ export interface CommandWords {
82
+ before: readonly CommandWord[];
83
+ head: CommandWord;
84
+ args: readonly CommandWord[];
85
+ }
86
+ /**
87
+ * The one rule for a variable reference: a word names a variable only when its raw source, quotes included, is exactly one reference: `$NAME`, `${NAME}`, or a PowerShell `$env:NAME` or scope form (`$script:NAME`, `$global:NAME`),
88
+ * bare or in double quotes. Every other word is literal: a `$` inside a longer word, a quote, a command substitution, an escape, and `%NAME%` (no POSIX shell or PowerShell expands it) name nothing, which sends less and never more.
89
+ * The tokenizer reads each word through this.
90
+ */
91
+ export declare const variableOf: (raw: string) => string | undefined;
92
+ /**
93
+ * The name tokens of a call (layer 2), read from the values layer 1 reads: the file basename of each path value and the directory names of its segments below the root (else the working folder), never a segment above it, so an account
94
+ * or home folder is not here; the variable names a shell command assigns (`VAR=` before its program), references (`$VAR`, `${VAR}`), or an `env` object holds; a flag value that is a name; and a custom agent's `subagent_type`.
95
+ * Never a value: an assignment's value, a message, a path, and a number are read for the name and go no further. Sorted by kind, then name, and single. The wire sends each through its one gate.
96
+ */
97
+ export declare const argNamesOf: (native: string, input: {
98
+ readonly [key: string]: unknown;
99
+ }, root: string | undefined, cwd: string | undefined, commands: readonly CommandWords[], closed: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>, restored?: readonly string[]) => ArgName[];
100
+ /** What a literal of a command becomes in its template: a closed list, so a template holds no argument text. `<flag>` is a flag-like word whose name is not spelled (an attached value, a cluster off the public list). */
101
+ export declare const TEMPLATE_SLOTS: readonly ["<text>", "<path>", "<ident>", "<num>", "<url>", "<var>", "<flag>"];
102
+ export type TemplateSlot = (typeof TEMPLATE_SLOTS)[number];
103
+ /** A call's command templates, one per command in the order they run (`templateOf`). Sent through the wire's one gate, which drops a template whose kept word fails it. */
104
+ export declare const templatesOf: (commands: readonly CommandWords[], redact: (text: string) => string) => string[];
105
+ /** What an edit did, as a closed set: the shape of the change and never its text. Written in this order on the wire. */
106
+ export declare const DIFF_SHAPES: readonly ["import-added", "import-removed", "identifier-renamed", "lines-added", "lines-removed", "lines-changed", "whitespace-only", "file-written"];
107
+ export type DiffShape = (typeof DIFF_SHAPES)[number];
108
+ /**
109
+ * What a call that edits a file did, as a closed set (layer 3), never its text: a `Write` is `file-written`; an `Edit` or each entry of a `MultiEdit` is read from its old and new text (`shapesOfEdit`), and the call has the union, in the list's order.
110
+ * A call that is no edit, or an edit that changes nothing, has none.
111
+ */
112
+ export declare const diffShapeOf: (tool: string, input: {
113
+ readonly [key: string]: unknown;
114
+ }) => DiffShape[];
115
+ /**
116
+ * What changed from the previous call of the same tool in the same prompt, as a closed set (layer 1): `same` when nothing did; else any of a key added, a key removed, a kept key's value whose features or closed value
117
+ * changed (`feature-changed`), a kept key's value that changed while its features did not (`value-changed`), and the target (`target-changed`). The first call of a tool in a prompt has none.
118
+ */
119
+ export declare const CHANGE_KINDS: readonly ["same", "key-added", "key-removed", "feature-changed", "value-changed", "target-changed"];
120
+ export type ChangeKind = (typeof CHANGE_KINDS)[number];
121
+ /** A call as the change read compares it: its raw input and target, held only while capture reads the transcript, with the features and closed values computed from them. */
122
+ export interface ComparedCall {
123
+ input: {
124
+ readonly [key: string]: unknown;
125
+ };
126
+ target: string | null;
127
+ features: readonly ArgFeature[];
128
+ values: readonly ArgValue[];
129
+ }
130
+ export declare const changeKindOf: (before: ComparedCall, after: ComparedCall) => ChangeKind[];