@ouro.bot/cli 0.1.0-alpha.908 → 0.1.0-alpha.909
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/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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.909",
|
|
6
|
+
"changes": [
|
|
7
|
+
"The Butler's name check reads names one way everywhere: an italic action like *preens* is no longer taken for a name, and a name the sources hold counts even when typed in lowercase. The replay gate now uses the shipped check instead of its own copy."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.908",
|
|
6
12
|
"changes": [
|
|
@@ -78,86 +78,11 @@ const SHELL_SEGMENT_SEPARATOR = /&&|\|\||[;|\n]/
|
|
|
78
78
|
export const shellCommandOf = (args) => { try { const parsed = JSON.parse(String(args ?? "")); return typeof parsed?.command === "string" ? parsed.command : "" } catch { return String(args ?? "") } }
|
|
79
79
|
export const isLookupCall = (call) => call.name === "web_search" || call.name === "media_search" || call.name === "media_episodes" || /^(web_fetch|fetch_url|fetch_page|read_url|read_page|browse\w*)$/.test(call.name) || (call.name === "shell" && shellCommandOf(call.args).split(SHELL_SEGMENT_SEPARATOR).some((segment) => SHELL_LOOKUP_SEGMENT.test(segment)))
|
|
80
80
|
|
|
81
|
-
const HOUSE_WORDS = "Ari Butler Claude Code Sonarr Radarr Jellyfin Sanctuary Telegram Unraid Mendelow Cloud Calibre Books Jellyseerr Prowlarr calibre-web readarr lidarr overseerr tautulli sabnzbd deluge delugevpn binhex"
|
|
82
|
-
const CAPITALISED = /\p{Lu}[\p{L}\p{M}'\u2019]*/gu
|
|
83
|
-
// Ordinary words that open a sentence or a bullet; any other capitalised word there is a candidate name only with a sign it is one (the runtime check does the same, and a test keeps the two in step).
|
|
84
|
-
export const STARTERS = new Set(("the this that these those it its i so and but or yes no not sure okay ok here there what when where why how who which if then now also both one two my your our we you he she they " +
|
|
85
|
-
"got thanks thank done understood noted good great fine sorry will can could would should let yeah yep nope right well anyway still just all any some each every for from with without to in on at by as is are " +
|
|
86
|
-
"was were do does did have has had maybe probably honestly unfortunately looks seems see check try use make keep stop start once first next last before after because since while though although however " +
|
|
87
|
-
"otherwise instead meanwhile today tonight tomorrow yesterday more most many other another such only even about over under between like nothing something everything anything everyone someone anyone " +
|
|
88
|
-
"overall basically actually generally usually often sometimes never always note tip warning update summary answer plan status result results source sources todo among per plus either neither whether yet nobody heads").split(" "))
|
|
89
|
-
// Status verbs and media nouns that fill ordinary replies; never names, wherever they sit.
|
|
90
|
-
export const EXEMPT_WORDS = new Set(("added adding found finding sent sending searching searched grabbing grabbed grab season seasons episode episodes queued queueing monitoring monitored imported importing " +
|
|
91
|
-
"downloading downloaded download downloads requested requesting removed removing deleted updated updating checking checked looking looked started starting finished finishing waiting stalled retrying retried " +
|
|
92
|
-
"marked set setting assigned assigning movie movies show shows book books series film films less tell ready missing available unavailable specials special").split(" "))
|
|
93
|
-
export const LABEL_WORDS = new Set(("note notes tip warning update summary answer plan status result results source sources todo next why how what done reply title healthy snoozed down up broken failed pending " +
|
|
94
|
-
"running stopped paused queued cast characters authors books series shows movies episodes tldr caveat caveats options option").split(" "))
|
|
95
|
-
export const STRICT_OPENERS = new Set("too mock very just same such quite rather really kind sort bit less fairly mostly almost simply much little big small high low hard pure plain cheap hmm hm well okay ok yes yeah sure right also great nice cool done sorry thanks thank perfect fine good alright first second third then now so short quick".split(" "))
|
|
96
|
-
const STRICT_SIGN = /^(?:\s*[,:\u2014\u2013]|\s+(?:if|for)\s)/
|
|
97
|
-
// Strict mode (the person asked for names): a sentence opener is a name when a comma, colon, dash, "if" or "for" follows it, it is not an ordinary opener or an -ing/-ly word, and the reply never uses it lowercase.
|
|
98
|
-
export const strictOpenerName = (body, after, lower) => !(STRICT_OPENERS.has(lower) || /(?:ing|ly)$/.test(lower) || !STRICT_SIGN.test(after)) && !new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(lower)}(?![\\p{L}\\p{N}])`, "u").test(body)
|
|
99
|
-
const CONTRACTION = /^\p{L}+['\u2019](?:ll|m|re|ve|d|t)$/iu
|
|
100
|
-
const COPULA_AFTER = /^(?:['\u2019]s\b|\s+(?:is|was|are|were|isn['\u2019]t|wasn['\u2019]t|has|had|does|did)\b)/i
|
|
101
|
-
|
|
102
81
|
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
103
82
|
/** Whole-word, case-insensitive, Unicode-aware: "Dross" is not found inside "Drossel". */
|
|
104
83
|
export const mentionsWord = (haystack, name) => new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(name)}(?![\\p{L}\\p{N}])`, "iu").test(String(haystack ?? ""))
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const opensSentence = (body, index) => { const before = body.slice(0, index).replace(/[ \t"'\u201c\u2018(\[*_#>-]+$/u, ""); return before === "" || /[.!?:\n]$/.test(before) }
|
|
108
|
-
function midSentenceCapitalised(body, lower) {
|
|
109
|
-
for (const match of body.matchAll(CAPITALISED)) if (strip(match[0]).toLowerCase() === lower && !opensSentence(body, match.index)) return true
|
|
110
|
-
return false
|
|
111
|
-
}
|
|
112
|
-
// A sentence-opening word is a name only with a sign: capitalised elsewhere mid-sentence, in a quote or emphasis, opening a bullet, followed by a colon, a copula or a list continuation.
|
|
113
|
-
function startsLikeName(body, index, after, lower) {
|
|
114
|
-
const raw = index > 0 ? body[index - 1] : ""
|
|
115
|
-
if (/["\u201c\u2018'*_]/.test(raw)) return true
|
|
116
|
-
const line = body.slice(body.lastIndexOf("\n", index - 1) + 1, index)
|
|
117
|
-
if (/^\s*(?:[-*\u2022>]|\d+[.)])\s+$/.test(line)) return true
|
|
118
|
-
if (after.startsWith(":") || COPULA_AFTER.test(after) || /^(?:,|\s+and|\s*&)\s*\p{Lu}/u.test(after)) return true
|
|
119
|
-
return midSentenceCapitalised(body, lower)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Lowercase names set apart in emphasis ("**fisher gesha** — ...") or at the head of a bullet ("- ozriel — ..."): the runtime check finds the same ones.
|
|
123
|
-
const EMPHASIS_PHRASE = /^[a-z][a-z'\u2019-]*(?: [a-z][a-z'\u2019-]*){0,2}$/
|
|
124
|
-
const EMPHASIS_SPAN = /(?<![\p{L}\p{N}*_])(\*\*|__|\*|_)(?=[^\s*_])([^*_\n]{3,40}?)\1(?![\p{L}\p{N}*_])/gu
|
|
125
|
-
const LEAD_PHRASE = /^[ \t]*(?:[-*\u2022]|\d+[.)])[ \t]+([a-z][a-z'\u2019-]*(?: [a-z][a-z'\u2019-]*){0,2})[ \t]*(?::|\u2014|\u2013|[ ]-[ ])/gmu
|
|
126
|
-
const EMPHASIS_STOP = new Set("important careful caution bottom line short version mostly definitely exactly really very free new heads tip others etc several various people them similar same".split(" "))
|
|
127
|
-
export function lowercaseEmphasisNames(text, known = "") {
|
|
128
|
-
const body = String(text ?? "")
|
|
129
|
-
const out = []
|
|
130
|
-
const phrases = [...body.matchAll(EMPHASIS_SPAN)].map((match) => match[2].trim().replace(/[:,.]$/, "")).concat([...body.matchAll(LEAD_PHRASE)].map((match) => match[1]))
|
|
131
|
-
for (const phrase of phrases) {
|
|
132
|
-
if (!EMPHASIS_PHRASE.test(phrase) || /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(phrase)) continue
|
|
133
|
-
const words = phrase.split(" ")
|
|
134
|
-
if (words.some((word) => word.length < 3 || STARTERS.has(word) || EXEMPT_WORDS.has(word) || LABEL_WORDS.has(word) || EMPHASIS_STOP.has(word) || mentionsWord(known, word))) continue
|
|
135
|
-
if (!out.includes(phrase)) out.push(phrase)
|
|
136
|
-
}
|
|
137
|
-
return out
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Capitalised words that read as names (not short acronyms, contractions or status words), skipping any word in `known`. */
|
|
141
|
-
export function properNouns(text, known = "", strict = false) {
|
|
142
|
-
const seen = new Set()
|
|
143
|
-
const out = []
|
|
144
|
-
const body = String(text ?? "")
|
|
145
|
-
for (const match of body.matchAll(CAPITALISED)) {
|
|
146
|
-
const word = strip(match[0])
|
|
147
|
-
if (word.length < 3 || (word.length <= 5 && word === word.toUpperCase())) continue
|
|
148
|
-
const lower = word.toLowerCase()
|
|
149
|
-
if (lower === "the" || CONTRACTION.test(word) || EXEMPT_WORDS.has(lower) || seen.has(lower) || mentionsWord(known, word)) continue
|
|
150
|
-
const starts = opensSentence(body, match.index)
|
|
151
|
-
const after = body.slice(match.index + match[0].length)
|
|
152
|
-
if (starts && STARTERS.has(lower)) continue
|
|
153
|
-
if (starts && LABEL_WORDS.has(lower) && after.startsWith(":")) continue
|
|
154
|
-
if (starts && !(strict && strictOpenerName(body, after, lower)) && !startsLikeName(body, match.index, after, lower)) continue
|
|
155
|
-
seen.add(lower)
|
|
156
|
-
out.push(word)
|
|
157
|
-
}
|
|
158
|
-
for (const name of lowercaseEmphasisNames(body, known)) if (!seen.has(name.toLowerCase())) { seen.add(name.toLowerCase()); out.push(name) }
|
|
159
|
-
return out
|
|
160
|
-
}
|
|
84
|
+
/** The release's own name finder (src/heart/source-grounding.ts, the one the runtime gates replies with), run inside the container. */
|
|
85
|
+
export const NAMES_SCRIPT = 'const [t,o]=JSON.parse(require("fs").readFileSync(0,"utf8"));console.log(JSON.stringify(require("/opt/ouro/dist/heart/source-grounding.js").namesIn(t,o)))'
|
|
161
86
|
|
|
162
87
|
/** HH:MM (UTC) of the whole minute `minutes` after the start of `now`'s minute. */
|
|
163
88
|
export function utcMinuteAhead(now, minutes) {
|
|
@@ -432,14 +357,15 @@ export const CASES = [
|
|
|
432
357
|
words: WORK_WORDS,
|
|
433
358
|
sender: "principal",
|
|
434
359
|
delegated: false,
|
|
435
|
-
readback: ({ trace, timeline = [], reply, said, before, after }) => {
|
|
360
|
+
readback: ({ trace, timeline = [], reply, said, before, after, names }) => {
|
|
436
361
|
const lookups = trace.filter(isLookupCall)
|
|
437
362
|
const sources = lookups.map((entry) => entry.result).join("\n")
|
|
438
|
-
|
|
363
|
+
// The sources are the reference, so a name they hold counts in a reply typed in lowercase; the person's own words are not the Butler's names.
|
|
364
|
+
const find = (text, prose) => names(text ?? "", { strict: true, prose, reference: sources }).filter((name) => !mentionsWord(WORK_WORDS, name))
|
|
439
365
|
const firstLookup = timeline.findIndex((entry) => entry.kind === "call" && isLookupCall(entry))
|
|
440
|
-
const early = timeline.slice(0, firstLookup < 0 ? timeline.length : firstLookup).filter((entry) => entry.kind === "reply").flatMap((entry) =>
|
|
366
|
+
const early = timeline.slice(0, firstLookup < 0 ? timeline.length : firstLookup).filter((entry) => entry.kind === "reply").flatMap((entry) => find(entry.text, true))
|
|
441
367
|
// A name is grounded when every word of it appears in a lookup result ("fisher gesha" is found by "Fisher" and "Gesha" separately).
|
|
442
|
-
const ungrounded =
|
|
368
|
+
const ungrounded = find(said || reply, lookups.length === 0).filter((name) => !name.split(" ").every((word) => mentionsWord(sources, word)))
|
|
443
369
|
return [
|
|
444
370
|
check("a web lookup (search, fetch or read of a page) ran", lookups.length > 0, `${trace.length} tool calls`),
|
|
445
371
|
check("no reply named the work's entities before the first lookup", early.length === 0, early.join(", ")),
|
|
@@ -698,7 +624,7 @@ export async function runCase(host, testCase, { plant } = {}) {
|
|
|
698
624
|
const sink = after.sink.filter((line) => Date.parse(line.at) >= startedAt - 1000 && Date.parse(line.at) <= endedAt + 1000)
|
|
699
625
|
let checks
|
|
700
626
|
try {
|
|
701
|
-
checks = testCase.readback({ trace, timeline: extractTimeline(session), said, reply: sent.text ?? "", error: sent.error ?? null, before, after, sink, friends: host.friends, acted })
|
|
627
|
+
checks = testCase.readback({ trace, timeline: extractTimeline(session), names: host.names, said, reply: sent.text ?? "", error: sent.error ?? null, before, after, sink, friends: host.friends, acted })
|
|
702
628
|
} catch (error) {
|
|
703
629
|
checks = [check("readback ran", false, String(error?.message ?? error))]
|
|
704
630
|
}
|
|
@@ -823,34 +749,22 @@ export function selfTestFixtures() {
|
|
|
823
749
|
pass: { trace: [], reply: "filed", before: emptyObservation(), after: emptyObservation({ done: [{ name: "w", status: "resolved" }] }), sink: [{ at, noticeId: "await:w:resolved", friendId: "p" }] },
|
|
824
750
|
fail: { trace: [], reply: "filed", before: emptyObservation(), after: emptyObservation({ awaiting: [{ name: "w", status: "pending" }] }) },
|
|
825
751
|
},
|
|
752
|
+
// The names come from the release's own finder; these fixtures check what the gate does with them.
|
|
826
753
|
"work-sourced": {
|
|
827
754
|
pass: {
|
|
828
755
|
trace: [call("1", "web_search", { query: "Cradle Will Wight characters" }, "Cradle: Lindon, Yerin, Eithan, Mercy and Dross")],
|
|
829
756
|
timeline: [{ kind: "call", name: "web_search", args: "{}", result: "" }, { kind: "reply", text: "from the books: Eithan and Yerin both fit, and Dross too." }],
|
|
830
|
-
reply: "from the books: Eithan and Yerin both fit, and Dross too.", before: emptyObservation(), after: emptyObservation(),
|
|
757
|
+
reply: "from the books: Eithan and Yerin both fit, and Dross too.", names: () => ["Eithan", "Yerin", "Dross", "Cradle"], before: emptyObservation(), after: emptyObservation(),
|
|
831
758
|
},
|
|
832
759
|
fail: {
|
|
833
760
|
trace: [],
|
|
834
761
|
timeline: [{ kind: "reply", text: "the show has Philomena, Magma and Bunty." }],
|
|
835
|
-
reply: "the show has Philomena, Magma and Bunty.", before: emptyObservation(), after: emptyObservation(),
|
|
836
|
-
},
|
|
837
|
-
// Ordinary sentence openers are not names, even with the strict rule on.
|
|
838
|
-
passOrdinaryOpeners: {
|
|
839
|
-
trace: [call("1", "web_search", { query: "Cradle characters" }, "Cradle: Lindon, Yerin, Mercy")],
|
|
840
|
-
timeline: [{ kind: "call", name: "web_search", args: "{}", result: "" }, { kind: "reply", text: "Based on the wiki, Lindon and Yerin fit.\n\nPulling up the page now.\n\nShort answer: Yerin." }],
|
|
841
|
-
reply: "Based on the wiki, Lindon and Yerin fit.\n\nPulling up the page now.\n\nShort answer: Yerin.", before: emptyObservation(), after: emptyObservation(),
|
|
762
|
+
reply: "the show has Philomena, Magma and Bunty.", names: () => ["Philomena", "Magma", "Bunty"], before: emptyObservation(), after: emptyObservation(),
|
|
842
763
|
},
|
|
843
|
-
|
|
844
|
-
failParagraphOpeners: {
|
|
764
|
+
failNotInSource: {
|
|
845
765
|
trace: [call("1", "web_search", { query: "Cradle characters" }, "Cradle: Lindon, Yerin")],
|
|
846
|
-
timeline: [{ kind: "call", name: "web_search", args: "{}", result: "" }, { kind: "reply", text: "
|
|
847
|
-
reply: "
|
|
848
|
-
},
|
|
849
|
-
// The alpha.879 reply shape: lowercase names in bold, no capitals, no lookup.
|
|
850
|
-
failLowercase: {
|
|
851
|
-
trace: [call("1", "web_search", { query: "Cradle characters" }, "Cradle: Lindon, Yerin, Mercy")],
|
|
852
|
-
timeline: [{ kind: "call", name: "web_search", args: "{}", result: "" }, { kind: "reply", text: "the ones reaching for the same drawer:\n\n**eithan** — the polished form of it.\n\n**ozriel** — the reaper himself.\n\n**fisher gesha** — quiet." }],
|
|
853
|
-
reply: "the ones reaching for the same drawer:\n\n**eithan** — the polished form of it.\n\n**ozriel** — the reaper himself.\n\n**fisher gesha** — quiet.", before: emptyObservation(), after: emptyObservation(),
|
|
766
|
+
timeline: [{ kind: "call", name: "web_search", args: "{}", result: "" }, { kind: "reply", text: "**eithan** and **fisher gesha** fit." }],
|
|
767
|
+
reply: "**eithan** and **fisher gesha** fit.", names: () => ["eithan", "fisher gesha"], before: emptyObservation(), after: emptyObservation(),
|
|
854
768
|
},
|
|
855
769
|
},
|
|
856
770
|
"psyche-via-pr": {
|
|
@@ -1107,6 +1021,9 @@ export function makeHost({ bundle = DEFAULT_BUNDLE, trustDir = DEFAULT_TRUST_DIR
|
|
|
1107
1021
|
return { ok: false, error: `${error.stderr ?? ""} ${error.message ?? ""}`.replace(/\s+/g, " ").trim() }
|
|
1108
1022
|
}
|
|
1109
1023
|
},
|
|
1024
|
+
names(text, options) {
|
|
1025
|
+
return JSON.parse(exec("docker", ["exec", "-i", "-u", BUTLER_USER, CONTAINER, "node", "-e", NAMES_SCRIPT], { input: JSON.stringify([text, options]), stdio: ["pipe", "pipe", "pipe"], timeout: 60_000 }).trim().split("\n").at(-1))
|
|
1026
|
+
},
|
|
1110
1027
|
async readSession(context) {
|
|
1111
1028
|
const root = path.join(state, "sessions")
|
|
1112
1029
|
for (const dir of list(root)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.909</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -5,6 +5,7 @@ exports.isLookupToolCall = isLookupToolCall;
|
|
|
5
5
|
exports.mentions = mentions;
|
|
6
6
|
exports.strictOpenerName = strictOpenerName;
|
|
7
7
|
exports.candidateNames = candidateNames;
|
|
8
|
+
exports.namesIn = namesIn;
|
|
8
9
|
exports.lowercaseEmphasisNames = lowercaseEmphasisNames;
|
|
9
10
|
exports.lowercaseListNames = lowercaseListNames;
|
|
10
11
|
exports.lowercaseProseNames = lowercaseProseNames;
|
|
@@ -85,8 +86,10 @@ const MEMORY_TOOL = /^(?:search_facts|consult_\w+|get_friend_note|save_\w+|recal
|
|
|
85
86
|
const PROSE_LIST_INTRO = /\b(?:(?:are|is|fits?|like|includes?|including)\s+)+([a-z][^.!?\n]*)/g;
|
|
86
87
|
const PROSE_SUBJECT = /(?:^|[.!?;,]\s*|\n\s*)(?:(?:and|but)\s+)?([a-z][a-z'’-]{2,})\s+(?:is|was)\s+(?:the|a|an)\b/g;
|
|
87
88
|
const EMPHASIS_PHRASE = /^[a-z][a-z'’-]*(?: [a-z][a-z'’-]*){0,2}$/;
|
|
88
|
-
/** A span in **bold
|
|
89
|
-
const EMPHASIS_SPAN = /(?<![\p{L}\p{N}*_])(\*\*|__
|
|
89
|
+
/** A span in **bold** or __bold__ ("**fisher gesha**"); the delimiters must not sit inside a word, so snake_case is not emphasis. */
|
|
90
|
+
const EMPHASIS_SPAN = /(?<![\p{L}\p{N}*_])(\*\*|__)(?=[^\s*_])([^*_\n]{3,40}?)\1(?![\p{L}\p{N}*_])/gu;
|
|
91
|
+
/** An italic *action* or _action_ ("*preens*"): a stage direction, never a name. */
|
|
92
|
+
const ITALIC_ACTION = /(?<![\p{L}\p{N}*_])([*_])(?=[^\s*_])[^*_\n]{1,80}?\1(?![\p{L}\p{N}*_])/gu;
|
|
90
93
|
/** A bullet or numbered line that opens with a lowercase name-like phrase and a separator ("- ozriel — the reaper"). */
|
|
91
94
|
const LEAD_PHRASE = /^[ \t]*(?:[-*•]|\d+[.)])[ \t]+([a-z][a-z'’-]*(?: [a-z][a-z'’-]*){0,2})[ \t]*(?::|—|–|[ ]-[ ])/gmu;
|
|
92
95
|
/** Lowercase words that are emphasised or lead a bullet without being a name. */
|
|
@@ -148,6 +151,18 @@ function candidateNames(text, strict = false) {
|
|
|
148
151
|
}
|
|
149
152
|
return names;
|
|
150
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The one name finder: the runtime check below and the replay gate (which runs this file from the shipped release) both call it.
|
|
156
|
+
* Italic actions are blanked first. `strict` is set when the person asked for names. `prose` adds lowercase names in plain prose,
|
|
157
|
+
* for a reply written before any lookup. `reference` is text whose names count in the reply in any case: a name it holds that the
|
|
158
|
+
* reply typed in lowercase ("eithan" for "Eithan") is still a name.
|
|
159
|
+
*/
|
|
160
|
+
function namesIn(text, options = {}) {
|
|
161
|
+
const body = text.replace(ITALIC_ACTION, (action) => " ".repeat(action.length));
|
|
162
|
+
const referenced = options.reference ? candidateNames(options.reference).filter((name) => mentions(body, name)) : [];
|
|
163
|
+
const found = [...candidateNames(body, options.strict), ...lowercaseListNames(body), ...(options.prose ? lowercaseProseNames(body) : []), ...referenced];
|
|
164
|
+
return found.filter((name, index) => found.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index);
|
|
165
|
+
}
|
|
151
166
|
/**
|
|
152
167
|
* Lowercase names the writer typed in emphasis ("**fisher gesha** — ...") or at the head of a bullet ("- ozriel — ..."): a model that writes in lowercase
|
|
153
168
|
* still names things, and these are the positions where a name is set apart. House vocabulary, sentence starters and status words are not names.
|
|
@@ -247,8 +262,7 @@ function sourceGroundingFinding(input) {
|
|
|
247
262
|
// What the house's own catalog and status tools returned is the house's record, not a claim about a work.
|
|
248
263
|
const houseRead = input.tools.filter((record) => HOUSE_READ_TOOL.test(record.name)).map((record) => record.result).join("\n");
|
|
249
264
|
const sawNothing = lookups.length === 0;
|
|
250
|
-
const
|
|
251
|
-
const names = [...new Set([...candidateNames(input.answer, askedAboutWork), ...lowercaseListNames(input.answer), ...proseNames])];
|
|
265
|
+
const names = namesIn(input.answer, { strict: askedAboutWork, prose: askedAboutWork && sawNothing && input.answer.length >= MIN_PROSE_LENGTH, reference: `${input.userText}\n${input.knownText ?? ""}` });
|
|
252
266
|
// Output of this turn's non-lookup tools (a calibredb listing, a sweep) is the house's record; memory tools are excluded because they may hold an earlier fabrication.
|
|
253
267
|
const otherRead = input.tools.filter((record) => !isLookupToolCall(record) && !MEMORY_TOOL.test(record.name) && !ECHO_COMMAND.test(record.args.command ?? "")).map((record) => record.result).join("\n");
|
|
254
268
|
// A name already said or read in an earlier turn is known by design: the rule judges what this turn introduces, not what the conversation already holds.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.909",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.909",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|