@manyoars/oars 0.4.1 → 0.5.2

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 (188) hide show
  1. package/README.md +79 -29
  2. package/dist/bin.js +3 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +207 -21
  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/main.js +5 -0
  15. package/dist/node-floor.d.ts +10 -0
  16. package/dist/node-floor.js +23 -0
  17. package/dist/oars-hook.d.ts +2 -0
  18. package/dist/oars-hook.js +7 -0
  19. package/dist/sessions/allow.d.ts +1 -0
  20. package/dist/sessions/allow.js +1 -1
  21. package/dist/sessions/call-shape.d.ts +111 -0
  22. package/dist/sessions/call-shape.js +464 -0
  23. package/dist/sessions/candidates.d.ts +65 -0
  24. package/dist/sessions/candidates.js +236 -0
  25. package/dist/sessions/card-command.d.ts +24 -0
  26. package/dist/sessions/card-command.js +71 -0
  27. package/dist/sessions/card-font.d.ts +6 -0
  28. package/dist/sessions/card-font.js +36 -0
  29. package/dist/sessions/card-line.d.ts +30 -0
  30. package/dist/sessions/card-line.js +105 -0
  31. package/dist/sessions/card-render.d.ts +15 -0
  32. package/dist/sessions/card-render.js +146 -0
  33. package/dist/sessions/card.d.ts +59 -0
  34. package/dist/sessions/card.js +107 -0
  35. package/dist/sessions/cell-increments.d.ts +14 -3
  36. package/dist/sessions/cell-increments.js +79 -12
  37. package/dist/sessions/cells.d.ts +42 -4
  38. package/dist/sessions/cells.js +72 -18
  39. package/dist/sessions/codex-hook-bin.d.ts +1 -0
  40. package/dist/sessions/codex-hook-bin.js +24 -0
  41. package/dist/sessions/codex-hook-install.d.ts +21 -0
  42. package/dist/sessions/codex-hook-install.js +47 -0
  43. package/dist/sessions/codex-hook-view.d.ts +20 -0
  44. package/dist/sessions/codex-hook-view.js +20 -0
  45. package/dist/sessions/codex-hook.d.ts +77 -0
  46. package/dist/sessions/codex-hook.js +255 -0
  47. package/dist/sessions/codex.d.ts +9 -0
  48. package/dist/sessions/codex.js +104 -18
  49. package/dist/sessions/command.d.ts +17 -2
  50. package/dist/sessions/command.js +145 -33
  51. package/dist/sessions/copilot.d.ts +3 -0
  52. package/dist/sessions/copilot.js +12 -4
  53. package/dist/sessions/derive.js +2 -1
  54. package/dist/sessions/empty-state.d.ts +17 -0
  55. package/dist/sessions/empty-state.js +38 -0
  56. package/dist/sessions/exclusion.js +48 -3
  57. package/dist/sessions/extract.d.ts +16 -3
  58. package/dist/sessions/extract.js +281 -24
  59. package/dist/sessions/gemini.d.ts +2 -0
  60. package/dist/sessions/gemini.js +29 -2
  61. package/dist/sessions/harness-files.d.ts +27 -0
  62. package/dist/sessions/harness-files.js +109 -0
  63. package/dist/sessions/head-only.d.ts +7 -0
  64. package/dist/sessions/head-only.js +21 -0
  65. package/dist/sessions/hook-calls.js +4 -3
  66. package/dist/sessions/hooks.d.ts +1 -1
  67. package/dist/sessions/hooks.js +6 -2
  68. package/dist/sessions/identity.d.ts +6 -0
  69. package/dist/sessions/injected.d.ts +13 -0
  70. package/dist/sessions/injected.js +260 -0
  71. package/dist/sessions/install.d.ts +31 -20
  72. package/dist/sessions/install.js +80 -82
  73. package/dist/sessions/load-status.d.ts +41 -0
  74. package/dist/sessions/load-status.js +156 -0
  75. package/dist/sessions/machine-child-deps.d.ts +5 -0
  76. package/dist/sessions/machine-child-deps.js +83 -0
  77. package/dist/sessions/machine-child.d.ts +43 -0
  78. package/dist/sessions/machine-child.js +120 -0
  79. package/dist/sessions/machine-credential.d.ts +10 -0
  80. package/dist/sessions/machine-credential.js +20 -0
  81. package/dist/sessions/machine-enroll.d.ts +47 -0
  82. package/dist/sessions/machine-enroll.js +230 -0
  83. package/dist/sessions/machine-hold.d.ts +49 -0
  84. package/dist/sessions/machine-hold.js +88 -0
  85. package/dist/sessions/machine-ids.d.ts +6 -0
  86. package/dist/sessions/machine-ids.js +9 -0
  87. package/dist/sessions/machine-notice.d.ts +21 -0
  88. package/dist/sessions/machine-notice.js +89 -0
  89. package/dist/sessions/machine-pull.d.ts +40 -0
  90. package/dist/sessions/machine-pull.js +220 -0
  91. package/dist/sessions/machine-repo.d.ts +7 -0
  92. package/dist/sessions/machine-repo.js +30 -0
  93. package/dist/sessions/machine-scan-cache.d.ts +12 -0
  94. package/dist/sessions/machine-scan-cache.js +47 -0
  95. package/dist/sessions/machine-send.d.ts +12 -0
  96. package/dist/sessions/machine-send.js +156 -0
  97. package/dist/sessions/machine-stop.d.ts +22 -0
  98. package/dist/sessions/machine-stop.js +53 -0
  99. package/dist/sessions/machine.d.ts +41 -0
  100. package/dist/sessions/machine.js +89 -0
  101. package/dist/sessions/opencode.d.ts +2 -0
  102. package/dist/sessions/opencode.js +6 -0
  103. package/dist/sessions/otel.d.ts +21 -0
  104. package/dist/sessions/otel.js +109 -38
  105. package/dist/sessions/owner-only.d.ts +7 -0
  106. package/dist/sessions/owner-only.js +34 -0
  107. package/dist/sessions/pi.d.ts +13 -1
  108. package/dist/sessions/pi.js +27 -13
  109. package/dist/sessions/pool-books.d.ts +1 -1
  110. package/dist/sessions/pool-books.js +1 -1
  111. package/dist/sessions/pool-choices.d.ts +4 -2
  112. package/dist/sessions/pool-choices.js +4 -2
  113. package/dist/sessions/pool-command.d.ts +46 -6
  114. package/dist/sessions/pool-command.js +613 -136
  115. package/dist/sessions/pool-delta.d.ts +7 -0
  116. package/dist/sessions/pool-delta.js +28 -5
  117. package/dist/sessions/pool-due.d.ts +43 -0
  118. package/dist/sessions/pool-due.js +113 -0
  119. package/dist/sessions/pool-nightly.d.ts +9 -1
  120. package/dist/sessions/pool-nightly.js +65 -21
  121. package/dist/sessions/pool-outbox.d.ts +51 -5
  122. package/dist/sessions/pool-outbox.js +217 -24
  123. package/dist/sessions/pool-print.d.ts +1 -1
  124. package/dist/sessions/pool-raw.js +4 -3
  125. package/dist/sessions/pool-replica.d.ts +4 -4
  126. package/dist/sessions/pool-replica.js +26 -7
  127. package/dist/sessions/pool-send-lock.js +1 -1
  128. package/dist/sessions/pool-sent.d.ts +2 -0
  129. package/dist/sessions/pool-sent.js +21 -4
  130. package/dist/sessions/pool-wire.d.ts +176 -15
  131. package/dist/sessions/pool-wire.js +297 -33
  132. package/dist/sessions/pool.js +4 -2
  133. package/dist/sessions/public-vocabulary.d.ts +5 -0
  134. package/dist/sessions/public-vocabulary.js +16 -0
  135. package/dist/sessions/read-call-shape.js +12 -6
  136. package/dist/sessions/read-fit.d.ts +5 -0
  137. package/dist/sessions/read-fit.js +23 -2
  138. package/dist/sessions/read-interaction.js +1 -1
  139. package/dist/sessions/read-lead-lag.js +1 -1
  140. package/dist/sessions/read-readable.js +1 -1
  141. package/dist/sessions/read.d.ts +1 -1
  142. package/dist/sessions/recall-consequence.d.ts +2 -2
  143. package/dist/sessions/recall-consequence.js +14 -9
  144. package/dist/sessions/recall-object.d.ts +9 -2
  145. package/dist/sessions/receipt.d.ts +8 -0
  146. package/dist/sessions/receipt.js +78 -0
  147. package/dist/sessions/recovery-calls.d.ts +3 -2
  148. package/dist/sessions/recovery-calls.js +9 -3
  149. package/dist/sessions/redact.d.ts +11 -2
  150. package/dist/sessions/redact.js +201 -23
  151. package/dist/sessions/repo-root.d.ts +7 -0
  152. package/dist/sessions/repo-root.js +17 -13
  153. package/dist/sessions/reversion.d.ts +8 -0
  154. package/dist/sessions/reversion.js +114 -0
  155. package/dist/sessions/schema.d.ts +78 -4
  156. package/dist/sessions/schema.js +31 -1
  157. package/dist/sessions/session-shape.d.ts +2 -1
  158. package/dist/sessions/session-shape.js +15 -0
  159. package/dist/sessions/sources.d.ts +9 -0
  160. package/dist/sessions/sources.js +16 -5
  161. package/dist/sessions/steps.js +3 -2
  162. package/dist/sessions/store.d.ts +41 -2
  163. package/dist/sessions/store.js +68 -16
  164. package/dist/sessions/try-demo.d.ts +8 -0
  165. package/dist/sessions/try-demo.js +90 -0
  166. package/dist/sessions/try.d.ts +13 -0
  167. package/dist/sessions/try.js +50 -0
  168. package/dist/sessions/vscode-copilot.d.ts +4 -0
  169. package/dist/sessions/vscode-copilot.js +25 -18
  170. package/dist/telemetry/command.js +12 -6
  171. package/dist/telemetry/consent.d.ts +9 -3
  172. package/dist/telemetry/consent.js +25 -9
  173. package/dist/telemetry/fields.d.ts +11 -0
  174. package/dist/telemetry/fields.js +56 -0
  175. package/dist/telemetry/measures.d.ts +13 -0
  176. package/dist/telemetry/measures.js +30 -0
  177. package/dist/telemetry/observe.d.ts +15 -2
  178. package/dist/telemetry/observe.js +34 -7
  179. package/dist/telemetry/region.d.ts +6 -0
  180. package/dist/telemetry/region.js +28 -0
  181. package/dist/telemetry/report.js +1 -1
  182. package/dist/telemetry/schema.d.ts +3 -0
  183. package/dist/telemetry/schema.js +11 -1
  184. package/dist/telemetry/send.d.ts +5 -2
  185. package/dist/telemetry/send.js +15 -7
  186. package/dist/telemetry/words.d.ts +6 -0
  187. package/dist/telemetry/words.js +6 -0
  188. package/package.json +24 -5
@@ -0,0 +1,109 @@
1
+ // Per harness, how many session files the readers saw, how many yielded records, and how many were in a form the reader could not parse.
2
+ // A harness whose log format moved then shows up as a count an operator can read, not as quietly thinner data. The counts are read from the
3
+ // harness's own store on this machine and sent nowhere. This file is the one place the three counts are defined; `sessions verify` prints them.
4
+ import { readFile, stat } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { compileAllow } from './allow.js';
7
+ import { captureSession } from './extract.js';
8
+ import { holdsOnlyKinds } from './head-only.js';
9
+ import { makeRedactor } from './redact.js';
10
+ import { listDir, mainTranscript, projectFolders, readState, sessionFolders, sourceRoots, sourcesOf, subagentTranscripts } from './store.js';
11
+ /** What the docs and the tests quote: where each kind of file goes, so `seen` is the sum of the outcomes. */
12
+ export const FILE_COUNT_RULES = 'A file counts once. It is seen when the reader opened it and it is not empty; seen files either yielded (at least one user or assistant turn came out) or are unreadable ' +
13
+ '(the read failed, or the text gave no turn, torn or with a record of a kind the reader does not know). An empty file (nothing in it to parse), a file with no turn whose ' +
14
+ 'records are all head records its own format defines (a session started and quit), and a file skipped by a rule (an id that is not a safe file name, a revert folded into ' +
15
+ 'its thread, a session folded into another) are not seen. An OpenTelemetry session none of whose spans (any span of its trace) holds a `gen_ai.*` attribute or a name leading with a GenAI operation (ordinary HTTP spans) is a known form with no turn and is not seen; one with a renamed `gen_ai.*` attribute on any span stays unreadable. A Codex rollout whose only turn-like records are assistant `response_item` messages stays unreadable. A file a reader dropped before it was a session (a Gemini chat that holds nothing, an OpenTelemetry file with no span found) is seen and unreadable when it held records in a shape the reader does not know. Where a store is a database, a session is the unit.';
16
+ /** `records` is undefined when the read itself failed; `headOnly` is the reader's word that a file with no turn holds only head records. */
17
+ export const outcomeOf = (file) => {
18
+ if (file.records === undefined)
19
+ return 'unreadable';
20
+ if (file.size === 0)
21
+ return 'empty';
22
+ if (file.records > 0)
23
+ return 'yielded';
24
+ return file.headOnly === true ? 'empty' : 'unreadable';
25
+ };
26
+ /** The Claude Code record types that carry no turn by design: a compaction summary, a file-history snapshot, the permission mode, an attachment, a system notice, and the cost state. A transcript of these alone is a session started and quit. */
27
+ const CLAUDE_HEAD_KINDS = new Set(['summary', 'file-history-snapshot', 'permission-mode', 'attachment', 'system', 'cost-state']);
28
+ export const tally = (outcomes) => {
29
+ const yielded = outcomes.filter((outcome) => outcome === 'yielded').length;
30
+ const unreadable = outcomes.filter((outcome) => outcome === 'unreadable').length;
31
+ return { seen: yielded + unreadable, yielded, unreadable };
32
+ };
33
+ const NO_ONE = makeRedactor([]);
34
+ /** The turns a transcript's text gives once the reader's lines go through the same capture the deposit runs; nothing is stored. */
35
+ const recordsOf = (text, harness, subagent) => {
36
+ const { session } = captureSession({ text, session: 'count', project: 'count', rev: 0, at: new Date(0).toISOString(), bytes: Buffer.byteLength(text), redact: NO_ONE, harness, ...(subagent === undefined ? {} : { subagent }) });
37
+ return session.userTurns + session.assistantTurns;
38
+ };
39
+ const claudeOutcomes = async (runtime, allows) => {
40
+ const outcomes = [];
41
+ for (const project of (await projectFolders(runtime.projectsRoot)).filter(allows)) {
42
+ const folder = join(runtime.projectsRoot, project);
43
+ const mains = (await listDir(folder)).filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')).map((entry) => mainTranscript(folder, entry.name));
44
+ for (const transcript of [...mains, ...(await subagentTranscripts(folder, await sessionFolders(folder)))]) {
45
+ try {
46
+ const size = (await stat(transcript.file)).size;
47
+ const text = await readFile(transcript.file, 'utf8');
48
+ outcomes.push(outcomeOf({ size, records: recordsOf(text, 'claude-code', transcript.subagent), headOnly: holdsOnlyKinds(text, CLAUDE_HEAD_KINDS) }));
49
+ }
50
+ catch {
51
+ outcomes.push('unreadable');
52
+ }
53
+ }
54
+ }
55
+ return outcomes;
56
+ };
57
+ const sessionOutcome = async (store, session, harness) => {
58
+ try {
59
+ const records = recordsOf(await store.transcript(session), harness, session.subagent);
60
+ // Asked only of a file that gave no turn: the reader is not read twice for a file that yielded.
61
+ return outcomeOf({ size: session.size, records, headOnly: records === 0 && (await store.headOnly?.(session)) === true });
62
+ }
63
+ catch {
64
+ return 'unreadable';
65
+ }
66
+ };
67
+ /**
68
+ * The counts for every harness the store reads that has files on this machine, and that the saved allowlist lets a deposit read. A harness with nothing
69
+ * to read has no entry. Read-only: it opens each store as the deposit does, reads each transcript, and writes nothing.
70
+ */
71
+ export const harnessFileCounts = async (runtime) => {
72
+ const allow = compileAllow((await readState(runtime.storeDir)).allow);
73
+ const found = {};
74
+ const claude = await claudeOutcomes(runtime, allow.allows);
75
+ if (claude.length > 0)
76
+ found['claude-code'] = tally(claude);
77
+ const roots = sourceRoots(runtime);
78
+ for (const source of sourcesOf(runtime)) {
79
+ const root = roots.get(source.name);
80
+ if (root === undefined || !allow.allows(source.name))
81
+ continue;
82
+ const outcomes = [];
83
+ try {
84
+ const store = await source.open(root);
85
+ if (store === undefined)
86
+ continue;
87
+ try {
88
+ // A session folded into another is no file of its own; an item the store could not inspect is one unreadable file.
89
+ outcomes.push(...store.unreadable.map(() => 'unreadable'));
90
+ // Files the reader dropped before they were sessions while they held records in a shape it does not know: seen, and unreadable.
91
+ for (let dropped = 0; dropped < (store.shapeMoved ?? 0); dropped += 1)
92
+ outcomes.push('unreadable');
93
+ for (const session of store.sessions.filter((one) => one.supersededBy === undefined))
94
+ outcomes.push(await sessionOutcome(store, session, source.harness));
95
+ }
96
+ finally {
97
+ store.close();
98
+ }
99
+ }
100
+ catch {
101
+ // A store that cannot be opened is one unreadable item, as the deposit reports it.
102
+ outcomes.push('unreadable');
103
+ }
104
+ const counts = tally(outcomes);
105
+ if (counts.seen > 0)
106
+ found[source.harness] = counts;
107
+ }
108
+ return found;
109
+ };
@@ -0,0 +1,7 @@
1
+ /** True when the text has at least one line and every line is a JSON object that `accepts`. */
2
+ export declare const holdsOnlyRecords: (text: string, accepts: (record: {
3
+ type: string;
4
+ [key: string]: unknown;
5
+ }) => boolean) => boolean;
6
+ /** True when the text has at least one line and every line is a JSON object whose `type` is one of `kinds`. */
7
+ export declare const holdsOnlyKinds: (text: string, kinds: ReadonlySet<string>) => boolean;
@@ -0,0 +1,21 @@
1
+ // A transcript file whose every record is a head record its harness's own format defines (the session header, the model in force) and that has no turn in
2
+ // it: a session started and quit. `sessions verify` counts such a file as empty, not unreadable. A torn line, a non-object line, or a record of any other
3
+ // type makes the file something else, so a format that moved still shows as unreadable.
4
+ /** True when the text has at least one line and every line is a JSON object that `accepts`. */
5
+ export const holdsOnlyRecords = (text, accepts) => {
6
+ const lines = text.split('\n').filter((raw) => raw.trim() !== '');
7
+ if (lines.length === 0)
8
+ return false;
9
+ return lines.every((raw) => {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ return typeof parsed === 'object' && parsed !== null && 'type' in parsed && typeof parsed.type === 'string' && accepts({ ...parsed, type: parsed.type });
18
+ });
19
+ };
20
+ /** True when the text has at least one line and every line is a JSON object whose `type` is one of `kinds`. */
21
+ export const holdsOnlyKinds = (text, kinds) => holdsOnlyRecords(text, (record) => kinds.has(record.type));
@@ -1,14 +1,15 @@
1
1
  // The hook counters: `hook-calls.jsonl`, one line per hook invocation, appended by the hook and read by `measure`.
2
- import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { appendFile, readFile } from 'node:fs/promises';
3
+ import { mkdirOwnerOnly } from './owner-only.js';
3
4
  import { paths } from './store.js';
4
5
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
5
6
  const eventOf = (value) => {
6
- if (value === 'session-start' || value === 'prompt' || value === 'pre-tool' || value === 'post-tool' || value === 'stop' || value === 'nightly')
7
+ if (value === 'session-start' || value === 'prompt' || value === 'pre-tool' || value === 'post-tool' || value === 'stop' || value === 'nightly' || value === 'codex-record')
7
8
  return value;
8
9
  return undefined;
9
10
  };
10
11
  export const appendHookCall = async (storeDir, call) => {
11
- await mkdir(storeDir, { recursive: true });
12
+ await mkdirOwnerOnly(storeDir);
12
13
  await appendFile(paths(storeDir).hookCalls, `${JSON.stringify(call)}\n`, { mode: 0o600 });
13
14
  };
14
15
  export const readHookCalls = async (storeDir) => {
@@ -2,7 +2,7 @@ import { type Emit } from '../telemetry/emit.js';
2
2
  import type { HookCall } from './schema.js';
3
3
  import { type SessionsRuntime } from './store.js';
4
4
  import type { NightlyTelemetry } from '../telemetry/health.js';
5
- export type HookEvent = HookCall['event'];
5
+ export type HookEvent = Exclude<HookCall['event'], 'codex-record'>;
6
6
  export declare const HOOK_EVENTS: readonly HookEvent[];
7
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
8
  export declare const hookRuntime: (event: HookEvent, env: {
@@ -1,7 +1,7 @@
1
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
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
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';
4
+ import { dirname, join } from 'node:path';
5
5
  import { performance } from 'node:perf_hooks';
6
6
  import { defaultConfigDir } from '../credentials.js';
7
7
  import { noEmit } from '../telemetry/emit.js';
@@ -10,6 +10,7 @@ import { cursorRoot } from './cursor.js';
10
10
  import { SOURCES, sourceLabel } from './sources.js';
11
11
  import { ACTION_TABLES, actionOf } from './actions.js';
12
12
  import { errorCodeOf, signatureOf } from './extract.js';
13
+ import { recordCandidates } from './candidates.js';
13
14
  import { appendHookCall } from './hook-calls.js';
14
15
  import { contextBlock, readIndex } from './hook-index.js';
15
16
  import { bundleNext, frameKeyOf, lastTable, mergePositions, positionsRecall, recoveryDecision, recoveryRecall, signatureRecall, isSignatureRetry } from './recall-next.js';
@@ -62,6 +63,9 @@ const positionsTable = async (runtime, input, now, index, repeat) => {
62
63
  };
63
64
  const sessionStart = async (runtime, input, now) => {
64
65
  const cwd = str(input.cwd);
66
+ const session = str(input.session_id);
67
+ if (session !== undefined && cwd !== undefined)
68
+ await recordCandidates(runtime.storeDir, { session, cwd, claudeDir: dirname(runtime.projectsRoot), at: new Date(now()).toISOString() });
65
69
  const index = await readIndex(runtime.storeDir);
66
70
  const block = index === undefined || cwd === undefined ? undefined : contextBlock(index, { cwd, top: TOP, maxChars: MAX_CHARS });
67
71
  const recall = await positionsTable(runtime, input, now, index, false);
@@ -271,7 +275,7 @@ export const runHook = async (event, stdin, runtime, now, telemetry, emit = noEm
271
275
  latencyMs: ms,
272
276
  outcome: failed ? 'error' : recall?.arm === 'held' ? 'held' : outcome.hit ? 'hit' : 'miss',
273
277
  bytes: Buffer.byteLength(outcome.output ?? ''),
274
- // The nightly is the scheduled task, not a harness's hook: it names no harness.
278
+ // The nightly is the detached sender (hook-bin.js nightly), not a harness's hook: it names no harness.
275
279
  ...frameWords(event === 'nightly' ? undefined : recall?.frame?.harness ?? HOOK_HARNESS, recall?.frame?.harnessVersion ?? stated?.harnessVersion, recall?.frame?.model ?? outcome.model ?? stated?.model),
276
280
  });
277
281
  // 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.
@@ -5,6 +5,12 @@ import type { PullIdentity, ShellIdentity, StepIdentity, ToolPull } from './sche
5
5
  export interface WordOfCommand {
6
6
  text: string;
7
7
  quoted: boolean;
8
+ /** The variable the whole word references, set only when its raw source is exactly one reference (`$NAME`, `"${NAME}"`); a `$` anywhere else in a word is text. */
9
+ variable?: string;
10
+ /** The word starts at or after the command's opaque point (`tokenize`), where a misread quote can turn literal text into words: the name read takes no variable, assignment, or flag value from it. */
11
+ opaque?: true;
12
+ /** The word's raw source starts with an unquoted `NAME=` or `--flag=` and holds a double quote after it (`API_REGION="eu"`, `--region="eu"`): `quoted` is true, but the text up to the `=` is as written, so the name and the joined value are read as for an unquoted word. Never set on a word that holds a single quote or a substitution after the `=`, since those are where literal text begins. */
13
+ joined?: true;
8
14
  }
9
15
  export declare const NUMBER_FLAG = "-N";
10
16
  export declare const isPositional: ({ text, quoted }: WordOfCommand) => boolean;
@@ -0,0 +1,13 @@
1
+ export type Json = string | number | boolean | null | Json[] | JsonObject;
2
+ export interface JsonObject {
3
+ [key: string]: Json;
4
+ }
5
+ /** One transcript line, decoded; the same object back when nothing in it was an injected block. */
6
+ export type InjectedRule = (line: JsonObject) => JsonObject;
7
+ /**
8
+ * Adds to `known` the normalized text of what `line` holds that a request blob repeats without the flag that marked it: the text of a meta message
9
+ * and the rendered copy of an injected attachment. The redactor runs this over a whole transcript before it marks any line.
10
+ */
11
+ export declare const learnInjected: (line: JsonObject, known: Set<string>) => Set<string>;
12
+ /** The rule for one machine: every injected block of a decoded transcript line becomes `[injected <kind> <hash>]`, hashed under `key`; `known` is what `learnInjected` collected from the transcript. */
13
+ export declare const injectedRule: (key: string, known?: ReadonlySet<string>) => InjectedRule;
@@ -0,0 +1,260 @@
1
+ // The blocks a harness writes into a transcript on its own account: instruction files (`CLAUDE.md`, `AGENTS.md`), the memory index, skill and
2
+ // tool lists, hook-added context, environment and session context, and the system prompt of a request. Every transcript carries them, and
3
+ // they are the operator's standing instructions rather than anything typed in a session, so a raw copy holds a marker in their place:
4
+ // `[injected <kind> <hash>]`, the hash a keyed hash of the block's whitespace-normalized text. The same block gives the same hash in every
5
+ // session of a machine (books can still count which instruction set a session ran under), and the key stays on the machine.
6
+ //
7
+ // A block is replaced only where the harness puts one: a record type or attachment type it owns, a leading `<system-reminder>` of a user
8
+ // message, a `<system-reminder>` in a tool result, or the text of a meta message. What an operator typed is left to the other rules. A block the
9
+ // harness writes in a shape not listed here is not replaced until a rule covers it (docs/sessions-store.md, "What it cannot catch").
10
+ import { createHmac } from 'node:crypto';
11
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ const MARKER = /^\[injected [a-z0-9_-]+ [0-9a-f]{12}\]$/;
13
+ // A string shorter than this is a label or a flag (`Project`, `user`), not a block.
14
+ const MIN_BLOCK = 8;
15
+ // Keys that name the record rather than hold its text: they stay readable.
16
+ const STRUCTURAL = new Set(['type', 'name', 'hookName', 'hookEvent', 'toolUseID', 'path', 'displayPath']);
17
+ const normalize = (text) => text.replace(/\s+/g, ' ').trim();
18
+ const markerOf = (key, kind, text) => `[injected ${kind} ${createHmac('sha256', key).update(normalize(text)).digest('hex').slice(0, 12)}]`;
19
+ /** Applies `change` to every string under `value`, with the key the string sits under; the same value back when nothing changed. */
20
+ const mapStrings = (value, name, change) => {
21
+ if (typeof value === 'string')
22
+ return change(value, name);
23
+ if (Array.isArray(value)) {
24
+ const items = value.map((item) => mapStrings(item, name, change));
25
+ return items.some((item, at) => item !== value[at]) ? items : value;
26
+ }
27
+ if (isObject(value)) {
28
+ const before = Object.entries(value);
29
+ const after = before.map(([key, item]) => [key, mapStrings(item, key, change)]);
30
+ return after.some(([, item], at) => item !== before[at]?.[1]) ? Object.fromEntries(after) : value;
31
+ }
32
+ return value;
33
+ };
34
+ // Attachment types the harness writes to carry context or instructions to the model. A type outside this list (a queued prompt, a file the
35
+ // operator mentioned, a task status) holds what a person or a tool produced and keeps the other rules.
36
+ // Left out on purpose: the harness's fixed notices (`silent_turn_reminder`, `batching_reminder_sent`, `bash_output_audience_note`,
37
+ // `read_truncation_notice`, `date`), which are the same wording on every machine or a bare date and hold nothing the operator wrote, and agent
38
+ // output (`<task-notification>`, `<teammate-message>`), which is what an agent said and not an instruction.
39
+ // Known misses, left to the other rules: the `content` of an `away_summary` system record, the `rendered` copy of a `compact_file_reference`
40
+ // attachment (a file's text), and `<local-command-*>` blocks (the echo of a command the operator ran); `slug` and `cwd` stay readable.
41
+ const INJECTED_ATTACHMENTS = new Set([
42
+ 'instructions', 'nested_memory', 'skill_listing', 'invoked_skills', 'dynamic_skill', 'agent_listing_delta', 'deferred_tools_record', 'deferred_tools_delta',
43
+ 'mcp_instructions_delta', 'prompt_snapshot', 'hook_success', 'hook_additional_context', 'hook_blocking_error', 'hook_non_blocking_error', 'hook_cancelled',
44
+ 'environment', 'session_context', 'context_sections', 'credential_org', 'model', 'output_style', 'output_style_instructions', 'command_permissions',
45
+ 'auto_mode', 'plan_mode', 'team_context',
46
+ ]);
47
+ const blocksIn = (key, kind) => (text, name) => (text.length < MIN_BLOCK || STRUCTURAL.has(name) || MARKER.test(text) ? text : markerOf(key, kind, text));
48
+ const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
49
+ const LEADING_REMINDERS = /^\s*(?:<system-reminder>[\s\S]*?<\/system-reminder>\s*)+/;
50
+ /** The descriptions of a tool, its schema's included, become blocks; its name and the schema's keys and types stay. */
51
+ const toolDescriptions = (key) => (value) => mapStrings(value, 'tool', (text, name) => (name === 'description' ? blocksIn(key, 'tool-definition')(text, name) : text));
52
+ /** The text blocks of a message's content: `text` runs on the message's own text, `result` on the text of a tool result. */
53
+ const mapContent = (content, text, result, definition) => {
54
+ if (typeof content === 'string')
55
+ return text(content);
56
+ if (!Array.isArray(content))
57
+ return content;
58
+ const items = content.map((block) => {
59
+ if (!isObject(block))
60
+ return block;
61
+ if (block['type'] === 'text' && typeof block['text'] === 'string') {
62
+ const next = text(block['text']);
63
+ return next === block['text'] ? block : { ...block, text: next };
64
+ }
65
+ if (block['type'] === 'tool_result') {
66
+ const inner = mapContent(block['content'], result, result, definition);
67
+ return inner === block['content'] || inner === undefined ? block : { ...block, content: inner };
68
+ }
69
+ // A request blob also carries the loaded definition of one tool, the text its request shape holds as `shape.tools`.
70
+ const tool = block['tool'];
71
+ if (isObject(tool) && isObject(tool['definition'])) {
72
+ const next = definition(tool['definition']);
73
+ return next === tool['definition'] ? block : { ...block, tool: { ...tool, definition: next } };
74
+ }
75
+ return block;
76
+ });
77
+ return items.some((item, at) => item !== content[at]) ? items : content;
78
+ };
79
+ // Harness text a request blob of the system role carries, by what it opens with or names. The whole block is marked, under the kind (and so the hash) its
80
+ // attachment record gets, because it is the same text. First match wins. A system text with none of these forms is left to the other rules.
81
+ const SYSTEM_FORMS = [
82
+ [/\bSessionStart(?::[\w-]+)? hook\b|\bhook additional context\b/, 'hook-additional-context'],
83
+ [/following skills are available for use with the Skill tool/i, 'skill-listing'],
84
+ [/Available agent types/i, 'agent-listing-delta'],
85
+ [/^\s*#{1,3} MCP Server Instructions\b|following MCP servers have provided instructions/m, 'mcp-instructions-delta'],
86
+ [/^\s*#{1,3} Environment\s*$|^\s*You have been invoked in the following environment/m, 'environment'],
87
+ [/Contents of [^\n]*?[\\/]\.claude[\\/]rules[\\/][^\n]*\.md\b|Contents of [^\n]*?(?:CLAUDE|AGENTS|MEMORY)(?:\.local)?\.md\b/, 'instructions'],
88
+ ];
89
+ // A memory file: an instruction file, the memory index, a note under a `memory` folder, or a rules file.
90
+ const MEMORY_PATH = /(?:^|[\\/])(?:CLAUDE|AGENTS|MEMORY)(?:\.local)?\.md$|[\\/]memory[\\/][^\\/]+\.md$|[\\/]\.claude[\\/]rules[\\/][^\\/]+\.md$/i;
91
+ const MEMORY_MENTION = /(?:CLAUDE|AGENTS|MEMORY)(?:\.local)?\.md\b|[\\/]memory[\\/][^\s"'`\\/]+\.md\b/i;
92
+ const CHANGED = /changed on disk|was modified/i;
93
+ /** The kind of harness text a system-role string is, or undefined: one of the forms above, or a note that a memory file changed. */
94
+ const systemKindOf = (text) => {
95
+ const form = SYSTEM_FORMS.find(([shape]) => shape.test(text));
96
+ if (form !== undefined)
97
+ return form[1];
98
+ return MEMORY_MENTION.test(text) && CHANGED.test(text) ? 'memory-file' : undefined;
99
+ };
100
+ const systemText = (key) => (text) => {
101
+ const kind = text.trim() === '' || MARKER.test(text.trim()) ? undefined : systemKindOf(text);
102
+ return kind === undefined ? text : markerOf(key, kind, text);
103
+ };
104
+ const messageRule = (key, position, known) => {
105
+ const whole = (text) => (text.trim() === '' || MARKER.test(text.trim()) ? text : markerOf(key, 'meta', text));
106
+ const leading = (text) => {
107
+ const run = LEADING_REMINDERS.exec(text);
108
+ return run === null ? text : `${run[0].replace(REMINDER, (block) => markerOf(key, 'system-reminder', block))}${text.slice(run[0].length)}`;
109
+ };
110
+ // A request blob repeats earlier messages and carries no `isMeta`: a text block that is the text of a meta message or an injected attachment
111
+ // is marked `meta`, whatever kind its first line carries; the hash is the same, so the two lines still match on the hash.
112
+ const repeated = (text) => (known.has(normalize(text)) ? markerOf(key, 'meta', text) : leading(text));
113
+ const anywhere = (text) => text.replace(REMINDER, (block) => markerOf(key, 'system-reminder', block));
114
+ return (message) => {
115
+ const content = mapContent(message['content'], position === 'meta' ? whole : position === 'blob' ? repeated : leading, anywhere, toolDescriptions(key));
116
+ return content === message['content'] || content === undefined ? message : { ...message, content };
117
+ };
118
+ };
119
+ const withMessage = (line, field, rule, role = 'user') => {
120
+ const message = line[field];
121
+ if (!isObject(message) || message['role'] !== role)
122
+ return line;
123
+ const next = rule(message);
124
+ return next === message ? line : { ...line, [field]: next };
125
+ };
126
+ /** The rule for a request blob of the system role: the harness's own forms first, then the text other lines repeat. */
127
+ const systemMessage = (key, known) => {
128
+ const mark = systemText(key);
129
+ const text = (value) => {
130
+ const next = mark(value);
131
+ return next !== value || !known.has(normalize(value)) ? next : markerOf(key, 'meta', value);
132
+ };
133
+ return (message) => {
134
+ const content = mapContent(message['content'], text, text, (tool) => tool);
135
+ return content === message['content'] || content === undefined ? message : { ...message, content };
136
+ };
137
+ };
138
+ const withField = (line, field, change) => {
139
+ const value = line[field];
140
+ if (value === undefined)
141
+ return line;
142
+ const next = change(value);
143
+ return next === value ? line : { ...line, [field]: next };
144
+ };
145
+ // Codex: a developer message is the harness talking, and a user message that opens with one of these tags is context it added. What a person typed has no tag.
146
+ const CODEX_USER_TAGS = /^\s*(<environment_context>|<user_instructions>|<guardian_tool_descriptions>|# AGENTS\.md instructions)/;
147
+ const CODEX_FIELDS = ['base_instructions', 'instructions', 'user_instructions', 'developer_instructions'];
148
+ const codexKind = (text, fallback) => {
149
+ const tag = /^\s*<([a-z_]+)[ >]/.exec(text);
150
+ return tag?.[1]?.replaceAll('_', '-') ?? fallback;
151
+ };
152
+ const codexMessage = (key, payload) => {
153
+ const role = payload['role'];
154
+ if (role !== 'developer' && role !== 'user')
155
+ return payload;
156
+ const content = payload['content'];
157
+ if (!Array.isArray(content))
158
+ return payload;
159
+ const items = content.map((block) => {
160
+ if (!isObject(block) || typeof block['text'] !== 'string')
161
+ return block;
162
+ const text = block['text'];
163
+ const injected = role === 'developer' ? text.trim() !== '' : CODEX_USER_TAGS.test(text);
164
+ if (!injected || MARKER.test(text.trim()))
165
+ return block;
166
+ return { ...block, text: markerOf(key, role === 'developer' ? codexKind(text, 'developer') : codexKind(text, 'agents-md'), text) };
167
+ });
168
+ return items.some((item, at) => item !== content[at]) ? { ...payload, content: items } : payload;
169
+ };
170
+ const codexFields = (key, payload) => CODEX_FIELDS.reduce((acc, name) => withField(acc, name, (value) => mapStrings(value, name, blocksIn(key, name.replaceAll('_', '-')))), payload);
171
+ const ORIGIN_TEXT = ['body', 'handback'];
172
+ const HOOK_SUMMARY_FIELDS = ['hookAdditionalContext', 'hookErrors', 'hookInfos'];
173
+ const textsOf = (content) => {
174
+ if (typeof content === 'string')
175
+ return [content];
176
+ if (!Array.isArray(content))
177
+ return [];
178
+ return content.flatMap((block) => (isObject(block) && block['type'] === 'text' && typeof block['text'] === 'string' ? [block['text']] : []));
179
+ };
180
+ /**
181
+ * Adds to `known` the normalized text of what `line` holds that a request blob repeats without the flag that marked it: the text of a meta message
182
+ * and the rendered copy of an injected attachment. The redactor runs this over a whole transcript before it marks any line.
183
+ */
184
+ export const learnInjected = (line, known) => {
185
+ const add = (text) => {
186
+ if (text.trim() !== '' && !MARKER.test(text.trim()))
187
+ known.add(normalize(text));
188
+ };
189
+ const message = line['message'];
190
+ if (line['type'] === 'user' && line['isMeta'] === true && isObject(message) && message['role'] === 'user')
191
+ textsOf(message['content']).forEach(add);
192
+ const attachment = line['attachment'];
193
+ const kind = isObject(attachment) ? attachment['type'] : undefined;
194
+ if (line['type'] === 'attachment' && typeof kind === 'string' && INJECTED_ATTACHMENTS.has(kind) && line['rendered'] !== undefined) {
195
+ mapStrings(line['rendered'], 'rendered', (text, name) => {
196
+ if (text.length >= MIN_BLOCK && !STRUCTURAL.has(name))
197
+ add(text);
198
+ return text;
199
+ });
200
+ }
201
+ return known;
202
+ };
203
+ /** The rule for one machine: every injected block of a decoded transcript line becomes `[injected <kind> <hash>]`, hashed under `key`; `known` is what `learnInjected` collected from the transcript. */
204
+ export const injectedRule = (key, known = new Set()) => {
205
+ const userMessage = messageRule(key, 'typed', known);
206
+ const metaMessage = messageRule(key, 'meta', known);
207
+ const blobMessage = messageRule(key, 'blob', known);
208
+ const systemBlob = systemMessage(key, known);
209
+ const memory = blocksIn(key, 'memory-file');
210
+ // A meta line's `origin` says where the injected text came from; its `body` and `handback` are that text again, while `kind` stays readable.
211
+ const originText = (origin) => {
212
+ if (!isObject(origin))
213
+ return origin;
214
+ return ORIGIN_TEXT.reduce((acc, name) => withField(acc, name, (value) => mapStrings(value, name, blocksIn(key, 'meta'))), origin);
215
+ };
216
+ return (line) => {
217
+ switch (line['type']) {
218
+ case 'user':
219
+ return line['isMeta'] === true ? withField(withMessage(line, 'message', metaMessage), 'origin', originText) : withMessage(line, 'message', userMessage);
220
+ case 'api-request-blob':
221
+ return withMessage(withMessage(line, 'message', blobMessage), 'message', systemBlob, 'system');
222
+ case 'attachment': {
223
+ const attachment = line['attachment'];
224
+ const kind = isObject(attachment) ? attachment['type'] : undefined;
225
+ // A memory file the model edited or that changed on disk: the snippet (and the message it was shown in) is the file's text.
226
+ const file = isObject(attachment) ? attachment['filename'] : undefined;
227
+ if (kind === 'edited_text_file' && typeof file === 'string' && MEMORY_PATH.test(file)) {
228
+ const snippet = (value) => (isObject(value) ? withField(value, 'snippet', (text) => mapStrings(text, 'snippet', memory)) : value);
229
+ return withField(withField(line, 'attachment', snippet), 'rendered', (value) => mapStrings(value, 'rendered', memory));
230
+ }
231
+ if (typeof kind !== 'string' || !INJECTED_ATTACHMENTS.has(kind))
232
+ return line;
233
+ // The record repeats its text as the message the model was shown (`rendered`), so that is the same block again.
234
+ const marked = (value) => mapStrings(value, 'attachment', blocksIn(key, kind.replaceAll('_', '-')));
235
+ return withField(withField(line, 'attachment', marked), 'rendered', marked);
236
+ }
237
+ case 'api-request-shape':
238
+ return withField(line, 'shape', (shape) => {
239
+ if (!isObject(shape))
240
+ return shape;
241
+ return withField(withField(shape, 'system', (system) => mapStrings(system, 'system', blocksIn(key, 'system-prompt'))), 'tools', toolDescriptions(key));
242
+ });
243
+ case 'system':
244
+ // The hook output a stop-hook summary carries: what a hook printed, its error text, and the command line it ran.
245
+ if (line['subtype'] === 'stop_hook_summary')
246
+ return HOOK_SUMMARY_FIELDS.reduce((acc, name) => withField(acc, name, (value) => mapStrings(value, name, blocksIn(key, 'hook-summary'))), line);
247
+ // Any other system text is the harness speaking (a changed memory file, a notice), except the echo of a command the operator ran: that is operator output, not an instruction.
248
+ if (line['subtype'] === 'local_command')
249
+ return line;
250
+ return withField(line, 'content', (value) => (typeof value === 'string' ? systemText(key)(value) : value));
251
+ case 'session_meta':
252
+ case 'turn_context':
253
+ return withField(line, 'payload', (payload) => (isObject(payload) ? codexFields(key, payload) : payload));
254
+ case 'response_item':
255
+ return withField(line, 'payload', (payload) => (isObject(payload) && payload['type'] === 'message' ? codexMessage(key, payload) : payload));
256
+ default:
257
+ return line;
258
+ }
259
+ };
260
+ };
@@ -1,9 +1,15 @@
1
+ import type { CodexInstall } from './codex-hook-install.js';
1
2
  export declare const RETENTION_FLOOR = 365;
2
3
  export declare const DEFAULT_RETENTION = 30;
3
4
  export declare const TASK_NAME = "OarsSessionsNightly";
5
+ /**
6
+ * What `--machine` writes for Stop and SessionEnd. A settings file committed from one machine is read on another (a cloud VM, a runner), where the installing machine's node and `hook-bin.js` paths do not exist, so this names no path:
7
+ * it resolves the `oars-hook` bin on PATH, and exits 0 when there is none (a hook that cannot start must not fail the session).
8
+ */
9
+ export declare const MACHINE_COMMAND = "command -v oars-hook >/dev/null || exit 0; oars-hook stop --machine";
4
10
  /** What the install needs from the machine, so a test can stand in for each. */
5
11
  export interface InstallRuntime {
6
- /** `process.platform`: only `win32` has the nightly task. */
12
+ /** `process.platform`: only `win32` ever had the nightly task. */
7
13
  platform: NodeJS.Platform;
8
14
  nodePath: string;
9
15
  /** Absolute path of the built `hook-bin.js`. */
@@ -11,7 +17,9 @@ export interface InstallRuntime {
11
17
  /** `<claude config dir>/settings.json`. */
12
18
  userSettings: string;
13
19
  cwd: string;
14
- /** Runs `schtasks` with these arguments. */
20
+ /** Where the Codex recording hook goes (`--harness codex`); absent in a run that has none. */
21
+ codex?: CodexInstall;
22
+ /** Runs `schtasks` with these arguments; used only to remove the task an earlier version registered. */
15
23
  schtasks: (args: string[]) => Promise<{
16
24
  code: number;
17
25
  output: string;
@@ -19,7 +27,7 @@ export interface InstallRuntime {
19
27
  }
20
28
  export type Scope = 'user' | 'project' | 'file';
21
29
  /** A settings file is open-ended JSON the harness owns; only the keys named here are read or written. */
22
- interface Json {
30
+ export interface Json {
23
31
  [key: string]: unknown;
24
32
  }
25
33
  /**
@@ -28,8 +36,14 @@ interface Json {
28
36
  * milliseconds); any other tool's failure goes straight to node, because its text has no such line and its recovery read does not need one.
29
37
  */
30
38
  export declare const commandOf: (runtime: InstallRuntime, arg: string) => string;
39
+ /** The groups with our entries taken out; a group that held only ours goes, one that held others keeps them. `own` tells ours by the command's tail. */
40
+ export declare const withoutOwn: (groups: unknown[], own?: RegExp) => unknown[];
41
+ export declare const hasOwn: (groups: unknown[], own?: RegExp) => boolean;
31
42
  export declare const settingsPath: (runtime: InstallRuntime, scope: Scope, file: string | undefined) => string;
32
43
  export declare const readSettings: (path: string) => Promise<Json>;
44
+ export declare const writeSettings: (path: string, settings: Json) => Promise<void>;
45
+ export declare const hooksOf: (settings: Json) => Json;
46
+ export declare const groupsOf: (hooks: Json, event: string) => unknown[];
33
47
  /** The retention the file sets: its `cleanupPeriodDays` when a number, else the harness default. */
34
48
  export declare const effectiveRetention: (settings: Json) => number;
35
49
  export interface InstallResult {
@@ -38,6 +52,13 @@ export interface InstallResult {
38
52
  retention: string;
39
53
  }
40
54
  export declare const installHooks: (runtime: InstallRuntime, path: string, keepRetention: boolean) => Promise<InstallResult>;
55
+ /**
56
+ * `hook install --machine`: the Stop and SessionEnd entries that run `oars-hook stop --machine`, merged with what is there (only entries of ours are replaced). With `env`, the enrollment block goes to this file's `env`
57
+ * block, merged with the operator's own entries, and the file is made owner-only, because it then holds the salts and the terms. A project file gets no `env`: it is committed.
58
+ */
59
+ export declare const installMachineHooks: (path: string, env: {
60
+ [name: string]: string;
61
+ } | undefined) => Promise<InstallResult>;
41
62
  export declare const uninstallHooks: (path: string) => Promise<{
42
63
  file: string;
43
64
  removed: string[];
@@ -49,22 +70,12 @@ export interface FileStatus {
49
70
  retentionDays: number;
50
71
  }
51
72
  export declare const hookStatus: (runtime: InstallRuntime, extra: string | undefined) => Promise<FileStatus[]>;
52
- /** The `schtasks` arguments for the nightly task: the same node and hook-bin the hooks use, at a fixed hour. */
53
- export declare const scheduleArgs: (runtime: InstallRuntime, hour: number) => string[];
54
- export declare const SCHEDULE_HOUR = 3;
55
- export declare const setSchedule: (runtime: InstallRuntime, mode: "nightly" | "off") => Promise<string>;
56
- /** True once the operator ran `--schedule off` and has not run `--schedule nightly` since. A record that cannot be read or parsed counts as no opt-out. */
57
- export declare const scheduleOptedOut: (storeDir: string) => Promise<boolean>;
58
- /** Records the choice of `--schedule`: `off` writes the opt-out; `nightly` clears it. Without a store the choice has nowhere to live and is not recorded. */
59
- export declare const recordSchedule: (storeDir: string | undefined, mode: "nightly" | "off") => Promise<void>;
60
- /** What a plain `hook install` says when the operator turned the nightly task off: it is left alone. */
61
- export declare const SCHEDULE_LEFT_OFF = "Scheduled task OarsSessionsNightly: left off, as you chose with --schedule off. Turn it back on with: oars sessions hook install --schedule nightly";
73
+ /** What `hook install` says to someone who asks for the task that earlier versions registered: sends ride the hook and the commands now. */
74
+ export declare const SCHEDULE_REFUSED = "oars no longer registers a scheduled task: sends now ride the session-end hook and any oars command (at most about hourly). Run: oars sessions hook install";
62
75
  /**
63
- * What a plain `hook install` does about the nightly task on Windows: register it unless it is already there. Registration is
64
- * best effort: the hooks are installed by then, so a machine that refuses the task (policy, no Task Scheduler) is told how to retry
65
- * and is not failed. `install --schedule off` removes the task and records the opt-out (`recordSchedule`), which the caller checks
66
- * (`scheduleOptedOut`) before calling this; `--schedule nightly` clears it.
76
+ * Removes the nightly task an earlier version registered, and says so in one line; nothing when it is not there. Only Windows ever had one, so any other platform makes no call. Install and uninstall both
77
+ * call this, so a machine that upgrades loses the task the first time either runs. A task that is there but refuses to go (policy) is reported, never failed: the hooks are the work of the command.
67
78
  */
68
- export declare const ensureSchedule: (runtime: InstallRuntime) => Promise<string>;
69
- export declare const scheduleStatus: (runtime: InstallRuntime) => Promise<string>;
70
- export {};
79
+ export declare const removeSchedule: (runtime: InstallRuntime) => Promise<string | undefined>;
80
+ /** The status line for a task left over from an earlier version; nothing when there is none. */
81
+ export declare const scheduleStatus: (runtime: InstallRuntime) => Promise<string | undefined>;