@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.
- package/SerpentGuide.ouro/agent.json +1 -0
- package/changelog.json +23 -0
- package/dist/heart/daemon/agent-discovery.js +26 -3
- package/dist/heart/daemon/agentic-repair.js +352 -14
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cli-defaults.js +4 -1
- package/dist/heart/daemon/cli-exec.js +110 -4
- package/dist/heart/hatch/hatch-specialist.js +4 -6
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +117 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/nerves/coverage/file-completeness.js +9 -0
- package/package.json +1 -1
package/dist/heart/sync.js
CHANGED
|
@@ -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));
|