@qvac/harness 0.2.7 → 0.2.8
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/.claude/skills/pr-review/SKILL.md +237 -40
- package/dist/harness.bundle +10 -9
- package/dist-lib/index.js +58 -58
- package/dist-lib/lib/audio-transcode-bare.js +2 -2
- package/dist-lib/lib/capabilities/completion.js +30 -30
- package/dist-lib/lib/capabilities/download.js +2 -2
- package/dist-lib/lib/capabilities/speech-text.js +1 -1
- package/dist-lib/lib/capabilities/tool-prompt-log.js +1 -1
- package/dist-lib/lib/capabilities/tool-selection.js +2 -2
- package/dist-lib/lib/capabilities/tool-turns.js +1 -1
- package/dist-lib/lib/capabilities/usage-breakdown.js +1 -1
- package/dist-lib/lib/capabilities/voice.js +3 -3
- package/dist-lib/lib/connect.js +11 -11
- package/dist-lib/lib/context-window.js +1 -1
- package/dist-lib/lib/docx/index.js +3 -3
- package/dist-lib/lib/hardware/capability-probe.js +3 -3
- package/dist-lib/lib/hardware/gpu-probe-addon.js +2 -2
- package/dist-lib/lib/hardware/gpu-probe-entry.js +1 -1
- package/dist-lib/lib/helpers/markup-stream-filter.js +1 -1
- package/dist-lib/lib/helpers/run-outcome.js +1 -1
- package/dist-lib/lib/helpers/spawn-path.d.ts +1 -1
- package/dist-lib/lib/helpers/thinking.js +2 -2
- package/dist-lib/lib/helpers/tool-intent.js +1 -1
- package/dist-lib/lib/helpers/tool-label.js +3 -3
- package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -1
- package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -1
- package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -1
- package/dist-lib/lib/hooks/built-in/egress.js +2 -2
- package/dist-lib/lib/hooks/built-in/index.js +3 -3
- package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -1
- package/dist-lib/lib/hooks/pipeline.js +4 -4
- package/dist-lib/lib/hooks/registry.js +1 -1
- package/dist-lib/lib/hooks/run.js +1 -1
- package/dist-lib/lib/models/model-catalog.js +4 -4
- package/dist-lib/lib/models/model-delete.js +1 -1
- package/dist-lib/lib/models/model-handle.js +1 -1
- package/dist-lib/lib/models/model-list.js +1 -1
- package/dist-lib/lib/models/model-pool.js +8 -8
- package/dist-lib/lib/models/model-source.js +1 -1
- package/dist-lib/lib/models/music-catalog.d.ts +1 -1
- package/dist-lib/lib/no-redirect-fetch-bare.js +3 -3
- package/dist-lib/lib/no-redirect-fetch-impl.js +1 -1
- package/dist-lib/lib/no-redirect-fetch-node.js +2 -2
- package/dist-lib/lib/resolution-detector.js +1 -1
- package/dist-lib/lib/sdk-loader.js +2 -2
- package/dist-lib/lib/sdk-storage.js +1 -1
- package/dist-lib/lib/serve.js +10 -10
- package/dist-lib/lib/spawn.js +3 -3
- package/dist-lib/lib/staging.js +1 -1
- package/dist-lib/lib/tools/catalog.js +1 -1
- package/dist-lib/lib/tools/create-documents.js +2 -2
- package/dist-lib/lib/tools/edit-documents.js +2 -2
- package/dist-lib/lib/tools/edit-image.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/attachments.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +6 -6
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +3 -3
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +5 -5
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +3 -3
- package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/cli-process.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
- package/dist-lib/lib/tools/exec/obsidian/command.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/route.js +7 -7
- package/dist-lib/lib/tools/exec/obsidian/vault-create.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +6 -6
- package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-read.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/vault-write.js +2 -2
- package/dist-lib/lib/tools/exec/runner.js +1 -1
- package/dist-lib/lib/tools/exec.js +13 -13
- package/dist-lib/lib/tools/file-glob.js +3 -3
- package/dist-lib/lib/tools/fs/local.js +1 -1
- package/dist-lib/lib/tools/fs/remote.js +1 -1
- package/dist-lib/lib/tools/fs/walk.js +2 -2
- package/dist-lib/lib/tools/generate-image.js +4 -4
- package/dist-lib/lib/tools/generate-music.js +6 -6
- package/dist-lib/lib/tools/grep.js +6 -6
- package/dist-lib/lib/tools/http-request/truncate-response.js +1 -1
- package/dist-lib/lib/tools/http-request.js +9 -9
- package/dist-lib/lib/tools/mcp/utils.js +1 -1
- package/dist-lib/lib/tools/mcp-call.js +8 -8
- package/dist-lib/lib/tools/operations/builders.d.ts +1 -1
- package/dist-lib/lib/tools/operations/builders.js +1 -1
- package/dist-lib/lib/tools/operations/compile.js +1 -1
- package/dist-lib/lib/tools/operations/manifest.d.ts +16 -16
- package/dist-lib/lib/tools/operations/manifest.js +1 -1
- package/dist-lib/lib/tools/outbound-guard.js +1 -1
- package/dist-lib/lib/tools/rag-search.js +1 -1
- package/dist-lib/lib/tools/read-files.js +3 -3
- package/dist-lib/lib/tools/skill.js +8 -8
- package/dist-lib/lib/tools/skills/auth-resolver.js +2 -2
- package/dist-lib/lib/tools/skills/bin-check.js +1 -1
- package/dist-lib/lib/tools/skills/bundled-dir.js +2 -2
- package/dist-lib/lib/tools/skills/catalog.js +10 -10
- package/dist-lib/lib/tools/skills/cli-schema-check.js +1 -1
- package/dist-lib/lib/tools/skills/cli-schema-load.js +1 -1
- package/dist-lib/lib/tools/skills/cli-schema.js +4 -4
- package/dist-lib/lib/tools/skills/credential-check.js +3 -3
- package/dist-lib/lib/tools/skills/credential-grants.js +1 -1
- package/dist-lib/lib/tools/skills/credential-health.js +2 -2
- package/dist-lib/lib/tools/skills/frontmatter-parser.js +1 -1
- package/dist-lib/lib/tools/skills/index.js +5 -5
- package/dist-lib/lib/tools/skills/mention.js +1 -1
- package/dist-lib/lib/tools/skills/overlay-crud.js +1 -1
- package/dist-lib/lib/tools/skills/setup-routes.js +1 -1
- package/dist-lib/lib/tools/skills/skill-settings.js +1 -1
- package/dist-lib/lib/tools/skills/skill-state-constants.d.ts +8 -8
- package/dist-lib/lib/tools/skills/skill-state.js +9 -9
- package/dist-lib/lib/tools/toolsets.d.ts +1 -1
- package/dist-lib/lib/tools/url-guard.js +2 -2
- package/dist-lib/lib/tools/weather-lookup/place.d.ts +11 -0
- package/dist-lib/lib/tools/weather-lookup/place.js +152 -0
- package/dist-lib/lib/tools/weather-lookup/report.d.ts +37 -0
- package/dist-lib/lib/tools/weather-lookup/report.js +56 -0
- package/dist-lib/lib/tools/weather-lookup.d.ts +8 -0
- package/dist-lib/lib/tools/weather-lookup.js +110 -0
- package/dist-lib/lib/tools/web-fetch.js +10 -10
- package/dist-lib/lib/tools/web-search.js +2 -2
- package/dist-lib/lib/transport.js +1 -1
- package/dist-lib/lib/validating-lookup.js +1 -1
- package/dist-lib/lib/web-fetch/embedded-state.js +1 -1
- package/dist-lib/lib/web-fetch/extract.js +1 -1
- package/dist-lib/lib/web-fetch/images.js +1 -1
- package/dist-lib/lib/web-fetch/video.js +1 -1
- package/dist-lib/lib/web-search/engine-registry.js +7 -7
- package/dist-lib/lib/web-search/engines/aol.js +3 -3
- package/dist-lib/lib/web-search/engines/bing.js +4 -4
- package/dist-lib/lib/web-search/engines/brave.js +2 -2
- package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -3
- package/dist-lib/lib/web-search/engines/google.js +4 -4
- package/dist-lib/lib/web-search/engines/wikipedia.js +2 -2
- package/dist-lib/lib/web-search/engines/yahoo.js +3 -3
- package/dist-lib/lib/web-search/fetch-html.js +1 -1
- package/dist-lib/lib/web-search/merge-results.js +1 -1
- package/dist-lib/lib/web-search/orchestrator.js +3 -3
- package/dist-lib/schema/baseline.js +1 -1
- package/dist-lib/schema/build.js +3 -3
- package/dist-lib/schema/release-tags.d.ts +2 -1
- package/dist-lib/schema/release-tags.js +9 -7
- package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +63 -0
- package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +70 -0
- package/docs/decisions/INDEX.md +17 -15
- package/package.json +16 -3
- package/worker/harness-sidecar.mjs +2 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describePlace } from './place.js';
|
|
2
|
+
const MAX_DAYS = 3;
|
|
3
|
+
const DAY_LABELS = ['today', 'tomorrow'];
|
|
4
|
+
const RANGE = '\u2013';
|
|
5
|
+
const NOON = 4;
|
|
6
|
+
function firstValue(values) {
|
|
7
|
+
return values?.[0]?.value?.trim() ?? '';
|
|
8
|
+
}
|
|
9
|
+
export function placeOf(json) {
|
|
10
|
+
const area = json.nearest_area?.[0];
|
|
11
|
+
if (area === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
const place = {
|
|
14
|
+
area: firstValue(area.areaName),
|
|
15
|
+
region: firstValue(area.region),
|
|
16
|
+
country: firstValue(area.country)
|
|
17
|
+
};
|
|
18
|
+
return place.area || place.region || place.country ? place : null;
|
|
19
|
+
}
|
|
20
|
+
export function renderReport(json, place, asked, verified) {
|
|
21
|
+
const lines = [headline(place, asked, verified)];
|
|
22
|
+
const now = json.current_condition?.[0];
|
|
23
|
+
if (now !== undefined)
|
|
24
|
+
lines.push(currentLine(now));
|
|
25
|
+
const days = json.weather?.slice(0, MAX_DAYS) ?? [];
|
|
26
|
+
for (const [index, day] of days.entries())
|
|
27
|
+
lines.push(dailyLine(day, index));
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
function headline(place, asked, verified) {
|
|
31
|
+
const resolved = describePlace(place);
|
|
32
|
+
if (verified)
|
|
33
|
+
return resolved;
|
|
34
|
+
return `${resolved} (not "${asked}" — this is where it resolved)`;
|
|
35
|
+
}
|
|
36
|
+
function currentLine(now) {
|
|
37
|
+
const parts = [`Now: ${now.temp_C ?? '?'}°C / ${now.temp_F ?? '?'}°F`];
|
|
38
|
+
const desc = firstValue(now.weatherDesc);
|
|
39
|
+
if (desc)
|
|
40
|
+
parts.push(desc);
|
|
41
|
+
if (now.FeelsLikeC !== undefined)
|
|
42
|
+
parts.push(`feels like ${now.FeelsLikeC}°C`);
|
|
43
|
+
if (now.humidity !== undefined)
|
|
44
|
+
parts.push(`humidity ${now.humidity}%`);
|
|
45
|
+
if (now.windspeedKmph !== undefined)
|
|
46
|
+
parts.push(`wind ${now.windspeedKmph} km/h`);
|
|
47
|
+
return parts.join(', ');
|
|
48
|
+
}
|
|
49
|
+
function dailyLine(day, index) {
|
|
50
|
+
const label = DAY_LABELS[index];
|
|
51
|
+
const date = `${day.date ?? '?'}${label === undefined ? '' : ` (${label})`}`;
|
|
52
|
+
const celsius = `${day.mintempC ?? '?'}${RANGE}${day.maxtempC ?? '?'}°C`;
|
|
53
|
+
const fahrenheit = `${day.mintempF ?? '?'}${RANGE}${day.maxtempF ?? '?'}°F`;
|
|
54
|
+
const desc = firstValue(day.hourly?.[NOON]?.weatherDesc);
|
|
55
|
+
return `${date}: ${celsius} / ${fahrenheit}${desc ? `, ${desc}` : ''}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../capabilities/completion.ts';
|
|
2
|
+
import type { FetchFn } from './http-request.ts';
|
|
3
|
+
import { type HostResolver } from './url-guard.ts';
|
|
4
|
+
export interface WeatherLookupToolOptions {
|
|
5
|
+
fetch?: FetchFn;
|
|
6
|
+
resolveHost?: HostResolver;
|
|
7
|
+
}
|
|
8
|
+
export declare function createWeatherLookupTool({ fetch: fetchImpl, resolveHost }?: WeatherLookupToolOptions): ToolDefinition;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import defaultFetch from '#no-redirect-fetch';
|
|
2
|
+
import { AbortController } from 'bare-abort-controller';
|
|
3
|
+
import { lookupHost } from '../dns-lookup.js';
|
|
4
|
+
import { isRedirectStatus } from './http-request/utils.js';
|
|
5
|
+
import { resolveOutboundUrl } from './url-guard.js';
|
|
6
|
+
import { checkQualifier, cityMatches, describePlace, qualifierOf } from './weather-lookup/place.js';
|
|
7
|
+
import { placeOf, renderReport } from './weather-lookup/report.js';
|
|
8
|
+
const WEATHER_HOST = 'wttr.in';
|
|
9
|
+
const LOOKUP_TIMEOUT_MS = 15_000;
|
|
10
|
+
const MAX_BODY_BYTES = 400_000;
|
|
11
|
+
export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookupHost } = {}) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'weather_lookup',
|
|
14
|
+
toolset: 'http-request',
|
|
15
|
+
skillGated: true,
|
|
16
|
+
autoApprove: true,
|
|
17
|
+
description: 'Look up the current weather and a three-day forecast for a place, via wttr.in. ' +
|
|
18
|
+
'Returns the place the service resolved on its first line, then the conditions now and ' +
|
|
19
|
+
'one line per day. Use it for any question about weather, temperature or forecast.',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
location: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'The place exactly as the user wrote it, including any country or state: ' +
|
|
26
|
+
'"Nassau, Bahamas", "Paris, Texas", "London"'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: ['location']
|
|
30
|
+
},
|
|
31
|
+
effect: () => 'read',
|
|
32
|
+
resource: () => ({ kind: 'host', label: WEATHER_HOST }),
|
|
33
|
+
async execute(args, ctx) {
|
|
34
|
+
const raw = args.location;
|
|
35
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
36
|
+
return { error: 'location must be a non-empty string' };
|
|
37
|
+
}
|
|
38
|
+
const location = raw.trim();
|
|
39
|
+
if (ctx?.signal?.aborted)
|
|
40
|
+
return { error: 'weather_lookup cancelled' };
|
|
41
|
+
const url = new URL(`https://${WEATHER_HOST}/${encodeURIComponent(location)}?format=j1`);
|
|
42
|
+
const fetch = fetchImpl ?? defaultFetch;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(undefined), LOOKUP_TIMEOUT_MS);
|
|
45
|
+
const onCallerAbort = () => controller.abort(undefined);
|
|
46
|
+
ctx?.signal?.addEventListener('abort', onCallerAbort);
|
|
47
|
+
try {
|
|
48
|
+
const check = await resolveOutboundUrl(url, resolveHost, ctx?.signal);
|
|
49
|
+
if (check.blocked)
|
|
50
|
+
return { error: check.blocked };
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method: 'GET',
|
|
53
|
+
headers: { Accept: 'application/json' },
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
redirect: 'manual',
|
|
56
|
+
validatedAddresses: check.addresses,
|
|
57
|
+
maxBytes: MAX_BODY_BYTES
|
|
58
|
+
});
|
|
59
|
+
if (isRedirectStatus(response.status)) {
|
|
60
|
+
return { error: `${WEATHER_HOST} redirected the lookup for "${location}"` };
|
|
61
|
+
}
|
|
62
|
+
const body = await response.text();
|
|
63
|
+
if (/location not found/i.test(body))
|
|
64
|
+
return { content: notFound(location) };
|
|
65
|
+
if (response.status !== 200) {
|
|
66
|
+
return { error: unavailable(`it answered HTTP ${response.status}`) };
|
|
67
|
+
}
|
|
68
|
+
let json;
|
|
69
|
+
try {
|
|
70
|
+
json = JSON.parse(body);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { error: unavailable('it did not return a forecast') };
|
|
74
|
+
}
|
|
75
|
+
const place = placeOf(json);
|
|
76
|
+
if (place === null)
|
|
77
|
+
return { content: notFound(location) };
|
|
78
|
+
const qualifier = qualifierOf(location);
|
|
79
|
+
const verdict = qualifier === null ? null : checkQualifier(qualifier, place);
|
|
80
|
+
const named = cityMatches(location, place);
|
|
81
|
+
if (verdict === 'contradiction' && !named) {
|
|
82
|
+
return {
|
|
83
|
+
content: `No such place: "${location}". ${WEATHER_HOST} resolved it to ` +
|
|
84
|
+
`${describePlace(place)}, which is somewhere else.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const verified = qualifier === null ? named : verdict === 'match';
|
|
88
|
+
return { content: renderReport(json, place, location, verified) };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (ctx?.signal?.aborted)
|
|
92
|
+
return { error: 'weather_lookup cancelled' };
|
|
93
|
+
if (controller.signal.aborted) {
|
|
94
|
+
return { error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms` };
|
|
95
|
+
}
|
|
96
|
+
return { error: unavailable(err instanceof Error ? err.message : String(err)) };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
ctx?.signal?.removeEventListener('abort', onCallerAbort);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function unavailable(reason) {
|
|
106
|
+
return `The weather service could not be reached: ${reason}.`;
|
|
107
|
+
}
|
|
108
|
+
function notFound(location) {
|
|
109
|
+
return `No such place: "${location}". ${WEATHER_HOST} does not know it.`;
|
|
110
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import defaultFetch from '#no-redirect-fetch';
|
|
2
2
|
import { AbortController } from 'bare-abort-controller';
|
|
3
|
-
import { lookupHost } from
|
|
4
|
-
import { AbortedError } from
|
|
5
|
-
import { extractEmbeddedState } from
|
|
6
|
-
import { extractPageText, looksLikeHtml } from
|
|
7
|
-
import { imageFileName, pageImageUrls } from
|
|
8
|
-
import { pageVideoRef } from
|
|
9
|
-
import { MAX_HTML_CHARS } from
|
|
10
|
-
import { BROWSER_UA } from
|
|
11
|
-
import { isRedirectStatus } from
|
|
12
|
-
import { resolveOutboundUrl } from
|
|
3
|
+
import { lookupHost } from '../dns-lookup.js';
|
|
4
|
+
import { AbortedError } from '../helpers/race-abort.js';
|
|
5
|
+
import { extractEmbeddedState } from '../web-fetch/embedded-state.js';
|
|
6
|
+
import { extractPageText, looksLikeHtml } from '../web-fetch/extract.js';
|
|
7
|
+
import { imageFileName, pageImageUrls } from '../web-fetch/images.js';
|
|
8
|
+
import { pageVideoRef } from '../web-fetch/video.js';
|
|
9
|
+
import { MAX_HTML_CHARS } from '../web-search/fetch-html.js';
|
|
10
|
+
import { BROWSER_UA } from '../web-search/user-agents.js';
|
|
11
|
+
import { isRedirectStatus } from './http-request/utils.js';
|
|
12
|
+
import { resolveOutboundUrl } from './url-guard.js';
|
|
13
13
|
const SMALL_CONTEXT_TOKENS = 12_288;
|
|
14
14
|
const SMALL_CONTEXT_CHARS = 6_000;
|
|
15
15
|
const LARGE_CONTEXT_CHARS = 16_000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import defaultFetch from '#fetch';
|
|
2
|
-
import { formatResultsForPrompt } from
|
|
3
|
-
import { searchWeb } from
|
|
2
|
+
import { formatResultsForPrompt } from '../web-search/format-results.js';
|
|
3
|
+
import { searchWeb } from '../web-search/orchestrator.js';
|
|
4
4
|
const nodeFetch = async (url, init) => {
|
|
5
5
|
const res = await defaultFetch(url, init);
|
|
6
6
|
return { ok: res.ok, status: res.status, text: () => res.text() };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import c from 'compact-encoding';
|
|
2
2
|
import { Duplex } from 'streamx';
|
|
3
|
-
export { settleOnClose } from
|
|
3
|
+
export { settleOnClose } from './rpc-session.js';
|
|
4
4
|
export function clientSessions(pipe) {
|
|
5
5
|
let current = session(pipe);
|
|
6
6
|
pipe.on('data', (chunk) => current.push(chunk));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities } from
|
|
1
|
+
import { decodeHtmlEntities } from '../web-search/html-helpers.js';
|
|
2
2
|
const LDJSON_SCRIPT = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
3
3
|
const JSON_SCRIPT = /<script[^>]*type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
4
4
|
const INLINE_SCRIPT = /<script\b(?![^>]*\bsrc=)(?![^>]*type=["']application\/(?:ld\+)?json["'])[^>]*>([\s\S]*?)<\/script>/gi;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities } from
|
|
1
|
+
import { decodeHtmlEntities } from '../web-search/html-helpers.js';
|
|
2
2
|
const META_TAG = /<meta\s[^>]*>/gi;
|
|
3
3
|
const IMAGE_ATTR = /(?:property|name)=["'](?:og:image(?::secure_url|:url)?|twitter:image)["']/i;
|
|
4
4
|
const META_CONTENT = /content=["']([^"']+)["']/i;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities } from
|
|
1
|
+
import { decodeHtmlEntities } from '../web-search/html-helpers.js';
|
|
2
2
|
const META_TAG = /<meta\s[^>]*>/gi;
|
|
3
3
|
const VIDEO_ATTR = /(?:property|name)=["']og:video(?::secure_url|:url)?["']/i;
|
|
4
4
|
const META_CONTENT = /content=["']([^"']+)["']/i;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { searchAol } from
|
|
2
|
-
import { searchBing } from
|
|
3
|
-
import { searchBrave } from
|
|
4
|
-
import { searchDuckDuckGo } from
|
|
5
|
-
import { searchGoogle } from
|
|
6
|
-
import { searchWikipedia } from
|
|
7
|
-
import { searchYahoo } from
|
|
1
|
+
import { searchAol } from './engines/aol.js';
|
|
2
|
+
import { searchBing } from './engines/bing.js';
|
|
3
|
+
import { searchBrave } from './engines/brave.js';
|
|
4
|
+
import { searchDuckDuckGo } from './engines/duckduckgo.js';
|
|
5
|
+
import { searchGoogle } from './engines/google.js';
|
|
6
|
+
import { searchWikipedia } from './engines/wikipedia.js';
|
|
7
|
+
import { searchYahoo } from './engines/yahoo.js';
|
|
8
8
|
export const DEFAULT_MAX_RESULTS = 8;
|
|
9
9
|
export const ENGINES = [
|
|
10
10
|
{ name: 'duckduckgo', fn: searchDuckDuckGo },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
4
|
function decodeRuUrl(encoded) {
|
|
5
5
|
try {
|
|
6
6
|
const href = decodeURIComponent(encoded);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
4
|
-
import { SAFARI_UA } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
|
+
import { SAFARI_UA } from '../user-agents.js';
|
|
5
5
|
function isOrganicBingUrl(url) {
|
|
6
6
|
return url.startsWith('http') && !urlMatchesHost(url, 'bing.com') && !url.includes('msockid=');
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
3
|
function parseSnippetBlock(inner) {
|
|
4
4
|
const urlMatch = inner.match(/<a[^>]*href="(https?:\/\/(?!imgs\.search\.brave)[^"]*)"[^>]*>/i);
|
|
5
5
|
if (!urlMatch) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
4
|
function extractDdgUrl(raw) {
|
|
5
5
|
if (raw.includes('uddg=')) {
|
|
6
6
|
const m = raw.match(/uddg=([^&]+)/);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
4
|
-
import { GOOGLE_GSA_UA } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
|
+
import { GOOGLE_GSA_UA } from '../user-agents.js';
|
|
5
5
|
function resolveGoogleUrl(rawUrl) {
|
|
6
6
|
if (rawUrl.startsWith('/url?q=')) {
|
|
7
7
|
return decodeURIComponent(rawUrl.slice(7).split('&')[0] || '');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fetchJson } from
|
|
2
|
-
import { clean } from
|
|
1
|
+
import { fetchJson } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
3
|
function extractSearchItems(data) {
|
|
4
4
|
if (typeof data !== 'object' || data === null) {
|
|
5
5
|
return [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
4
|
function decodeRuUrl(encoded) {
|
|
5
5
|
try {
|
|
6
6
|
const href = decodeURIComponent(encoded);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
|
-
import { BROWSER_UA } from
|
|
2
|
+
import { BROWSER_UA } from './user-agents.js';
|
|
3
3
|
export const ENGINE_TIMEOUT_MS = 8000;
|
|
4
4
|
export const MAX_HTML_CHARS = 2_000_000;
|
|
5
5
|
export class TimeoutError extends Error {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_MAX_RESULTS, ENGINES } from
|
|
2
|
-
import { TimeoutError } from
|
|
3
|
-
import { mergeResults } from
|
|
1
|
+
import { DEFAULT_MAX_RESULTS, ENGINES } from './engine-registry.js';
|
|
2
|
+
import { TimeoutError } from './fetch-html.js';
|
|
3
|
+
import { mergeResults } from './merge-results.js';
|
|
4
4
|
export function classifyEngineError(reason) {
|
|
5
5
|
return reason instanceof TimeoutError ? 'timeout' : 'error';
|
|
6
6
|
}
|
|
@@ -2,7 +2,7 @@ import fs from 'bare-fs';
|
|
|
2
2
|
import path from 'bare-path';
|
|
3
3
|
import process from 'bare-process';
|
|
4
4
|
import { spawnSync } from 'bare-subprocess';
|
|
5
|
-
import { previousReleaseTags } from
|
|
5
|
+
import { previousReleaseTags } from './release-tags.js';
|
|
6
6
|
export function rewindSpec(root, dirs, { carries, keep }) {
|
|
7
7
|
if (process.env.SCHEMA_NO_BASELINE)
|
|
8
8
|
return [];
|
package/dist-lib/schema/build.js
CHANGED
|
@@ -3,9 +3,9 @@ import { fileURLToPath } from 'bare-url';
|
|
|
3
3
|
import HRPCBuilder from 'hrpc';
|
|
4
4
|
import Hyperschema from 'hyperschema';
|
|
5
5
|
import HyperschemaTS from 'hyperschema-ts';
|
|
6
|
-
import { resetHrpcIds, rewindSpec, unstageSpec } from
|
|
7
|
-
import { writeBind, writeClient } from
|
|
8
|
-
import { registerHarnessApi, registerHarnessTypes } from
|
|
6
|
+
import { resetHrpcIds, rewindSpec, unstageSpec } from './baseline.js';
|
|
7
|
+
import { writeBind, writeClient } from './generate.js';
|
|
8
|
+
import { registerHarnessApi, registerHarnessTypes } from './harness.js';
|
|
9
9
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const ROOT = path.join(dir, '..');
|
|
11
11
|
const SPEC_DIR = path.join(dir, '..', 'spec');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { Version } from 'bare-semver';
|
|
2
|
+
export declare function currentReleaseVersion(root: string): Version;
|
|
2
3
|
export declare function previousReleaseTags(root: string, { count, carries }: {
|
|
3
4
|
count?: number;
|
|
4
5
|
carries: string;
|
|
@@ -2,24 +2,26 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { Version } from 'bare-semver';
|
|
5
|
-
export function
|
|
5
|
+
export function currentReleaseVersion(root) {
|
|
6
6
|
const file = path.join(root, 'package.json');
|
|
7
7
|
const { version } = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
try {
|
|
9
|
+
return Version.parse(version);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error(`no release version in package version ${version}`);
|
|
13
|
+
}
|
|
12
14
|
}
|
|
13
15
|
const TAG_PREFIX = 'v';
|
|
14
16
|
export function previousReleaseTags(root, { count = 3, carries }) {
|
|
15
|
-
const current =
|
|
17
|
+
const current = currentReleaseVersion(root);
|
|
16
18
|
const listed = spawnSync('git', ['tag', '-l', `${TAG_PREFIX}*`], { cwd: root });
|
|
17
19
|
if (listed.status !== 0)
|
|
18
20
|
throw new Error(`git tag -l failed: ${listed.stderr ?? ''}`);
|
|
19
21
|
const newestPerLine = new Map();
|
|
20
22
|
for (const line of (listed.stdout?.toString() ?? '').split('\n')) {
|
|
21
23
|
const parsed = parseTag(line.trim());
|
|
22
|
-
if (!parsed || parsed.
|
|
24
|
+
if (!parsed || Version.compare(parsed.version, current) > 0)
|
|
23
25
|
continue;
|
|
24
26
|
const winner = newestPerLine.get(parsed.line);
|
|
25
27
|
if (!winner || Version.compare(parsed.version, winner.version) > 0) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-2026-09-10-skill-wording-is-measured-offline
|
|
3
|
+
status: active
|
|
4
|
+
introduced_by: PR-2160
|
|
5
|
+
supersedes: []
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A skill's wording is measured on the real small model before it ships
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
A change to a bundled skill's `SKILL.md` that is meant to change model behaviour is measured before
|
|
13
|
+
it lands: the candidate body and the current one run through `examples/skill-eval` on
|
|
14
|
+
the real local 2B model, against a corpus of user-worded prompts, with the skill's tools answered
|
|
15
|
+
from recorded fixtures and every reply judged by deterministic rules. The winning variant records
|
|
16
|
+
its scores in the skill's front matter (`version:` plus a comment the model never sees).
|
|
17
|
+
|
|
18
|
+
The eval drives `Harness.completion` in-process, swaps variants through the skills overlay
|
|
19
|
+
(`writeSkillFile` / `deleteOverlaySkill`), and never uses a second model as a judge. Baseline is
|
|
20
|
+
the bundled `@qvac/skills` tree, or any checkout named by `--skills-dir`.
|
|
21
|
+
|
|
22
|
+
## Context
|
|
23
|
+
|
|
24
|
+
The default mobile model is a 2B. It copies examples it is shown and ignores rules it cannot copy,
|
|
25
|
+
and a wording that reads better to a human routinely scores worse on it. The weather skill's Nassau
|
|
26
|
+
defect (QVAC-24701) was one line of guidance; nine candidate rewrites were tried and seven of them
|
|
27
|
+
fixed the ticket prompt while regressing plain-city prompts through wrong URL suffixes. Only a
|
|
28
|
+
measured comparison separated them, and only categories, not totals, showed the trade.
|
|
29
|
+
|
|
30
|
+
Judging with a larger model was rejected: its verdicts vary run to run, cost a second inference per
|
|
31
|
+
sample, and hide which rule failed. String laws over the call the model made and the reply it
|
|
32
|
+
gave are reproducible and name the failure.
|
|
33
|
+
|
|
34
|
+
Live network was rejected for the loop itself: wttr.in's answers change hourly and the source
|
|
35
|
+
keys its format on the User-Agent. Recorded fixtures keep runs comparable; `--record` refreshes them
|
|
36
|
+
deliberately, with the product's User-Agent.
|
|
37
|
+
|
|
38
|
+
## Alternatives Considered
|
|
39
|
+
|
|
40
|
+
- A Playwright smoke that sends one prompt through the desktop UI. It proves the wiring, not the
|
|
41
|
+
wording: one sample, no categories, no variant swap, and the UI adds minutes per run.
|
|
42
|
+
- Tuning by hand in the TUI. Two repeats of a 130-run corpus already swing by about three runs;
|
|
43
|
+
a handful of manual tries cannot see a five-point change.
|
|
44
|
+
|
|
45
|
+
## Consequences
|
|
46
|
+
|
|
47
|
+
- A skill PR that changes model-facing text states its before/after category table, from this eval
|
|
48
|
+
or an equivalent pack, and keeps the score history in the front matter.
|
|
49
|
+
- A new skill that needs measuring adds a pack (corpus, fake tools, judge) under
|
|
50
|
+
`examples/skill-eval/packs/`; the runner, routes, gap accounting and report are shared.
|
|
51
|
+
- Differences under about five runs per 130 are noise at two repeats. Three repeats decide.
|
|
52
|
+
- The eval is not CI: it needs the model weights and about four seconds per run. It is run by hand
|
|
53
|
+
and its output lives under the gitignored `output/`.
|
|
54
|
+
- It lives here rather than in `@qvac/skills` because a pack is harness code: `world()` builds the
|
|
55
|
+
tools the model is offered. Here it needs no dependency the repo does not already have, and
|
|
56
|
+
`--variant` and `--skills-dir` already reach a skill body in another checkout.
|
|
57
|
+
- Revisit if the SDK exposes a sampler seed (paired repeats would tighten the noise band) or if a
|
|
58
|
+
judge model becomes cheap and stable enough to grade open-ended replies.
|
|
59
|
+
|
|
60
|
+
## When Agents Should Read This
|
|
61
|
+
|
|
62
|
+
Read this when changing the paths, workflows, runtime boundaries, or platform
|
|
63
|
+
behavior listed for this ADR in `docs/decisions/INDEX.md`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-2026-09-16-a-lookup-reports-the-place-it-resolved
|
|
3
|
+
status: active
|
|
4
|
+
introduced_by: PR-52
|
|
5
|
+
supersedes: []
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A provider that can silently resolve the wrong thing gets a typed tool, not a skill rule
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
When a third-party lookup can answer `200` for a resource the user did not ask for, the harness owns
|
|
13
|
+
the check in a typed tool rather than leaving it to skill prose. `weather_lookup` is the first: it
|
|
14
|
+
asks wttr.in for the structured `format=j1` body, compares the place the geocoder resolved against
|
|
15
|
+
the country or state the request named, refuses the answer when they contradict, and puts the
|
|
16
|
+
resolved place on the first line of every report it does return.
|
|
17
|
+
|
|
18
|
+
A skill keeps the judgement that is genuinely the model's: whether to call at all, and what location
|
|
19
|
+
to send. The tool keeps the part that is arithmetic.
|
|
20
|
+
|
|
21
|
+
## Context
|
|
22
|
+
|
|
23
|
+
QVAC-24701 shipped a tuned `weather` skill and still failed QA. "What's the weather like in
|
|
24
|
+
Xyzzyville, Atlantis?" produced a correct call, `200`, and the body
|
|
25
|
+
`Xyzzyville, Atlantis: 🌦 +86°F`, which the model relayed. wttr.in echoes the request string back as
|
|
26
|
+
the label and geocodes it fuzzily, so its answer for a place that does not exist is the weather of
|
|
27
|
+
whatever it landed on — Atlantis, Florida. The same mechanism means a bare `Rome` currently answers
|
|
28
|
+
with Lomé, in Togo.
|
|
29
|
+
|
|
30
|
+
No wording can catch that. The skill already tells the model to report a failure on a non-200 or a
|
|
31
|
+
`location not found` body; here the status and the body both look right, and the only evidence to the
|
|
32
|
+
contrary is `nearest_area` inside a 40 KB JSON document the agent's context cannot hold and the
|
|
33
|
+
response truncator would clip. Deciding whether "Atlantis" agrees with "Florida, United States of
|
|
34
|
+
America" is string comparison with an alias table, which is the work a 2B is worst at and code is
|
|
35
|
+
best at. Measured behaviour agrees: "ask before acting" rules top out near 30 % on that model in any
|
|
36
|
+
wording.
|
|
37
|
+
|
|
38
|
+
Only the qualifier is verified, never the city. The geocoder answers with the nearest populated area,
|
|
39
|
+
which for a large city is routinely a neighbourhood that does not carry the city's name — São Paulo
|
|
40
|
+
resolves to Liberdade, Sydney to Kirribilli, Tbilisi to Okrokana. Nine of thirty large cities probed
|
|
41
|
+
would have been rejected by a city check. The region and country are reliable, and they are exactly
|
|
42
|
+
what a wrong resolution gets wrong.
|
|
43
|
+
|
|
44
|
+
## Alternatives Considered
|
|
45
|
+
|
|
46
|
+
- An `operations.json` entry, the existing declarative path for provider calls. Every compiled
|
|
47
|
+
operation is a write: approval-gated, and its only response shaping is `pick` over top-level keys.
|
|
48
|
+
It cannot validate or refuse, and it would put an approval dialog in front of a weather question.
|
|
49
|
+
- Keeping `http_request` and asking the skill for `format=j1`. The body does not fit the context, the
|
|
50
|
+
truncator clips it, and the comparison still lands on the model.
|
|
51
|
+
- A wttr.in-specific hook that issues a second `j1` request and appends the resolved place to the
|
|
52
|
+
`http_request` result. Hidden coupling, two requests per answer, and the model still has to read it.
|
|
53
|
+
|
|
54
|
+
## Consequences
|
|
55
|
+
|
|
56
|
+
- The tool is `skillGated`, so its schema costs nothing until the weather skill body is in context,
|
|
57
|
+
and `autoApprove`, so the SDK does not inject a second `intent` argument into a one-argument read.
|
|
58
|
+
It names the `http-request` toolset, so the per-agent switch still turns it off.
|
|
59
|
+
- A skill that grants `weather_lookup` requires a harness that has it. The bundled `weather` skill
|
|
60
|
+
keeps working on older harnesses only while it still grants `http_request`.
|
|
61
|
+
- The alias table is data and will need names added. A qualifier it does not know falls back to a
|
|
62
|
+
plain substring match against the region and country, which is the behaviour it had before.
|
|
63
|
+
- Adding a tool for every provider is not the intent. The trigger is specific: a provider that can
|
|
64
|
+
answer successfully about the wrong resource, where the evidence exists but not in a form the model
|
|
65
|
+
can be asked to check.
|
|
66
|
+
|
|
67
|
+
## When Agents Should Read This
|
|
68
|
+
|
|
69
|
+
Read this when changing the paths, workflows, runtime boundaries, or platform
|
|
70
|
+
behavior listed for this ADR in `docs/decisions/INDEX.md`.
|