@ouro.bot/cli 0.1.0-alpha.517 → 0.1.0-alpha.519

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.
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.preTurnPull = preTurnPull;
37
37
  exports.postTurnPush = postTurnPush;
38
+ exports.preTurnPullAsync = preTurnPullAsync;
38
39
  const child_process_1 = require("child_process");
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
@@ -330,3 +331,119 @@ function postTurnPush(agentRoot, config) {
330
331
  return { ok: false, error };
331
332
  }
332
333
  }
334
+ /**
335
+ * Layer 2 — async, signal-aware sibling of `preTurnPull`.
336
+ *
337
+ * Used by `runBootSyncProbe` (the `ouro up` boot orchestrator) to perform
338
+ * the pre-flight pull with end-to-end `AbortSignal` propagation. The
339
+ * underlying `child_process.execFile` accepts the signal and kills the git
340
+ * child process when it aborts, so a hung remote (DNS hole, slow server)
341
+ * can be cut by the boot timeout wrapper rather than hanging the whole
342
+ * boot.
343
+ *
344
+ * The legacy sync `preTurnPull` is preserved unchanged for the per-turn
345
+ * pipeline at `src/senses/pipeline.ts:522`. The two functions share the
346
+ * same `.git` and remote-availability gates — the only difference is the
347
+ * pull itself: `execFileSync` (no signal) vs `execFile` + `{ signal }`.
348
+ *
349
+ * Honour-the-signal contract:
350
+ * - If `options.signal` is already aborted at call time, the pull is
351
+ * skipped and the result is `{ ok: false, error: "aborted" }`.
352
+ * - If `options.signal` aborts mid-fetch, the child receives `SIGTERM`
353
+ * via Node's built-in AbortSignal handling, and the result is
354
+ * `{ ok: false, error: <abort message> }`.
355
+ * - With no signal supplied, behaviour matches the sync version (subject
356
+ * to the small differences listed above — same git-repo / no-remote
357
+ * gates and same nerves events).
358
+ */
359
+ function preTurnPullAsync(agentRoot, config, options = {}) {
360
+ (0, runtime_1.emitNervesEvent)({
361
+ component: "heart",
362
+ event: "heart.sync_pull_start",
363
+ message: "pre-turn pull starting (async)",
364
+ meta: { agentRoot, remote: config.remote },
365
+ });
366
+ // Bail early when the caller has already aborted — saves a git invocation
367
+ // and signals failure consistently.
368
+ if (options.signal?.aborted) {
369
+ (0, runtime_1.emitNervesEvent)({
370
+ level: "warn",
371
+ component: "heart",
372
+ event: "heart.sync_pull_aborted",
373
+ message: "pre-turn pull skipped: signal already aborted",
374
+ meta: { agentRoot },
375
+ });
376
+ return Promise.resolve({ ok: false, error: "aborted before pull started" });
377
+ }
378
+ // Same .git presence check as the sync version.
379
+ const repoCheck = ensureGitRepo(agentRoot);
380
+ if (!repoCheck.ok) {
381
+ (0, runtime_1.emitNervesEvent)({
382
+ level: "warn",
383
+ component: "heart",
384
+ event: "heart.sync_not_a_repo",
385
+ message: "pre-turn pull failed: bundle is not a git repo (async)",
386
+ meta: { agentRoot },
387
+ });
388
+ return Promise.resolve(repoCheck);
389
+ }
390
+ // Remote-presence check stays sync — it's a fast local op and doesn't
391
+ // need cancellation. The hangable op is the actual pull.
392
+ try {
393
+ const remoteOutput = (0, child_process_1.execFileSync)("git", ["remote"], {
394
+ cwd: agentRoot,
395
+ stdio: "pipe",
396
+ timeout: 5000,
397
+ }).toString().trim();
398
+ if (remoteOutput.length === 0) {
399
+ (0, runtime_1.emitNervesEvent)({
400
+ component: "heart",
401
+ event: "heart.sync_pull_end",
402
+ message: "pre-turn pull skipped: no remote configured (async)",
403
+ meta: { agentRoot },
404
+ });
405
+ return Promise.resolve({ ok: true });
406
+ }
407
+ }
408
+ catch (err) {
409
+ const error = err instanceof Error ? err.message : String(err);
410
+ (0, runtime_1.emitNervesEvent)({
411
+ component: "heart",
412
+ event: "heart.sync_pull_error",
413
+ message: "pre-turn pull failed: git remote check failed (async)",
414
+ meta: { agentRoot, error },
415
+ });
416
+ return Promise.resolve({ ok: false, error });
417
+ }
418
+ // The hangable op. `execFile` accepts `{ signal }` and kills the child
419
+ // when the signal aborts — that's the whole point of the async path.
420
+ const execOptions = {
421
+ cwd: agentRoot,
422
+ timeout: 30000,
423
+ };
424
+ if (options.signal) {
425
+ execOptions.signal = options.signal;
426
+ }
427
+ return new Promise((resolve) => {
428
+ (0, child_process_1.execFile)("git", ["pull", config.remote], execOptions, (err) => {
429
+ if (err) {
430
+ const error = err instanceof Error ? err.message : String(err);
431
+ (0, runtime_1.emitNervesEvent)({
432
+ component: "heart",
433
+ event: "heart.sync_pull_error",
434
+ message: "pre-turn pull failed (async)",
435
+ meta: { agentRoot, error },
436
+ });
437
+ resolve({ ok: false, error });
438
+ return;
439
+ }
440
+ (0, runtime_1.emitNervesEvent)({
441
+ component: "heart",
442
+ event: "heart.sync_pull_end",
443
+ message: "pre-turn pull complete (async)",
444
+ meta: { agentRoot },
445
+ });
446
+ resolve({ ok: true });
447
+ });
448
+ });
449
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Soft/hard timeout pattern for boot-path operations — Layer 2.
4
+ *
5
+ * Soft timeout = "log a warning, keep going". The op continues; the consumer
6
+ * records the warning and moves on.
7
+ * Hard timeout = "abort the op via AbortSignal". The underlying op is
8
+ * expected to honour the signal (Node child_process accepts `{ signal }`,
9
+ * fetch accepts `{ signal }`, etc.). When the signal aborts, the op should
10
+ * reject with an AbortError, and the wrapper returns
11
+ * `{ classification: "timeout-hard" }` rather than re-throwing.
12
+ *
13
+ Two optional env overrides (currently only `GIT` is consumed):
14
+ * - `OURO_BOOT_TIMEOUT_GIT_SOFT` / `OURO_BOOT_TIMEOUT_GIT_HARD` — boot
15
+ * git operations (fetch / pull). Used when `envKey === "GIT"`.
16
+ *
17
+ * Env values are parsed as integer milliseconds. Non-numeric or non-positive
18
+ * values are ignored (the explicit `softMs` / `hardMs` defaults from the
19
+ * caller win in that case).
20
+ *
21
+ * Pattern guarantee: timers cleared on resolve / reject so the function
22
+ * holds no refs after settlement. Important because `ouro up` chains
23
+ * many of these and a leaking timer would block process exit.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.runWithTimeouts = runWithTimeouts;
27
+ function readEnvMs(name) {
28
+ const raw = process.env[name];
29
+ if (raw === undefined || raw === null)
30
+ return null;
31
+ const parsed = Number.parseInt(raw, 10);
32
+ if (!Number.isFinite(parsed) || parsed <= 0)
33
+ return null;
34
+ return parsed;
35
+ }
36
+ function resolveTimeouts(options) {
37
+ let softMs = options.softMs;
38
+ let hardMs = options.hardMs;
39
+ if (options.envKey === "GIT") {
40
+ const envSoft = readEnvMs("OURO_BOOT_TIMEOUT_GIT_SOFT");
41
+ if (envSoft !== null)
42
+ softMs = envSoft;
43
+ const envHard = readEnvMs("OURO_BOOT_TIMEOUT_GIT_HARD");
44
+ if (envHard !== null)
45
+ hardMs = envHard;
46
+ }
47
+ return { softMs, hardMs };
48
+ }
49
+ /**
50
+ * Run `fn` with soft and hard timeouts.
51
+ *
52
+ * - Returns `{ result }` on success.
53
+ * - Returns `{ classification: "timeout-hard", warnings }` when aborted.
54
+ * - Returns `{ result, warnings: [...] }` when soft tripped but op completed.
55
+ * - Rejects when `fn` throws a non-abort error (callers can wrap with
56
+ * classifier).
57
+ */
58
+ async function runWithTimeouts(fn, options) {
59
+ const { softMs, hardMs } = resolveTimeouts(options);
60
+ const controller = new AbortController();
61
+ const warnings = [];
62
+ let softTimer = setTimeout(() => {
63
+ softTimer = null;
64
+ warnings.push(`${options.label}: soft timeout exceeded (${softMs}ms) — warning, continuing until hard cut`);
65
+ }, softMs);
66
+ let hardTimer = setTimeout(() => {
67
+ hardTimer = null;
68
+ controller.abort();
69
+ }, hardMs);
70
+ const cleanup = () => {
71
+ if (softTimer !== null) {
72
+ clearTimeout(softTimer);
73
+ softTimer = null;
74
+ }
75
+ if (hardTimer !== null) {
76
+ clearTimeout(hardTimer);
77
+ hardTimer = null;
78
+ }
79
+ };
80
+ try {
81
+ const result = await fn(controller.signal);
82
+ cleanup();
83
+ // If the abort fired and the op resolved gracefully (e.g., the inner
84
+ // function caught the AbortError and returned a structured result), we
85
+ // still classify the outcome as timeout-hard — the op was aborted from
86
+ // the caller's perspective even if no exception propagated. The caller
87
+ // can ignore the classification and use `result` if both are present.
88
+ if (controller.signal.aborted) {
89
+ return { classification: "timeout-hard", warnings };
90
+ }
91
+ return { result, warnings };
92
+ }
93
+ catch (err) {
94
+ cleanup();
95
+ if (controller.signal.aborted) {
96
+ // Hard timeout fired — abort wins over whatever error the op threw.
97
+ return { classification: "timeout-hard", warnings };
98
+ }
99
+ throw err;
100
+ }
101
+ }
@@ -149,6 +149,15 @@ const DISPATCH_EXEMPT_PATTERNS = [
149
149
  // Diagnostics-only utility; output is human-readable summary.
150
150
  "heart/session-stats-cli-main",
151
151
  "heart/session-stats",
152
+ // Layer 2 sync classifier: pure pattern-matcher mapping (error, context)
153
+ // to a SyncClassification. The orchestrator (boot-sync-probe.ts) owns
154
+ // observability via daemon.boot_sync_probe_start/end events; the
155
+ // post-turn push path (sync.ts) emits its own classification events.
156
+ "heart/sync-classification",
157
+ // Layer 2 timeout wrapper: pure soft/hard timeout abstraction over
158
+ // AbortController + setTimeout. Callers (boot-sync-probe.ts and any
159
+ // future consumer) own observability; the wrapper itself is mechanical.
160
+ "heart/timeouts",
152
161
  ];
153
162
  function isDispatchExempt(filePath) {
154
163
  return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.517",
3
+ "version": "0.1.0-alpha.519",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",