Give repair its own profile, reading what was said before and what fixed it before

Conf writing is granted to this profile alone and it is not offered as a button — making it
selectable would put a write one click from any question. Its prompt leads on what it does
not decide, because the resolver picks the key and the probe picks the value.
This commit is contained in:
Gmer4Lfe
2026-08-09 20:49:43 -04:00
parent 4ab3387db4
commit 9c150532fc
3 changed files with 134 additions and 0 deletions
+25
View File
@@ -109,6 +109,26 @@ const VV_AI_PROFILES_DEF = [
'ui' => true,
'caps' => ['retrieve', 'health', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', 'file_bugs'],
],
// The only profile that may change a setting, and the only one not offered as a button.
//
// It is entered by opening a finding, the way troubleshoot was originally entered by opening
// a log — a repair conversation with nothing to repair is a contract with no subject. That is
// not the only reason though: capabilities are granted per profile, so making this selectable
// would put conf_write one click away from any question at all. The narrow grant is the point.
//
// Reads two records of what has happened before. The phrasebook holds what the operator calls
// things and, more usefully, what they have had to correct; closed findings hold what actually
// fixed a fault last time. Between them the profile starts a conversation already knowing the
// operator's vocabulary and this installation's history, instead of relearning both each time.
'repair' => [
'label' => 'Repair',
'short' => 'Repair',
'hint' => 'Works through a finding with you, and can apply a fix you approve.',
'turns' => 3,
'ui' => false,
'caps' => ['retrieve', 'health', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup',
'conf_write', 'probe', 'file_findings', 'phrasebook', 'past_fixes'],
],
];
// Documentation only — nothing branches on it. Kept beside the table because a capability name
@@ -123,6 +143,11 @@ const VV_AI_CAP_MEANING = [
'conf_lookup' => 'deterministic "where does this conf key actually live" lookup',
'file_bugs' => 'may file a bug report against Varaverk itself',
'code_scan' => 'destructive-operation scan of generated shell',
'conf_write' => 'may change a setting — through the guarded write path, never directly',
'probe' => 'may test a candidate address before anything is written to conf',
'file_findings'=> 'may record, acknowledge and close findings about this installation',
'phrasebook' => 'what the operator calls things, and what they have corrected before',
'past_fixes' => 'findings already closed — what fixed this last time',
];
function vv_ai_profiles(): array { return VV_AI_PROFILES_DEF; }
+60
View File
@@ -915,6 +915,66 @@ function vv_ai_phrase_lookup(string $term, int $minSeen = 3): ?array {
return vv_ai_phrase_aliases($minSeen)[strtolower(trim($term))] ?? null;
}
// ── What the repair profile is given before it answers ───────────────────────────────────────
// Two records of what has already happened, assembled as plain text for the prompt.
//
// The phrasebook half is the operator's vocabulary — and the corrections matter more than the
// settled terms, because a term that has been corrected is one the assistant has already got
// wrong once and would otherwise get wrong again.
//
// The closed-findings half is this installation's history: the same fault, and what actually
// ended it. "Emby stopped answering last month and the address had changed" is worth more at the
// start of a repair conversation than any amount of reasoning from first principles.
//
// Bounded hard. This goes into a 16k context that retrieval and a log tail are also competing
// for, and an unbounded history would crowd out the evidence for the fault actually being
// discussed.
function vv_ai_repair_context(int $maxAliases = 20, int $maxFixes = 8): string {
$out = [];
$aliases = vv_ai_phrase_aliases();
if ($aliases) {
$lines = [];
foreach (array_slice($aliases, 0, $maxAliases, true) as $term => $a) {
$lines[] = ' "' . $term . '" means ' . $a['target'];
}
$out[] = "What the operator calls things:\n" . implode("\n", $lines);
}
$unsettled = vv_ai_phrase_unsettled();
if ($unsettled) {
$lines = [];
foreach (array_slice($unsettled, 0, $maxAliases, true) as $term => $c) {
$last = end($c);
$lines[] = ' "' . $term . '" was read as ' . ($last['took_it_as'] ?: '?')
. ' and meant ' . ($last['meant'] ?: '?');
}
$out[] = "Corrected before — do not repeat these:\n" . implode("\n", $lines);
}
$closed = vv_ai_findings_list(['fixed', 'resolved']);
if ($closed) {
$lines = [];
foreach (array_slice($closed, 0, $maxFixes) as $f) {
$lines[] = ' ' . ($f['subject'] ?? '?') . ' / ' . ($f['ref'] ?? $f['conf_key'] ?? '?')
. ' — ' . ($f['state'] ?? '?')
. (!empty($f['note']) ? ': ' . mb_substr((string)$f['note'], 0, 140) : '');
}
$out[] = "Already dealt with on this host:\n" . implode("\n", $lines);
}
$spellings = vv_ai_spellings();
if ($spellings) {
$lines = [];
foreach (array_slice($spellings, 0, $maxAliases, true) as $typo => $meant) {
$lines[] = ' "' . $typo . '" = "' . $meant . '"';
}
$out[] = "The operator types quickly; known shorthand:\n" . implode("\n", $lines);
}
return implode("\n\n", $out);
}
// ── Resolving what the operator named ────────────────────────────────────────────────────────
// "turn off the zfs scrub", "change the emby api key" — a phrase in, an exact target out, or an
// honest refusal with the candidates that were considered.