@qvac/harness 0.2.6 → 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.
Files changed (174) hide show
  1. package/.claude/skills/gh-stack/SKILL.md +24 -24
  2. package/.claude/skills/gh-stack/references/commands.md +2 -2
  3. package/.claude/skills/js-house-style/SKILL.md +89 -68
  4. package/.claude/skills/kvcache/SKILL.md +7 -7
  5. package/.claude/skills/pr-create/SKILL.md +58 -15
  6. package/.claude/skills/pr-review/SKILL.md +307 -41
  7. package/CLAUDE.md +3 -0
  8. package/README.md +26 -4
  9. package/dist/harness.bundle +12 -10
  10. package/dist-lib/index.js +58 -58
  11. package/dist-lib/lib/audio-transcode-bare.js +2 -2
  12. package/dist-lib/lib/capabilities/completion.js +30 -30
  13. package/dist-lib/lib/capabilities/download.js +2 -2
  14. package/dist-lib/lib/capabilities/speech-text.js +1 -1
  15. package/dist-lib/lib/capabilities/tool-prompt-log.js +1 -1
  16. package/dist-lib/lib/capabilities/tool-selection.js +2 -2
  17. package/dist-lib/lib/capabilities/tool-turns.js +1 -1
  18. package/dist-lib/lib/capabilities/usage-breakdown.js +1 -1
  19. package/dist-lib/lib/capabilities/voice.js +3 -3
  20. package/dist-lib/lib/connect.js +11 -11
  21. package/dist-lib/lib/context-window.js +1 -1
  22. package/dist-lib/lib/docx/index.d.ts +5 -0
  23. package/dist-lib/lib/docx/index.js +49 -0
  24. package/dist-lib/lib/hardware/capability-probe.js +3 -3
  25. package/dist-lib/lib/hardware/gpu-probe-addon.js +2 -2
  26. package/dist-lib/lib/hardware/gpu-probe-entry.js +1 -1
  27. package/dist-lib/lib/helpers/markup-stream-filter.js +1 -1
  28. package/dist-lib/lib/helpers/run-outcome.js +1 -1
  29. package/dist-lib/lib/helpers/spawn-path.d.ts +1 -1
  30. package/dist-lib/lib/helpers/thinking.js +2 -2
  31. package/dist-lib/lib/helpers/tool-intent.js +1 -1
  32. package/dist-lib/lib/helpers/tool-label.js +3 -3
  33. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -1
  34. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -1
  35. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -1
  36. package/dist-lib/lib/hooks/built-in/egress.js +2 -2
  37. package/dist-lib/lib/hooks/built-in/index.js +3 -3
  38. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -1
  39. package/dist-lib/lib/hooks/pipeline.js +4 -4
  40. package/dist-lib/lib/hooks/registry.js +1 -1
  41. package/dist-lib/lib/hooks/run.js +1 -1
  42. package/dist-lib/lib/models/model-catalog.js +4 -4
  43. package/dist-lib/lib/models/model-delete.js +1 -1
  44. package/dist-lib/lib/models/model-handle.js +1 -1
  45. package/dist-lib/lib/models/model-list.js +1 -1
  46. package/dist-lib/lib/models/model-pool.js +8 -8
  47. package/dist-lib/lib/models/model-source.js +1 -1
  48. package/dist-lib/lib/models/music-catalog.d.ts +1 -1
  49. package/dist-lib/lib/no-redirect-fetch-bare.js +3 -3
  50. package/dist-lib/lib/no-redirect-fetch-impl.js +1 -1
  51. package/dist-lib/lib/no-redirect-fetch-node.js +2 -2
  52. package/dist-lib/lib/resolution-detector.js +1 -1
  53. package/dist-lib/lib/sdk-loader.js +2 -2
  54. package/dist-lib/lib/sdk-storage.js +1 -1
  55. package/dist-lib/lib/serve.js +10 -10
  56. package/dist-lib/lib/spawn.js +3 -3
  57. package/dist-lib/lib/staging.js +1 -1
  58. package/dist-lib/lib/tools/catalog.js +1 -1
  59. package/dist-lib/lib/tools/create-documents.js +2 -2
  60. package/dist-lib/lib/tools/edit-documents.js +2 -2
  61. package/dist-lib/lib/tools/edit-image.js +2 -2
  62. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +2 -2
  63. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +1 -1
  64. package/dist-lib/lib/tools/exec/adapters/python/loader.js +1 -1
  65. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
  66. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -6
  67. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +3 -3
  68. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +5 -5
  69. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -2
  70. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +4 -4
  71. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +4 -4
  72. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +1 -1
  73. package/dist-lib/lib/tools/exec/obsidian/cli-process.js +2 -2
  74. package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
  75. package/dist-lib/lib/tools/exec/obsidian/command.js +1 -1
  76. package/dist-lib/lib/tools/exec/obsidian/route.js +7 -7
  77. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +2 -2
  78. package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +6 -6
  79. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +2 -2
  80. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +1 -1
  81. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +2 -2
  82. package/dist-lib/lib/tools/exec/runner.js +1 -1
  83. package/dist-lib/lib/tools/exec.js +13 -13
  84. package/dist-lib/lib/tools/file-glob.js +3 -3
  85. package/dist-lib/lib/tools/fs/local.js +1 -1
  86. package/dist-lib/lib/tools/fs/remote.js +1 -1
  87. package/dist-lib/lib/tools/fs/walk.js +2 -2
  88. package/dist-lib/lib/tools/generate-image.js +4 -4
  89. package/dist-lib/lib/tools/generate-music.js +6 -6
  90. package/dist-lib/lib/tools/grep.js +6 -6
  91. package/dist-lib/lib/tools/http-request/truncate-response.js +1 -1
  92. package/dist-lib/lib/tools/http-request.js +9 -9
  93. package/dist-lib/lib/tools/mcp/utils.js +1 -1
  94. package/dist-lib/lib/tools/mcp-call.js +8 -8
  95. package/dist-lib/lib/tools/operations/builders.d.ts +1 -1
  96. package/dist-lib/lib/tools/operations/builders.js +1 -1
  97. package/dist-lib/lib/tools/operations/compile.js +1 -1
  98. package/dist-lib/lib/tools/operations/manifest.d.ts +16 -16
  99. package/dist-lib/lib/tools/operations/manifest.js +1 -1
  100. package/dist-lib/lib/tools/outbound-guard.js +1 -1
  101. package/dist-lib/lib/tools/rag-search.js +1 -1
  102. package/dist-lib/lib/tools/read-files.js +3 -3
  103. package/dist-lib/lib/tools/skill.js +8 -8
  104. package/dist-lib/lib/tools/skills/auth-resolver.js +2 -2
  105. package/dist-lib/lib/tools/skills/bin-check.js +1 -1
  106. package/dist-lib/lib/tools/skills/bundled-dir.js +2 -2
  107. package/dist-lib/lib/tools/skills/catalog.js +10 -10
  108. package/dist-lib/lib/tools/skills/cli-schema-check.js +1 -1
  109. package/dist-lib/lib/tools/skills/cli-schema-load.js +1 -1
  110. package/dist-lib/lib/tools/skills/cli-schema.js +4 -4
  111. package/dist-lib/lib/tools/skills/credential-check.js +3 -3
  112. package/dist-lib/lib/tools/skills/credential-grants.js +1 -1
  113. package/dist-lib/lib/tools/skills/credential-health.js +2 -2
  114. package/dist-lib/lib/tools/skills/frontmatter-parser.js +1 -1
  115. package/dist-lib/lib/tools/skills/index.js +5 -5
  116. package/dist-lib/lib/tools/skills/mention.js +1 -1
  117. package/dist-lib/lib/tools/skills/overlay-crud.js +1 -1
  118. package/dist-lib/lib/tools/skills/setup-routes.js +1 -1
  119. package/dist-lib/lib/tools/skills/skill-settings.js +1 -1
  120. package/dist-lib/lib/tools/skills/skill-state-constants.d.ts +8 -8
  121. package/dist-lib/lib/tools/skills/skill-state.js +9 -9
  122. package/dist-lib/lib/tools/toolsets.d.ts +1 -1
  123. package/dist-lib/lib/tools/url-guard.js +2 -2
  124. package/dist-lib/lib/tools/weather-lookup/place.d.ts +11 -0
  125. package/dist-lib/lib/tools/weather-lookup/place.js +152 -0
  126. package/dist-lib/lib/tools/weather-lookup/report.d.ts +37 -0
  127. package/dist-lib/lib/tools/weather-lookup/report.js +56 -0
  128. package/dist-lib/lib/tools/weather-lookup.d.ts +8 -0
  129. package/dist-lib/lib/tools/weather-lookup.js +110 -0
  130. package/dist-lib/lib/tools/web-fetch.js +10 -10
  131. package/dist-lib/lib/tools/web-search.js +2 -2
  132. package/dist-lib/lib/transport.js +1 -1
  133. package/dist-lib/lib/validating-lookup.js +1 -1
  134. package/dist-lib/lib/web-fetch/embedded-state.js +1 -1
  135. package/dist-lib/lib/web-fetch/extract.js +1 -1
  136. package/dist-lib/lib/web-fetch/images.js +1 -1
  137. package/dist-lib/lib/web-fetch/video.js +1 -1
  138. package/dist-lib/lib/web-search/engine-registry.js +7 -7
  139. package/dist-lib/lib/web-search/engines/aol.js +3 -3
  140. package/dist-lib/lib/web-search/engines/bing.js +4 -4
  141. package/dist-lib/lib/web-search/engines/brave.js +2 -2
  142. package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -3
  143. package/dist-lib/lib/web-search/engines/google.js +4 -4
  144. package/dist-lib/lib/web-search/engines/wikipedia.js +2 -2
  145. package/dist-lib/lib/web-search/engines/yahoo.js +3 -3
  146. package/dist-lib/lib/web-search/fetch-html.js +1 -1
  147. package/dist-lib/lib/web-search/merge-results.js +1 -1
  148. package/dist-lib/lib/web-search/orchestrator.js +3 -3
  149. package/dist-lib/schema/baseline.js +1 -1
  150. package/dist-lib/schema/build.js +3 -3
  151. package/dist-lib/schema/release-tags.d.ts +2 -1
  152. package/dist-lib/schema/release-tags.js +9 -7
  153. package/docs/decisions/2026-05-29-kvcache-tool-turns.md +75 -0
  154. package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +127 -0
  155. package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +126 -0
  156. package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +88 -0
  157. package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +79 -0
  158. package/docs/decisions/2026-08-11-pyodide-worker-thread.md +68 -0
  159. package/docs/decisions/2026-08-17-harness-hook-pipeline.md +84 -0
  160. package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +176 -0
  161. package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +95 -0
  162. package/docs/decisions/2026-08-20-kvcache-static-placement.md +109 -0
  163. package/docs/decisions/2026-08-21-vendored-python-wheels.md +85 -0
  164. package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +64 -0
  165. package/docs/decisions/2026-08-27-writable-skills-overlay.md +56 -0
  166. package/docs/decisions/2026-09-01-skill-operation-manifests.md +53 -0
  167. package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +78 -0
  168. package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +136 -0
  169. package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +63 -0
  170. package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +70 -0
  171. package/docs/decisions/INDEX.md +40 -0
  172. package/docs/decisions/TEMPLATE.md +32 -0
  173. package/package.json +21 -4
  174. package/worker/harness-sidecar.mjs +2 -0
@@ -0,0 +1,78 @@
1
+ ---
2
+ id: ADR-2026-09-01-usage-breakdown-is-measured
3
+ status: superseded
4
+ introduced_by: PR-1953
5
+ superseded_by: ADR-2026-09-03-usage-breakdown-reported-by-the-harness
6
+ supersedes: []
7
+ ---
8
+
9
+ # The Usage breakdown divides a measured total, it never estimates tokens
10
+
11
+ ## Decision
12
+
13
+ Only the Usage modal's total is a token count: `cacheTokens` from the turn's SDK
14
+ metrics. Every row below it is that total split by character share, at a
15
+ chars-per-token ratio fitted per chat against the SDK's own count on the first
16
+ turn that reports one. No row converts characters to tokens with a hardcoded
17
+ ratio, and the fit is never recomputed on a later turn.
18
+
19
+ Two costs the model receives but we do not author are declared as measured
20
+ constants rather than absorbed: the chat template's tool preamble
21
+ (`FRAMING_CHARS_BY_DIALECT`, read through `toolBlockFramingChars`) and one
22
+ exchange's fixed overhead (`TURN_OVERHEAD_TOKENS`, charged to the turn via the
23
+ fit's denominator). Only the first is keyed by dialect — a template either has a
24
+ tool preamble or has none, whereas every template wraps its turns, so declaring
25
+ zero overhead would be further from the truth than Qwen's number is elsewhere.
26
+
27
+ ## Context
28
+
29
+ The SDK exposes no tokenizer — `@qvac/sdk` 0.18.2 ships tokenization only for
30
+ VLA, whose docs tell consumers to tokenize their own input — so a real count is
31
+ unavailable and `cacheTokens` is the only ground truth.
32
+
33
+ A single hardcoded ratio cannot work. Measured on Qwen3.5-4B, real density is
34
+ 3.17 chars-per-token for a prompt with no tools and 4.23 for a seven-tool
35
+ prompt, because tool schemas are dense JSON and the rest is prose. The retired
36
+ 3.5 constant therefore overshot the measured total, and the scaling fallback
37
+ squeezed every row to fit — which is why the rows appeared to grow between turns
38
+ while Conversation collapsed to single digits.
39
+
40
+ Character counting is also blind to two real costs: the template's own tool
41
+ section, and per-turn role wrappers plus the harness time annotation. Left
42
+ unattributed they are spread across the rows that do have characters, which
43
+ overstated a tool-less agent's System Prompt by 47%.
44
+
45
+ ## Alternatives Considered
46
+
47
+ - **Better hardcoded ratios per segment.** Rejected: it replaces one invented
48
+ constant with three, all model-specific, and all silently wrong on a new model.
49
+ - **A two-point fit** using the delta between the first two turns to measure the
50
+ per-turn cost directly. Correct and constant-free, but it reports nothing until
51
+ a chat has two turns.
52
+ - **Keying the framing globally rather than by dialect.** Rejected: dialect is
53
+ the axis the template varies on, and an unmeasured model should declare zero
54
+ rather than borrow another template's number.
55
+
56
+ ## Consequences
57
+
58
+ - The rows sum exactly to the measured total on every turn, and the prompt rows
59
+ hold still across turns of one chat. Both are load-bearing: QA asserts them.
60
+ - Getting a constant wrong is cosmetic. It shifts the split between rows and can
61
+ never move the total, the sum, or the flatness — so a stale value degrades
62
+ silently, and only a real-model measurement can catch it.
63
+ - The estimator must keep mirroring the production selection functions
64
+ (`permittedTools`, `enabledTools`, `skillIsActive`); a drift test pins this.
65
+ - The fit's numerator must keep pace with what the cache holds. Tool calls and
66
+ their output are re-sent and counted; generated `thinking` is not, and
67
+ `cacheTokens` excludes it (measured: 408 generated tokens against 162 reported).
68
+ - Re-measure a constant by diffing the addon's `PROMPT_SEND` dump against
69
+ `systemPromptChars` + `toolsChars`. Drive it from `@qvac/core → examples/chat-tui.mjs`
70
+ over a pty with `QVAC_STORAGE` pointed at a scratch dir, and read `N ctx` from
71
+ the footer.
72
+ - Revisit if the SDK ever exposes a token-count or prompt-render op: that would
73
+ retire the fit and both constants.
74
+
75
+ ## When Agents Should Read This
76
+
77
+ Read this when changing the paths, workflows, runtime boundaries, or platform
78
+ behavior listed for this ADR in `docs/decisions/INDEX.md`.
@@ -0,0 +1,136 @@
1
+ ---
2
+ id: ADR-2026-09-03-usage-breakdown-reported-by-the-harness
3
+ status: active
4
+ introduced_by: PR-2067
5
+ supersedes: [ADR-2026-09-01-usage-breakdown-is-measured]
6
+ ---
7
+
8
+ # The device that ran the turn reports the Usage breakdown; the app only renders it
9
+
10
+ ## Decision
11
+
12
+ The Usage modal's rows come from the run. The harness attributes the prompt it
13
+ just assembled — `systemPrompt`, `agentInstructions`, `activeSkills`, `tools` —
14
+ measured as `.length` on the strings it sent, converts each to tokens at the
15
+ measured density **for that kind of text**, and ships the result inside the
16
+ existing `metrics` JSON of the terminal run status
17
+ (`lib/capabilities/usage-breakdown.ts`).
18
+
19
+ A prompt row is never a share of the measured total. It is its own characters at
20
+ its own density, so it holds still across every turn of a chat while the
21
+ conversation grows — the property QA asks for. `conversation` is what is left of
22
+ `cacheTokens` once the prompt rows are subtracted, which is also where the costs
23
+ no segment authors end up: the template's per-message wrappers (~28 tokens a
24
+ message on Qwen3.5) and the reply the run just generated, which the cache holds.
25
+
26
+ `app-runtime` re-derives nothing. `buildUsageSegments` reads the newest reported
27
+ breakdown off the messages and maps it to labels and colors. Conversation is the
28
+ remainder against the live `usedTokens`, never a reported number, so occupancy
29
+ the report cannot know about lands on the turns rather than skewing a prompt row.
30
+
31
+ Three constants per template, keyed off `detectToolDialect`: prose chars/token
32
+ (the prompt body, the bio, the skills list), schema chars/token (the serialized
33
+ SDK function schemas), and the tool block's token count. Measured on
34
+ Qwen3.5-2B-Q4_K_M as `{ prose: 4.38, schema: 4.08, toolBlockTokens: 189 }` by
35
+ least squares over the addon's own `cacheTokens` across 14 tool/skill/bio
36
+ configurations x 3 turns — 42 samples, leave-one-configuration-out worst error
37
+ 5.9%. An unmeasured template borrows these rather than reporting zeros.
38
+
39
+ Two things the previous design needed are gone: the per-chat chars-per-token fit
40
+ (`usage-calibration.ts`) and the app's copy of the selection gates
41
+ (`usage-segment-system-prompt.ts`, and with it the drift test that policed the
42
+ copy). `TURN_OVERHEAD_TOKENS` is gone too, and so is the 935-char framing
43
+ constant — the tool block is a fixed string, so it is declared in tokens.
44
+
45
+ The row for attachment filenames is retired. Those labels are composed into the
46
+ user turn by the assistant layer, so they are conversation characters; the app
47
+ was counting raw filenames that never appear in the prompt verbatim.
48
+
49
+ ## Context
50
+
51
+ Every row except the total was re-derived app-side by re-running
52
+ `permittedTools`, `enabledTools` and `skillIsActive` against a device tool
53
+ report and a skill catalog. The mirror was only ever as current as the last
54
+ person to remember it existed, and because the fit normalizes the known
55
+ characters onto the measured total, an input that goes missing does not read as
56
+ zero in place — it silently reweights every other row.
57
+
58
+ That failure shipped. After PR-1960 the six provider skills declared tools
59
+ compiled from `operations.json`, so on any host that had not wired
60
+ `compileOperationTools` (the mobile worklet until QVAC-24473; the in-process
61
+ path still) `isToolSupported` marked them unavailable, the estimator dropped
62
+ them, their tool grants went with them, and the System Prompt row absorbed the
63
+ entire window: 2,361 of 2,616 tokens, with Active Skills and Tools at 0.
64
+
65
+ No tokenizer is exposed — `@qvac/sdk` 0.18.2 ships tokenization only for VLA,
66
+ whose docs tell consumers to tokenize their own input — so `cacheTokens` remains
67
+ the only ground truth and the rows remain shares of it. What changed is where
68
+ the characters are counted: at the point the prompt is built, where being wrong
69
+ is not possible without also sending a different prompt.
70
+
71
+ ## Alternatives Considered
72
+
73
+ - **Keep the mirror, add a stronger drift test.** Rejected: the drift test runs
74
+ against a stubbed completion, so it cannot see inputs that arrive over the
75
+ wire — exactly the class that broke. It also grows a second maintenance
76
+ surface for every prompt change.
77
+ - **Report chars, fit in the app.** The fit needs the first metrics-bearing turn
78
+ of the chat, whose prompt the harness can no longer reconstruct. Rejected
79
+ anyway: per-turn attribution against that turn's own measurement needs no fit,
80
+ and the prompt rows stay flat regardless (the shares move together).
81
+ - **Report tokens per segment.** Requires a tokenizer we do not have. This is
82
+ the shape to move to when the SDK grows one: the harness swaps its char counts
83
+ for tokenize calls and nothing downstream changes.
84
+ - **One density for the whole prompt** (what shipped first, and what QA caught).
85
+ Rejected on measurement: with rows taken as shares of the total, a chat whose
86
+ tool schemas are dense JSON and whose turns are prose re-weights every row as
87
+ the mix changes. Three "hi" turns moved Tools 3,166 -> 3,252 -> 3,268 with an
88
+ unchanged tool set, and fitting one density across the matrix above is off by
89
+ up to 46% against six per-kind constants' 1.7%.
90
+ - **Keep the attachment-filenames row, reported by the assistant layer.** Held
91
+ back: it would put a third layer in the attribution path for a row that reads
92
+ zero in every chat we have looked at.
93
+
94
+ ## Consequences
95
+
96
+ - A prompt-shaping change cannot desync the breakdown. There is nothing to keep
97
+ in sync: the numbers are measured off the prompt that was sent.
98
+ - A delegated chat is attributed by the device that ran it, so the observer no
99
+ longer needs the host's tool catalog or skill names to price the window.
100
+ Everything that fed the estimator therefore loses its only reader — the
101
+ reported tool catalog (`agentToolCatalog`, `selectToolCatalog`,
102
+ `selectActiveAgentHostToolCatalog`, the `skill.toolCatalog` slice and the
103
+ `coreV2.tools({})` call that filled it) and the app-side fs-grant mirror
104
+ (`agent.actor.fs-grants.ts`, its actions, its reducer branches,
105
+ `state.agent.fsGrants`, its selectors and `AgentFsGrants`/`NO_FS_GRANTS`).
106
+ PR-2068 deletes them. The real grant plumbing in `@qvac/core → lib/fs-grants.ts`
107
+ stays either way — that is what gives the harness its roots.
108
+ - The rows still sum exactly to the measured total, and the prompt rows still
109
+ hold still across turns of one chat. Both are asserted by QA (QA-723).
110
+ - The app parses only the four prompt rows out of the report. The run also sends
111
+ its own `conversation` and `totalTokens`, which stay useful in a log or a
112
+ debug view, but requiring them here would turn a harness that trimmed either
113
+ one into no breakdown at all.
114
+ - A turn recorded before this change, or run by an older peer, carries no
115
+ breakdown: the modal shows the window on Conversation and holds the System
116
+ Prompt row back rather than inventing a split. There is no estimator fallback
117
+ on purpose — a stale mirror is the defect this removes.
118
+ - `promptChars` on the reported tool catalog now has no reader, and neither does
119
+ the `toolCatalog` field on a device row (still parsed in
120
+ `device/core-v2-device.ts`, still listed in the Core v2 compatibility matrix).
121
+ Both stay until one schema bump retires them together.
122
+ - Re-measure the densities by driving `Harness.completion` headlessly with the
123
+ real SDK (no TUI or pty needed: it takes `model`, `agent` and `messages`
124
+ directly), reading the char set the harness logs beside the `cacheTokens` the
125
+ addon reports, and fitting the four terms by least squares. **One fresh SDK
126
+ home per configuration, one configuration per process**: the SDK persists cache
127
+ state under its home, and `cacheTokens` counts that whole occupancy, so a reused
128
+ home reports the same prompt as 152, 219 or 469 tokens. That also means a
129
+ device's total can include cache another chat left behind — a separate question
130
+ from this attribution, and the reason `conversation` is the remainder rather
131
+ than a reported number.
132
+
133
+ ## When Agents Should Read This
134
+
135
+ Read this when changing the paths, workflows, runtime boundaries, or platform
136
+ behavior listed for this ADR in `docs/decisions/INDEX.md`.
@@ -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`.
@@ -0,0 +1,40 @@
1
+ # Harness Architecture Decision Records
2
+
3
+ Decision records for `@qvac/harness`: the inference engine and the tool runtime
4
+ skills execute in. They answer HOW we chose to build something, when the reason
5
+ is not obvious from the code or the PR that introduced it.
6
+
7
+ Read this index first. Open a record only when the touched paths, area, or
8
+ keywords match the task at hand.
9
+
10
+ Paths are relative to this repository. A path prefixed `@qvac/core →`,
11
+ `@qvac/sdk →` or `qvac-app →` lives in another repository and is context, not a
12
+ local file. `node scripts/docs-audit.cjs` checks this directory.
13
+
14
+ ## Active Decisions
15
+
16
+ | Status | Area | Paths | Decision | Keywords |
17
+ | ------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | active | harness, skills, tools, product | `lib/tools/weather-lookup.ts`, `lib/tools/weather-lookup/place.ts`, `lib/tools/weather-lookup/report.ts`, `worker/harness-sidecar.mjs`, `@qvac/skills → skills/weather/SKILL.md` | [A provider that can silently resolve the wrong thing gets a typed tool, not a skill rule](./2026-09-16-a-lookup-reports-the-place-it-resolved.md) | `weather_lookup`, `wttr.in`, `nearest_area`, `geocoder`, `resolved place`, `skillGated`, `autoApprove`, `format=j1`, `QVAC-24701` |
19
+ | active | assistant, harness, skills, security | `lib/tools/operations/manifest.ts`, `lib/tools/operations/compile.ts`, `lib/tools/operations/builders.ts`, `lib/tools/http-request.ts`, `lib/tools/http-request/truncate-response.ts`, `lib/capabilities/tool-selection.ts`, `skills/gmail/operations.json`, `skills/google-calendar/operations.json` | [A provider write is a compiled skill operation, not a shaping hook in `http_request`](./2026-09-01-skill-operation-manifests.md) | `operations.json`, `compileOperationTools`, `parseOperationsManifest`, `OperationSpec`, `BUILDERS`, `gmail_send`, `gmail_draft`, `calendar_create_meet_event`, `calendar_add_meet`, `owningSkill`, `grantedOwners`, `host shaping`, `shapeRequest`, `truncateResponseBody`, `fitJson`, `MAX_RESPONSE_CHARS` |
20
+ | active | assistant, harness, skills, security | `lib/tools/skills/overlay-dir.ts`, `lib/tools/skills/catalog.ts`, `lib/tools/skills/credential-grants.ts`, `lib/tools/skill.ts` | [A user-added skill layers over the bundled base and is trusted only by shadow](./2026-08-27-writable-skills-overlay.md) | `skills overlay`, `writable overlay`, `resolveSkillsOverlayDir`, `loadSkillCatalog`, `mergeOverlay`, `origin overlay`, `shadow`, `credentialGrants`, `mcpReadMethods`, `OVERLAY_DEFAULT_PLATFORMS`, `platform gate`, `untrusted skill`, `user-added skill`, `not available on this device` |
21
+ | active | assistant, harness, security, product | `lib/tools/exec.ts`, `lib/tools/exec/scope.ts`, `lib/capabilities/completion.ts`, `lib/capabilities/tool-selection.ts`, `lib/tools/skill.ts` | [A skill no longer decides what `exec` may run; approval does](./2026-08-26-exec-is-not-gated-by-skills.md) | `exec`, `skillGated`, `scopeRequired`, `grantingSkills`, `grantedScopes`, `toolScopes`, `checkScopedCommand`, `checkPythonScope`, `sanitizeCommand`, `locked until you load the skill`, `binary allowlist`, `RESTRICTED`, `approval is the boundary`, `PERM-7`, `PERM-10` |
22
+ | active | assistant, harness, runtime, inference | `lib/capabilities/prompt-prefix.ts`, `lib/capabilities/completion.ts`, `lib/tools/skills/mention.ts` | [A chat's prompt prefix changes only when the user changes something](./2026-08-20-chat-prompt-prefix-is-stable.md) | `promptPrefixHash`, `prompt prefix`, `round start`, `cacheTokens collapse`, `CACHE_INIT`, `KV cache re-prime`, `skillIsActive`, `SkillActivation`, `activeSkills`, `hasActiveSkills`, `loadedSkillsFrom`, `skillBlocked`, `blockedSkillMessage`, `skills section`, `turn-scoped activation`, `SKILL-3` |
23
+ | active | assistant, harness, runtime | `lib/hooks/**`, `lib/hooks/built-in/arg-repair.ts`, `lib/capabilities/completion.ts` | [Harness hooks fold a whole round before any call runs, and fail closed](./2026-08-17-harness-hook-pipeline.md) | `HarnessHook`, `HookPipeline`, `harnessHooks`, `runHooks`, `preToolCall`, `postToolCall`, `runStart`, `runEnd`, `HookRejection`, `guidance`, `round fold`, `deep clone`, `fail closed`, `tool executed; output withheld`, `abandoned`, `ArgRepairHook`, `arg repair`, `jsonrepair`, `parameters` |
24
+ | active | assistant, storage, desktop, mobile | `@qvac/core → lib/storage.ts`, `@qvac/core → lib/image-cache-dir.ts`, `@qvac/core → lib/sdk-storage.ts`, `qvac-app → desktop/electron/main.ts`, `qvac-app → mobile/worker/assistant-worklet.mjs` | [Assistant-owned storage separates shared models from private KV](./2026-07-29-assistant-owned-storage-layout.md) | `bare-storage`, `models`, `~/.qvac/models`, `SDK shared cache`, `image cache`, `KV cache`, `sdk-home`, `release channel`, `QVAC_MODEL_CACHE_PATH`, `Bare.argv[0]`, `SNAP_USER_COMMON`, `no migration` |
25
+ | active | runtime, inference, harness | `lib/models/model-pool.ts`, `lib/context-window.ts`, `@qvac/core → lib/assistant.ts`, `lib/capabilities/completion.ts` | [Static tool placement, no sliding window](./2026-08-20-kvcache-static-placement.md) | `kvCache`, `prompt cache`, `static placement`, `toolsMode`, `n_discarded`, `sliding window`, `savedCount`, `cold prime`, `modelId re-key`, `cache reset`, `overflow ladder` |
26
+ | active | runtime, inference, mobile, harness | `qvac-app → mobile/lib/android-llm-device.ts`, `qvac-app → mobile/lib/core-v2-harness-config.ts`, `index.ts`, `lib/models/kv-cache.ts`, `lib/models/opencl-cache.ts` | [Android LLM defaults to CPU except Tensor-era Pixels](./2026-06-23-android-cpu-llm-defaults.md) | `Android`, `Pixel 9`, `Pixel 10`, `Pixel Tablet`, `Pixel Fold`, `Adreno`, `Vulkan`, `OpenCL`, `openclCacheDir`, `kernel cache`, `backendDevice`, `Snapdragon`, `CPU inference`, `GPU inference`, `KV cache`, `q4_0`, `cache-type-k`, `cache-type-v`, `gpu_layers`, `transcriptionDevice`, `parakeet`, `useGPU`, `empty transcript` |
27
+ | active | runtime, harness, mobile, desktop | `lib/tools/exec/adapters/python/**`, `lib/tools/exec/scope.ts` | [Pyodide runs in a worker thread with a shared interrupt buffer](./2026-08-11-pyodide-worker-thread.md) | `Pyodide`, `python worker`, `Bare.Thread`, `bare-channel`, `SharedArrayBuffer`, `setInterruptBuffer`, `KeyboardInterrupt`, `terminate`, `join`, `stowed bundle`, `QVAC_PYODIDE_WORKER_ENTRY`, `QVAC_PYODIDE_TEST_LOADER`, `exec`, `in-process fallback` |
28
+ | active | runtime, harness, desktop, build, security | `wheels/manifest.json`, `lib/tools/exec/adapters/python/wheels.ts`, `scripts/vendor-python-wheels.mjs`, `qvac-app → desktop/electron.vite.config.ts` | [Python packages ship with the app, pinned by hash](./2026-08-21-vendored-python-wheels.md) | `python wheels`, `micropip`, `packageCacheDir`, `emfs:`, `deps=False`, `vendored wheels`, `offline`, `PyPI`, `sha256`, `supply chain`, `QVAC_PYTHON_WHEELS_DIR`, `fpdf2`, `pypdf`, `openpyxl`, `python-docx`, `python-pptx`, `cryptography` |
29
+ | active | desktop, harness, runtime | `qvac-app → desktop/electron/assistant.ts`, `qvac-app → desktop/electron/assistant-sidecar-lifecycle.ts`, `qvac-app → desktop/electron/assistant-sidecar-process.ts`, `worker/harness-sidecar.mjs` | [The host reaps the sidecar's process group; the harness cannot reap itself](./2026-08-04-sidecar-orphan-reaping.md) | `orphan`, `process group`, `detached`, `SIGKILL`, `reparent to init`, `taskkill`, `Job Object`, `libuv`, `KILL_ON_JOB_CLOSE`, `orphan watchdog`, `os.ppid`, `model memory leak`, `killStowChildren` |
30
+ | active | harness, security, runtime, mobile, desktop | `lib/hooks/built-in/egress.ts`, `lib/hooks/built-in/skill-catalog.ts`, `lib/hooks/registry.ts`, `lib/tools/url-guard.ts`, `lib/tools/private-ip.ts`, `lib/dns-lookup.ts`, `lib/validating-lookup.ts`, `lib/no-redirect-fetch-bare.ts`, `lib/no-redirect-fetch-node.ts`, `lib/helpers/spawn-path.ts`, `lib/tools/http-request.ts`, `lib/tools/mcp-call.ts`, `lib/tools/exec.ts`, `lib/tools/skill.ts`, `lib/tools/fs/scope.ts` | [Harness tool egress and spawn environments are allowlists, not filters](./2026-08-17-harness-outbound-egress-guard.md) | `SSRF`, `resolveOutboundUrl`, `private-ip`, `isPrivateOrBlocked`, `DNS rebinding`, `validating lookup`, `resolveHost`, `lookupHost`, `bare-dns`, `metadata endpoint`, `redirect hop`, `EXEC_ENV_ALLOWLIST`, `spawn env allowlist`, `workdir`, `resolveKey`, `granted roots`, `deniedRoots`, `canonicalDenyRoots`, `deny outranks grant`, `storage root`, `skill catalog`, `fail closed` |
31
+ | active | harness, app-runtime, desktop, mobile, chat | `lib/capabilities/usage-breakdown.ts`, `lib/capabilities/completion.ts`, `qvac-app → app-runtime/src/chat/usage-segment.ts`, `qvac-app → app-runtime/src/chat/chunk-to-turn.ts`, `qvac-app → app-runtime/src/agent/**`, `qvac-app → app-runtime/src/skill/skill.actor.catalog.ts` | [The device that ran the turn reports the Usage breakdown; the app only renders it](./2026-09-03-usage-breakdown-reported-by-the-harness.md) | `Usage modal`, `usage segments`, `usageBreakdown`, `promptUsageParts`, `buildUsageSegments`, `cacheTokens`, `charsPerToken`, `FRAMING_CHARS_BY_DIALECT`, `toolBlockFramingChars`, `detectToolDialect`, `chat template framing`, `PROMPT_SEND`, `context used`, `no tokenizer`, `metrics json` |
32
+ | active | harness, skills, inference | `examples/skill-eval/run.ts`, `examples/skill-eval/pack.ts`, `examples/skill-eval/packs/weather/index.ts`, `test/skill-eval.ts` | [A skill's wording is measured on the real small model before it ships](./2026-09-10-skill-wording-is-measured-offline.md) | `skill-eval`, `SkillPack`, `PackWorld`, `judge`, `gap`, `fixtures`, `--record`, `mention route`, `prose route`, `self-load`, `deterministic scoring`, `no judge model`, `version:` front matter, `QVAC-24701` |
33
+
34
+ ## Superseded Decisions
35
+
36
+ | Status | Area | Paths | Decision | Keywords |
37
+ | ---------- | ---- | ----- | ---------------------------------------------------------------------------------------------------------------------- | -------- |
38
+ | superseded | | | [KV-cache session is disabled for tool-using completions](./2026-05-29-kvcache-tool-turns.md) | |
39
+ | superseded | | | [KV-cache session is re-enabled for tool-using completions](./2026-06-02-kvcache-tool-turns-reenabled.md) | |
40
+ | superseded | | | [The Usage breakdown divides a measured total, it never estimates tokens](./2026-09-01-usage-breakdown-is-measured.md) | |
@@ -0,0 +1,32 @@
1
+ ---
2
+ id: ADR-YYYY-MM-DD-short-name
3
+ status: active
4
+ introduced_by: PR-000
5
+ supersedes: []
6
+ ---
7
+
8
+ # Short Decision Title
9
+
10
+ ## Decision
11
+
12
+ State the durable technical choice in one or two short paragraphs.
13
+
14
+ ## Context
15
+
16
+ Explain the constraints or trade-offs that made this decision necessary.
17
+
18
+ ## Alternatives Considered
19
+
20
+ Optional. Include only rejected approaches that future maintainers or agents
21
+ are likely to rediscover.
22
+
23
+ ## Consequences
24
+
25
+ - What future changes must preserve.
26
+ - What this deliberately does not solve.
27
+ - What should trigger revisiting the decision.
28
+
29
+ ## When Agents Should Read This
30
+
31
+ Read this when changing the paths, workflows, runtime boundaries, or platform
32
+ behavior listed for this ADR in `docs/decisions/INDEX.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "An AI harness implementing generate(context) via @qvac/sdk. Knows nothing about workbench, hyperdb, or chunks — just inference in, events out.",
6
6
  "exports": {
@@ -8,6 +8,10 @@
8
8
  "types": "./dist-lib/index.d.ts",
9
9
  "default": "./dist-lib/index.js"
10
10
  },
11
+ "./docx": {
12
+ "types": "./dist-lib/lib/docx/index.d.ts",
13
+ "default": "./dist-lib/lib/docx/index.js"
14
+ },
11
15
  "./lib/dns-lookup.ts": {
12
16
  "types": "./dist-lib/lib/dns-lookup.d.ts",
13
17
  "default": "./dist-lib/lib/dns-lookup.js"
@@ -535,16 +539,29 @@
535
539
  "./lib/tools/url-guard.ts": {
536
540
  "types": "./dist-lib/lib/tools/url-guard.d.ts",
537
541
  "default": "./dist-lib/lib/tools/url-guard.js"
542
+ },
543
+ "./lib/tools/weather-lookup.ts": {
544
+ "types": "./dist-lib/lib/tools/weather-lookup.d.ts",
545
+ "default": "./dist-lib/lib/tools/weather-lookup.js"
546
+ },
547
+ "./lib/tools/weather-lookup/place.ts": {
548
+ "types": "./dist-lib/lib/tools/weather-lookup/place.d.ts",
549
+ "default": "./dist-lib/lib/tools/weather-lookup/place.js"
550
+ },
551
+ "./lib/tools/weather-lookup/report.ts": {
552
+ "types": "./dist-lib/lib/tools/weather-lookup/report.d.ts",
553
+ "default": "./dist-lib/lib/tools/weather-lookup/report.js"
538
554
  }
539
555
  },
540
556
  "scripts": {
541
557
  "build:schema": "bare schema/build.ts",
542
- "test": "npm run test:node && npm run test:bare && npm run test:addons",
558
+ "test": "npm run vendor:wheels && npm run test:node && npm run test:bare && npm run test:addons",
543
559
  "test:node": "QVAC_NO_SKILL_BIN_PROBE=1 brittle-node test/all.ts | prettytap",
544
560
  "test:bare": "QVAC_NO_SKILL_BIN_PROBE=1 brittle-bare test/all.ts | prettytap",
545
561
  "format": "prettier . --write",
546
562
  "lint": "prettier . --check && lunte && tsc --noEmit",
547
563
  "build:bundle": "node scripts/build-bundle.mjs",
564
+ "skill-eval": "bare examples/skill-eval/run.ts",
548
565
  "prepack": "node scripts/vendor-python-wheels.mjs && node scripts/build-lib.mjs && node scripts/build-bundle.mjs --minify",
549
566
  "vendor:wheels": "node scripts/vendor-python-wheels.mjs",
550
567
  "build:lib": "node scripts/build-lib.mjs",
@@ -635,7 +652,7 @@
635
652
  "dependencies": {
636
653
  "@qvac/inference": "0.19.0",
637
654
  "@qvac/sdk": "0.19.0",
638
- "@qvac/skills": "^0.1.3",
655
+ "@qvac/skills": "^0.1.4",
639
656
  "asenion": "^0.1.0",
640
657
  "b4a": "^1.8.1",
641
658
  "bare-abort-controller": "^1.1.2",
@@ -699,7 +716,7 @@
699
716
  "prettier-config-holepunch": "^2.0.0",
700
717
  "prettytap": "^0.1.3",
701
718
  "terser": "^5.51.2",
702
- "typescript": "^5.9.3"
719
+ "typescript": "^7.0.2"
703
720
  },
704
721
  "patchedDependencies": {
705
722
  "pyodide@314.0.2": "patches/pyodide@314.0.2.patch",
@@ -31,6 +31,7 @@ import { createReadFilesTool } from '../dist-lib/lib/tools/read-files.js'
31
31
  import { createSkillTool } from '../dist-lib/lib/tools/skill.js'
32
32
  import { ensureBundledSkillsDir } from '../dist-lib/lib/tools/skills/bundled-dir.js'
33
33
  import { credentialAllowListFor, mcpReadMethods } from '../dist-lib/lib/tools/skills/index.js'
34
+ import { createWeatherLookupTool } from '../dist-lib/lib/tools/weather-lookup.js'
34
35
  import { createWebFetchTool } from '../dist-lib/lib/tools/web-fetch.js'
35
36
  import { createWebSearchTool } from '../dist-lib/lib/tools/web-search.js'
36
37
 
@@ -137,6 +138,7 @@ export default async function start(ipc, ready) {
137
138
  mcpCallTool,
138
139
  createWebSearchTool(),
139
140
  createWebFetchTool(),
141
+ createWeatherLookupTool(),
140
142
  createReadFilesTool({ deniedRoots }),
141
143
  createFileGlobTool({ deniedRoots }),
142
144
  createGrepTool({ deniedRoots }),