@ouro.bot/cli 0.1.0-alpha.518 → 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 +13 -0
- package/dist/heart/daemon/agent-discovery.js +26 -3
- package/dist/heart/daemon/agentic-repair.js +352 -14
- package/dist/heart/daemon/cli-defaults.js +4 -1
- package/dist/heart/daemon/cli-exec.js +39 -2
- package/dist/heart/hatch/hatch-specialist.js +4 -6
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.519",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Layer 3 (final) of the harness-hardening sequence (1→4→2→3 from `docs/planning/2026-04-28-1900-planning-harness-hardening-and-repairguide.md`). Ships the `RepairGuide.ouro/` library bundle alongside `SerpentGuide.ouro/` containing five diagnostic skills (`diagnose-bootstrap-drift`, `diagnose-broken-remote`, `diagnose-sync-blocked`, `diagnose-vault-expired`, `diagnose-stacked-typed-issues`) plus `psyche/SOUL.md` + `psyche/IDENTITY.md`. Loaded by the existing `agentic-repair.ts` pipeline as a content source — not a runtime agent (no senses, no vault, no `ouro status` presence).",
|
|
8
|
+
"Introduces `kind: \"library\"` field on bundle `agent.json`. `agent-discovery.ts` filters bundles where `kind === \"library\"` so they're never instantiated as runtime agents. `SerpentGuide.ouro/agent.json` tagged with `kind: \"library\"` to formalize what was previously an implicit `enabled: false` convention.",
|
|
9
|
+
"Activation gate `shouldFireRepairGuide` consumes the existing `untypedDegraded` / `typedDegraded` partitioning at `cli-exec.ts:6693-6694`. Fires when `untypedDegraded.length > 0` OR `typedDegraded.length >= 3`. The existing `--no-repair` flag remains the operator escape hatch — no new env toggle.",
|
|
10
|
+
"Drops the `~/AgentBundles/SerpentGuide.ouro/` override fallback in `getSpecialistIdentitySourceDir` — the in-repo bundle is now the only source. Reasoning per the planning doc: drift surface we don't currently need; cleaner ownership; no override path to maintain. Five referencing files updated (`hatch-flow.ts`, `cli-defaults.ts`, plus their tests). Same constraint extends to RepairGuide from day one — no override mechanism.",
|
|
11
|
+
"`parseRepairProposals` typed parser maps RepairGuide's structured-proposal output into the existing `RepairAction` catalog from `readiness-repair.ts` (`vault-unlock`, `provider-auth`, `provider-use`, etc.). Backfills lane variants and missing fields where unambiguous; rejects malformed proposals.",
|
|
12
|
+
"Structured Layer-2 + Layer-4 findings threaded into the diagnostic prompt (post-review fix). `AgenticRepairDeps` gains optional `driftFindings: DriftFinding[]` and `syncFindings: BootSyncProbeFinding[]`. `cli-exec.ts` collects both at boot and passes them through to `runAgenticRepair`; `buildUserMessage` appends each as a JSON block when non-empty so the `diagnose-bootstrap-drift` / `diagnose-broken-remote` / `diagnose-sync-blocked` skills can reason over real structured shapes (`lane`, `intentProvider`, `intentModel`, `observedProvider`, `observedModel`, `repairCommand` for drift; `classification`, `conflictFiles`, `advisory`, etc. for sync). Synthetic test (`repair-guide-skill-types.test.ts`) asserts skill markdown references current TS field names so the contract can't silently drift.",
|
|
13
|
+
"Slugger-style compound integration fixture as canonical acceptance test (per O6): bad bootstrap state + expired creds + broken remote + drift between agent.json and state/providers.json simultaneously. Validates the full layer 1→4→2→3 pipeline end-to-end.",
|
|
14
|
+
"All gates green: tsc clean, lint clean, code coverage 100%, nerves audit pass."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.518",
|
|
6
19
|
"changes": [
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isLibraryKind = isLibraryKind;
|
|
36
37
|
exports.listAllBundleAgents = listAllBundleAgents;
|
|
37
38
|
exports.listEnabledBundleAgents = listEnabledBundleAgents;
|
|
38
39
|
exports.listBundleSyncRows = listBundleSyncRows;
|
|
@@ -41,10 +42,18 @@ const path = __importStar(require("path"));
|
|
|
41
42
|
const child_process_1 = require("child_process");
|
|
42
43
|
const identity_1 = require("../identity");
|
|
43
44
|
const runtime_1 = require("../../nerves/runtime");
|
|
45
|
+
/**
|
|
46
|
+
* True when the value is the string `"library"`. Library bundles are
|
|
47
|
+
* content-only resources — never run as agents, never appear in sync surfaces.
|
|
48
|
+
*/
|
|
49
|
+
function isLibraryKind(kind) {
|
|
50
|
+
return kind === "library";
|
|
51
|
+
}
|
|
44
52
|
/**
|
|
45
53
|
* Walk the bundles root and return one row per `<name>.ouro` directory whose
|
|
46
54
|
* `agent.json` is readable and parseable. Includes both enabled and disabled
|
|
47
|
-
* agents
|
|
55
|
+
* agents AND library-kind bundles — callers that need only real agents should
|
|
56
|
+
* use `listEnabledBundleAgents` (which filters both `enabled` and `kind`).
|
|
48
57
|
*
|
|
49
58
|
* Bundles whose `agent.json` is missing, malformed, or unreadable are skipped
|
|
50
59
|
* silently (they aren't real agents from the harness's perspective).
|
|
@@ -76,22 +85,36 @@ function listAllBundleAgents(options = {}) {
|
|
|
76
85
|
const agentName = entry.name.slice(0, -5);
|
|
77
86
|
const configPath = path.join(bundlesRoot, entry.name, "agent.json");
|
|
78
87
|
let enabled = true;
|
|
88
|
+
let kind;
|
|
79
89
|
try {
|
|
80
90
|
const raw = readFileSync(configPath, "utf-8");
|
|
81
91
|
const parsed = JSON.parse(raw);
|
|
82
92
|
if (typeof parsed.enabled === "boolean") {
|
|
83
93
|
enabled = parsed.enabled;
|
|
84
94
|
}
|
|
95
|
+
if (typeof parsed.kind === "string") {
|
|
96
|
+
kind = parsed.kind;
|
|
97
|
+
}
|
|
85
98
|
}
|
|
86
99
|
catch {
|
|
87
100
|
continue;
|
|
88
101
|
}
|
|
89
|
-
|
|
102
|
+
const row = { name: agentName, enabled };
|
|
103
|
+
if (kind !== undefined)
|
|
104
|
+
row.kind = kind;
|
|
105
|
+
discovered.push(row);
|
|
90
106
|
}
|
|
91
107
|
return discovered.sort((left, right) => left.name.localeCompare(right.name));
|
|
92
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Real agents only — excludes both disabled bundles and library-kind bundles.
|
|
111
|
+
* Library bundles (SerpentGuide, RepairGuide, …) are content-only and must
|
|
112
|
+
* never appear in spawn lists, status rollups, or sync rows.
|
|
113
|
+
*/
|
|
93
114
|
function listEnabledBundleAgents(options = {}) {
|
|
94
|
-
return listAllBundleAgents(options)
|
|
115
|
+
return listAllBundleAgents(options)
|
|
116
|
+
.filter((row) => row.enabled && !isLibraryKind(row.kind))
|
|
117
|
+
.map((row) => row.name);
|
|
95
118
|
}
|
|
96
119
|
/**
|
|
97
120
|
* Read the per-agent sync block from each enabled bundle's agent.json.
|
|
@@ -6,11 +6,50 @@
|
|
|
6
6
|
* when no local repair was attempted and a working LLM provider is available.
|
|
7
7
|
* This is a lightweight integration: one diagnostic LLM call, not a chat loop.
|
|
8
8
|
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
9
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
43
|
exports.createAgenticDiagnosisProviderRuntime = createAgenticDiagnosisProviderRuntime;
|
|
11
44
|
exports.runAgenticRepair = runAgenticRepair;
|
|
45
|
+
exports.shouldFireRepairGuide = shouldFireRepairGuide;
|
|
46
|
+
exports.loadRepairGuideContent = loadRepairGuideContent;
|
|
47
|
+
exports.parseRepairProposals = parseRepairProposals;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
12
50
|
const runtime_1 = require("../../nerves/runtime");
|
|
13
51
|
const interactive_repair_1 = require("./interactive-repair");
|
|
52
|
+
const identity_1 = require("../identity");
|
|
14
53
|
const provider_ping_1 = require("../provider-ping");
|
|
15
54
|
const readiness_repair_1 = require("./readiness-repair");
|
|
16
55
|
function buildSystemPrompt(degraded) {
|
|
@@ -29,20 +68,34 @@ function buildSystemPrompt(degraded) {
|
|
|
29
68
|
"and the simplest fix the user can apply.",
|
|
30
69
|
].join("\n");
|
|
31
70
|
}
|
|
32
|
-
function buildUserMessage(degraded, logsTail) {
|
|
71
|
+
function buildUserMessage(degraded, logsTail, driftFindings = [], syncFindings = []) {
|
|
33
72
|
const agentDetails = degraded
|
|
34
73
|
.map((d) => `Agent: ${d.agent}\n Error: ${d.errorReason}\n Fix hint: ${d.fixHint}`)
|
|
35
74
|
.join("\n\n");
|
|
36
|
-
|
|
75
|
+
const sections = [
|
|
37
76
|
"Here are the degraded agents and recent daemon logs:",
|
|
38
77
|
"",
|
|
39
78
|
agentDetails,
|
|
40
79
|
"",
|
|
41
80
|
"Recent daemon logs:",
|
|
42
81
|
logsTail,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
82
|
+
];
|
|
83
|
+
// Layer 3: thread Layer 4's structured drift findings into the prompt as
|
|
84
|
+
// a JSON block. The `diagnose-bootstrap-drift` skill (loaded into the
|
|
85
|
+
// system prompt by `buildSystemPromptWithRepairGuide`) instructs the LLM
|
|
86
|
+
// how to read this shape and what `ouro use` repair to propose.
|
|
87
|
+
if (driftFindings.length > 0) {
|
|
88
|
+
sections.push("", "driftFindings (DriftFinding[]):", "```json", JSON.stringify(driftFindings, null, 2), "```");
|
|
89
|
+
}
|
|
90
|
+
// Layer 3: thread Layer 2's structured sync-probe findings into the
|
|
91
|
+
// prompt as a JSON block. `diagnose-broken-remote` (auth/404/network/
|
|
92
|
+
// timeout-hard) and `diagnose-sync-blocked` (dirty/non-fast-forward/
|
|
93
|
+
// merge-conflict/timeout-soft) consume this shape.
|
|
94
|
+
if (syncFindings.length > 0) {
|
|
95
|
+
sections.push("", "bootSyncFindings (BootSyncProbeFinding[]):", "```json", JSON.stringify(syncFindings, null, 2), "```");
|
|
96
|
+
}
|
|
97
|
+
sections.push("", "What is the most likely cause and how should I fix it?");
|
|
98
|
+
return sections.join("\n");
|
|
46
99
|
}
|
|
47
100
|
function makeInteractiveRepairDeps(deps) {
|
|
48
101
|
return {
|
|
@@ -72,11 +125,40 @@ function createAgenticDiagnosisProviderRuntime(provider) {
|
|
|
72
125
|
model: discoveredProviderModel(provider),
|
|
73
126
|
});
|
|
74
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Layer 3: build the system prompt for the diagnostic call. Prepends
|
|
130
|
+
* RepairGuide bundle content (`psyche/SOUL.md`, `psyche/IDENTITY.md`, all
|
|
131
|
+
* skills) when the bundle is present and readable; falls back to today's
|
|
132
|
+
* pre-RepairGuide prompt when the loader returns null.
|
|
133
|
+
*/
|
|
134
|
+
function buildSystemPromptWithRepairGuide(degraded, repairGuide) {
|
|
135
|
+
const basePrompt = buildSystemPrompt(degraded);
|
|
136
|
+
if (repairGuide === null)
|
|
137
|
+
return basePrompt;
|
|
138
|
+
const sections = [];
|
|
139
|
+
if (repairGuide.psyche.soul) {
|
|
140
|
+
sections.push("# RepairGuide SOUL\n\n" + repairGuide.psyche.soul);
|
|
141
|
+
}
|
|
142
|
+
if (repairGuide.psyche.identity) {
|
|
143
|
+
sections.push("# RepairGuide IDENTITY\n\n" + repairGuide.psyche.identity);
|
|
144
|
+
}
|
|
145
|
+
for (const [name, body] of Object.entries(repairGuide.skills)) {
|
|
146
|
+
sections.push(`# RepairGuide skill: ${name}\n\n${body}`);
|
|
147
|
+
}
|
|
148
|
+
if (sections.length === 0)
|
|
149
|
+
return basePrompt;
|
|
150
|
+
return [...sections, "---", basePrompt].join("\n\n");
|
|
151
|
+
}
|
|
75
152
|
async function tryAgenticDiagnosis(degraded, provider, deps) {
|
|
76
153
|
const logsTail = deps.readDaemonLogsTail();
|
|
77
154
|
const runtime = deps.createProviderRuntime(provider);
|
|
78
|
-
|
|
79
|
-
|
|
155
|
+
// Layer 3: prepend RepairGuide bundle content when present. Loader
|
|
156
|
+
// returns null on missing bundle / I/O error — caller silently falls back
|
|
157
|
+
// to today's pre-RepairGuide prompt.
|
|
158
|
+
const repoRoot = deps.repoRootOverride ?? (0, identity_1.getRepoRoot)();
|
|
159
|
+
const repairGuide = loadRepairGuideContent(repoRoot);
|
|
160
|
+
const systemPrompt = buildSystemPromptWithRepairGuide(degraded, repairGuide);
|
|
161
|
+
const userMessage = buildUserMessage(degraded, logsTail, deps.driftFindings, deps.syncFindings);
|
|
80
162
|
const messages = [
|
|
81
163
|
{ role: "system", content: systemPrompt },
|
|
82
164
|
{ role: "user", content: userMessage },
|
|
@@ -98,11 +180,40 @@ async function tryAgenticDiagnosis(degraded, provider, deps) {
|
|
|
98
180
|
callbacks: noopCallbacks,
|
|
99
181
|
});
|
|
100
182
|
if (result.content) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
183
|
+
// Layer 3: when RepairGuide content was prepended, parse the LLM output
|
|
184
|
+
// through the typed-action catalog. If actions are extracted, surface
|
|
185
|
+
// them as structured proposals; otherwise fall back to today's plain
|
|
186
|
+
// text-blob diagnosis. `parseRepairProposals` handles the unparseable
|
|
187
|
+
// case by returning `fallbackBlob` set to the raw output.
|
|
188
|
+
if (repairGuide !== null) {
|
|
189
|
+
const parsed = parseRepairProposals(result.content);
|
|
190
|
+
if (parsed.actions.length > 0) {
|
|
191
|
+
deps.writeStdout("");
|
|
192
|
+
deps.writeStdout("--- RepairGuide proposals ---");
|
|
193
|
+
for (const action of parsed.actions) {
|
|
194
|
+
deps.writeStdout(` • ${action.kind}: ${action.label}`);
|
|
195
|
+
}
|
|
196
|
+
for (const warning of parsed.warnings) {
|
|
197
|
+
deps.writeStdout(` (warning) ${warning}`);
|
|
198
|
+
}
|
|
199
|
+
deps.writeStdout("--- End RepairGuide proposals ---");
|
|
200
|
+
deps.writeStdout("");
|
|
201
|
+
}
|
|
202
|
+
else if (parsed.fallbackBlob !== undefined) {
|
|
203
|
+
deps.writeStdout("");
|
|
204
|
+
deps.writeStdout("--- AI Diagnosis ---");
|
|
205
|
+
deps.writeStdout(parsed.fallbackBlob);
|
|
206
|
+
deps.writeStdout("--- End Diagnosis ---");
|
|
207
|
+
deps.writeStdout("");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
deps.writeStdout("");
|
|
212
|
+
deps.writeStdout("--- AI Diagnosis ---");
|
|
213
|
+
deps.writeStdout(result.content);
|
|
214
|
+
deps.writeStdout("--- End Diagnosis ---");
|
|
215
|
+
deps.writeStdout("");
|
|
216
|
+
}
|
|
106
217
|
}
|
|
107
218
|
return true;
|
|
108
219
|
}
|
|
@@ -119,16 +230,17 @@ async function runAgenticRepair(degraded, deps) {
|
|
|
119
230
|
}
|
|
120
231
|
const hasLocalRepair = degraded.some(interactive_repair_1.hasRunnableInteractiveRepair);
|
|
121
232
|
const hasKnownTypedRepair = degraded.some((entry) => (0, readiness_repair_1.isKnownReadinessIssue)(entry.issue));
|
|
233
|
+
const forceDiagnosis = deps.forceDiagnosis === true;
|
|
122
234
|
if (hasLocalRepair) {
|
|
123
235
|
const interactiveResult = await runDeterministicRepair(degraded, deps);
|
|
124
236
|
if (interactiveResult.repairsAttempted) {
|
|
125
237
|
return { repairsAttempted: true, usedAgentic: false };
|
|
126
238
|
}
|
|
127
|
-
if (hasKnownTypedRepair) {
|
|
239
|
+
if (hasKnownTypedRepair && !forceDiagnosis) {
|
|
128
240
|
return { repairsAttempted: false, usedAgentic: false };
|
|
129
241
|
}
|
|
130
242
|
}
|
|
131
|
-
else if (hasKnownTypedRepair) {
|
|
243
|
+
else if (hasKnownTypedRepair && !forceDiagnosis) {
|
|
132
244
|
return { repairsAttempted: false, usedAgentic: false };
|
|
133
245
|
}
|
|
134
246
|
// Try to discover a working provider for agentic diagnosis
|
|
@@ -214,3 +326,229 @@ async function runAgenticRepair(degraded, deps) {
|
|
|
214
326
|
});
|
|
215
327
|
return { repairsAttempted: interactiveResult.repairsAttempted, usedAgentic };
|
|
216
328
|
}
|
|
329
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
330
|
+
// Layer 3: RepairGuide activation contract
|
|
331
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
332
|
+
/**
|
|
333
|
+
* Threshold for compound typed-degraded findings to activate RepairGuide.
|
|
334
|
+
* Set to 3 (not 2) so that common pairs — vault-locked + provider-auth-needed,
|
|
335
|
+
* for example — do NOT trigger the new path on every boot. Encoded once here;
|
|
336
|
+
* never duplicate at call sites.
|
|
337
|
+
*/
|
|
338
|
+
const REPAIR_GUIDE_TYPED_THRESHOLD = 3;
|
|
339
|
+
/**
|
|
340
|
+
* Single decision function for whether to fire the RepairGuide-driven
|
|
341
|
+
* diagnostic flow.
|
|
342
|
+
*
|
|
343
|
+
* Contract (LOCKED, planning O4):
|
|
344
|
+
* - `noRepair: true` → false unconditionally (escape hatch).
|
|
345
|
+
* - `untypedDegraded.length > 0` → true (preserves today's gate at
|
|
346
|
+
* `cli-exec.ts:6706`).
|
|
347
|
+
* - `typedDegraded.length >= REPAIR_GUIDE_TYPED_THRESHOLD` → true (compound
|
|
348
|
+
* stack of typed issues; the new behavior this PR introduces).
|
|
349
|
+
* - Otherwise → false.
|
|
350
|
+
*/
|
|
351
|
+
function shouldFireRepairGuide(input) {
|
|
352
|
+
if (input.noRepair)
|
|
353
|
+
return false;
|
|
354
|
+
if (input.untypedDegraded.length > 0)
|
|
355
|
+
return true;
|
|
356
|
+
if (input.typedDegraded.length >= REPAIR_GUIDE_TYPED_THRESHOLD)
|
|
357
|
+
return true;
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Read `RepairGuide.ouro/{psyche,skills}/*.md` from the given repo root and
|
|
362
|
+
* return a structured shape suitable for prepending to a diagnostic LLM call.
|
|
363
|
+
*
|
|
364
|
+
* Behavior:
|
|
365
|
+
* - Returns `null` if `RepairGuide.ouro/` does not exist at all (graceful: caller
|
|
366
|
+
* should fall back to today's pre-RepairGuide pipeline).
|
|
367
|
+
* - Returns `null` on any I/O error (`readdirSync`/`readFileSync` throws).
|
|
368
|
+
* - Returns a populated `RepairGuideContent` when the bundle exists, even if
|
|
369
|
+
* psyche or skills are partially populated.
|
|
370
|
+
* - Skips files that are not `.md`, are not regular files, or have empty
|
|
371
|
+
* contents.
|
|
372
|
+
* - `skills` map is iterated in alphabetical order so callers can rely on
|
|
373
|
+
* deterministic prompt assembly.
|
|
374
|
+
*
|
|
375
|
+
* The loader is intentionally inline in `agentic-repair.ts` per the planning
|
|
376
|
+
* O5 lock — splitting into its own module is allowed only if the validator
|
|
377
|
+
* grows large.
|
|
378
|
+
*/
|
|
379
|
+
function loadRepairGuideContent(repoRoot) {
|
|
380
|
+
const bundleRoot = path.join(repoRoot, "RepairGuide.ouro");
|
|
381
|
+
if (!fs.existsSync(bundleRoot))
|
|
382
|
+
return null;
|
|
383
|
+
try {
|
|
384
|
+
const psyche = {};
|
|
385
|
+
const skills = {};
|
|
386
|
+
const psycheDir = path.join(bundleRoot, "psyche");
|
|
387
|
+
if (fs.existsSync(psycheDir)) {
|
|
388
|
+
const psycheEntries = fs.readdirSync(psycheDir, { withFileTypes: true });
|
|
389
|
+
for (const entry of psycheEntries) {
|
|
390
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
391
|
+
continue;
|
|
392
|
+
const content = fs.readFileSync(path.join(psycheDir, entry.name), "utf-8");
|
|
393
|
+
if (entry.name === "SOUL.md")
|
|
394
|
+
psyche.soul = content;
|
|
395
|
+
else if (entry.name === "IDENTITY.md")
|
|
396
|
+
psyche.identity = content;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const skillsDir = path.join(bundleRoot, "skills");
|
|
400
|
+
if (fs.existsSync(skillsDir)) {
|
|
401
|
+
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
402
|
+
const sorted = skillEntries
|
|
403
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
404
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
405
|
+
for (const entry of sorted) {
|
|
406
|
+
const content = fs.readFileSync(path.join(skillsDir, entry.name), "utf-8");
|
|
407
|
+
if (content.length === 0)
|
|
408
|
+
continue;
|
|
409
|
+
skills[entry.name] = content;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { psyche, skills };
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// Best-effort: any I/O error in the bundle leads to null and the caller
|
|
416
|
+
// falls back to today's pre-RepairGuide diagnostic prompt.
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
421
|
+
// Layer 3: RepairGuide LLM output parser → typed `RepairAction[]`
|
|
422
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
423
|
+
/**
|
|
424
|
+
* The set of `RepairActionKind` literals the typed catalog recognizes.
|
|
425
|
+
* Encoded once here; the parser uses this for membership checks. Any other
|
|
426
|
+
* kind in the LLM output is dropped with a warning.
|
|
427
|
+
*/
|
|
428
|
+
const KNOWN_REPAIR_ACTION_KINDS = new Set([
|
|
429
|
+
"vault-create",
|
|
430
|
+
"vault-unlock",
|
|
431
|
+
"vault-replace",
|
|
432
|
+
"vault-recover",
|
|
433
|
+
"provider-auth",
|
|
434
|
+
"provider-retry",
|
|
435
|
+
"provider-use",
|
|
436
|
+
]);
|
|
437
|
+
function extractFirstJsonBlock(text) {
|
|
438
|
+
// Look for a triple-backtick fence labeled `json` and grab its body.
|
|
439
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
440
|
+
if (fenceMatch)
|
|
441
|
+
return fenceMatch[1];
|
|
442
|
+
// Fallback: the LLM may have emitted a bare JSON object (no fence).
|
|
443
|
+
const objectMatch = text.match(/\{[\s\S]*\}/);
|
|
444
|
+
if (objectMatch)
|
|
445
|
+
return objectMatch[0];
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
function isPlainObject(value) {
|
|
449
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Parse a single LLM-emitted action object into a typed `RepairAction`. Returns
|
|
453
|
+
* `null` when the entry is shaped wrong — the caller logs a warning and drops
|
|
454
|
+
* it. The parser backfills `label`, `command`, and `actor` so the result plugs
|
|
455
|
+
* into the existing interactive-repair surface without further massaging.
|
|
456
|
+
*/
|
|
457
|
+
function buildRepairAction(entry) {
|
|
458
|
+
const kind = entry.kind;
|
|
459
|
+
if (typeof kind !== "string")
|
|
460
|
+
return null;
|
|
461
|
+
if (!KNOWN_REPAIR_ACTION_KINDS.has(kind))
|
|
462
|
+
return null;
|
|
463
|
+
const reason = typeof entry.reason === "string" ? entry.reason : "(no reason given)";
|
|
464
|
+
const agent = typeof entry.agent === "string" ? entry.agent : "(unknown agent)";
|
|
465
|
+
const label = `RepairGuide: ${kind} for ${agent}`;
|
|
466
|
+
const command = `# ${kind} (${agent}): ${reason}`;
|
|
467
|
+
switch (kind) {
|
|
468
|
+
case "provider-auth": {
|
|
469
|
+
const provider = typeof entry.provider === "string" ? entry.provider : "anthropic";
|
|
470
|
+
return {
|
|
471
|
+
kind: "provider-auth",
|
|
472
|
+
label,
|
|
473
|
+
command,
|
|
474
|
+
actor: "human-required",
|
|
475
|
+
provider: provider,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
case "provider-use": {
|
|
479
|
+
// `provider-use` may carry an optional `lane` — pass it through when
|
|
480
|
+
// present and validly typed (`outward` or `inner`).
|
|
481
|
+
const lane = entry.lane === "outward" || entry.lane === "inner" ? entry.lane : undefined;
|
|
482
|
+
const action = {
|
|
483
|
+
kind: "provider-use",
|
|
484
|
+
label,
|
|
485
|
+
command,
|
|
486
|
+
actor: "human-choice",
|
|
487
|
+
...(lane ? { lane } : {}),
|
|
488
|
+
};
|
|
489
|
+
return action;
|
|
490
|
+
}
|
|
491
|
+
case "vault-create":
|
|
492
|
+
case "vault-unlock":
|
|
493
|
+
case "vault-replace":
|
|
494
|
+
case "vault-recover":
|
|
495
|
+
case "provider-retry": {
|
|
496
|
+
return {
|
|
497
|
+
kind: kind,
|
|
498
|
+
label,
|
|
499
|
+
command,
|
|
500
|
+
actor: "human-required",
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/* v8 ignore next 4 -- exhaustiveness guard: unreachable since membership
|
|
504
|
+
* was checked above; left in place to satisfy `never` on future
|
|
505
|
+
* RepairActionKind extensions. @preserve */
|
|
506
|
+
default: {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Parse RepairGuide LLM output. The persona content (`SOUL.md`) instructs the
|
|
513
|
+
* model to emit exactly one ```json fenced block containing
|
|
514
|
+
* `{ actions: RepairAction[], notes?: string[] }`. The parser extracts that
|
|
515
|
+
* block, walks `actions[]`, drops entries with unknown kinds (with warnings),
|
|
516
|
+
* and falls back to the raw output when no JSON can be extracted at all
|
|
517
|
+
* (preserving today's text-blob behavior).
|
|
518
|
+
*/
|
|
519
|
+
function parseRepairProposals(llmOutput) {
|
|
520
|
+
const block = extractFirstJsonBlock(llmOutput);
|
|
521
|
+
if (block === null) {
|
|
522
|
+
return { actions: [], warnings: [], fallbackBlob: llmOutput };
|
|
523
|
+
}
|
|
524
|
+
let parsed;
|
|
525
|
+
try {
|
|
526
|
+
parsed = JSON.parse(block);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return { actions: [], warnings: [], fallbackBlob: llmOutput };
|
|
530
|
+
}
|
|
531
|
+
if (!isPlainObject(parsed)) {
|
|
532
|
+
return { actions: [], warnings: [] };
|
|
533
|
+
}
|
|
534
|
+
const rawActions = parsed.actions;
|
|
535
|
+
if (!Array.isArray(rawActions)) {
|
|
536
|
+
return { actions: [], warnings: [] };
|
|
537
|
+
}
|
|
538
|
+
const actions = [];
|
|
539
|
+
const warnings = [];
|
|
540
|
+
for (const entry of rawActions) {
|
|
541
|
+
if (!isPlainObject(entry)) {
|
|
542
|
+
warnings.push(`dropped non-object entry from actions[]: ${JSON.stringify(entry)}`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const built = buildRepairAction(entry);
|
|
546
|
+
if (built === null) {
|
|
547
|
+
const kindLabel = typeof entry.kind === "string" ? entry.kind : "(no kind)";
|
|
548
|
+
warnings.push(`dropped action with unknown or missing kind: ${kindLabel}`);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
actions.push(built);
|
|
552
|
+
}
|
|
553
|
+
return { actions, warnings };
|
|
554
|
+
}
|
|
@@ -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);
|
|
@@ -5753,6 +5753,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5753
5753
|
//
|
|
5754
5754
|
// Errors in the probe path itself are caught and logged — they must not
|
|
5755
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 = [];
|
|
5756
5762
|
progress.startPhase("sync probe");
|
|
5757
5763
|
try {
|
|
5758
5764
|
const syncProbeImpl = deps.runBootSyncProbeImpl ?? boot_sync_probe_1.runBootSyncProbe;
|
|
@@ -5760,6 +5766,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5760
5766
|
const syncProbeResult = await syncProbeImpl(syncRows, {
|
|
5761
5767
|
bundlesRoot: deps.bundlesRoot ?? bundlesRoot,
|
|
5762
5768
|
});
|
|
5769
|
+
bootSyncFindings = syncProbeResult.findings;
|
|
5763
5770
|
progress.completePhase("sync probe", summarizeSyncProbeFindings(syncProbeResult.findings));
|
|
5764
5771
|
if (syncProbeResult.findings.length > 0) {
|
|
5765
5772
|
writeSyncProbeSummary(deps, syncProbeResult.findings);
|
|
@@ -5877,8 +5884,35 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5877
5884
|
typedDegraded.forEach((entry) => repairedAgents.add(entry.agent));
|
|
5878
5885
|
}
|
|
5879
5886
|
}
|
|
5880
|
-
|
|
5881
|
-
|
|
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, {
|
|
5882
5916
|
/* v8 ignore start -- production provider discovery wiring @preserve */
|
|
5883
5917
|
discoverWorkingProvider: async (agentName) => {
|
|
5884
5918
|
const { discoverWorkingProvider: discover } = await Promise.resolve().then(() => __importStar(require("./provider-discovery")));
|
|
@@ -5922,6 +5956,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5922
5956
|
skipQueueSummary: true,
|
|
5923
5957
|
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
5924
5958
|
stdoutColumns: deps.stdoutColumns ?? process.stdout.columns,
|
|
5959
|
+
forceDiagnosis,
|
|
5960
|
+
driftFindings: repairDriftFindings,
|
|
5961
|
+
syncFindings: bootSyncFindings,
|
|
5925
5962
|
});
|
|
5926
5963
|
if (repairResult.repairsAttempted) {
|
|
5927
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() {
|