@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Boot sync probe — Layer 2 orchestrator for `ouro up`.
|
|
4
|
+
*
|
|
5
|
+
* For each sync-enabled bundle, runs `preTurnPullAsync` wrapped in
|
|
6
|
+
* `runWithTimeouts`, classifies failures via `classifySyncFailure`, and
|
|
7
|
+
* returns aggregated findings. Findings surface in the boot stdout
|
|
8
|
+
* summary written by `cli-exec.ts` after the probe phase.
|
|
9
|
+
*
|
|
10
|
+
* Layer 2 is intentionally a **boot-time preflight surface only** — it
|
|
11
|
+
* does NOT feed into the running daemon's rollup state. `computeDaemonRollup`
|
|
12
|
+
* is unaware of these findings; nothing is persisted to daemon health for
|
|
13
|
+
* the running daemon to consume. Layer 3 RepairGuide is the planned consumer
|
|
14
|
+
* (it reads the in-memory `BootSyncProbeFinding[]` at boot time and
|
|
15
|
+
* dispatches the right diagnostic skill).
|
|
16
|
+
*
|
|
17
|
+
* The probe NEVER:
|
|
18
|
+
* - Writes to `state/` (verified by Unit 7's grep gate).
|
|
19
|
+
* - Throws — every failure becomes a finding.
|
|
20
|
+
* - Hangs — `AbortSignal` from `runWithTimeouts` cuts hung remotes
|
|
21
|
+
* within `hardMs`.
|
|
22
|
+
*
|
|
23
|
+
* Advisory vs blocking classifications (consumed by layer 3, not by the
|
|
24
|
+
* layer 1 rollup — see comment above):
|
|
25
|
+
*
|
|
26
|
+
* | classification | advisory? | rationale |
|
|
27
|
+
* | ---------------------- | --------- | ------------------------------------------- |
|
|
28
|
+
* | auth-failed | no | agent can't sync; needs human intervention |
|
|
29
|
+
* | not-found-404 | no | remote is gone; needs human intervention |
|
|
30
|
+
* | network-down | no | agent can't reach the remote at all |
|
|
31
|
+
* | timeout-hard | no | abort cut the op; remote is hung |
|
|
32
|
+
* | dirty-working-tree | yes | local edits prevent merge; agent still runs |
|
|
33
|
+
* | non-fast-forward | yes | local commits ahead; agent still runs |
|
|
34
|
+
* | merge-conflict | yes | rebase failed; needs cleanup |
|
|
35
|
+
* | timeout-soft | yes | warning surfaced, op completed |
|
|
36
|
+
* | unknown | yes | unrecognised; surface for diagnosis |
|
|
37
|
+
*
|
|
38
|
+
* `advisory: true` is a hint for layer 3 ("warn-and-continue, agent likely
|
|
39
|
+
* still works") vs `advisory: false` ("blocking, agent can't sync until
|
|
40
|
+
* fixed"). Layer 3 will route the right diagnostic skill on this signal.
|
|
41
|
+
*/
|
|
42
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
43
|
+
if (k2 === undefined) k2 = k;
|
|
44
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
45
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
46
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
47
|
+
}
|
|
48
|
+
Object.defineProperty(o, k2, desc);
|
|
49
|
+
}) : (function(o, m, k, k2) {
|
|
50
|
+
if (k2 === undefined) k2 = k;
|
|
51
|
+
o[k2] = m[k];
|
|
52
|
+
}));
|
|
53
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
54
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
55
|
+
}) : function(o, v) {
|
|
56
|
+
o["default"] = v;
|
|
57
|
+
});
|
|
58
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
59
|
+
var ownKeys = function(o) {
|
|
60
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
61
|
+
var ar = [];
|
|
62
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
return ownKeys(o);
|
|
66
|
+
};
|
|
67
|
+
return function (mod) {
|
|
68
|
+
if (mod && mod.__esModule) return mod;
|
|
69
|
+
var result = {};
|
|
70
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
71
|
+
__setModuleDefault(result, mod);
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
})();
|
|
75
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76
|
+
exports.runBootSyncProbe = runBootSyncProbe;
|
|
77
|
+
const path = __importStar(require("path"));
|
|
78
|
+
const sync_1 = require("../sync");
|
|
79
|
+
const sync_classification_1 = require("../sync-classification");
|
|
80
|
+
const timeouts_1 = require("../timeouts");
|
|
81
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
82
|
+
/** Default soft / hard timeout windows for the boot git op. Matches Layer 2 O1 lock. */
|
|
83
|
+
const DEFAULT_SOFT_MS = 8000;
|
|
84
|
+
const DEFAULT_HARD_MS = 15000;
|
|
85
|
+
const BLOCKING_CLASSIFICATIONS = new Set([
|
|
86
|
+
"auth-failed",
|
|
87
|
+
"not-found-404",
|
|
88
|
+
"network-down",
|
|
89
|
+
"timeout-hard",
|
|
90
|
+
]);
|
|
91
|
+
function isAdvisory(classification) {
|
|
92
|
+
return !BLOCKING_CLASSIFICATIONS.has(classification);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Probe sync state for every enabled, git-initialised bundle. Returns
|
|
96
|
+
* findings for every non-clean result. Probes run sequentially (not in
|
|
97
|
+
* parallel) so the boot path's progress reporter has a stable per-agent
|
|
98
|
+
* narrative; the per-probe hard cap means worst-case total wait is
|
|
99
|
+
* `bundles.length * hardMs`, which is acceptable for typical 1-3 agent
|
|
100
|
+
* deployments.
|
|
101
|
+
*/
|
|
102
|
+
async function runBootSyncProbe(bundles, options) {
|
|
103
|
+
const startedAt = Date.now();
|
|
104
|
+
const softMs = options.softMs ?? DEFAULT_SOFT_MS;
|
|
105
|
+
const hardMs = options.hardMs ?? DEFAULT_HARD_MS;
|
|
106
|
+
(0, runtime_1.emitNervesEvent)({
|
|
107
|
+
component: "daemon",
|
|
108
|
+
event: "daemon.boot_sync_probe_start",
|
|
109
|
+
message: "boot sync probe starting",
|
|
110
|
+
meta: { bundleCount: bundles.length, softMs, hardMs },
|
|
111
|
+
});
|
|
112
|
+
const findings = [];
|
|
113
|
+
for (const bundle of bundles) {
|
|
114
|
+
if (!bundle.enabled)
|
|
115
|
+
continue;
|
|
116
|
+
const agentRoot = path.join(options.bundlesRoot, `${bundle.agent}.ouro`);
|
|
117
|
+
// gitInitialized=false: bundle is enabled for sync but never had `git init`.
|
|
118
|
+
// Surface as advisory finding without invoking git.
|
|
119
|
+
if (bundle.gitInitialized === false) {
|
|
120
|
+
findings.push({
|
|
121
|
+
agent: bundle.agent,
|
|
122
|
+
classification: "unknown",
|
|
123
|
+
error: `bundle is not a git repo; run \`git init\` inside ${agentRoot} to enable sync (or disable sync in agent.json)`,
|
|
124
|
+
conflictFiles: [],
|
|
125
|
+
warnings: [],
|
|
126
|
+
advisory: true,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const syncConfig = { enabled: bundle.enabled, remote: bundle.remote };
|
|
131
|
+
const outcome = await (0, timeouts_1.runWithTimeouts)((signal) => (0, sync_1.preTurnPullAsync)(agentRoot, syncConfig, { signal }), { softMs, hardMs, label: `boot-sync-probe ${bundle.agent}`, envKey: "GIT" });
|
|
132
|
+
// Hard timeout — the probe was aborted. Synthesise a finding from the
|
|
133
|
+
// outcome's classification (the underlying `preTurnPullAsync` may also
|
|
134
|
+
// have rejected with an AbortError, but the wrapper already swallowed
|
|
135
|
+
// it).
|
|
136
|
+
if (outcome.classification === "timeout-hard") {
|
|
137
|
+
findings.push({
|
|
138
|
+
agent: bundle.agent,
|
|
139
|
+
classification: "timeout-hard",
|
|
140
|
+
error: `boot sync probe for ${bundle.agent} aborted after ${hardMs}ms hard timeout`,
|
|
141
|
+
conflictFiles: [],
|
|
142
|
+
warnings: outcome.warnings,
|
|
143
|
+
advisory: isAdvisory("timeout-hard"),
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const result = outcome.result;
|
|
148
|
+
/* v8 ignore start -- defensive: exclusive state from runWithTimeouts contract — either result or classification, never both undefined @preserve */
|
|
149
|
+
if (!result) {
|
|
150
|
+
findings.push({
|
|
151
|
+
agent: bundle.agent,
|
|
152
|
+
classification: "unknown",
|
|
153
|
+
error: "boot sync probe returned without result or classification",
|
|
154
|
+
conflictFiles: [],
|
|
155
|
+
warnings: outcome.warnings,
|
|
156
|
+
advisory: true,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
/* v8 ignore stop */
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
const classification = (0, sync_classification_1.classifySyncFailure)(new Error(result.error ?? "unknown sync error"), { agentRoot });
|
|
163
|
+
findings.push({
|
|
164
|
+
agent: bundle.agent,
|
|
165
|
+
classification: classification.classification,
|
|
166
|
+
error: classification.error,
|
|
167
|
+
conflictFiles: classification.conflictFiles,
|
|
168
|
+
warnings: outcome.warnings,
|
|
169
|
+
advisory: isAdvisory(classification.classification),
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Probe succeeded. If the soft warning fired, surface it as an advisory
|
|
174
|
+
// finding so the operator sees the slow-pull warning even on success.
|
|
175
|
+
if (outcome.warnings.length > 0) {
|
|
176
|
+
findings.push({
|
|
177
|
+
agent: bundle.agent,
|
|
178
|
+
classification: "timeout-soft",
|
|
179
|
+
error: outcome.warnings.join("; "),
|
|
180
|
+
conflictFiles: [],
|
|
181
|
+
warnings: outcome.warnings,
|
|
182
|
+
advisory: true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Stable order — sort by agent name so renderers and tests see the same
|
|
187
|
+
// sequence.
|
|
188
|
+
findings.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
189
|
+
const durationMs = Date.now() - startedAt;
|
|
190
|
+
(0, runtime_1.emitNervesEvent)({
|
|
191
|
+
component: "daemon",
|
|
192
|
+
event: "daemon.boot_sync_probe_end",
|
|
193
|
+
message: "boot sync probe complete",
|
|
194
|
+
meta: { findingCount: findings.length, durationMs },
|
|
195
|
+
});
|
|
196
|
+
return { findings, durationMs };
|
|
197
|
+
}
|
|
@@ -432,7 +432,10 @@ async function defaultRunSerpentGuide() {
|
|
|
432
432
|
const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
|
|
433
433
|
setRuntimeLogger(createLogger({ level: "error" }));
|
|
434
434
|
// Configure runtime: set agent identity + config override so runAgent
|
|
435
|
-
// doesn't try to read from ~/AgentBundles/SerpentGuide.ouro
|
|
435
|
+
// doesn't try to read from ~/AgentBundles/SerpentGuide.ouro/. (As of
|
|
436
|
+
// Layer 3, SerpentGuide identities live in-repo only — the
|
|
437
|
+
// `~/AgentBundles/SerpentGuide.ouro/psyche/identities` override path
|
|
438
|
+
// was removed. The override path is no longer read or honored.)
|
|
436
439
|
setAgentName("SerpentGuide");
|
|
437
440
|
// Build specialist system prompt
|
|
438
441
|
const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
|
|
@@ -42,6 +42,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
exports.summarizeDaemonStartupFailure = summarizeDaemonStartupFailure;
|
|
43
43
|
exports.writeDriftAdvisorySummary = writeDriftAdvisorySummary;
|
|
44
44
|
exports.collectAgentDriftAdvisories = collectAgentDriftAdvisories;
|
|
45
|
+
exports.writeSyncProbeSummary = writeSyncProbeSummary;
|
|
46
|
+
exports.summarizeSyncProbeFindings = summarizeSyncProbeFindings;
|
|
45
47
|
exports.mergeStartupStability = mergeStartupStability;
|
|
46
48
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
47
49
|
exports.listGithubCopilotModels = listGithubCopilotModels;
|
|
@@ -110,6 +112,7 @@ const up_progress_1 = require("./up-progress");
|
|
|
110
112
|
const provider_ping_progress_1 = require("./provider-ping-progress");
|
|
111
113
|
const provider_ping_1 = require("../provider-ping");
|
|
112
114
|
const agent_discovery_1 = require("./agent-discovery");
|
|
115
|
+
const boot_sync_probe_1 = require("./boot-sync-probe");
|
|
113
116
|
const connect_bay_1 = require("./connect-bay");
|
|
114
117
|
const runtime_capability_check_1 = require("../runtime-capability-check");
|
|
115
118
|
const vault_items_1 = require("./vault-items");
|
|
@@ -471,10 +474,44 @@ function providerRepairCountSummary(count) {
|
|
|
471
474
|
return "selected providers answered live checks";
|
|
472
475
|
return `${count} ${count === 1 ? "needs" : "need"} attention`;
|
|
473
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Layer 2: human-readable summary of boot-sync-probe findings, written to
|
|
479
|
+
* stdout when any non-clean finding surfaces. The order reads "blockers
|
|
480
|
+
* first, then advisories" so the operator's eye lands on the actionable
|
|
481
|
+
* items before the warnings.
|
|
482
|
+
*/
|
|
483
|
+
function writeSyncProbeSummary(deps, findings) {
|
|
484
|
+
const lines = ["sync probe findings:"];
|
|
485
|
+
const sortKey = (f) => (f.advisory ? 1 : 0);
|
|
486
|
+
const sorted = [...findings].sort((a, b) => sortKey(a) - sortKey(b) || a.agent.localeCompare(b.agent));
|
|
487
|
+
for (const finding of sorted) {
|
|
488
|
+
const severity = finding.advisory ? "warn" : "block";
|
|
489
|
+
lines.push(` [${severity}] ${finding.agent}: ${finding.classification} — ${finding.error.split("\n")[0]}`);
|
|
490
|
+
}
|
|
491
|
+
deps.writeStdout(lines.join("\n"));
|
|
492
|
+
}
|
|
474
493
|
function bootPhasePlan(daemonAlive) {
|
|
475
494
|
return daemonAlive
|
|
476
|
-
? ["update check", "system setup", "starting daemon", "provider checks", "final daemon check"]
|
|
477
|
-
: ["update check", "system setup", "provider checks", "starting daemon", "final daemon check"];
|
|
495
|
+
? ["update check", "system setup", "sync probe", "starting daemon", "provider checks", "final daemon check"]
|
|
496
|
+
: ["update check", "system setup", "sync probe", "provider checks", "starting daemon", "final daemon check"];
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Layer 2: brief, scannable summary of a boot-sync-probe finding for the
|
|
500
|
+
* progress reporter. Renderer-style hints (full repair guidance) live in
|
|
501
|
+
* `inner-status.ts` / `startup-tui.ts` consumers; this helper is just for
|
|
502
|
+
* the progress phase blurb.
|
|
503
|
+
*/
|
|
504
|
+
function summarizeSyncProbeFindings(findings) {
|
|
505
|
+
if (findings.length === 0)
|
|
506
|
+
return "all sync-enabled bundles healthy";
|
|
507
|
+
const blocking = findings.filter((f) => !f.advisory).length;
|
|
508
|
+
const advisory = findings.filter((f) => f.advisory).length;
|
|
509
|
+
const parts = [];
|
|
510
|
+
if (blocking > 0)
|
|
511
|
+
parts.push(`${blocking} blocking`);
|
|
512
|
+
if (advisory > 0)
|
|
513
|
+
parts.push(`${advisory} advisory`);
|
|
514
|
+
return `${findings.length} finding${findings.length === 1 ? "" : "s"} (${parts.join(", ")})`;
|
|
478
515
|
}
|
|
479
516
|
function createHumanCommandProgress(deps, commandName) {
|
|
480
517
|
return new up_progress_1.CommandProgress({
|
|
@@ -5707,6 +5744,45 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5707
5744
|
bundlesRoot: deps.bundlesRoot ?? bundlesRoot,
|
|
5708
5745
|
promptInput: deps.promptInput,
|
|
5709
5746
|
});
|
|
5747
|
+
// ── Layer 2 sync probe: pre-flight `git pull` for every sync-enabled bundle ──
|
|
5748
|
+
// Runs BEFORE provider checks so that any agent.json changes pulled from
|
|
5749
|
+
// the remote are visible to the live-check that follows. The probe has a
|
|
5750
|
+
// hard 15s per-agent timeout via `runWithTimeouts`, so a hung remote
|
|
5751
|
+
// can't stall the boot. `--no-repair` doesn't change probe behavior — it
|
|
5752
|
+
// gates layer 3's RepairGuide invocation, which lands in a separate PR.
|
|
5753
|
+
//
|
|
5754
|
+
// Errors in the probe path itself are caught and logged — they must not
|
|
5755
|
+
// break the boot, since the probe is best-effort visibility, not a gate.
|
|
5756
|
+
// Layer 3: hoist sync-probe findings to outer scope so the
|
|
5757
|
+
// RepairGuide diagnostic call later in this function can include
|
|
5758
|
+
// them in its prompt. Default to [] when the probe was skipped or
|
|
5759
|
+
// threw — the runAgenticRepair callsite then sees "no sync
|
|
5760
|
+
// findings to report" rather than missing data.
|
|
5761
|
+
let bootSyncFindings = [];
|
|
5762
|
+
progress.startPhase("sync probe");
|
|
5763
|
+
try {
|
|
5764
|
+
const syncProbeImpl = deps.runBootSyncProbeImpl ?? boot_sync_probe_1.runBootSyncProbe;
|
|
5765
|
+
const syncRows = (0, agent_discovery_1.listBundleSyncRows)({ bundlesRoot: deps.bundlesRoot ?? bundlesRoot });
|
|
5766
|
+
const syncProbeResult = await syncProbeImpl(syncRows, {
|
|
5767
|
+
bundlesRoot: deps.bundlesRoot ?? bundlesRoot,
|
|
5768
|
+
});
|
|
5769
|
+
bootSyncFindings = syncProbeResult.findings;
|
|
5770
|
+
progress.completePhase("sync probe", summarizeSyncProbeFindings(syncProbeResult.findings));
|
|
5771
|
+
if (syncProbeResult.findings.length > 0) {
|
|
5772
|
+
writeSyncProbeSummary(deps, syncProbeResult.findings);
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
catch (probeError) {
|
|
5776
|
+
const message = probeError instanceof Error ? probeError.message : String(probeError);
|
|
5777
|
+
(0, runtime_1.emitNervesEvent)({
|
|
5778
|
+
level: "warn",
|
|
5779
|
+
component: "daemon",
|
|
5780
|
+
event: "daemon.boot_sync_probe_failed",
|
|
5781
|
+
message: "boot sync probe failed; continuing boot",
|
|
5782
|
+
meta: { error: message },
|
|
5783
|
+
});
|
|
5784
|
+
progress.completePhase("sync probe", "skipped (probe error)");
|
|
5785
|
+
}
|
|
5710
5786
|
const daemonAliveBeforeStart = await deps.checkSocketAlive(deps.socketPath);
|
|
5711
5787
|
progress.setPhasePlan?.(bootPhasePlan(daemonAliveBeforeStart));
|
|
5712
5788
|
let providerChecksAlreadyRun = false;
|
|
@@ -5808,8 +5884,35 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5808
5884
|
typedDegraded.forEach((entry) => repairedAgents.add(entry.agent));
|
|
5809
5885
|
}
|
|
5810
5886
|
}
|
|
5811
|
-
|
|
5812
|
-
|
|
5887
|
+
// Layer 3: extended activation contract — fires when there are
|
|
5888
|
+
// untyped degraded entries OR when typed entries stack to ≥3 (compound
|
|
5889
|
+
// situations that warrant a RepairGuide-driven proposal pass). The
|
|
5890
|
+
// `--no-repair` flag short-circuits the entire decision via
|
|
5891
|
+
// `shouldFireRepairGuide`. The set passed into `runAgenticRepair` is
|
|
5892
|
+
// the union of typed + untyped so the diagnostic prompt has the full
|
|
5893
|
+
// picture. When the gate fires solely on typed-stacking,
|
|
5894
|
+
// `runAgenticRepair` is told via `forceDiagnosis: true` to bypass the
|
|
5895
|
+
// early-return that normally defers typed-only sets to the
|
|
5896
|
+
// deterministic typed repair flow that already ran above.
|
|
5897
|
+
const repairGuideShouldFire = (0, agentic_repair_1.shouldFireRepairGuide)({
|
|
5898
|
+
untypedDegraded,
|
|
5899
|
+
typedDegraded,
|
|
5900
|
+
noRepair: Boolean(command.noRepair),
|
|
5901
|
+
});
|
|
5902
|
+
if (repairGuideShouldFire) {
|
|
5903
|
+
const repairInput = [...untypedDegraded, ...typedDegraded];
|
|
5904
|
+
const forceDiagnosis = untypedDegraded.length === 0 && typedDegraded.length >= 3;
|
|
5905
|
+
// Layer 3: collect drift findings here so the RepairGuide
|
|
5906
|
+
// prompt receives them as a structured JSON block. Drift is
|
|
5907
|
+
// already collected for the no-repair path above; we collect
|
|
5908
|
+
// again here because the repair path is a separate branch.
|
|
5909
|
+
// Filter to agents in repairInput so the diagnostic prompt
|
|
5910
|
+
// doesn't carry drift from healthy peers — narrows the
|
|
5911
|
+
// signal to the set being diagnosed.
|
|
5912
|
+
const repairAgentNames = new Set(repairInput.map((entry) => entry.agent));
|
|
5913
|
+
const repairDriftFindings = (await collectAgentDriftAdvisories(deps))
|
|
5914
|
+
.filter((finding) => repairAgentNames.has(finding.agent));
|
|
5915
|
+
const repairResult = await (0, agentic_repair_1.runAgenticRepair)(repairInput, {
|
|
5813
5916
|
/* v8 ignore start -- production provider discovery wiring @preserve */
|
|
5814
5917
|
discoverWorkingProvider: async (agentName) => {
|
|
5815
5918
|
const { discoverWorkingProvider: discover } = await Promise.resolve().then(() => __importStar(require("./provider-discovery")));
|
|
@@ -5853,6 +5956,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5853
5956
|
skipQueueSummary: true,
|
|
5854
5957
|
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
5855
5958
|
stdoutColumns: deps.stdoutColumns ?? process.stdout.columns,
|
|
5959
|
+
forceDiagnosis,
|
|
5960
|
+
driftFindings: repairDriftFindings,
|
|
5961
|
+
syncFindings: bootSyncFindings,
|
|
5856
5962
|
});
|
|
5857
5963
|
if (repairResult.repairsAttempted) {
|
|
5858
5964
|
repairsAttempted = true;
|
|
@@ -38,15 +38,13 @@ exports.getRepoSpecialistIdentitiesDir = getRepoSpecialistIdentitiesDir;
|
|
|
38
38
|
exports.syncSpecialistIdentities = syncSpecialistIdentities;
|
|
39
39
|
exports.pickRandomSpecialistIdentity = pickRandomSpecialistIdentity;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
|
-
const os = __importStar(require("os"));
|
|
42
41
|
const path = __importStar(require("path"));
|
|
43
42
|
const runtime_1 = require("../../nerves/runtime");
|
|
44
43
|
function getSpecialistIdentitySourceDir() {
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Fall back to the bundled copy shipped with the npm package
|
|
44
|
+
// Layer 3: in-repo is the only source. The previous `~/AgentBundles/`
|
|
45
|
+
// override branch was removed because there's no scenario where an
|
|
46
|
+
// operator should be editing identities outside the repo — they should
|
|
47
|
+
// edit the in-repo copy and let the daemon read from there.
|
|
50
48
|
return path.join(__dirname, "..", "..", "..", "SerpentGuide.ouro", "psyche", "identities");
|
|
51
49
|
}
|
|
52
50
|
function getRepoSpecialistIdentitiesDir() {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sync failure taxonomy classifier — Layer 2 of the harness-hardening sequence.
|
|
4
|
+
*
|
|
5
|
+
* Pure pattern-matcher over (error, context) that turns common git failure
|
|
6
|
+
* shapes into the locked taxonomy variants. Used by `runBootSyncProbe` (the
|
|
7
|
+
* `ouro up` pre-flight pull orchestrator) and by `postTurnPush`'s legacy
|
|
8
|
+
* push-rejected/conflict path so both producers share one vocabulary.
|
|
9
|
+
*
|
|
10
|
+
* Pattern priority — most actionable / specific wins:
|
|
11
|
+
* 1. Abort signal (timeout-soft / timeout-hard) — caller explicitly aborted.
|
|
12
|
+
* 2. not-found-404 — 404 / "Repository not found": remote endpoint gone.
|
|
13
|
+
* 3. auth-failed — 401 / 403 / "Authentication failed".
|
|
14
|
+
* 4. network-down — ENOTFOUND / ECONNREFUSED / "Could not resolve host".
|
|
15
|
+
* 5. dirty-working-tree — "would be overwritten" / "stash them".
|
|
16
|
+
* 6. merge-conflict — CONFLICT marker in stderr (also collects file list).
|
|
17
|
+
* 7. non-fast-forward — "non-fast-forward" / "fetch first".
|
|
18
|
+
* 8. unknown — fallthrough.
|
|
19
|
+
*
|
|
20
|
+
* The classifier never throws and never writes to disk. It calls
|
|
21
|
+
* `git status --porcelain=v1` only when it has already classified the error
|
|
22
|
+
* as a merge conflict, to enumerate unmerged paths for the consumer.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.classifySyncFailure = classifySyncFailure;
|
|
26
|
+
const child_process_1 = require("child_process");
|
|
27
|
+
/**
|
|
28
|
+
* Enumerate unmerged paths via `git status --porcelain=v1`. Pulled out as a
|
|
29
|
+
* pure helper so the classifier can be tested without a live git repo (the
|
|
30
|
+
* caller mocks `child_process.execFileSync`).
|
|
31
|
+
*
|
|
32
|
+
* Mirrors `sync.ts:collectRebaseConflictFiles` — kept as a separate copy here
|
|
33
|
+
* so this module has zero internal dep on `sync.ts`. The caller of
|
|
34
|
+
* `classifySyncFailure` doesn't see the duplication; the runtime cost is one
|
|
35
|
+
* git invocation per merge-conflict classification, same as before.
|
|
36
|
+
*/
|
|
37
|
+
function collectConflictFiles(agentRoot) {
|
|
38
|
+
try {
|
|
39
|
+
const output = (0, child_process_1.execFileSync)("git", ["status", "--porcelain=v1"], {
|
|
40
|
+
cwd: agentRoot,
|
|
41
|
+
stdio: "pipe",
|
|
42
|
+
timeout: 5000,
|
|
43
|
+
}).toString();
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const line of output.split("\n")) {
|
|
46
|
+
// Unmerged paths in porcelain v1 are prefixed with UU/AA/DD/AU/UA/DU/UD.
|
|
47
|
+
if (/^(UU|AA|DD|AU|UA|DU|UD) /.test(line)) {
|
|
48
|
+
files.push(line.slice(3).trim());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* v8 ignore next -- defensive: git status failure inside a git repo would require a corrupt repo @preserve */
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isAbortError(error) {
|
|
59
|
+
if (typeof error !== "object" || error === null)
|
|
60
|
+
return false;
|
|
61
|
+
const candidate = error;
|
|
62
|
+
if (candidate.name === "AbortError")
|
|
63
|
+
return true;
|
|
64
|
+
if (candidate.code === "ABORT_ERR")
|
|
65
|
+
return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function readMessage(error) {
|
|
69
|
+
if (error instanceof Error)
|
|
70
|
+
return error.message;
|
|
71
|
+
if (typeof error === "string")
|
|
72
|
+
return error;
|
|
73
|
+
if (error === null || error === undefined)
|
|
74
|
+
return String(error);
|
|
75
|
+
try {
|
|
76
|
+
return JSON.stringify(error);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* v8 ignore next -- defensive: JSON.stringify only fails on circular/BigInt; real-world git errors don't trigger it @preserve */
|
|
80
|
+
return String(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function readErrorCode(error) {
|
|
84
|
+
if (typeof error !== "object" || error === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
const code = error.code;
|
|
87
|
+
return typeof code === "string" ? code : undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Classify a sync failure into one of the locked taxonomy variants.
|
|
91
|
+
*
|
|
92
|
+
* Pure: never throws, never writes. Calls `git status` only when the error
|
|
93
|
+
* was already classified as a merge conflict, to enumerate unmerged files.
|
|
94
|
+
*/
|
|
95
|
+
function classifySyncFailure(error, context) {
|
|
96
|
+
const message = readMessage(error);
|
|
97
|
+
const errorCode = readErrorCode(error);
|
|
98
|
+
// 1. Abort signal — highest priority. Caller's signal trumps content match.
|
|
99
|
+
if (isAbortError(error)) {
|
|
100
|
+
const reason = context.abortReason ?? "hard";
|
|
101
|
+
return {
|
|
102
|
+
classification: reason === "soft" ? "timeout-soft" : "timeout-hard",
|
|
103
|
+
error: message,
|
|
104
|
+
conflictFiles: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Lowercased copy for case-insensitive substring matching.
|
|
108
|
+
const lower = message.toLowerCase();
|
|
109
|
+
// 2. Not-found-404 — most actionable diagnosis when both 404 and other
|
|
110
|
+
// signals are present. "404" and "Repository not found" are the canonical
|
|
111
|
+
// shapes.
|
|
112
|
+
if (lower.includes("404") || lower.includes("repository not found")) {
|
|
113
|
+
return {
|
|
114
|
+
classification: "not-found-404",
|
|
115
|
+
error: message,
|
|
116
|
+
conflictFiles: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// 3. Auth failed — 401 / 403 / "Authentication failed" / "Permission denied".
|
|
120
|
+
if (lower.includes("401")
|
|
121
|
+
|| lower.includes("403")
|
|
122
|
+
|| lower.includes("authentication failed")
|
|
123
|
+
|| lower.includes("permission denied")) {
|
|
124
|
+
return {
|
|
125
|
+
classification: "auth-failed",
|
|
126
|
+
error: message,
|
|
127
|
+
conflictFiles: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// 4. Network down — DNS / connection errors.
|
|
131
|
+
if (errorCode === "ENOTFOUND"
|
|
132
|
+
|| errorCode === "ECONNREFUSED"
|
|
133
|
+
|| lower.includes("enotfound")
|
|
134
|
+
|| lower.includes("econnrefused")
|
|
135
|
+
|| lower.includes("could not resolve host")
|
|
136
|
+
|| lower.includes("connection refused")) {
|
|
137
|
+
return {
|
|
138
|
+
classification: "network-down",
|
|
139
|
+
error: message,
|
|
140
|
+
conflictFiles: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// 5. Dirty working tree — pull / merge would clobber uncommitted changes.
|
|
144
|
+
if (lower.includes("would be overwritten")
|
|
145
|
+
|| lower.includes("commit your changes or stash them")) {
|
|
146
|
+
return {
|
|
147
|
+
classification: "dirty-working-tree",
|
|
148
|
+
error: message,
|
|
149
|
+
conflictFiles: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// 6. Merge conflict — CONFLICT marker in stderr. Collect unmerged files.
|
|
153
|
+
if (lower.includes("conflict")) {
|
|
154
|
+
return {
|
|
155
|
+
classification: "merge-conflict",
|
|
156
|
+
error: message,
|
|
157
|
+
conflictFiles: collectConflictFiles(context.agentRoot),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// 7. Non-fast-forward — push rejected because remote moved.
|
|
161
|
+
if (lower.includes("non-fast-forward")
|
|
162
|
+
|| lower.includes("fetch first")
|
|
163
|
+
|| lower.includes("rejected")) {
|
|
164
|
+
return {
|
|
165
|
+
classification: "non-fast-forward",
|
|
166
|
+
error: message,
|
|
167
|
+
conflictFiles: [],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// 8. Fallthrough.
|
|
171
|
+
return {
|
|
172
|
+
classification: "unknown",
|
|
173
|
+
error: message,
|
|
174
|
+
conflictFiles: [],
|
|
175
|
+
};
|
|
176
|
+
}
|