@qvac/harness 0.8.6 → 0.8.7

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +1259 -507
  2. package/dist-lib/index.d.ts +11 -0
  3. package/dist-lib/index.js +102 -25
  4. package/dist-lib/lib/capabilities/completion.d.ts +4 -4
  5. package/dist-lib/lib/capabilities/completion.js +86 -57
  6. package/dist-lib/lib/capabilities/tool-selection.d.ts +1 -3
  7. package/dist-lib/lib/capabilities/tool-selection.js +2 -4
  8. package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
  9. package/dist-lib/lib/capabilities/tool-turns.js +0 -6
  10. package/dist-lib/lib/connect.js +2 -0
  11. package/dist-lib/lib/contract-types.d.ts +2 -0
  12. package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
  13. package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
  14. package/dist-lib/lib/models/context-choice.d.ts +15 -0
  15. package/dist-lib/lib/models/context-choice.js +55 -0
  16. package/dist-lib/lib/models/model-pool.d.ts +3 -1
  17. package/dist-lib/lib/models/model-pool.js +8 -11
  18. package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
  19. package/dist-lib/lib/models/sdk-faults.js +6 -0
  20. package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
  21. package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
  22. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
  23. package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
  24. package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
  25. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
  26. package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
  27. package/dist-lib/lib/serve.js +5 -1
  28. package/dist-lib/lib/spawn.js +4 -10
  29. package/dist-lib/lib/tools/exec.d.ts +4 -1
  30. package/dist-lib/lib/tools/exec.js +55 -11
  31. package/dist-lib/lib/tools/office/ops.js +1 -1
  32. package/dist-lib/lib/tools/office.js +29 -6
  33. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  34. package/dist-lib/lib/tools/skills/catalog.js +2 -0
  35. package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
  36. package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
  37. package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
  38. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
  39. package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
  40. package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
  41. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  42. package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
  43. package/dist-lib/lib/tools/skills/mention.d.ts +0 -1
  44. package/dist-lib/lib/tools/skills/mention.js +0 -3
  45. package/dist-lib/schema/harness.js +2 -1
  46. package/dist-lib/spec/hrpc/messages.js +10 -5
  47. package/dist-lib/spec/hyperschema/index.js +10 -5
  48. package/dist-lib/spec/hyperschema/schema.json +6 -1
  49. package/dist-lib/spec/hyperschema/types.d.ts +2 -1
  50. package/package.json +5 -7
  51. package/worker/harness-sidecar.mjs +2 -1
@@ -14,6 +14,7 @@ export interface ExecToolOptions {
14
14
  readonly pythonNetwork?: boolean;
15
15
  readonly credentialStore?: CredentialStoreFn;
16
16
  readonly skillSettings?: SkillSettingsFn;
17
+ readonly skillEnv?: SkillEnvFn;
17
18
  readonly realpath?: Realpath;
18
19
  readonly deniedRoots?: readonly string[];
19
20
  readonly skillsDir?: string;
@@ -23,4 +24,6 @@ export interface ScriptRun {
23
24
  trusted: boolean;
24
25
  }
25
26
  export declare function resolveScriptRun(rawArgs: Record<string, unknown>, skillsDir: string | undefined): ScriptRun | ExecError | null;
26
- export declare function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide, pythonNetwork, credentialStore, skillSettings, realpath, deniedRoots, skillsDir }?: ExecToolOptions): ToolDefinition;
27
+ export type SkillEnvFn = (skill: string) => Readonly<Record<string, string>> | undefined;
28
+ export declare function declaredEnv(declared: Readonly<Record<string, string>> | undefined, session: string | undefined): Record<string, string>;
29
+ export declare function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide, pythonNetwork, credentialStore, skillSettings, skillEnv, realpath, deniedRoots, skillsDir }?: ExecToolOptions): ToolDefinition;
@@ -6,7 +6,7 @@ import { namedHosts } from './exec/adapters/python/named-hosts.js';
6
6
  import { pythonReachesNetwork } from './exec/adapters/python/net-shim.js';
7
7
  import { createShellAdapter } from './exec/adapters/shell/shell-adapter.js';
8
8
  import { routeObsidianCommand } from './exec/obsidian/route.js';
9
- import { MAX_OUTPUT_CHARS_LIMIT } from './exec/runner.js';
9
+ import { MAX_OUTPUT_CHARS, MAX_OUTPUT_CHARS_LIMIT, TRUNCATED_OUTPUT_SUFFIX } from './exec/runner.js';
10
10
  import { bareBin, firstCommandToken, sanitizeCommand } from './exec/scope.js';
11
11
  import { FILES_NEED_PYTHON_ERROR, isPythonSourceShaped, PACKAGES_NEED_PYTHON_ERROR, pythonRedirectError, startsLikePython, videoToolError } from './exec/shell-guidance.js';
12
12
  import { shellArgv } from './exec/shell-argv.js';
@@ -47,6 +47,30 @@ function shellEffect(command, toolOps) {
47
47
  }
48
48
  return matched.every((op) => op.annotation(argv) === 'read') ? 'read' : 'write';
49
49
  }
50
+ function shellOutputCap(command, toolOps) {
51
+ const { ops, argv } = resolveShellOps(command, toolOps);
52
+ const caps = ops.map((op) => op.outputCap(argv)).filter((cap) => cap !== undefined);
53
+ return caps.length > 0 ? Math.min(...caps) : undefined;
54
+ }
55
+ function noteCeiling(result, ceiling) {
56
+ if (!result || typeof result !== 'object')
57
+ return result;
58
+ const out = { ...result };
59
+ for (const stream of ['stdout', 'stderr']) {
60
+ const text = out[stream];
61
+ if (typeof text === 'string' && text.endsWith(TRUNCATED_OUTPUT_SUFFIX)) {
62
+ out[stream] =
63
+ text.slice(0, -TRUNCATED_OUTPUT_SUFFIX.length) +
64
+ `… [truncated at this skill's ${ceiling}-character limit for this command; a larger maxOutputChars does not apply]`;
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ function cappedOutput(requested, ceiling) {
70
+ if (ceiling === undefined)
71
+ return requested;
72
+ return Math.min(requested ?? MAX_OUTPUT_CHARS, ceiling);
73
+ }
50
74
  function resolveExecution({ adapters, fallback }, args, ctx) {
51
75
  for (const adapter of adapters) {
52
76
  const plan = adapter.resolve(args, ctx);
@@ -289,18 +313,33 @@ function checkExecutionScope(args, { toolOps, disablePyodide, sandbox, network }
289
313
  }
290
314
  return checkShellScope(command, toolOps);
291
315
  }
292
- async function skillsEnv(skillSettings, ctx) {
316
+ async function skillsEnv(skillSettings, skillEnv, ctx) {
293
317
  const skills = ctx?.skills ?? [];
294
318
  if (skills.length === 0) {
295
319
  return undefined;
296
320
  }
297
321
  const env = {};
298
322
  for (const skill of skills) {
299
- Object.assign(env, settingsEnv(skill, await skillSettings(skill, ctx?.agentId)));
323
+ if (skillSettings) {
324
+ Object.assign(env, settingsEnv(skill, await skillSettings(skill, ctx?.agentId)));
325
+ }
326
+ Object.assign(env, declaredEnv(skillEnv?.(skill), ctx?.session));
300
327
  }
301
328
  return Object.keys(env).length > 0 ? env : undefined;
302
329
  }
303
- export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide = false, pythonNetwork = !disablePyodide && pythonReachesNetwork(), credentialStore, skillSettings, realpath = defaultRealpath, deniedRoots, skillsDir } = {}) {
330
+ export function declaredEnv(declared, session) {
331
+ const env = {};
332
+ for (const [name, value] of Object.entries(declared ?? {})) {
333
+ if (!value.includes('${session}')) {
334
+ env[name] = value;
335
+ }
336
+ else if (session) {
337
+ env[name] = value.replaceAll('${session}', session);
338
+ }
339
+ }
340
+ return env;
341
+ }
342
+ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide = false, pythonNetwork = !disablePyodide && pythonReachesNetwork(), credentialStore, skillSettings, skillEnv, realpath = defaultRealpath, deniedRoots, skillsDir } = {}) {
304
343
  const adapters = [createPythonAdapter(disablePyodide, loadPyodide, pythonNetwork)];
305
344
  const fallback = createShellAdapter({ spawn, killProcess, loadRuntime: bareRuntime });
306
345
  return {
@@ -349,7 +388,7 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
349
388
  },
350
389
  maxOutputChars: {
351
390
  type: 'number',
352
- description: 'Optional stdout/stderr cap in characters (default 8192, max 65536). Raise when reading long content, e.g. a full note.'
391
+ description: 'Optional stdout/stderr cap in characters (default 8192, max 65536). Raise when reading long content, e.g. a full note. A skill may cap one of its commands lower; the truncation note says so, and raising this does not lift that cap.'
353
392
  },
354
393
  script: {
355
394
  type: 'string',
@@ -564,10 +603,12 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
564
603
  timeout = Math.min(timeout, remaining);
565
604
  }
566
605
  }
567
- const maxOutputChars = parsePositiveInt(rawArgs.maxOutputChars, 'maxOutputChars', MAX_OUTPUT_CHARS_LIMIT);
568
- if (maxOutputChars && typeof maxOutputChars === 'object') {
569
- return maxOutputChars;
606
+ const requestedCap = parsePositiveInt(rawArgs.maxOutputChars, 'maxOutputChars', MAX_OUTPUT_CHARS_LIMIT);
607
+ if (requestedCap && typeof requestedCap === 'object') {
608
+ return requestedCap;
570
609
  }
610
+ const ceiling = shellOutputCap(command, ctx?.toolOps);
611
+ const maxOutputChars = cappedOutput(requestedCap, ceiling);
571
612
  if (!isPythonLanguage(rawArgs.language)) {
572
613
  const obsidian = await routeObsidianCommand(command, ctx, {
573
614
  credentialStore,
@@ -581,7 +622,7 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
581
622
  return obsidian;
582
623
  }
583
624
  }
584
- const skillEnv = skillSettings ? await skillsEnv(skillSettings, ctx) : undefined;
625
+ const childEnv = await skillsEnv(skillSettings, skillEnv, ctx);
585
626
  try {
586
627
  const execution = resolveExecution({ adapters, fallback }, rawArgs, {
587
628
  attach: ctx?.attach,
@@ -593,13 +634,16 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
593
634
  approval: ctx?.approval,
594
635
  confirmConnect: ctx?.askUser ? connectAsker(ctx.askUser) : undefined
595
636
  });
596
- return await execution.run({
637
+ const result = await execution.run({
597
638
  cwd,
598
639
  timeoutMs: timeout,
599
640
  maxOutputChars,
600
641
  signal: ctx?.signal,
601
- env: skillEnv
642
+ env: childEnv
602
643
  });
644
+ return ceiling !== undefined && maxOutputChars === ceiling
645
+ ? noteCeiling(result, ceiling)
646
+ : result;
603
647
  }
604
648
  catch (err) {
605
649
  const error = err instanceof Error ? err : new Error(String(err));
@@ -7,7 +7,7 @@ export const PPTX_OPS = {
7
7
  setText: { slots: [['slide', 'shape'], 'text'] },
8
8
  setCellText: { slots: [['slide', 'shape', 'row', 'column'], 'text'] },
9
9
  setStyle: { slots: [['slide', 'shape'], 'style', ['paragraph']] },
10
- addSlide: { slots: [['like', 'layout', 'at', 'title', 'body']] },
10
+ addSlide: { slots: [['like', 'layout', 'at', 'title', 'body', 'image']] },
11
11
  addPicture: { slots: ['slide', 'image', [...BOX, 'name', 'description']], inches: true },
12
12
  addTextBox: { slots: ['slide', [...BOX, 'text', 'style', 'fill', 'name']], inches: true },
13
13
  addTable: { slots: ['slide', [...BOX, 'rows', 'header', 'name']], inches: true },
@@ -1,12 +1,13 @@
1
1
  import b4a from 'b4a';
2
2
  import { errorMessage, isRecord } from '../guards.js';
3
- import { docx, pptx, units, xlsx } from '../office/engine/index.js';
3
+ import { DocsError, docx, pptx, units, xlsx } from '../office/engine/index.js';
4
4
  import { stageInputs } from './exec/adapters/python/attachments.js';
5
5
  import { checkOutputBytes, RUN_OUTPUT_BUDGET_BYTES } from './exec/adapters/python/output-policy.js';
6
6
  import { DOCX_OPS, opArguments, opKeys, PPTX_OPS, withAliases, XLSX_OPS } from './office/ops.js';
7
7
  const READ_CELL_LIMIT = 10_000;
8
8
  const attachedThisTurn = new WeakMap();
9
9
  const FIRST_SHEET = /^sheet ?1$/i;
10
+ const NEW_DECK_HINT = '; a new deck has only the slides its addSlide ops make, slides count from 0, and an op on a slide goes after the addSlide that makes it';
10
11
  const SHAPE_ROLES = new Set(['title', 'subtitle', 'body']);
11
12
  const HOISTED = ['attachmentId', 'output'];
12
13
  const WHERE_IDS_ARE = 'copy the id from the [Attached file "…" — attachmentId: …] line or an earlier tool result';
@@ -25,6 +26,9 @@ export function shapeFor(deck, op) {
25
26
  const found = deck.read()[slide]?.shapes.find((entry) => entry.name.toLowerCase() === wanted);
26
27
  return found && found.id !== null ? { ...op, shape: found.id } : op;
27
28
  }
29
+ function newDeckHint(err, created) {
30
+ return created && err instanceof DocsError && err.code === 'SLIDE_NOT_FOUND' ? NEW_DECK_HINT : '';
31
+ }
28
32
  export function sheetFor(book, op, created) {
29
33
  if (typeof op.sheet !== 'string')
30
34
  return op;
@@ -70,7 +74,8 @@ const PPTX_FORMAT = {
70
74
  create: (options) => pptx.create(options),
71
75
  read: (deck) => deck.read(),
72
76
  count: (deck) => ({ slides: deck.slideCount }),
73
- prepare: (deck, op) => shapeFor(deck, op)
77
+ prepare: (deck, op) => shapeFor(deck, op),
78
+ hint: newDeckHint
74
79
  };
75
80
  const DOCX_FORMAT = {
76
81
  name: 'docx',
@@ -141,7 +146,11 @@ function parseOp(format, op, i) {
141
146
  return `ops[${i}] sheets lists the sheets and takes no arguments; to add a sheet use { op: "addSheet", name }`;
142
147
  }
143
148
  const takes = keys.length ? `it takes ${keys.join(', ')}` : 'it takes no arguments';
144
- return `ops[${i}] ${op.op} does not take ${unknown.join(', ')}; ${takes}`;
149
+ const nested = unknown.filter((key) => format.ops[key]);
150
+ const apart = nested.length
151
+ ? `; ${nested.map((name) => `{ "op": "${name}", ... }`).join(' and ')} goes in ops as its own entry, next to this one, not inside it`
152
+ : '';
153
+ return `ops[${i}] ${op.op} does not take ${unknown.join(', ')}; ${takes}${apart}`;
145
154
  }
146
155
  function parseOps(format, ops) {
147
156
  if (!Array.isArray(ops)) {
@@ -252,8 +261,11 @@ async function imageInput(op, i, ctx) {
252
261
  }
253
262
  return { attachmentId: id ? await imageId(ctx, id) : undefined, path: `image-${i}.png` };
254
263
  }
264
+ function takesImage(op) {
265
+ return op.spec.slots.includes('image') || op.args.image !== undefined;
266
+ }
255
267
  async function stagedImages(request, ctx) {
256
- const wanted = request.ops.flatMap((op, i) => (opKeys(op.spec).includes('image') ? [i] : []));
268
+ const wanted = request.ops.flatMap((op, i) => (takesImage(op) ? [i] : []));
257
269
  const images = new Map();
258
270
  if (!wanted.length) {
259
271
  return images;
@@ -358,7 +370,8 @@ function runOps({ format, document, request, images }) {
358
370
  results.push(runOp(document, op, input));
359
371
  }
360
372
  catch (err) {
361
- return { error: `ops[${i}] ${op.name}: ${errorMessage(err)}` };
373
+ const hint = format.hint?.(err, created) ?? '';
374
+ return { error: `ops[${i}] ${op.name}: ${errorMessage(err)}${hint}` };
362
375
  }
363
376
  }
364
377
  return results;
@@ -452,6 +465,7 @@ async function execute(format, args, ctx) {
452
465
  const renamed = output !== request.output
453
466
  ? `Saved as ${output}, so the attached ${fileName} keeps its own name. `
454
467
  : '';
468
+ const unplaced = !!ctx.turnImages?.length && !images.size;
455
469
  return {
456
470
  results,
457
471
  attachments: [attachment],
@@ -459,9 +473,18 @@ async function execute(format, args, ctx) {
459
473
  reply: `${attachment.fileName} — ${countLine(counts)}`,
460
474
  next: renamed +
461
475
  `Done: ${attachment.fileName} ${repeated ? 'was already' : 'is'} attached to the chat and shown to the user. ` +
462
- `Do not call ${format.name} again for it. Your answer is the reply value alone, word for word.`
476
+ afterSave(format, attachment.attachmentId, unplaced)
463
477
  };
464
478
  }
479
+ function afterSave(format, attachmentId, unplaced) {
480
+ const reply = 'Your answer is the reply value alone, word for word.';
481
+ if (!unplaced) {
482
+ return `Do not call ${format.name} again for it. ${reply}`;
483
+ }
484
+ return (`The image the user attached to their message is not in it. If they asked for that image in the ${format.noun}, ` +
485
+ `call ${format.name} once more with attachmentId "${attachmentId}" and one op that adds it with "image": {}. ` +
486
+ `Otherwise: ${reply}`);
487
+ }
465
488
  function createOfficeTool(format) {
466
489
  return {
467
490
  name: format.name,
@@ -20,6 +20,7 @@ export interface SkillCatalogEntry {
20
20
  requires?: SkillRequires;
21
21
  install?: InstallStep[];
22
22
  setup?: SkillSetup;
23
+ env?: Readonly<Record<string, string>>;
23
24
  cliValidator?: CliValidator;
24
25
  stub?: boolean;
25
26
  minCapabilityProfile?: string;
@@ -82,6 +82,7 @@ function mergeOverlay(base, overlay) {
82
82
  credentials: base.credentials,
83
83
  allowList: base.allowList,
84
84
  platform: base.platform,
85
+ env: base.env,
85
86
  operations: base.operations,
86
87
  mcpReads: base.mcpReads
87
88
  };
@@ -166,6 +167,7 @@ async function loadEntry(skillsDir, dir, origin) {
166
167
  requires: meta.requires,
167
168
  install: meta.install,
168
169
  setup: meta.setup,
170
+ env: origin === 'overlay' ? undefined : meta.env,
169
171
  cliValidator: cli ? buildCliValidator(cli) : undefined,
170
172
  stub: meta.stub,
171
173
  minCapabilityProfile: meta.minCapabilityProfile || undefined,
@@ -1,3 +1,4 @@
1
1
  import type { JsonSchema } from './cli-schema-types.ts';
2
2
  export declare function buildSchemaAnnotation(schema: JsonSchema): (argv: readonly string[]) => 'read' | 'write';
3
+ export declare function buildSchemaOutputCap(schema: JsonSchema): (argv: readonly string[]) => number | undefined;
3
4
  export declare function buildSchemaCheck(schema: JsonSchema, resourcePaths: readonly string[] | undefined): (argv: readonly string[]) => string | null;
@@ -13,6 +13,9 @@ export function buildSchemaAnnotation(schema) {
13
13
  return walkCommands(argv, schema).node['x-effect'] === 'read' ? 'read' : 'write';
14
14
  };
15
15
  }
16
+ export function buildSchemaOutputCap(schema) {
17
+ return (argv) => walkCommands(argv, schema).node['x-max-output-chars'];
18
+ }
16
19
  function basename(p) {
17
20
  return p.slice(Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\')) + 1);
18
21
  }
@@ -28,6 +28,9 @@ export async function resolveCliSchema(skillDir, bin, readFile = (p) => fs.promi
28
28
  if (!resourceEffectsValid(schema)) {
29
29
  return { bin, schemaFile: CLI_SCHEMA_FILENAME };
30
30
  }
31
+ if (!outputCapsValid(schema)) {
32
+ return { bin, schemaFile: CLI_SCHEMA_FILENAME };
33
+ }
31
34
  const resourcePaths = resolveResourcePaths(skillDir, schema);
32
35
  return {
33
36
  bin,
@@ -63,6 +66,13 @@ function resourceEffectsValid(schema) {
63
66
  const files = new Set(schema['x-resource-files'] ?? []);
64
67
  return Object.entries(effects).every(([file, effect]) => effect === 'read' && files.has(file));
65
68
  }
69
+ function outputCapsValid(schema) {
70
+ const cap = schema['x-max-output-chars'];
71
+ if (cap !== undefined && !(Number.isInteger(cap) && cap > 0)) {
72
+ return false;
73
+ }
74
+ return Object.values(schema.properties ?? {}).every(outputCapsValid);
75
+ }
66
76
  function resolveResourcePaths(skillDir, schema) {
67
77
  const resources = schema['x-resource-files'] ?? [];
68
78
  return resources
@@ -13,6 +13,7 @@ export interface JsonSchema {
13
13
  readonly 'x-resource-files'?: readonly string[];
14
14
  readonly 'x-resource-effects'?: Readonly<Record<string, string>>;
15
15
  readonly 'x-effect'?: string;
16
+ readonly 'x-max-output-chars'?: number;
16
17
  readonly [key: string]: unknown;
17
18
  }
18
19
  export interface SkillCli {
@@ -27,4 +28,5 @@ export interface CliValidator {
27
28
  readonly check: (argv: readonly string[]) => string | null;
28
29
  readonly annotation: (argv: readonly string[]) => 'read' | 'write';
29
30
  readonly effect: (argv: readonly string[]) => 'read' | 'write';
31
+ readonly outputCap: (argv: readonly string[]) => number | undefined;
30
32
  }
@@ -1,7 +1,7 @@
1
1
  export { CLI_SCHEMA_FILENAME } from './cli-schema-types.js';
2
2
  export { argvToObject } from './cli-argv.js';
3
3
  export { loadSkillCli, resolveCliSchema, execBinFromTools } from './cli-schema-load.js';
4
- import { buildSchemaAnnotation, buildSchemaCheck } from './cli-schema-check.js';
4
+ import { buildSchemaAnnotation, buildSchemaCheck, buildSchemaOutputCap } from './cli-schema-check.js';
5
5
  export function buildCliValidator(def) {
6
6
  const bin = def.bin.toLowerCase();
7
7
  if (!def.schema) {
@@ -9,16 +9,19 @@ export function buildCliValidator(def) {
9
9
  bin,
10
10
  check: () => 'skill cli schema is missing',
11
11
  annotation: () => 'write',
12
- effect: () => 'write'
12
+ effect: () => 'write',
13
+ outputCap: () => undefined
13
14
  };
14
15
  }
15
16
  const check = buildSchemaCheck(def.schema, def.resourcePaths);
16
17
  const annotation = buildSchemaAnnotation(def.schema);
18
+ const outputCap = buildSchemaOutputCap(def.schema);
17
19
  return {
18
20
  bin,
19
21
  check,
20
22
  annotation,
21
- effect: (argv) => (check(argv) === null ? annotation(argv) : 'write')
23
+ effect: (argv) => (check(argv) === null ? annotation(argv) : 'write'),
24
+ outputCap: (argv) => (check(argv) === null ? outputCap(argv) : undefined)
22
25
  };
23
26
  }
24
27
  export function checkCliOps(argv, compiled) {
@@ -12,6 +12,7 @@ export interface SkillFrontMatter {
12
12
  requires?: SkillRequires;
13
13
  install?: InstallStep[];
14
14
  setup?: SkillSetup;
15
+ env?: Readonly<Record<string, string>>;
15
16
  stub?: boolean;
16
17
  minCapabilityProfile?: string;
17
18
  emoji?: string;
@@ -3,6 +3,7 @@ export interface ParsedManifest {
3
3
  requires?: SkillRequires;
4
4
  install?: InstallStep[];
5
5
  setup?: SkillSetup;
6
+ env?: Readonly<Record<string, string>>;
6
7
  stub?: boolean;
7
8
  minCapabilityProfile?: string;
8
9
  emoji?: string;
@@ -12,11 +12,23 @@ export function parseManifestBlock(block, key = 'metadata') {
12
12
  requires,
13
13
  install,
14
14
  setup,
15
+ env: readEnv(oc.env),
15
16
  stub: typeof oc.stub === 'boolean' ? oc.stub : undefined,
16
17
  minCapabilityProfile: typeof oc.minCapabilityProfile === 'string' ? oc.minCapabilityProfile : undefined,
17
18
  emoji: typeof oc.emoji === 'string' ? oc.emoji : undefined
18
19
  };
19
20
  }
21
+ function readEnv(v) {
22
+ if (!isObj(v))
23
+ return undefined;
24
+ const env = {};
25
+ for (const [name, value] of Object.entries(v)) {
26
+ if (typeof value === 'string' && /^[A-Z_][A-Z0-9_]*$/.test(name)) {
27
+ env[name] = value;
28
+ }
29
+ }
30
+ return Object.keys(env).length > 0 ? env : undefined;
31
+ }
20
32
  function extractJsonBlock(block, key) {
21
33
  const at = block.indexOf(`${key}:`);
22
34
  if (at === -1) {
@@ -3,4 +3,3 @@ import { skillMentionName } from './mention-name.ts';
3
3
  export { 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;
6
- export declare function skillPreloadMessage(content: string, name: string, body: string): string;
@@ -38,6 +38,3 @@ export function resolveImplicitSkill(text, activeSkills) {
38
38
  }
39
39
  return best;
40
40
  }
41
- export function skillPreloadMessage(content, name, body) {
42
- return `[Skill invoked by user: ${name}]\n${body}\n\nThe user's message:\n${content}`;
43
- }
@@ -113,7 +113,8 @@ export function registerHarnessTypes(ns) {
113
113
  { name: 'captureInferenceCalls', type: 'bool' },
114
114
  { name: 'capability', type: 'string' },
115
115
  { name: 'arguments', type: 'json' },
116
- { name: 'enhance', type: 'string' }
116
+ { name: 'enhance', type: 'string' },
117
+ { name: 'session', type: 'string' }
117
118
  ]
118
119
  });
119
120
  ns.register({
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 19
2
+ // Schema Version: 20
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 = 19
9
+ const VERSION = 20
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -491,7 +491,8 @@ const encoding11 = {
491
491
  ((version >= 9 && m.captureInferenceCalls) ? 33554432 : 0) |
492
492
  ((version >= 11 && m.capability) ? 67108864 : 0) |
493
493
  ((version >= 11 && m.arguments) ? 134217728 : 0) |
494
- ((version >= 12 && m.enhance) ? 268435456 : 0)
494
+ ((version >= 12 && m.enhance) ? 268435456 : 0) |
495
+ ((version >= 20 && m.session) ? 536870912 : 0)
495
496
 
496
497
  encoding10.preencode(state, m.type)
497
498
  c.uint.preencode(state, flags)
@@ -522,6 +523,7 @@ const encoding11 = {
522
523
  if (version >= 11 && m.capability) c.string.preencode(state, m.capability)
523
524
  if (version >= 11 && m.arguments) c.json.preencode(state, m.arguments)
524
525
  if (version >= 12 && m.enhance) c.string.preencode(state, m.enhance)
526
+ if (version >= 20 && m.session) c.string.preencode(state, m.session)
525
527
  },
526
528
  encode(state, m) {
527
529
  const flags =
@@ -553,7 +555,8 @@ const encoding11 = {
553
555
  ((version >= 9 && m.captureInferenceCalls) ? 33554432 : 0) |
554
556
  ((version >= 11 && m.capability) ? 67108864 : 0) |
555
557
  ((version >= 11 && m.arguments) ? 134217728 : 0) |
556
- ((version >= 12 && m.enhance) ? 268435456 : 0)
558
+ ((version >= 12 && m.enhance) ? 268435456 : 0) |
559
+ ((version >= 20 && m.session) ? 536870912 : 0)
557
560
 
558
561
  encoding10.encode(state, m.type)
559
562
  c.uint.encode(state, flags)
@@ -584,6 +587,7 @@ const encoding11 = {
584
587
  if (version >= 11 && m.capability) c.string.encode(state, m.capability)
585
588
  if (version >= 11 && m.arguments) c.json.encode(state, m.arguments)
586
589
  if (version >= 12 && m.enhance) c.string.encode(state, m.enhance)
590
+ if (version >= 20 && m.session) c.string.encode(state, m.session)
587
591
  },
588
592
  decode(state) {
589
593
  const r0 = encoding10.decode(state)
@@ -619,7 +623,8 @@ const encoding11 = {
619
623
  captureInferenceCalls: (version >= 9 && (flags & 33554432) !== 0),
620
624
  capability: (version >= 11 && (flags & 67108864) !== 0) ? c.string.decode(state) : null,
621
625
  arguments: (version >= 11 && (flags & 134217728) !== 0) ? c.json.decode(state) : null,
622
- enhance: (version >= 12 && (flags & 268435456) !== 0) ? c.string.decode(state) : null
626
+ enhance: (version >= 12 && (flags & 268435456) !== 0) ? c.string.decode(state) : null,
627
+ session: (version >= 20 && (flags & 536870912) !== 0) ? c.string.decode(state) : null
623
628
  }
624
629
  }
625
630
  }
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 19
2
+ // Schema Version: 20
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 = 19
9
+ const VERSION = 20
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -491,7 +491,8 @@ const encoding11 = {
491
491
  ((version >= 9 && m.captureInferenceCalls) ? 33554432 : 0) |
492
492
  ((version >= 11 && m.capability) ? 67108864 : 0) |
493
493
  ((version >= 11 && m.arguments) ? 134217728 : 0) |
494
- ((version >= 12 && m.enhance) ? 268435456 : 0)
494
+ ((version >= 12 && m.enhance) ? 268435456 : 0) |
495
+ ((version >= 20 && m.session) ? 536870912 : 0)
495
496
 
496
497
  encoding10.preencode(state, m.type)
497
498
  c.uint.preencode(state, flags)
@@ -522,6 +523,7 @@ const encoding11 = {
522
523
  if (version >= 11 && m.capability) c.string.preencode(state, m.capability)
523
524
  if (version >= 11 && m.arguments) c.json.preencode(state, m.arguments)
524
525
  if (version >= 12 && m.enhance) c.string.preencode(state, m.enhance)
526
+ if (version >= 20 && m.session) c.string.preencode(state, m.session)
525
527
  },
526
528
  encode(state, m) {
527
529
  const flags =
@@ -553,7 +555,8 @@ const encoding11 = {
553
555
  ((version >= 9 && m.captureInferenceCalls) ? 33554432 : 0) |
554
556
  ((version >= 11 && m.capability) ? 67108864 : 0) |
555
557
  ((version >= 11 && m.arguments) ? 134217728 : 0) |
556
- ((version >= 12 && m.enhance) ? 268435456 : 0)
558
+ ((version >= 12 && m.enhance) ? 268435456 : 0) |
559
+ ((version >= 20 && m.session) ? 536870912 : 0)
557
560
 
558
561
  encoding10.encode(state, m.type)
559
562
  c.uint.encode(state, flags)
@@ -584,6 +587,7 @@ const encoding11 = {
584
587
  if (version >= 11 && m.capability) c.string.encode(state, m.capability)
585
588
  if (version >= 11 && m.arguments) c.json.encode(state, m.arguments)
586
589
  if (version >= 12 && m.enhance) c.string.encode(state, m.enhance)
590
+ if (version >= 20 && m.session) c.string.encode(state, m.session)
587
591
  },
588
592
  decode(state) {
589
593
  const r0 = encoding10.decode(state)
@@ -619,7 +623,8 @@ const encoding11 = {
619
623
  captureInferenceCalls: (version >= 9 && (flags & 33554432) !== 0),
620
624
  capability: (version >= 11 && (flags & 67108864) !== 0) ? c.string.decode(state) : null,
621
625
  arguments: (version >= 11 && (flags & 134217728) !== 0) ? c.json.decode(state) : null,
622
- enhance: (version >= 12 && (flags & 268435456) !== 0) ? c.string.decode(state) : null
626
+ enhance: (version >= 12 && (flags & 268435456) !== 0) ? c.string.decode(state) : null,
627
+ session: (version >= 20 && (flags & 536870912) !== 0) ? c.string.decode(state) : null
623
628
  }
624
629
  }
625
630
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 19,
2
+ "version": 20,
3
3
  "schema": [
4
4
  {
5
5
  "name": "empty",
@@ -475,6 +475,11 @@
475
475
  "name": "enhance",
476
476
  "type": "string",
477
477
  "version": 12
478
+ },
479
+ {
480
+ "name": "session",
481
+ "type": "string",
482
+ "version": 20
478
483
  }
479
484
  ]
480
485
  },
@@ -1,5 +1,5 @@
1
1
  // This file is autogenerated by hyperschema-ts
2
- // Schema version: 19
2
+ // Schema version: 20
3
3
  // Do not edit manually.
4
4
 
5
5
  export interface HarnessEmpty {
@@ -91,6 +91,7 @@ export interface HarnessCompletionFrame {
91
91
  capability?: string | null
92
92
  arguments?: unknown | null
93
93
  enhance?: string | null
94
+ session?: string | null
94
95
  }
95
96
 
96
97
  export type HarnessCompletionEventType = "content" | "thinking" | "tool-call" | "tool-result" | "approval-ask" | "metrics" | "error" | "aborted" | "attach-ask" | "fetch-ask" | "progress" | "rag-ask" | "question-ask" | "done" | "tool-activity" | "drive-ask" | "inference-call" | "inference-sent"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
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.12",
750
+ "@qvac/skills": "0.1.14",
751
751
  "@xmldom/xmldom": "^0.9.12",
752
752
  "asenion": "^0.1.0",
753
753
  "b4a": "^1.9.0",
@@ -773,7 +773,7 @@
773
773
  "bare-process": "4.4.1",
774
774
  "bare-sidecar": "0.5.7",
775
775
  "bare-storage": "^1.1.0",
776
- "bare-stow": "^0.1.4",
776
+ "bare-stow": "^0.1.7",
777
777
  "bare-stream": "^2.13.3",
778
778
  "bare-subprocess": "^6.1.0",
779
779
  "bare-supervisor": "^0.1.1",
@@ -809,8 +809,8 @@
809
809
  "holepunch-types": "^0.2.1",
810
810
  "hyperdrive": "^13.3.3",
811
811
  "jsdom": "~30.0.1",
812
- "lunte": "^1.8.4",
813
- "lunte-plugin-qvac": "^0.1.0",
812
+ "lunte": "^1.8.6",
813
+ "lunte-plugin-qvac": "^0.2.0",
814
814
  "mermaid": "~11.16.1",
815
815
  "prettier": "^3.6.2",
816
816
  "prettier-config-holepunch": "^2.0.0",
@@ -819,8 +819,6 @@
819
819
  "patchedDependencies": {
820
820
  "pyodide@314.0.2": "patches/pyodide@314.0.2.patch",
821
821
  "localwatch@1.4.3": "patches/localwatch@1.4.3.patch",
822
- "hypercore-blob-server@1.15.0": "patches/hypercore-blob-server@1.15.0.patch",
823
- "bare-stow@0.1.5": "patches/bare-stow@0.1.5.patch",
824
822
  "@qvac/inference@0.21.0": "patches/@qvac%2Finference@0.21.0.patch"
825
823
  },
826
824
  "repository": {
@@ -124,7 +124,8 @@ export default async function start(ipc, ready) {
124
124
  ...credentialed,
125
125
  deniedRoots,
126
126
  skillsDir,
127
- skillSettings: (skill, agentId) => harness?.skillSettings(skill, agentId) ?? {}
127
+ skillSettings: (skill, agentId) => harness?.skillSettings(skill, agentId) ?? {},
128
+ skillEnv: (skill) => skillCatalog?.find((entry) => entry.name === skill)?.env
128
129
  }),
129
130
  mcpCallTool,
130
131
  createWebSearchTool(),