@qvac/harness 0.8.7 → 0.8.9

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.md CHANGED
@@ -8,6 +8,61 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.9] - 2026-10-15
12
+
13
+ ### Changed
14
+
15
+ - A skill with a `display_name` answers only to that handle; a skill without
16
+ one still answers to its folder name. `@documents` resolves the word skill
17
+ and `@word` no longer does, which replaces the 0.8.8 note that both resolve.
18
+ Implicit preload skips a leading `@name`, so a handle that resolves to no
19
+ skill loads nothing, while a plain "make an excel sheet" still preloads
20
+ Spreadsheets. A paired device on an older app build still sends `@word` and
21
+ `@excel`; once the skills package declares `display_name`, a host on this
22
+ version answers those requests without the skill. (#505)
23
+ - `@qvac/skills` 0.1.15: the Word and Excel skills declare `display_name`, so
24
+ their handles are `@documents` and `@spreadsheets`. The browser skill hands a
25
+ CAPTCHA, interstitial or login page to the user instead of clicking through
26
+ or waiting, and keeps its Chrome cookies and logins across chats. (#513)
27
+
28
+ ## [0.8.8] - 2026-10-15
29
+
30
+ ### Added
31
+
32
+ - A skill's manifest may declare `display_name`, the `@` handle users see and
33
+ type, as one word (letters, digits, `-` or `_`); a `display_name` that is
34
+ not one word is ignored. The Word and Excel skills are shown as Documents
35
+ and Spreadsheets, and their folder names are trademarks, so a host needs a
36
+ handle other than the name. Once the word skill's manifest declares
37
+ `display_name: Documents`, `@documents` resolves it, and `@word` still does,
38
+ so an app that sends the folder name keeps working. The catalog entry and
39
+ the `skills` wire carry `displayName`, a customized copy of a skill keeps
40
+ the shipped one, and `skillMentionHandle` gives a host the same rule the
41
+ harness matches on. The folder name stays the skill's key everywhere else.
42
+ (#469)
43
+
44
+ ### Fixed
45
+
46
+ - A replayed tool call keeps its markup when history is rendered from scratch.
47
+ Every assistant turn lost its tool-call markup on the way in, so a retry,
48
+ compaction, a dropped chat cache, an overflow candidate or another device
49
+ showed the model an empty assistant turn followed by a result answering
50
+ nothing. Only an assistant turn that no tool result follows is stripped now,
51
+ which still keeps markup leaked into a final answer out of the prompt. (#402)
52
+ - A web search the harness runs for "search the web" now ends on the same
53
+ continuation nudge as a search the model makes. Without it, the round after
54
+ the search often failed on Qwen3.5-9B with thinking on instead of answering.
55
+ (#486)
56
+ - An identical call to a read tool that already succeeded this turn is
57
+ answered with a note pointing at the earlier result instead of running
58
+ again, and counts as a strike. A small model could repeat the same call
59
+ hundreds of times in one round until the context filled. Any other tool
60
+ that runs clears the memo, and a round is cut to its first 16 calls. (#377)
61
+ - Asking the `skill` tool for a file a loaded skill does not have returns
62
+ guidance listing its real files instead of an error, so the app no longer
63
+ shows a failed tool call for it. A file asked of an unknown skill returns
64
+ `skill not found`. (#383)
65
+
11
66
  ## [0.8.7] - 2026-10-12
12
67
 
13
68
  ### Added
@@ -1953,7 +2008,9 @@ Install 0.2.12 or later to get them.
1953
2008
  - Every dependency installs from the public npm registry: GPU stats are read
1954
2009
  through `bare-gpu-info`. (#12)
1955
2010
 
1956
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.7...HEAD
2011
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.9...HEAD
2012
+ [0.8.9]: https://github.com/tetherto/qvac-harness/compare/v0.8.8...v0.8.9
2013
+ [0.8.8]: https://github.com/tetherto/qvac-harness/compare/v0.8.7...v0.8.8
1957
2014
  [0.8.7]: https://github.com/tetherto/qvac-harness/compare/v0.8.6...v0.8.7
1958
2015
  [0.8.6]: https://github.com/tetherto/qvac-harness/compare/v0.8.5...v0.8.6
1959
2016
  [0.8.5]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...v0.8.5
@@ -185,6 +185,7 @@ export interface ToolDefinition {
185
185
  toolset: Toolset | null;
186
186
  validate?: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => string | null;
187
187
  effect: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => ToolEffect;
188
+ stableRead?: true;
188
189
  resource?: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => ResourceDescriptor | null;
189
190
  compiled?: true;
190
191
  execute: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => Promise<unknown>;
@@ -60,6 +60,9 @@ const MAX_MUTE_ROUNDS = 1;
60
60
  const NARRATED_INTENT = /\b(I['’]ll|I will|Now I|Let me(?! know)|Next,? I)\b[^.!?]*[.!?]?\s*$/i;
61
61
  const MAX_WEB_SEARCHES_PER_TURN = 3;
62
62
  const MAX_CAPPED_ROUNDS = 1;
63
+ const MAX_CALLS_PER_ROUND = 16;
64
+ const repeatedReadMessage = (name) => `The result of this exact ${name} call is already above. Answer from it now; ` +
65
+ `do not call ${name} again with the same arguments.`;
63
66
  const WEB_SEARCH_LIMIT_MESSAGE = 'Web search limit reached for this question. Answer now from the results already gathered. ' +
64
67
  'If something was not found, say so plainly and mention the most relevant sources. ' +
65
68
  'Do not call web_search again.';
@@ -101,9 +104,10 @@ export function toSdkTool(tool) {
101
104
  parameters: intentParameters(tool, tool.prompt?.parameters ?? tool.parameters)
102
105
  };
103
106
  }
104
- function sanitizeTurn(turn) {
107
+ function sanitizeTurn(turn, index, turns) {
105
108
  const stripped = stripControlTokens(turn.content);
106
- const content = turn.role === 'assistant' ? stripToolCallMarkup(stripped) : stripped;
109
+ const leaked = turn.role === 'assistant' && turns[index + 1]?.role !== 'tool';
110
+ const content = leaked ? stripToolCallMarkup(stripped) : stripped;
107
111
  return content === turn.content ? turn : { ...turn, content };
108
112
  }
109
113
  function lastUserContent(messages) {
@@ -143,7 +147,8 @@ function directToolHistory(history, call, output) {
143
147
  return [
144
148
  ...history,
145
149
  { role: 'assistant', content: toolCallMarkup([call]) },
146
- { role: 'tool', content: toolResultTurn(call.name, DIRECT_CALL_ID, output) }
150
+ { role: 'tool', content: toolResultTurn(call.name, DIRECT_CALL_ID, output) },
151
+ { role: 'user', content: TOOL_CONTINUATION_NUDGE }
147
152
  ];
148
153
  }
149
154
  function assistantToolTurn(contentText, calls) {
@@ -203,12 +208,19 @@ function deniedResult() {
203
208
  }
204
209
  function callKey(name, args) {
205
210
  try {
206
- return `${name}:${JSON.stringify(args)}`;
211
+ return `${name}:${JSON.stringify(args, sortedKeys)}`;
207
212
  }
208
213
  catch {
209
214
  return name;
210
215
  }
211
216
  }
217
+ function sortedKeys(_key, value) {
218
+ if (!isRecord(value) || Array.isArray(value))
219
+ return value;
220
+ return Object.fromEntries(Object.keys(value)
221
+ .sort()
222
+ .map((key) => [key, value[key]]));
223
+ }
212
224
  const RETRYABLE_STATUS = new Set([408, 429]);
213
225
  const RETRYABLE_CODE = new Set(['timeout', 'transport', 'busy']);
214
226
  function callSucceeded(result, outcome) {
@@ -875,12 +887,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
875
887
  let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...messages] : messages);
876
888
  if (preload && preloadedSkill !== null) {
877
889
  history = directToolHistory(history, { name: SKILL_TOOL_NAME, arguments: { name: preload.name } }, preloadedResult);
878
- history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
879
890
  }
880
891
  let roundCacheKey = cacheKey;
881
892
  const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
882
893
  const denied = new Set();
883
894
  const failed = new Map();
895
+ const answered = new Set();
884
896
  let blockedRepeats = 0;
885
897
  let webSearches = 0;
886
898
  const sources = trustedSourcesFrom(history);
@@ -939,6 +951,9 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
939
951
  if (fold.ok) {
940
952
  webSearches++;
941
953
  }
954
+ if (webSearch.stableRead && execution.executed && callSucceeded(output, executionOutcome)) {
955
+ answered.add(callKey(webSearch.name, args));
956
+ }
942
957
  if (!activity && !fold.ok) {
943
958
  activity = {
944
959
  type: 'tool-activity',
@@ -1074,6 +1089,10 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1074
1089
  }
1075
1090
  mute = 0;
1076
1091
  narrated = false;
1092
+ if (toolCalls.length > MAX_CALLS_PER_ROUND) {
1093
+ hlog('round over the call cap; the rest are dropped', { round, calls: toolCalls.length });
1094
+ toolCalls = toolCalls.slice(0, MAX_CALLS_PER_ROUND);
1095
+ }
1077
1096
  const resolved = await resolveToolCalls(toolCalls, {
1078
1097
  enabled,
1079
1098
  agentId: agent?.id ?? null,
@@ -1099,7 +1118,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1099
1118
  for (const { call, tool, args, rawCall, rejection, intent } of resolved) {
1100
1119
  if (signal?.aborted)
1101
1120
  throw new AbortedError();
1102
- const priorFailure = failed.get(callKey(call.name, args));
1121
+ const key = callKey(call.name, args);
1122
+ const priorFailure = failed.get(key);
1103
1123
  const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
1104
1124
  const callCtx = {
1105
1125
  ...toolContext(turnImages, ctxSize),
@@ -1115,6 +1135,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1115
1135
  };
1116
1136
  const skillCall = call.name === SKILL_TOOL_NAME;
1117
1137
  const effect = tool?.effect(args, callCtx) ?? (skillCall ? 'read' : undefined);
1138
+ const stableRead = tool?.stableRead === true && effect === 'read';
1139
+ const priorSuccess = !priorFailure && stableRead && answered.has(key);
1118
1140
  const resource = tool?.resource?.(args) ?? null;
1119
1141
  const skillName = unquoteArg(args?.name ?? args?.skill_name ?? args?.skill);
1120
1142
  const askedFile = skillFileArg(args);
@@ -1123,7 +1145,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1123
1145
  (mainRequested
1124
1146
  ? loadedThisTurn.has(skillName)
1125
1147
  : loadedFilesThisTurn.has(fileKey(skillName, unquoteArg(askedFile))));
1126
- const silent = searchCapped || skillReloaded || priorFailure !== undefined;
1148
+ const silent = searchCapped || skillReloaded || priorFailure !== undefined || priorSuccess;
1127
1149
  const display = silent
1128
1150
  ? undefined
1129
1151
  : labelOf(call.name, args, skillCall ? skillEmoji(skillName) : skillIcon);
@@ -1145,6 +1167,14 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1145
1167
  });
1146
1168
  execution = terminalCall(priorFailure.result, priorFailure.outcome, { executed: false });
1147
1169
  }
1170
+ else if (priorSuccess) {
1171
+ blockedRepeats++;
1172
+ hlog('repeat of a read that already answered; not run again', {
1173
+ tool: call.name,
1174
+ blockedRepeats
1175
+ });
1176
+ execution = terminalCall({ content: repeatedReadMessage(call.name) }, { kind: 'success' }, { executed: false });
1177
+ }
1148
1178
  else if (skillReloaded) {
1149
1179
  const loadedWhat = mainRequested ? skillName : `${skillName} ${unquoteArg(askedFile)}`;
1150
1180
  execution = terminalCall({
@@ -1196,18 +1226,27 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1196
1226
  const output = settled.result;
1197
1227
  const executionOutcome = settled.outcome;
1198
1228
  if (!priorFailure && settledFailure(output, executionOutcome)) {
1199
- failed.set(callKey(call.name, args), settled);
1229
+ failed.set(key, settled);
1200
1230
  }
1201
1231
  else if (execution.executed && callSucceeded(output, executionOutcome)) {
1202
1232
  progressed = true;
1203
1233
  answer ??= answerOf(output);
1204
1234
  }
1235
+ if (execution.executed && !stableRead && !skillCall) {
1236
+ answered.clear();
1237
+ }
1238
+ else if (stableRead && execution.executed && callSucceeded(output, executionOutcome)) {
1239
+ answered.add(key);
1240
+ }
1205
1241
  runOutcomes.record(output, executionOutcome);
1206
1242
  if (searchCapped) {
1207
1243
  cappedCalls++;
1208
1244
  hlog('web_search capped', { searches: webSearches });
1209
1245
  }
1210
- else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection && !priorFailure) {
1246
+ else if (tool?.name === WEB_SEARCH_TOOL_NAME &&
1247
+ !rejection &&
1248
+ !priorFailure &&
1249
+ !priorSuccess) {
1211
1250
  webSearches++;
1212
1251
  }
1213
1252
  if (call.name === SKILL_TOOL_NAME) {
@@ -511,6 +511,7 @@ export class RemoteHarness extends EventEmitter {
511
511
  }));
512
512
  return {
513
513
  name: skill.name,
514
+ displayName: skill.displayName || undefined,
514
515
  description: skill.description,
515
516
  tools: skill.tools ?? [],
516
517
  credentials: skill.credentials ?? [],
@@ -236,6 +236,7 @@ function toWireSkill(skill) {
236
236
  }));
237
237
  return {
238
238
  name: skill.name,
239
+ displayName: skill.displayName || undefined,
239
240
  description: skill.description,
240
241
  tools: skill.tools,
241
242
  credentials: skill.credentials,
@@ -57,6 +57,7 @@ export function createFileGlobTool({ realpath = defaultRealpath, deniedRoots } =
57
57
  required: ['pattern']
58
58
  },
59
59
  effect: () => 'read',
60
+ stableRead: true,
60
61
  autoApprove: true,
61
62
  rootsRequired: true,
62
63
  validate(rawArgs) {
@@ -99,6 +99,7 @@ export function createGrepTool({ realpath = defaultRealpath, deniedRoots } = {})
99
99
  required: ['queries']
100
100
  },
101
101
  effect: () => 'read',
102
+ stableRead: true,
102
103
  autoApprove: true,
103
104
  rootsRequired: true,
104
105
  validate(rawArgs) {
@@ -91,6 +91,7 @@ export function createReadFilesTool({ realpath = defaultRealpath, deniedRoots }
91
91
  required: ['files']
92
92
  },
93
93
  effect: () => 'read',
94
+ stableRead: true,
94
95
  autoApprove: true,
95
96
  rootsRequired: true,
96
97
  validate(rawArgs) {
@@ -185,12 +185,14 @@ export function createSkillTool({ skillsDir, overlayDir = resolveSkillsOverlayDi
185
185
  try {
186
186
  return await mainInstead(file, available);
187
187
  }
188
- catch { }
188
+ catch {
189
+ return { error: `skill not found: ${name}` };
190
+ }
189
191
  }
190
192
  const rest = available.length
191
- ? ` — files this skill has: ${available.join(', ')}`
192
- : '. This skill has no other files; you already have its full instructions — act now.';
193
- return { error: `file not found in skill ${name}: ${file}${rest}` };
193
+ ? `Its other files: ${available.join(', ')}.`
194
+ : 'It has no other files; you already have its full instructions, so act now.';
195
+ return { name, content: `There is no file ${file} in this skill. ${rest}` };
194
196
  }
195
197
  }
196
198
  };
@@ -9,6 +9,7 @@ import { type OperationSpec } from '../operations/manifest.ts';
9
9
  import type { CapabilityProfile } from '../../models/model-catalog.ts';
10
10
  export interface SkillCatalogEntry {
11
11
  name: string;
12
+ displayName?: string;
12
13
  description: string;
13
14
  aliases?: string[];
14
15
  preloadOnName?: boolean;
@@ -79,6 +79,7 @@ function mergeOverlay(base, overlay) {
79
79
  const { origin: _origin, mcpReads: _mcpReads, operations: _operations, ...rest } = overlay;
80
80
  return {
81
81
  ...rest,
82
+ displayName: base.displayName,
82
83
  credentials: base.credentials,
83
84
  allowList: base.allowList,
84
85
  platform: base.platform,
@@ -156,6 +157,7 @@ async function loadEntry(skillsDir, dir, origin) {
156
157
  const platform = platformsOf(meta.platform, origin);
157
158
  return {
158
159
  name: dir,
160
+ displayName: origin === 'overlay' ? undefined : meta.displayName || undefined,
159
161
  description: meta.description ?? '',
160
162
  aliases: meta.aliases,
161
163
  preloadOnName: meta.preloadOnName === false ? false : undefined,
@@ -1,6 +1,7 @@
1
1
  import type { InstallStep, SkillRequires, SkillSetup } from './manifest.ts';
2
2
  export interface SkillFrontMatter {
3
3
  name?: string;
4
+ displayName?: string;
4
5
  description?: string;
5
6
  aliases?: string[];
6
7
  preloadOnName?: boolean;
@@ -1,4 +1,5 @@
1
1
  import { parseManifestBlock } from './manifest-parse.js';
2
+ import { isMentionHandle } from './mention-name.js';
2
3
  const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
3
4
  const FIELD_RE = /^([A-Za-z_][\w-]*):(.*)$/;
4
5
  export function parseSkillFrontMatter(raw) {
@@ -54,6 +55,9 @@ function parseFields(block) {
54
55
  else if (key === 'name' || key === 'description') {
55
56
  meta[key] = stripQuotes(value);
56
57
  }
58
+ else if (key === 'display_name' && isMentionHandle(stripQuotes(value))) {
59
+ meta.displayName = stripQuotes(value);
60
+ }
57
61
  else if (key === 'preload_on_name') {
58
62
  meta.preloadOnName = stripQuotes(value).toLowerCase() !== 'false';
59
63
  }
@@ -1 +1,7 @@
1
1
  export declare function skillMentionName(text: string): string | null;
2
+ export declare function isMentionHandle(value: string): boolean;
3
+ export interface MentionableSkill {
4
+ readonly name: string;
5
+ readonly displayName?: string | undefined;
6
+ }
7
+ export declare function skillMentionHandle(skill: MentionableSkill): string;
@@ -2,3 +2,9 @@ const MENTION_PATTERN = /^@([a-z0-9][a-z0-9_-]*)(?=\s|$)/;
2
2
  export function skillMentionName(text) {
3
3
  return MENTION_PATTERN.exec(text.trimStart().toLowerCase())?.[1] ?? null;
4
4
  }
5
+ export function isMentionHandle(value) {
6
+ return skillMentionName(`@${value}`) === value.toLowerCase();
7
+ }
8
+ export function skillMentionHandle(skill) {
9
+ return (skill.displayName ?? skill.name).toLowerCase();
10
+ }
@@ -1,5 +1,5 @@
1
1
  import type { SkillCatalogEntry } from './catalog.ts';
2
- import { skillMentionName } from './mention-name.ts';
3
- export { skillMentionName };
2
+ import { skillMentionHandle, skillMentionName } from './mention-name.ts';
3
+ export { skillMentionHandle, skillMentionName };
4
4
  export declare function resolveSkillMention(text: string, catalog: readonly SkillCatalogEntry[]): SkillCatalogEntry | null;
5
5
  export declare function resolveImplicitSkill(text: string, activeSkills: readonly SkillCatalogEntry[]): SkillCatalogEntry | null;
@@ -1,12 +1,14 @@
1
1
  import { has } from 'asenion';
2
- import { skillMentionName } from './mention-name.js';
3
- export { skillMentionName };
2
+ import { skillMentionHandle, skillMentionName } from './mention-name.js';
3
+ export { skillMentionHandle, skillMentionName };
4
4
  export function resolveSkillMention(text, catalog) {
5
5
  const name = skillMentionName(text);
6
6
  if (!name) {
7
7
  return null;
8
8
  }
9
- return catalog.find((skill) => skill.name.toLowerCase() === name) ?? null;
9
+ return (catalog.find((skill) => skill.displayName?.toLowerCase() === name) ??
10
+ catalog.find((skill) => !skill.displayName && skill.name.toLowerCase() === name) ??
11
+ null);
10
12
  }
11
13
  const MIN_IMPLICIT_NAME_LENGTH = 4;
12
14
  function matchesName(text, candidate) {
@@ -16,7 +18,12 @@ function matchesName(text, candidate) {
16
18
  }
17
19
  return true;
18
20
  }
21
+ function withoutMention(text) {
22
+ const name = skillMentionName(text);
23
+ return name ? text.trimStart().slice(name.length + 1) : text;
24
+ }
19
25
  export function resolveImplicitSkill(text, activeSkills) {
26
+ const prose = withoutMention(text);
20
27
  let best = null;
21
28
  let bestLength = 0;
22
29
  for (const skill of activeSkills) {
@@ -26,7 +33,7 @@ export function resolveImplicitSkill(text, activeSkills) {
26
33
  if (candidate.length < MIN_IMPLICIT_NAME_LENGTH) {
27
34
  continue;
28
35
  }
29
- if (!matchesName(text, candidate)) {
36
+ if (!matchesName(prose, candidate)) {
30
37
  continue;
31
38
  }
32
39
  const weight = candidate.replace(/\+/g, '').length;
@@ -32,6 +32,7 @@ export function createWeatherLookupTool({ fetch = noRedirectFetch, resolveHost =
32
32
  required: ['location']
33
33
  },
34
34
  effect: () => 'read',
35
+ stableRead: true,
35
36
  resource: () => ({ kind: 'host', label: `${GEOCODER_HOST}, ${WEATHER_HOST}` }),
36
37
  execute: (args, ctx) => execute(args.location, ctx?.signal, { fetch, resolveHost, timeoutMs: geocodeTimeoutMs })
37
38
  };
@@ -43,6 +43,7 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
43
43
  required: ['url']
44
44
  },
45
45
  effect: () => 'read',
46
+ stableRead: true,
46
47
  resource: (args) => {
47
48
  try {
48
49
  return { kind: 'host', label: new URL(String(args.url)).hostname };
@@ -30,6 +30,7 @@ export function createWebSearchTool({ fetch } = {}) {
30
30
  required: ['query']
31
31
  },
32
32
  effect: () => 'read',
33
+ stableRead: true,
33
34
  async execute(args, ctx) {
34
35
  const query = args.query;
35
36
  if (typeof query !== 'string' || query.trim().length === 0) {
@@ -322,7 +322,8 @@ export function registerHarnessTypes(ns) {
322
322
  { name: 'surfaceWhenUnsupported', type: 'bool' },
323
323
  { name: 'minCapabilityProfile', type: 'string' },
324
324
  { name: 'emoji', type: 'string' },
325
- { name: 'setupV2', type: '@harness/skill-setup-v2' }
325
+ { name: 'setupV2', type: '@harness/skill-setup-v2' },
326
+ { name: 'displayName', type: 'string' }
326
327
  ]
327
328
  });
328
329
  ns.register({
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 20
2
+ // Schema Version: 21
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
5
  /* eslint-disable space-before-function-paren */
6
6
 
7
7
  import { c } from 'hyperschema/runtime'
8
8
 
9
- const VERSION = 20
9
+ const VERSION = 21
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -1502,7 +1502,8 @@ const encoding28 = {
1502
1502
  (m.surfaceWhenUnsupported ? 256 : 0) |
1503
1503
  (m.minCapabilityProfile ? 512 : 0) |
1504
1504
  (m.emoji ? 1024 : 0) |
1505
- (m.setupV2 ? 2048 : 0)
1505
+ (m.setupV2 ? 2048 : 0) |
1506
+ ((version >= 21 && m.displayName) ? 4096 : 0)
1506
1507
 
1507
1508
  c.string.preencode(state, m.name)
1508
1509
  c.string.preencode(state, m.description)
@@ -1519,6 +1520,7 @@ const encoding28 = {
1519
1520
  if (m.minCapabilityProfile) c.string.preencode(state, m.minCapabilityProfile)
1520
1521
  if (m.emoji) c.string.preencode(state, m.emoji)
1521
1522
  if (m.setupV2) encoding28_14.preencode(state, m.setupV2)
1523
+ if (version >= 21 && m.displayName) c.string.preencode(state, m.displayName)
1522
1524
  },
1523
1525
  encode(state, m) {
1524
1526
  const flags =
@@ -1533,7 +1535,8 @@ const encoding28 = {
1533
1535
  (m.surfaceWhenUnsupported ? 256 : 0) |
1534
1536
  (m.minCapabilityProfile ? 512 : 0) |
1535
1537
  (m.emoji ? 1024 : 0) |
1536
- (m.setupV2 ? 2048 : 0)
1538
+ (m.setupV2 ? 2048 : 0) |
1539
+ ((version >= 21 && m.displayName) ? 4096 : 0)
1537
1540
 
1538
1541
  c.string.encode(state, m.name)
1539
1542
  c.string.encode(state, m.description)
@@ -1550,6 +1553,7 @@ const encoding28 = {
1550
1553
  if (m.minCapabilityProfile) c.string.encode(state, m.minCapabilityProfile)
1551
1554
  if (m.emoji) c.string.encode(state, m.emoji)
1552
1555
  if (m.setupV2) encoding28_14.encode(state, m.setupV2)
1556
+ if (version >= 21 && m.displayName) c.string.encode(state, m.displayName)
1553
1557
  },
1554
1558
  decode(state) {
1555
1559
  const r0 = c.string.decode(state)
@@ -1571,7 +1575,8 @@ const encoding28 = {
1571
1575
  surfaceWhenUnsupported: (flags & 256) !== 0,
1572
1576
  minCapabilityProfile: (flags & 512) !== 0 ? c.string.decode(state) : null,
1573
1577
  emoji: (flags & 1024) !== 0 ? c.string.decode(state) : null,
1574
- setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null
1578
+ setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null,
1579
+ displayName: (version >= 21 && (flags & 4096) !== 0) ? c.string.decode(state) : null
1575
1580
  }
1576
1581
  }
1577
1582
  }
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 20
2
+ // Schema Version: 21
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
5
  /* eslint-disable space-before-function-paren */
6
6
 
7
7
  import { c } from 'hyperschema/runtime'
8
8
 
9
- const VERSION = 20
9
+ const VERSION = 21
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -1502,7 +1502,8 @@ const encoding28 = {
1502
1502
  (m.surfaceWhenUnsupported ? 256 : 0) |
1503
1503
  (m.minCapabilityProfile ? 512 : 0) |
1504
1504
  (m.emoji ? 1024 : 0) |
1505
- (m.setupV2 ? 2048 : 0)
1505
+ (m.setupV2 ? 2048 : 0) |
1506
+ ((version >= 21 && m.displayName) ? 4096 : 0)
1506
1507
 
1507
1508
  c.string.preencode(state, m.name)
1508
1509
  c.string.preencode(state, m.description)
@@ -1519,6 +1520,7 @@ const encoding28 = {
1519
1520
  if (m.minCapabilityProfile) c.string.preencode(state, m.minCapabilityProfile)
1520
1521
  if (m.emoji) c.string.preencode(state, m.emoji)
1521
1522
  if (m.setupV2) encoding28_14.preencode(state, m.setupV2)
1523
+ if (version >= 21 && m.displayName) c.string.preencode(state, m.displayName)
1522
1524
  },
1523
1525
  encode(state, m) {
1524
1526
  const flags =
@@ -1533,7 +1535,8 @@ const encoding28 = {
1533
1535
  (m.surfaceWhenUnsupported ? 256 : 0) |
1534
1536
  (m.minCapabilityProfile ? 512 : 0) |
1535
1537
  (m.emoji ? 1024 : 0) |
1536
- (m.setupV2 ? 2048 : 0)
1538
+ (m.setupV2 ? 2048 : 0) |
1539
+ ((version >= 21 && m.displayName) ? 4096 : 0)
1537
1540
 
1538
1541
  c.string.encode(state, m.name)
1539
1542
  c.string.encode(state, m.description)
@@ -1550,6 +1553,7 @@ const encoding28 = {
1550
1553
  if (m.minCapabilityProfile) c.string.encode(state, m.minCapabilityProfile)
1551
1554
  if (m.emoji) c.string.encode(state, m.emoji)
1552
1555
  if (m.setupV2) encoding28_14.encode(state, m.setupV2)
1556
+ if (version >= 21 && m.displayName) c.string.encode(state, m.displayName)
1553
1557
  },
1554
1558
  decode(state) {
1555
1559
  const r0 = c.string.decode(state)
@@ -1571,7 +1575,8 @@ const encoding28 = {
1571
1575
  surfaceWhenUnsupported: (flags & 256) !== 0,
1572
1576
  minCapabilityProfile: (flags & 512) !== 0 ? c.string.decode(state) : null,
1573
1577
  emoji: (flags & 1024) !== 0 ? c.string.decode(state) : null,
1574
- setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null
1578
+ setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null,
1579
+ displayName: (version >= 21 && (flags & 4096) !== 0) ? c.string.decode(state) : null
1575
1580
  }
1576
1581
  }
1577
1582
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 20,
2
+ "version": 21,
3
3
  "schema": [
4
4
  {
5
5
  "name": "empty",
@@ -1246,6 +1246,11 @@
1246
1246
  "name": "setupV2",
1247
1247
  "type": "@harness/skill-setup-v2",
1248
1248
  "version": 1
1249
+ },
1250
+ {
1251
+ "name": "displayName",
1252
+ "type": "string",
1253
+ "version": 21
1249
1254
  }
1250
1255
  ]
1251
1256
  },
@@ -1,5 +1,5 @@
1
1
  // This file is autogenerated by hyperschema-ts
2
- // Schema version: 20
2
+ // Schema version: 21
3
3
  // Do not edit manually.
4
4
 
5
5
  export interface HarnessEmpty {
@@ -235,6 +235,7 @@ export interface HarnessSkill {
235
235
  minCapabilityProfile?: string | null
236
236
  emoji?: string | null
237
237
  setupV2?: HarnessSkillSetupV2 | null
238
+ displayName?: string | null
238
239
  }
239
240
 
240
241
  export interface HarnessSkillRequest {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bare": ">=1.34.0"
@@ -747,7 +747,7 @@
747
747
  "@qvac/inference": "0.21.0",
748
748
  "@qvac/langdetect-text": "0.1.2",
749
749
  "@qvac/sdk": "0.21.0",
750
- "@qvac/skills": "0.1.14",
750
+ "@qvac/skills": "0.1.15",
751
751
  "@xmldom/xmldom": "^0.9.12",
752
752
  "asenion": "^0.1.0",
753
753
  "b4a": "^1.9.0",