@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
@@ -1,15 +1,28 @@
1
1
  // `oars sessions hook install|uninstall|status`: writes the four Claude Code hook entries into one settings file, merging with what is
2
2
  // there. An entry is ours when its command ends with `hook-bin.js" <session-start|pre-tool|post-tool|stop>`, so uninstall removes those entries and nothing else. Also the retention floor
3
- // (the harness deletes raw transcripts after `cleanupPeriodDays`, 30 when unset) and the Windows nightly task.
4
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ // (the harness deletes raw transcripts after `cleanupPeriodDays`, 30 when unset) and removing the Windows task earlier versions registered.
4
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { CliError, ExitCode } from '../errors.js';
7
7
  import { ACTION_TABLES } from './actions.js';
8
+ import { writeOwnerOnly } from './owner-only.js';
8
9
  export const RETENTION_FLOOR = 365;
9
10
  export const DEFAULT_RETENTION = 30;
10
11
  export const TASK_NAME = 'OarsSessionsNightly';
11
12
  // The tail of a command this module wrote; anything else in a settings file is the operator's, whatever it is called.
12
- const OWN_COMMAND = /hook-bin\.js" (?:session-start|prompt|pre-tool|post-tool|stop)$/;
13
+ // The machine command is host-independent: it names no install path, only the `oars-hook` bin on PATH (see `MACHINE_COMMAND`).
14
+ // A plain install and a machine install each replace only their own entries, so one never strips the other's; uninstall and status read both.
15
+ const PLAIN_COMMAND = /hook-bin\.js" (?:session-start|prompt|pre-tool|post-tool|stop)$/;
16
+ const MACHINE_OWN = /oars-hook stop --machine$/;
17
+ const OWN_COMMAND = /(?:hook-bin\.js" (?:session-start|prompt|pre-tool|post-tool|stop)|oars-hook stop --machine)$/;
18
+ /**
19
+ * 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:
20
+ * 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).
21
+ */
22
+ export const MACHINE_COMMAND = 'command -v oars-hook >/dev/null || exit 0; oars-hook stop --machine';
23
+ const MACHINE_EVENTS = ['Stop', 'SessionEnd'];
24
+ // The hook body is a node start and a detach (well under a second); this is the harness's ceiling, not its budget.
25
+ const MACHINE_TIMEOUT = 10;
13
26
  const SPECS = [
14
27
  { event: 'SessionStart', arg: 'session-start', timeout: 5 },
15
28
  // The split-positions table again, only when it changed since this session's last one (`hooks.ts`); a prompt with nothing new costs one node start and prints nothing.
@@ -35,15 +48,15 @@ export const commandOf = (runtime, arg) => {
35
48
  return `${present}${launch}`;
36
49
  return `${present}IFS= read -r event; case "$event" in '') exit 0;; *'"tool_name":"Bash"'*|*'"tool_name":"PowerShell"'*) case "$event" in *'Exit code'*) ;; *) exit 0;; esac;; esac; { printf '%s\\n' "$event"; cat; } | ${launch}`;
37
50
  };
38
- const isOwnHook = (hook) => isObject(hook) && typeof hook.command === 'string' && OWN_COMMAND.test(hook.command);
39
- /** The groups with our entries taken out; a group that held only ours goes, one that held others keeps them. */
40
- const withoutOwn = (groups) => groups.flatMap((group) => {
41
- if (!isObject(group) || !Array.isArray(group.hooks) || !group.hooks.some(isOwnHook))
51
+ const isOwnHook = (hook, own) => isObject(hook) && typeof hook.command === 'string' && own.test(hook.command);
52
+ /** 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. */
53
+ export const withoutOwn = (groups, own = OWN_COMMAND) => groups.flatMap((group) => {
54
+ if (!isObject(group) || !Array.isArray(group.hooks) || !group.hooks.some((hook) => isOwnHook(hook, own)))
42
55
  return [group];
43
- const rest = group.hooks.filter((hook) => !isOwnHook(hook));
56
+ const rest = group.hooks.filter((hook) => !isOwnHook(hook, own));
44
57
  return rest.length === 0 ? [] : [{ ...group, hooks: rest }];
45
58
  });
46
- const hasOwn = (groups) => groups.some((group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some(isOwnHook));
59
+ export const hasOwn = (groups, own = OWN_COMMAND) => groups.some((group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some((hook) => isOwnHook(hook, own)));
47
60
  export const settingsPath = (runtime, scope, file) => {
48
61
  if (scope === 'user')
49
62
  return runtime.userSettings;
@@ -77,12 +90,12 @@ export const readSettings = async (path) => {
77
90
  throw new CliError(`${path} is not a JSON object; nothing was changed.`, ExitCode.USAGE, 'usage');
78
91
  return parsed;
79
92
  };
80
- const writeSettings = async (path, settings) => {
93
+ export const writeSettings = async (path, settings) => {
81
94
  await mkdir(dirname(path), { recursive: true });
82
95
  await writeFile(path, `${JSON.stringify(settings, null, 2)}\n`);
83
96
  };
84
- const hooksOf = (settings) => (isObject(settings.hooks) ? settings.hooks : {});
85
- const groupsOf = (hooks, event) => {
97
+ export const hooksOf = (settings) => (isObject(settings.hooks) ? settings.hooks : {});
98
+ export const groupsOf = (hooks, event) => {
86
99
  const groups = hooks[event];
87
100
  return Array.isArray(groups) ? groups : [];
88
101
  };
@@ -91,11 +104,11 @@ export const effectiveRetention = (settings) => (typeof settings.cleanupPeriodDa
91
104
  /** Events an earlier build registered our entries under; install and uninstall clear them so a stale entry cannot keep running. */
92
105
  const LEGACY_EVENTS = ['Stop'];
93
106
  /** Takes our entries out of `event`, dropping the key when that empties it. */
94
- const clearLegacy = (hooks, event) => {
107
+ const clearLegacy = (hooks, event, own) => {
95
108
  const groups = groupsOf(hooks, event);
96
- if (!hasOwn(groups))
109
+ if (!hasOwn(groups, own))
97
110
  return false;
98
- const kept = withoutOwn(groups);
111
+ const kept = withoutOwn(groups, own);
99
112
  if (kept.length === 0)
100
113
  Reflect.deleteProperty(hooks, event);
101
114
  else
@@ -106,9 +119,9 @@ export const installHooks = async (runtime, path, keepRetention) => {
106
119
  const settings = await readSettings(path);
107
120
  const hooks = hooksOf(settings);
108
121
  for (const event of LEGACY_EVENTS)
109
- clearLegacy(hooks, event);
122
+ clearLegacy(hooks, event, PLAIN_COMMAND);
110
123
  for (const spec of SPECS) {
111
- const others = withoutOwn(groupsOf(hooks, spec.event));
124
+ const others = withoutOwn(groupsOf(hooks, spec.event), PLAIN_COMMAND);
112
125
  const entry = { type: 'command', command: commandOf(runtime, spec.arg), timeout: spec.timeout };
113
126
  hooks[spec.event] = [...others, spec.matcher === undefined ? { hooks: [entry] } : { matcher: spec.matcher, hooks: [entry] }];
114
127
  }
@@ -139,11 +152,38 @@ export const installHooks = async (runtime, path, keepRetention) => {
139
152
  await writeSettings(path, settings);
140
153
  return { file: path, installed: SPECS.map((spec) => spec.event), retention };
141
154
  };
155
+ /**
156
+ * `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`
157
+ * 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.
158
+ */
159
+ export const installMachineHooks = async (path, env) => {
160
+ const settings = await readSettings(path);
161
+ const hooks = hooksOf(settings);
162
+ for (const event of MACHINE_EVENTS) {
163
+ hooks[event] = [...withoutOwn(groupsOf(hooks, event), MACHINE_OWN), { hooks: [{ type: 'command', command: MACHINE_COMMAND, timeout: MACHINE_TIMEOUT }] }];
164
+ }
165
+ settings.hooks = hooks;
166
+ if (env === undefined) {
167
+ await writeSettings(path, settings);
168
+ }
169
+ else {
170
+ const merged = { ...(isObject(settings.env) ? settings.env : {}), ...env };
171
+ // The terms are given once: a re-install that names one variable replaces the other. A Stop lets the inline list win, so an earlier one would silently outlive the new list and keep terms in a file the operator moved them out of.
172
+ if (env.OARS_EXCLUSION_TERMS !== undefined)
173
+ Reflect.deleteProperty(merged, 'OARS_EXCLUSION_TERMS_INLINE');
174
+ if (env.OARS_EXCLUSION_TERMS_INLINE !== undefined)
175
+ Reflect.deleteProperty(merged, 'OARS_EXCLUSION_TERMS');
176
+ settings.env = merged;
177
+ await mkdir(dirname(path), { recursive: true });
178
+ await writeOwnerOnly(path, `${JSON.stringify(settings, null, 2)}\n`);
179
+ }
180
+ return { file: path, installed: [...MACHINE_EVENTS], retention: 'Left cleanupPeriodDays alone: a machine builds its batch at each Stop, not from a stored transcript.' };
181
+ };
142
182
  export const uninstallHooks = async (path) => {
143
183
  const settings = await readSettings(path);
144
184
  const hooks = hooksOf(settings);
145
185
  const removed = [];
146
- const cleared = LEGACY_EVENTS.map((event) => clearLegacy(hooks, event)).includes(true);
186
+ const cleared = LEGACY_EVENTS.map((event) => clearLegacy(hooks, event, OWN_COMMAND)).includes(true);
147
187
  for (const spec of SPECS) {
148
188
  const groups = groupsOf(hooks, spec.event);
149
189
  if (!hasOwn(groups))
@@ -152,7 +192,10 @@ export const uninstallHooks = async (path) => {
152
192
  removed.push(spec.event);
153
193
  hooks[spec.event] = kept;
154
194
  }
155
- if (removed.length > 0 || cleared) {
195
+ // Stop is no spec's event: a machine install writes it, and an earlier build did.
196
+ if (cleared)
197
+ removed.push('Stop');
198
+ if (removed.length > 0) {
156
199
  // An event this call emptied goes; an event that was already an empty list in the file is the operator's and stays.
157
200
  const remaining = Object.entries(hooks).filter(([event, groups]) => !(removed.includes(event) && Array.isArray(groups) && groups.length === 0));
158
201
  // `undefined` is left out of the JSON, so an emptied `hooks` key goes.
@@ -171,76 +214,31 @@ export const hookStatus = async (runtime, extra) => {
171
214
  for (const target of targets) {
172
215
  const settings = await readSettings(target.file);
173
216
  const hooks = hooksOf(settings);
174
- statuses.push({ ...target, installed: SPECS.filter((spec) => hasOwn(groupsOf(hooks, spec.event))).map((spec) => spec.event), retentionDays: effectiveRetention(settings) });
217
+ statuses.push({ ...target, installed: [...SPECS.filter((spec) => hasOwn(groupsOf(hooks, spec.event))).map((spec) => spec.event), ...LEGACY_EVENTS.filter((event) => hasOwn(groupsOf(hooks, event)))], retentionDays: effectiveRetention(settings) });
175
218
  }
176
219
  return statuses;
177
220
  };
178
- /** The `schtasks` arguments for the nightly task: the same node and hook-bin the hooks use, at a fixed hour. */
179
- export const scheduleArgs = (runtime, hour) => [
180
- '/Create',
181
- '/TN',
182
- TASK_NAME,
183
- '/TR',
184
- `"${runtime.nodePath}" "${runtime.hookBin}" nightly`,
185
- '/SC',
186
- 'DAILY',
187
- '/ST',
188
- `${String(hour).padStart(2, '0')}:00`,
189
- '/F',
190
- ];
191
- export const SCHEDULE_HOUR = 3;
192
- const registered = () => `Scheduled task ${TASK_NAME}: the full deposit and the recurrence refresh run daily at ${String(SCHEDULE_HOUR).padStart(2, '0')}:00.`;
193
- export const setSchedule = async (runtime, mode) => {
194
- if (mode === 'nightly') {
195
- const created = await runtime.schtasks(scheduleArgs(runtime, SCHEDULE_HOUR));
196
- if (created.code !== 0)
197
- throw new CliError(`Could not register the scheduled task: ${created.output.trim()}`, ExitCode.UNEXPECTED, 'unexpected');
198
- return registered();
199
- }
200
- const deleted = await runtime.schtasks(['/Delete', '/TN', TASK_NAME, '/F']);
201
- return deleted.code === 0 ? `Removed the scheduled task ${TASK_NAME}.` : `No scheduled task ${TASK_NAME} to remove.`;
202
- };
203
- /** Where `--schedule off` is remembered: a file of its own in the sessions store, because `state.json` is rewritten whole by every deposit. */
204
- const scheduleRecord = (storeDir) => join(storeDir, 'schedule.json');
205
- /** 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. */
206
- export const scheduleOptedOut = async (storeDir) => {
207
- try {
208
- const parsed = JSON.parse(await readFile(scheduleRecord(storeDir), 'utf8'));
209
- return typeof parsed === 'object' && parsed !== null && 'schedule' in parsed && parsed.schedule === 'off';
210
- }
211
- catch {
212
- return false;
213
- }
214
- };
215
- /** 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. */
216
- export const recordSchedule = async (storeDir, mode) => {
217
- if (storeDir === undefined)
218
- return;
219
- if (mode === 'nightly') {
220
- await rm(scheduleRecord(storeDir), { force: true });
221
- return;
222
- }
223
- await mkdir(storeDir, { recursive: true });
224
- await writeFile(scheduleRecord(storeDir), `${JSON.stringify({ schedule: 'off' })}\n`, { mode: 0o600 });
225
- };
226
- /** What a plain `hook install` says when the operator turned the nightly task off: it is left alone. */
227
- export const SCHEDULE_LEFT_OFF = `Scheduled task ${TASK_NAME}: left off, as you chose with --schedule off. Turn it back on with: oars sessions hook install --schedule nightly`;
221
+ /** What `hook install` says to someone who asks for the task that earlier versions registered: sends ride the hook and the commands now. */
222
+ export 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';
228
223
  /**
229
- * What a plain `hook install` does about the nightly task on Windows: register it unless it is already there. Registration is
230
- * best effort: the hooks are installed by then, so a machine that refuses the task (policy, no Task Scheduler) is told how to retry
231
- * and is not failed. `install --schedule off` removes the task and records the opt-out (`recordSchedule`), which the caller checks
232
- * (`scheduleOptedOut`) before calling this; `--schedule nightly` clears it.
224
+ * 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
225
+ * 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.
233
226
  */
234
- export const ensureSchedule = async (runtime) => {
227
+ export const removeSchedule = async (runtime) => {
228
+ if (runtime.platform !== 'win32')
229
+ return undefined;
235
230
  const queried = await runtime.schtasks(['/Query', '/TN', TASK_NAME]);
236
- if (queried.code === 0)
237
- return `Scheduled task ${TASK_NAME}: already registered.`;
238
- const created = await runtime.schtasks(scheduleArgs(runtime, SCHEDULE_HOUR));
239
- if (created.code !== 0)
240
- return `Could not register the scheduled task ${TASK_NAME} (${created.output.trim()}); the hooks are installed. Retry with: oars sessions hook install --schedule nightly`;
241
- return registered();
231
+ if (queried.code !== 0)
232
+ return undefined;
233
+ const deleted = await runtime.schtasks(['/Delete', '/TN', TASK_NAME, '/F']);
234
+ return deleted.code === 0
235
+ ? `Removed the scheduled task ${TASK_NAME} that an earlier version registered; sends now ride the session-end hook and any oars command.`
236
+ : `Could not remove the scheduled task ${TASK_NAME} (${deleted.output.trim()}); remove it yourself with: schtasks /Delete /TN ${TASK_NAME} /F`;
242
237
  };
238
+ /** The status line for a task left over from an earlier version; nothing when there is none. */
243
239
  export const scheduleStatus = async (runtime) => {
240
+ if (runtime.platform !== 'win32')
241
+ return undefined;
244
242
  const queried = await runtime.schtasks(['/Query', '/TN', TASK_NAME]);
245
- return queried.code === 0 ? `Scheduled task ${TASK_NAME}: registered.` : `Scheduled task ${TASK_NAME}: not registered.`;
243
+ return queried.code === 0 ? `Scheduled task ${TASK_NAME}: registered by an earlier version; oars sessions hook install or uninstall removes it.` : undefined;
246
244
  };
@@ -0,0 +1,41 @@
1
+ import { type CandidateSnapshot, type HookSource, type InstructionPosition, type WorkingKind } from './candidates.js';
2
+ import type { LoadedInstructions, SessionObservation } from './schema.js';
3
+ export declare const LOAD_STATUS_VERSION = 3;
4
+ export declare const LOAD_STATUS_FILE = "load-status.jsonl";
5
+ /** `loaded` the log shows it. `miss` a file existed, the rule loads it, the log does not show it. `excluded` a file existed where the rule skips by design. `absent` no file existed there. `unknown` this cannot be decided. */
6
+ export type LoadLabel = 'loaded' | 'miss' | 'excluded' | 'absent' | 'unknown';
7
+ export interface LoadStatus {
8
+ v: number;
9
+ session: string;
10
+ harness: string;
11
+ instructions: {
12
+ [position in InstructionPosition]: LoadLabel;
13
+ };
14
+ /** `CLAUDE.local.md`, `.claude/CLAUDE.md`, and the unconditional `.claude/rules/` files, in the working folder. */
15
+ files: {
16
+ [kind in WorkingKind]: LoadLabel;
17
+ };
18
+ skills: {
19
+ project: LoadLabel;
20
+ user: LoadLabel;
21
+ };
22
+ hooks: {
23
+ [source in HookSource]: LoadLabel;
24
+ };
25
+ }
26
+ /**
27
+ * The loaded set of a transcript's `instructions` record, as levels above the working folder: the path itself is read here and goes no further. `CLAUDE.md` files are placed by level;
28
+ * `CLAUDE.local.md`, `.claude/CLAUDE.md`, and `.claude/rules/*.md` are counted when they sit in the working folder, the only place they were measured.
29
+ */
30
+ export declare const loadedInstructionsOf: (files: readonly {
31
+ path: string;
32
+ type: string;
33
+ }[], cwd: string) => LoadedInstructions;
34
+ export declare const loadStatusOf: ({ session, harness, snapshot, loaded }: {
35
+ session: string;
36
+ harness: string;
37
+ snapshot: CandidateSnapshot | undefined;
38
+ loaded: LoadedInstructions | undefined;
39
+ }) => LoadStatus;
40
+ /** One row per session, as the text of `load-status.jsonl`. Snapshots that cannot be read leave every session `unknown`: a store problem here must not stop a deposit. */
41
+ export declare const loadStatusLines: (storeDir: string, sessions: readonly Pick<SessionObservation, "session" | "harness" | "loadedInstructions">[]) => Promise<string>;
@@ -0,0 +1,156 @@
1
+ // Did the harness load what existed: the session-start snapshot (`candidates.ts`) joined with the loaded set a transcript shows. A pure derivation, recomputed from those two
2
+ // records, so a change to a rule below bumps LOAD_STATUS_VERSION and needs no new capture. `load-status.jsonl` is rewritten from them and is safe to delete.
3
+ //
4
+ // The rule is what a fixture measured for each harness, not what a harness documents: a live session per case on a temp tree, reading only that session's own log. Each position
5
+ // is one of `load` (the harness read a file there), `skip` (it did not, by design), or `unmeasured` (the case was not measured). What was not measured stays `unknown`.
6
+ //
7
+ // What a log can show decides which kind gets a loaded or missed label. A transcript names each instruction file it loaded, so those kinds do. It lists each skill by name (only when the session offers the Skill tool) and each Stop hook by command, with no source for either; a `SessionStart` hook that prints nothing leaves no line. The snapshot holds
8
+ // kinds and counts only, so it has no name or command to join against, and a skill or hook that exists stays `unknown`. A hook in a folder above the working folder is the one by-design skip measured for them: those settings files do not run.
9
+ import { positionOfLevel, readCandidates } from './candidates.js';
10
+ export const LOAD_STATUS_VERSION = 3;
11
+ export const LOAD_STATUS_FILE = 'load-status.jsonl';
12
+ const RULES = new Map([
13
+ [
14
+ 'claude-code',
15
+ {
16
+ // Climbs past the git root to the filesystem root. A linked worktree nested inside its main checkout skips exactly that span, and the climb resumes above it.
17
+ positions: { working: 'load', parent: 'load', above: 'load', mainCheckout: 'skip', user: 'load' },
18
+ files: true,
19
+ // Project settings come from the working folder alone: a settings file in a parent folder, the repo root, or a worktree's main checkout did not run.
20
+ hooksAbove: 'skip',
21
+ },
22
+ ],
23
+ [
24
+ 'codex',
25
+ {
26
+ // Reads from the git root down to the working folder and stops there. The user-level file was not measured: this machine has none, and measuring needs a copied login.
27
+ positions: { working: 'load', parent: 'load', above: 'skip', mainCheckout: 'skip', user: 'unmeasured' },
28
+ files: false,
29
+ hooksAbove: 'unmeasured',
30
+ },
31
+ ],
32
+ ]);
33
+ const INSTRUCTION_NAME = 'claude.md';
34
+ const LOCAL_NAME = 'claude.local.md';
35
+ const segmentsOf = (path) => {
36
+ const parts = path.split(/[\\/]+/).filter((part) => part !== '');
37
+ // Only a path with a drive letter is compared without case: elsewhere two spellings can be two folders.
38
+ return /^[A-Za-z]:$/.test(parts[0] ?? '') ? parts.map((part) => part.toLowerCase()) : parts;
39
+ };
40
+ const sameFolder = (folder, other) => folder.length === other.length && folder.every((part, index) => part === other[index]);
41
+ /**
42
+ * The loaded set of a transcript's `instructions` record, as levels above the working folder: the path itself is read here and goes no further. `CLAUDE.md` files are placed by level;
43
+ * `CLAUDE.local.md`, `.claude/CLAUDE.md`, and `.claude/rules/*.md` are counted when they sit in the working folder, the only place they were measured.
44
+ */
45
+ export const loadedInstructionsOf = (files, cwd) => {
46
+ const here = segmentsOf(cwd);
47
+ const levels = [];
48
+ let user = 0;
49
+ const kinds = { local: 0, dotClaude: 0, rules: 0 };
50
+ for (const { path, type } of files) {
51
+ const segments = segmentsOf(path);
52
+ const name = segments.at(-1)?.toLowerCase();
53
+ const folder = segments.slice(0, -1);
54
+ if (type === 'User') {
55
+ if (name === INSTRUCTION_NAME)
56
+ user += 1;
57
+ }
58
+ else if (type === 'Local') {
59
+ if (name === LOCAL_NAME && sameFolder(folder, here))
60
+ kinds.local += 1;
61
+ }
62
+ else if (type === 'Project') {
63
+ if (name?.endsWith('.md') === true && sameFolder(folder, [...here, '.claude', 'rules']))
64
+ kinds.rules += 1;
65
+ else if (name === INSTRUCTION_NAME && sameFolder(folder, [...here, '.claude']))
66
+ kinds.dotClaude += 1;
67
+ else if (name === INSTRUCTION_NAME) {
68
+ if (folder.length <= here.length && folder.every((part, index) => part === here[index]))
69
+ levels.push(here.length - folder.length);
70
+ }
71
+ }
72
+ }
73
+ return { user, levels, ...kinds };
74
+ };
75
+ /** How many files the log shows loaded at a position: the user count, or the project files whose folder level falls in the position. */
76
+ const seenAt = (position, snapshot, loaded) => {
77
+ if (position === 'user')
78
+ return loaded.user;
79
+ return loaded.levels.filter((level) => positionOfLevel(level, snapshot) === position).length;
80
+ };
81
+ const instructionLabel = (position, snapshot, loaded, rules) => {
82
+ const present = snapshot.instructions[position];
83
+ if (loaded === undefined)
84
+ return present === 0 ? 'absent' : 'unknown';
85
+ const seen = seenAt(position, snapshot, loaded);
86
+ // What the log shows wins over what the snapshot counted and over the rule: the file was loaded.
87
+ if (seen > 0)
88
+ return 'loaded';
89
+ if (present === 0)
90
+ return 'absent';
91
+ const rule = rules[position];
92
+ if (rule === 'unmeasured')
93
+ return 'unknown';
94
+ // A file the harness does not read by design is not a candidate, so it is no miss; it is not absent either, since it exists.
95
+ return rule === 'load' ? 'miss' : 'excluded';
96
+ };
97
+ /** A working-folder kind: the log shows how many of its files loaded, once a capture of version 30 or later recorded it. */
98
+ const kindLabel = (present, seen, measured) => {
99
+ if (seen === undefined)
100
+ return present === 0 ? 'absent' : 'unknown';
101
+ if (seen > 0)
102
+ return 'loaded';
103
+ if (present === 0)
104
+ return 'absent';
105
+ return measured ? 'miss' : 'unknown';
106
+ };
107
+ /** The log names skills without a source and the snapshot holds only counts, so a count above zero cannot be called either way. */
108
+ const countLabel = (count) => (count === 0 ? 'absent' : 'unknown');
109
+ const total = (counts) => Object.values(counts).reduce((sum, count) => sum + count, 0);
110
+ /** A hook is `absent` at zero. Above zero the log gives no source and the snapshot holds only counts, so it is `unknown`, except where the rule skips its settings file by design. */
111
+ const hookLabel = (counts, skippedByDesign) => {
112
+ if (total(counts) === 0)
113
+ return 'absent';
114
+ return skippedByDesign ? 'excluded' : 'unknown';
115
+ };
116
+ const allPositions = (label) => ({
117
+ working: label('working'),
118
+ parent: label('parent'),
119
+ above: label('above'),
120
+ mainCheckout: label('mainCheckout'),
121
+ user: label('user'),
122
+ });
123
+ export const loadStatusOf = ({ session, harness, snapshot, loaded }) => {
124
+ const rule = RULES.get(harness);
125
+ // No snapshot means nothing is known of what existed: never `absent`.
126
+ if (snapshot === undefined) {
127
+ return {
128
+ v: LOAD_STATUS_VERSION,
129
+ session,
130
+ harness,
131
+ instructions: allPositions(() => 'unknown'),
132
+ files: { local: 'unknown', dotClaude: 'unknown', rules: 'unknown' },
133
+ skills: { project: 'unknown', user: 'unknown' },
134
+ hooks: { user: 'unknown', working: 'unknown', ancestors: 'unknown' },
135
+ };
136
+ }
137
+ const fileLabel = (kind) => (rule === undefined ? 'unknown' : kindLabel(snapshot.workingKinds[kind], loaded?.[kind], rule.files));
138
+ return {
139
+ v: LOAD_STATUS_VERSION,
140
+ session,
141
+ harness,
142
+ instructions: allPositions((position) => (rule === undefined ? 'unknown' : instructionLabel(position, snapshot, loaded, rule.positions))),
143
+ files: { local: fileLabel('local'), dotClaude: fileLabel('dotClaude'), rules: fileLabel('rules') },
144
+ skills: { project: countLabel(snapshot.skills.project), user: countLabel(snapshot.skills.user) },
145
+ hooks: {
146
+ user: hookLabel(snapshot.hooks.user, false),
147
+ working: hookLabel(snapshot.hooks.working, false),
148
+ ancestors: hookLabel(snapshot.hooks.ancestors, rule?.hooksAbove === 'skip'),
149
+ },
150
+ };
151
+ };
152
+ /** One row per session, as the text of `load-status.jsonl`. Snapshots that cannot be read leave every session `unknown`: a store problem here must not stop a deposit. */
153
+ export const loadStatusLines = async (storeDir, sessions) => {
154
+ const snapshots = await readCandidates(storeDir).catch(() => new Map());
155
+ return sessions.map((session) => `${JSON.stringify(loadStatusOf({ session: session.session, harness: session.harness ?? 'claude-code', snapshot: snapshots.get(session.session), loaded: session.loadedInstructions }))}\n`).join('');
156
+ };
@@ -0,0 +1,5 @@
1
+ import { type SettleTimes } from './machine-child.js';
2
+ import { type Sender } from './pool-command.js';
3
+ import { type SessionsRuntime } from './store.js';
4
+ /** Runs the child for one Stop's forwarded input (`text`, the one argument `machine-stop.ts` passes). `settle` replaces the settle wait's times, which a test shortens, and `via` the network a send goes over. */
5
+ export declare const runMachineChild: (env: NodeJS.ProcessEnv, sessions: SessionsRuntime, version: string, now: () => number, text: string, settle?: SettleTimes, via?: Sender) => Promise<void>;
@@ -0,0 +1,83 @@
1
+ // The real parts of the machine child (`machine-child.ts`): the session store's capture, the pool's batch build, the file system and the clock. Loaded only by `hook-bin.js machine-child`, so a per-tool hook and the Stop
2
+ // itself never pay for the store and pool modules. It never throws: a failure is one masked line in `pool-send.log`, and the next Stop builds the same batch.
3
+ import { stat } from 'node:fs/promises';
4
+ import { basename } from 'node:path';
5
+ import { setTimeout as sleep } from 'node:timers/promises';
6
+ import { exclusionPaths, loadTerms } from './exclusion.js';
7
+ import { machineChild, parseChildInput } from './machine-child.js';
8
+ import { holdBatch, runNetGit } from './machine-hold.js';
9
+ import { DEPOSIT, machineKeyOf, poolDeposit, poolServer, vocabularyExpired } from './pool-command.js';
10
+ import { machineOf, withInlineTerms } from './machine.js';
11
+ import { machineSend } from './machine-send.js';
12
+ import { recordNotice } from './machine-notice.js';
13
+ import { repoAllowed } from './machine-repo.js';
14
+ import { logFailure } from './pool-due.js';
15
+ import { nightlyPoolDeps } from './pool-nightly-deps.js';
16
+ import { currentStamp, heldBatchOf, outboxFolder } from './pool-outbox.js';
17
+ import { depositSession } from './store.js';
18
+ import { runGit } from './survival.js';
19
+ /**
20
+ * Holds the session's batch on its branch in the session's own repository (`machine-hold.ts`). A machine holds only when its enrollment gave it a machine id (the branch's `<machine>`) and `OARS_SESSION_SALT` (the key of the terms digest); one
21
+ * enrolled without them builds into its outbox as before, and `runMachineChild` has told the person (before any gate, so a session a gate withdrew or a build that threw is told too). A session whose batch a gate withdrew has none in the outbox, so nothing is held for it.
22
+ */
23
+ const holdSession = async (env, gated, sessions, now, machine, forwarded) => {
24
+ if (machine === undefined)
25
+ return;
26
+ const { machine: name, sessionSalt: salt } = machine;
27
+ if (name === undefined || salt === undefined)
28
+ return;
29
+ const id = machineKeyOf(machine, basename(forwarded.transcript, '.jsonl'));
30
+ const held = await heldBatchOf(outboxFolder(sessions.storeDir, poolServer(env, DEPOSIT)), id);
31
+ if (held === undefined)
32
+ return;
33
+ const terms = await loadTerms(exclusionPaths(sessions.projectsRoot, gated).terms);
34
+ await holdBatch({ git: runGit, net: runNetGit, log: (error) => logFailure(sessions.storeDir, now(), error) }, { cwd: forwarded.cwd, allowRepos: machine.allowRepos, machine: name, id, batch: held.bytes, stamp: currentStamp(), terms, salt });
35
+ };
36
+ /** Runs the child for one Stop's forwarded input (`text`, the one argument `machine-stop.ts` passes). `settle` replaces the settle wait's times, which a test shortens, and `via` the network a send goes over. */
37
+ export const runMachineChild = async (env, sessions, version, now, text, settle, via) => {
38
+ const input = parseChildInput(text);
39
+ if (input === undefined)
40
+ return;
41
+ try {
42
+ // The terms the enrollment put in one variable are written to an owner-only file first, so every gate below reads the term file the way it always does.
43
+ const gated = await withInlineTerms(env, sessions.storeDir);
44
+ const repoSalt = env.OARS_REPO_SALT?.trim();
45
+ const keyed = repoSalt === undefined || repoSalt === '' ? sessions : { ...sessions, repoSalt };
46
+ const deps = nightlyPoolDeps(gated, keyed, version, now);
47
+ // A client past its vocabulary's age builds no batch and its sessions wait. The child has no terminal, so the next Stop tells the person, from the notice file; nothing is captured or built until the client is upgraded.
48
+ if (vocabularyExpired(deps) !== undefined) {
49
+ await recordNotice(sessions.storeDir, 'vocabulary_expired', now());
50
+ return;
51
+ }
52
+ const machine = machineOf(env);
53
+ // A machine that cannot hold loses its session's data unseen, so the person is told at every Stop, before the work that can fail or withdraw: the next Stop prints it once.
54
+ if (machine !== undefined && (machine.machine === undefined || machine.sessionSalt === undefined))
55
+ await recordNotice(sessions.storeDir, 'hold_unconfigured', now());
56
+ const allowRepos = machine?.allowRepos ?? [];
57
+ await machineChild({
58
+ outbox: outboxFolder(sessions.storeDir, poolServer(env, DEPOSIT)),
59
+ now,
60
+ ...(settle === undefined ? {} : { settle }),
61
+ // The operator's repository list: the child builds a session only for a repository `origin` names on it.
62
+ allowed: (forwarded) => repoAllowed(allowRepos, forwarded.cwd, runGit),
63
+ sleep: async (ms) => {
64
+ await sleep(ms);
65
+ },
66
+ size: (path) => stat(path).then((found) => found.size, () => undefined),
67
+ capture: async (forwarded) => {
68
+ await depositSession(keyed, forwarded.transcript, now);
69
+ },
70
+ // The store names a session by its transcript's file name, which is what the Stop's session is on every harness this build runs under.
71
+ build: async (forwarded) => {
72
+ await poolDeposit(deps, { session: basename(forwarded.transcript, '.jsonl') });
73
+ },
74
+ hold: (forwarded) => holdSession(env, gated, sessions, now, machine, forwarded),
75
+ send: async () => {
76
+ await machineSend(deps, via);
77
+ },
78
+ }, input);
79
+ }
80
+ catch (error) {
81
+ await logFailure(sessions.storeDir, now(), error);
82
+ }
83
+ };
@@ -0,0 +1,43 @@
1
+ import { type LockTimes } from './pool-send-lock.js';
2
+ /** What a Stop forwards: the session, its transcript, and the folder it ran in (the repository the allow list is checked against). */
3
+ export interface ChildInput {
4
+ session: string;
5
+ transcript: string;
6
+ cwd: string | undefined;
7
+ }
8
+ export interface SettleTimes {
9
+ /** The transcript must be this long unchanged. */
10
+ quietMs: number;
11
+ /** The wait is never longer than this: a transcript still growing is captured as it is. */
12
+ maxMs: number;
13
+ pollMs: number;
14
+ }
15
+ export declare const SETTLE: SettleTimes;
16
+ export interface MachineChildDeps {
17
+ outbox: string;
18
+ now: () => number;
19
+ sleep: (ms: number) => Promise<void>;
20
+ /** The transcript's size, or `undefined` when it cannot be read. */
21
+ size: (path: string) => Promise<number | undefined>;
22
+ /** Whether the session's repository is on the operator's allow list (`machine-repo.ts`); a session that is not is neither captured nor built. */
23
+ allowed: (input: ChildInput) => Promise<boolean>;
24
+ /** Captures the transcript (and its subagents) into the session store. */
25
+ capture: (input: ChildInput) => Promise<void>;
26
+ /** Builds the session's batch into the outbox, with the gates. */
27
+ build: (input: ChildInput) => Promise<void>;
28
+ /** Commits the batch just built to the session's holding branch and pushes it (`machine-hold.ts`); a refused push is the step's own log row, not a throw. */
29
+ hold: (input: ChildInput) => Promise<void>;
30
+ /** Activates the machine's key and sends what its outbox holds once the covenant is accepted (`machine-send.ts`); it never throws, and a refusal is its own log row. Under the same lock as the build. */
31
+ send?: () => Promise<void>;
32
+ settle?: SettleTimes;
33
+ lock?: Partial<LockTimes>;
34
+ }
35
+ /** The input a Stop forwarded, or `undefined` when it names no session or transcript. */
36
+ export declare const parseChildInput: (text: string) => ChildInput | undefined;
37
+ /** Waits until the transcript has been the same size for `quietMs`, at most `maxMs` in all. */
38
+ export declare const settle: (path: string, deps: Pick<MachineChildDeps, "now" | "sleep" | "size">, times?: SettleTimes) => Promise<"quiet" | "timeout">;
39
+ /**
40
+ * `built` when this child ran the session (and anything marked while it did), `marked` when another child held the lock and the mark is left for it. Two windows are closed on purpose: a Stop that marks just after the holder's last look
41
+ * finds the lock free on its second try, and a holder that lets go while a mark is being written looks once more after it has released.
42
+ */
43
+ export declare const machineChild: (deps: MachineChildDeps, input: ChildInput) => Promise<"built" | "marked">;