first commit

This commit is contained in:
2026-09-11 13:37:34 -04:00
commit 0f00e11823
27 changed files with 802 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
qvac.config.local.json
.env
dist/
*.log
+38
View File
@@ -0,0 +1,38 @@
# QVAC integration pin
Pinned against the live QVAC documentation for SDK release **v0.19.0** on
2026-09-11. Source of truth: <https://docs.qvac.tether.io/reference/api/> and
<https://docs.qvac.tether.io/reference/release-notes/>.
## Rules for Jarvis
- Use the function-centric `@qvac/sdk` API at version `0.19.0`.
- Do not use removed `startQVACProvider`, `delegate`, or `no_mmap` APIs.
- Use `load_mode` for the replacement load behavior described by the release notes.
- Use `completion()` for streamed LLM work; its canonical surfaces are `events`
and `final`.
- Use `loadModel()` / `unloadModel()` for model lifecycle.
- Use `textToSpeech()` or `textToSpeechStream()` for TTS.
- Use `transcribe()` / `transcribeStream()` for ASR, `ocr()` for OCR,
`translate()` for text translation, and `assessModelFit()` before model fetch.
- Use `cancel({ requestId })` for a specific operation and `suspend()` /
`resume()` for runtime lifecycle.
- QVAC's local HTTP server is `qvac serve --openai`; it exposes OpenAI-compatible
`/v1/*` at `http://127.0.0.1:11434/v1/`. `qvac serve openai` is deprecated.
## v0.19 API names used by the capability registry
`completion`, `batchCompletion`, `embed`, `ragIngest`, `ragSearch`,
`ragReindex`, `ragDeleteEmbeddings`, `ragListWorkspaces`, `diffusion`,
`video`, `audioGen`, `transcribe`, `transcribeStream`,
`textToSpeech`, `textToSpeechStream`, `translate`, `ocr`, `classify`,
`bciTranscribe`, `vla`, `worldCreateScene`, `worldStep`, `assessModelFit`,
`downloadAsset`, `cancel`, `loadModel`, `unloadModel`, `suspend`, `resume`,
`getSystemResources`, `getLoadedModelInfo`, `getModelInfo`, `modelRegistryList`,
`modelRegistrySearch`, `modelRegistryGetModel`, and `state` are documented
public surfaces. Confirm parameter types against the installed `.d.ts` files
before implementing each skill.
The HTTP server mounts `/v1/chat/completions`, `/v1/responses`,
`/v1/completions`, `/v1/embeddings`, files/vector stores, image/video
generation, audio transcription/speech/translation, and `/qvac/v1/translate`.
+32
View File
@@ -0,0 +1,32 @@
# JARVIS-QVAC
Local-first Ubuntu GNOME voice assistant scaffold. The cognitive core is the
already-extracted `/home/raven/dev/agent-harness`, exposed through the symlink
`vendor/agent-harness`; Jarvis does not copy or depend on `/home/raven/dev/dlinux`.
## Bootstrap
```bash
npm install
npm test
npm run cu-doctor
npm run gpu-doctor
npx qvac doctor
npx qvac serve --openai --host 127.0.0.1
```
The daemon bridge loads the harness in-process with `Agent.engine.load()` and
creates sessions with `Agent.create()`. The optional QVAC HTTP sibling is
configured for `127.0.0.1:11434`. The GNOME extension is an intentionally thin
ESM panel shell; inference, audio, portals, and computer-use actuation belong
in user services.
GPU inference is a hard requirement. `gpu-doctor` must report a QVAC-visible
GPU and `jarvisd` refuses to continue if the loaded model reports `device: cpu`.
This prevents a misleading CPU mode and competing QVAC workers.
Current repository scope is phase-0/phase-1 bootstrap plus the computer-use
grant boundary. Portal ScreenCast/RemoteDesktop, AT-SPI, PipeWire audio, the
full D-Bus adapter, GTK control-center pages, and the voice state machine are
the next implementation slices. See [docs/agent-harness-map.md](docs/agent-harness-map.md)
and [docs/cu-acceptance.md](docs/cu-acceptance.md).
+6
View File
@@ -0,0 +1,6 @@
# Jarvis Control Center
This package is the GTK4/libadwaita control-plane boundary. Its pages are
General, Voice, Models, Memory, Skills, Computer use, Privacy, Lab, and About.
The daemon remains the owner of QVAC, audio, portals, and the harness; the UI
will communicate over session D-Bus.
+5
View File
@@ -0,0 +1,5 @@
{
"name": "@jarvis-qvac/control-center",
"private": true,
"type": "module"
}
@@ -0,0 +1,17 @@
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import St from 'gi://St';
export default class JarvisExtension extends Extension {
enable() {
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC');
this._indicator.add_child(new St.Label({ text: '◯', y_align: 2 }));
Main.panel.addToStatusArea('jarvis-qvac', this._indicator, 0, 'right');
}
disable() {
this._indicator?.destroy();
this._indicator = null;
}
}
@@ -0,0 +1,8 @@
{
"uuid": "[email protected]",
"name": "Jarvis QVAC",
"description": "Local-first QVAC voice assistant HUD for GNOME",
"shell-version": ["46", "47", "48", "49", "50"],
"version": 1,
"settings-schema": "org.gnome.shell.extensions.jarvis"
}
@@ -0,0 +1,7 @@
import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
export default class JarvisPreferences extends ExtensionPreferences {
fillPreferencesWindow(window) {
window.set_title('Jarvis QVAC');
}
}
@@ -0,0 +1,18 @@
<schemalist>
<schema id="org.gnome.shell.extensions.jarvis" path="/org/gnome/shell/extensions/jarvis/">
<key name="wake-phrase" type="s"><default>'hey jarvis'</default></key>
<key name="aliases" type="as"><default>['jarvis', 'okay jarvis']</default></key>
<key name="hotkey" type="s"><default>'&lt;Super&gt;&lt;Space&gt;'</default></key>
<key name="voice-id" type="s"><default>''</default></key>
<key name="language" type="s"><default>'en-US'</default></key>
<key name="privacy-mode" type="s"><default>'full-listen-after-wake'</default></key>
<key name="overlay-style" type="s"><default>'arc'</default></key>
<key name="confirm-destructive" type="b"><default>true</default></key>
<key name="computer-use-mode" type="s"><default>'off'</default></key>
<key name="computer-use-legacy-input" type="b"><default>false</default></key>
<key name="model-profile" type="s"><default>'laptop-8gb'</default></key>
<key name="accent-color" type="s"><default>'#F4B942'</default></key>
<key name="tts-enabled" type="b"><default>true</default></key>
<key name="chime-enabled" type="b"><default>true</default></key>
</schema>
</schemalist>
+22
View File
@@ -0,0 +1,22 @@
import { access } from 'node:fs/promises';
import { constants } from 'node:fs';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile);
const checks = [
['session', async () => process.env.XDG_SESSION_TYPE || 'unknown'],
['portal', async () => { await access('/usr/share/dbus-1/services/org.freedesktop.portal.Desktop.service', constants.F_OK); return 'installed'; }],
['pipewire', async () => { await run('sh', ['-lc', 'command -v pw-cat']); return 'installed'; }],
['at-spi', async () => { await run('sh', ['-lc', 'command -v gsettings']); return 'desktop tools present'; }],
['libei', async () => { await run('sh', ['-lc', 'ldconfig -p 2>/dev/null | grep -q libei']); return 'installed'; }],
['ydotool', async () => { await run('sh', ['-lc', 'command -v ydotool']); return 'optional fallback present'; }],
];
let failed = 0;
for (const [name, check] of checks) {
try { console.log(`ok ${name}: ${await check()}`); }
catch { failed += 1; console.log(`---- ${name}: unavailable`); }
}
console.log(failed ? `cu-doctor: ${failed} optional/required checks unavailable` : 'cu-doctor: all checks passed');
process.exitCode = failed ? 1 : 0;
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@jarvis-qvac/computer-use",
"private": true,
"type": "module",
"main": "session.js"
}
+48
View File
@@ -0,0 +1,48 @@
const MAX_STEPS = 100;
export class ComputerUseSession {
constructor({ stepsMax = 20, clock = () => Date.now() } = {}) {
this.clock = clock;
this.stepsMax = Math.min(MAX_STEPS, Math.max(1, stepsMax));
this.active = false;
this.stepsUsed = 0;
this.sessionId = null;
this.backend = 'none';
this.expiresAt = null;
}
grant({ persist = false, monitors = 'focused' } = {}) {
this.active = true;
this.stepsUsed = 0;
this.sessionId = `cu_${this.clock().toString(36)}`;
this.expiresAt = this.clock() + 3 * 60 * 1000;
return { session_id: this.sessionId, restore_token_present: Boolean(persist), monitors };
}
revoke() {
this.active = false;
this.sessionId = null;
this.expiresAt = null;
this.backend = 'none';
}
status() {
return {
active: this.active,
steps_used: this.stepsUsed,
steps_max: this.stepsMax,
grant_expires_at: this.expiresAt,
backend: this.backend,
};
}
beginStep() {
if (!this.active) throw new Error('computer-use grant is inactive');
if (this.expiresAt && this.clock() >= this.expiresAt) {
this.revoke();
throw new Error('computer-use grant expired');
}
if (this.stepsUsed >= this.stepsMax) throw new Error('computer-use step budget exhausted');
this.stepsUsed += 1;
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Agent } from './qvac-master.js';
try {
const resources = await Agent.engine.resources();
const hasGpu = Array.isArray(resources.gpus) && resources.gpus.length > 0;
console.log(JSON.stringify({
policy: 'gpu-required',
hasGpu,
backendHints: resources.drivers || {},
gpus: resources.gpus || [],
vramBytes: resources.vramBytes || 0,
}, null, 2));
if (!hasGpu) {
console.error('gpu-doctor: QVAC did not observe a usable GPU; Jarvis will refuse CPU inference');
process.exitCode = 1;
}
} catch (error) {
console.error(`gpu-doctor: ${error.message}`);
process.exitCode = 1;
}
+42
View File
@@ -0,0 +1,42 @@
import { EventEmitter } from 'node:events';
import { acquireQvac, closeQvac, releaseQvac, Agent } from './qvac-master.js';
export class HarnessBridge extends EventEmitter {
constructor({ cwd = process.cwd(), model = 'qwen3.5-4b', tools = [], permissionMode = 'ask' } = {}) {
super();
this.options = { cwd, model, tools, permissionMode, origin: 'jarvis-qvac', system: 'You are Jarvis on a local Ubuntu GNOME desktop. Keep spoken replies short and never claim cloud access.' };
this.session = null;
}
async start() {
if (this.session) return this.session;
if (process.env.JARVIS_QVAC_MODEL && this.options.model !== process.env.JARVIS_QVAC_MODEL) {
throw new Error(`Jarvis uses one QVAC master model (${process.env.JARVIS_QVAC_MODEL}); requested ${this.options.model}`);
}
await acquireQvac();
try {
this.session = await Agent.create(this.options);
} catch (error) {
releaseQvac();
await closeQvac();
throw error;
}
for (const event of ['agent_message_chunk', 'tool_call', 'permission', 'ask_user', 'cap-chunk', 'error']) {
this.session.on(event, (payload) => this.emit(event, payload));
}
return this.session;
}
async ask(text) {
if (!this.session) await this.start();
return this.session.prompt(text);
}
cancel() { this.session?.cancel(); }
async close() {
await this.session?.dispose();
releaseQvac();
await closeQvac();
}
}
+27
View File
@@ -0,0 +1,27 @@
import { EventEmitter } from 'node:events';
import { HarnessBridge } from './harness-bridge.js';
import { ComputerUseSession } from '../computer-use/session.js';
export class JarvisDaemon extends EventEmitter {
constructor() {
super();
this.state = 'ARMED';
this.computer = new ComputerUseSession();
this.harness = new HarnessBridge({ cwd: process.cwd() });
this.harness.on('agent_message_chunk', (ev) => this.emit('Token', ev?.text || ev?.delta || ''));
this.harness.on('permission', (ev) => this.emit('ConfirmationRequired', ev));
}
setState(state) { this.state = state; this.emit('StateChanged', state); }
async ask(text) { this.setState('THINKING'); try { const reply = await this.harness.ask(text); this.emit('Reply', reply); return reply; } finally { this.setState('LISTENING'); } }
cancel() { this.harness.cancel(); this.computer.revoke(); this.setState('ARMED'); }
computerGrant(persist = false) { const result = this.computer.grant({ persist }); this.emit('ComputerStep', JSON.stringify({ action: 'grant', ...result })); return result; }
computerRevoke() { this.computer.revoke(); this.emit('ComputerStep', JSON.stringify({ action: 'revoke' })); }
async close() { this.computerRevoke(); await this.harness.close(); }
}
if (import.meta.url === `file://${process.argv[1]}`) {
const daemon = new JarvisDaemon();
process.on('SIGINT', () => daemon.close().finally(() => process.exit(0)));
console.log('jarvisd scaffold ready; use the D-Bus adapter when installed');
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@jarvis-qvac/daemon",
"private": true,
"type": "module",
"main": "index.js"
}
+54
View File
@@ -0,0 +1,54 @@
import { createRequire } from 'node:module';
import path from 'node:path';
const harnessPath = process.env.JARVIS_HARNESS_PATH || '/home/raven/dev/agent-harness';
const require = createRequire(import.meta.url);
process.env.QVAC_CONFIG_PATH ||= path.resolve(new URL('../qvac.config.json', import.meta.url).pathname);
const Agent = require(path.join(harnessPath, 'index.js'));
let loadPromise = null;
let ownerCount = 0;
export const QVAC_MASTER = Object.freeze({
configPath: process.env.QVAC_CONFIG_PATH,
model: process.env.JARVIS_QVAC_MODEL || 'qwen3.5-4b',
device: 'gpu',
gpuLayers: 99,
});
export async function acquireQvac() {
ownerCount += 1;
if (!loadPromise) {
loadPromise = Agent.engine.load({
model: QVAC_MASTER.model,
tools: true,
device: 'gpu',
gpu_layers: QVAC_MASTER.gpuLayers,
mmprojUseGpu: true,
}).then((loaded) => {
if (loaded.device !== 'gpu') {
throw new Error(`Jarvis requires GPU QVAC inference; loaded device was ${loaded.device}`);
}
return loaded;
}).catch((error) => {
loadPromise = null;
ownerCount = Math.max(0, ownerCount - 1);
throw error;
});
}
return loadPromise;
}
export function releaseQvac() { ownerCount = Math.max(0, ownerCount - 1); }
export async function closeQvac() {
if (ownerCount > 0) return;
loadPromise = null;
await Agent.engine.close();
}
export function qvacStatus() {
return { ...QVAC_MASTER, owners: ownerCount, loaded: Agent.engine.getLoaded() };
}
export { Agent };
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-Bus Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="io.qvac.Jarvis.Session">
<method name="Arm"/><method name="Sleep"/><method name="Shutdown"/>
<method name="Say"><arg name="text" type="s" direction="in"/></method>
<method name="Ask"><arg name="text" type="s" direction="in"/></method>
<method name="Cancel"/><method name="SetMode"><arg name="mode" type="s" direction="in"/></method>
<method name="GetState"><arg type="s" direction="out"/></method>
<method name="ComputerGrant"><arg name="persist" type="b" direction="in"/></method>
<method name="ComputerRevoke"/><method name="ComputerStatus"><arg type="s" direction="out"/></method>
<signal name="StateChanged"><arg type="s"/></signal><signal name="Reply"><arg type="s"/></signal>
<signal name="Token"><arg type="s"/></signal><signal name="Error"><arg type="s"/><arg type="s"/></signal>
</interface>
</node>
+234
View File
@@ -0,0 +1,234 @@
# JARVIS-QVAC roadmap
This is the implementation tracker for the local Ubuntu GNOME assistant. The
roadmap is ordered by dependency: runtime authority first, then cognition,
voice, desktop observation, actuation, UI, capability coverage, hardening, and
packaging.
## Product contract
Jarvis is local-first and session-scoped. The GNOME Shell extension stays thin;
all inference, audio, jobs, memory, and computer-use decisions run outside
gnome-shell. The extracted `/home/raven/dev/agent-harness` remains the sole
cognitive loop and is wrapped rather than rewritten.
## Non-negotiable runtime architecture
- One user service, `jarvisd`, owns the QVAC SDK worker.
- One `QVAC_CONFIG_PATH` points every Jarvis process at the same
`qvac.config.json`.
- One model profile is active at a time. The QVAC master owns load, unload,
suspend, resume, cancellation, and model status.
- Harness sessions call the master; skills never import QVAC independently and
never call `loadModel()` directly.
- `qvac serve --openai` is an optional diagnostic/API surface and is never
started by `jarvisd`; it must not run beside the master in production.
- GPU is mandatory for Jarvis runtime inference. The bridge requests GPU,
enables all GPU layers and GPU multimodal projection, and rejects a load
that QVAC reports as CPU. There is no silent CPU fallback.
- Audio capture, TTS, OCR, embeddings, RAG, media jobs, and multimodal work
use the same QVAC master scheduler and obey a GPU memory budget.
- A capability may report `unavailable: gpu-required` without weakening the
GPU policy or starting a competing runtime.
## Status legend
- `[x]` implemented and locally verified
- `[~]` scaffolded or partially implemented
- `[ ]` planned
## Phase 0 — repository and authority bootstrap
- [x] Confirm `/home/raven/dev/agent-harness` exists.
- [x] Link `vendor/agent-harness` to the extracted harness.
- [x] Document harness layout, entrypoints, sessions, tools, memory, planner,
and model path in `docs/agent-harness-map.md`.
- [x] Pin the QVAC 0.19.0 API rules in `QVAC.md`.
- [x] Create Node 22.17+ workspaces.
- [x] Add one root `qvac.config.json` with localhost server settings and a
single model alias.
- [x] Add the initial daemon, computer-use, GNOME extension, D-Bus, and
control-center boundaries.
- [ ] Install and lock dependencies with a successful `npm install`.
- [ ] Add repository CI for syntax, unit, schema, and extension checks.
Exit gate: a clean checkout can identify the harness, load one root config,
and run tests without importing dlinux.
## Phase 1 — single GPU QVAC master
- [x] Add `daemon/qvac-master.js` as the only Jarvis QVAC owner.
- [x] Serialize model loading and share the resulting harness engine.
- [x] Request `device: "gpu"`, `gpu_layers: 99`, and GPU multimodal projection.
- [x] Reject a CPU result instead of accepting QVAC's internal fallback.
- [x] Add owner counting and a single close path.
- [x] Set `QVAC_CONFIG_PATH`, `JARVIS_QVAC_MODEL`, and GPU policy in the user
service.
- [~] Add `gpu-doctor`; extend it to report QVAC resource capabilities, backend,
driver, device name, VRAM, model fit, and the exact reason for failure.
- [ ] Add master scheduler lanes: interactive voice, computer-use vision,
background media, and maintenance.
- [ ] Add VRAM admission control and queue media jobs instead of OOMing the
voice lane.
- [ ] Add model profile selection using `assessModelFit()` before downloads.
- [ ] Add master-level cancellation by request ID and job ID.
- [ ] Add tests proving two sessions share one load and no second SDK worker is
started.
Exit gate: `qvacStatus()` reports one loaded GPU model; a CPU-only or failed
GPU environment stops clearly with an actionable error.
## Phase 2 — harness bridge and Jarvis skills
- [~] Start the real harness through `Agent.create()` and route token events.
- [ ] Add the voice-native system prompt and structured HUD sidecar.
- [ ] Register all Jarvis tools through the harness custom-tool registry.
- [ ] Implement permission classes: read, write, dangerous, and computer-use.
- [ ] Connect confirmation events to spoken confirmation and HUD controls.
- [ ] Add desktop tools: launch/list apps, focus/list windows, workspaces,
notify, screenshot, clipboard, media, settings, and focused text injection.
- [ ] Add file search/read/write with trash-first destructive handling.
- [ ] Add memory and RAG workspace tools.
- [ ] Add QVAC wrappers for embeddings, translation, OCR, classification,
image/video/music jobs, transcription, TTS, LoRA, BCI, VLA, and ABot-World.
- [ ] Ensure every wrapper obtains the master lease and never loads QVAC itself.
- [ ] Add fake-QVAC fixture tests for every tool schema and permission gate.
Exit gate: a typed prompt completes through the real harness, streams tokens,
executes a Jarvis tool, and returns a short local response through the same
GPU-owned worker.
## Phase 3 — daemon lifecycle and D-Bus
- [~] Define `io.qvac.Jarvis.Session` XML.
- [ ] Implement `Arm`, `Sleep`, `Shutdown`, `Say`, `Ask`, `Cancel`, `SetMode`,
and state queries.
- [ ] Implement token, transcript, reply, audio-level, chip, job, computer
step, and error signals.
- [ ] Keep payloads small; stream PCM and screenshots through a Unix socket or
tmpfs paths.
- [ ] Add lock-screen handling: mute, hide UI, revoke computer use.
- [ ] Add idle sleep using QVAC `suspend()` and resume on wake.
- [ ] Add structured JSON logging with no prompt/audio/image contents by default.
Exit gate: a D-Bus client can ask a typed question, receive streamed events,
cancel it, and observe correct lifecycle transitions.
## Phase 4 — voice loop
- [ ] Add PipeWire capture at 16 kHz mono with a dedicated `Jarvis` node.
- [ ] Add wake-word engine behind a `WakeEngine` interface.
- [ ] Add VAD segmentation and the documented QVAC ASR stream.
- [ ] Implement `ARMED → LISTENING → THINKING → SPEAKING → LISTENING`.
- [ ] Add transcript filtering, TTS anti-feedback gate, and playback cooldown.
- [ ] Add sentence buffering from streamed harness output into QVAC TTS.
- [ ] Add fast-path cancel, sleep, privacy, dictate, screen, and computer-use
commands.
- [ ] Add push-to-talk and typed fallback.
- [ ] Add wake false-accept/false-reject and feedback measurements.
Exit gate: “Hey Jarvis” starts a local GPU-backed turn, speaks a response, and
does not self-trigger from its own TTS.
## Phase 5 — GNOME ARC surface
- [~] Add GNOME 45+ ESM extension metadata, panel indicator, and settings keys.
- [ ] Add async D-Bus client and panel state glyphs.
- [ ] Add ARC overlay, waveform, transcript rows, chips, local/model status,
reduced motion, high contrast, keyboard navigation, and screen-reader labels.
- [ ] Add listening halo, privacy slash, job theater, target highlights, and
visible computer-use cursor.
- [ ] Keep all extension work nonblocking and free of QVAC/native inference.
Exit gate: enabling/disabling the extension never starts inference in the Shell
process and reflects daemon state without jank.
## Phase 6 — computer-use observe and semantic action
- [ ] Complete `cu-doctor`: Wayland/X11, portal, PipeWire, AT-SPI, libei, and
optional fallback detection.
- [ ] Implement portal ScreenCast/Screenshot and tmpfs frame normalization.
- [ ] Implement GNOME Shell window truth and focus methods.
- [ ] Implement AT-SPI tree snapshots with per-step stable refs.
- [ ] Implement OCR and QVAC multimodal observe bundles.
- [ ] Implement `cu.observe`, `cu.zoom`, `cu.tree`, and `cu.find`.
- [ ] Implement `cu.act` and semantic `cu.click`.
- [ ] Implement grant/revoke, expiry, step budget, audit hashes, and no-frame
retention by default.
Exit gate: “whats on my screen?” returns grounded local observations without
actuation.
## Phase 7 — computer-use portal actuation
- [ ] Implement RemoteDesktop portal consent and restore tokens.
- [ ] Implement libei/EIS pointer, keyboard, scroll, drag, hover, and key input.
- [ ] Implement typed Unicode and submit behavior.
- [ ] Add target preview, agent cursor, and step ticker.
- [ ] Prefer domain tools, app D-Bus, AT-SPI, Shell helper, then vision
coordinates in that order.
- [ ] Refuse password/PAM roles and lock-screen/greeter actions.
- [ ] Require confirmation for destructive or high-impact actions.
- [ ] Keep ydotool and X11 tools disabled unless explicitly enabled.
- [ ] Add state-change self-checks, animation waits, and no-progress aborts.
Exit gate: Night Light, Text Editor save, Firefox URL entry, hands-off abort,
and lock-screen kill pass `docs/cu-acceptance.md`.
## Phase 8 — GTK4/libadwaita control center
- [ ] Implement General, Voice, Models, Memory, Skills, Computer use, Privacy,
Lab, and About pages.
- [ ] Show the single QVAC master status, GPU backend, VRAM, model, fit result,
queue, and failure reason.
- [ ] Make every capability visible even when its model does not fit.
- [ ] Add model download pause/resume through the master.
- [ ] Add voice enrollment and local preview.
- [ ] Add RAG workspace management, retention controls, audit export/delete,
and computer-use permissions.
Exit gate: a user can configure the complete system without editing JSON.
## Phase 9 — full QVAC capability coverage
- [ ] Chat, plan, summarize, rewrite, code, embeddings, RAG, batch prompts.
- [ ] Multimodal screenshot/file analysis, OCR, classification.
- [ ] Image generation/editing, video jobs, music jobs.
- [ ] ASR, diarized meetings, TTS, voice clone enrollment, translation relay.
- [ ] LoRA training with explicit confirmation and overnight job controls.
- [ ] BCI, VLA, and ABot-World as clearly labeled Lab capabilities.
- [ ] Registry/model manager, checksums, fit assessments, profiler, and job
cancellation.
- [ ] Optional P2P model fetch and memory sync only after local mode is stable.
Exit gate: every capability in the product inventory has a working skill or a
truthful GPU/model-unavailable state.
## Phase 10 — reliability, privacy, and performance
- [ ] OOM isolation: failed media jobs cannot kill the voice lane.
- [ ] GPU telemetry: utilization, VRAM, queue wait, load time, tokens/sec,
ASR latency, TTS latency, and dropped audio.
- [ ] Wake, feedback, Wayland/X11, accessibility, lock/unlock, crash/restart,
and nested Shell smoke tests.
- [ ] Verify no network calls except explicitly enabled model fetch/P2P paths.
- [ ] Verify no screenshots, microphone buffers, prompts, or transcripts are
retained unless the user enables retention.
- [ ] Audit computer actions with target metadata and screenshot hashes only.
- [ ] Add graceful restart and state recovery for daemon crashes.
Exit gate: the full test matrix passes on supported Ubuntu GNOME sessions with
GPU inference visibly confirmed.
## Phase 11 — packaging and release
- [ ] Build a `.deb`, extension bundle, and user-service installer.
- [ ] Add first-run wizard for microphone, wake phrase, GPU/model profile, TTS,
and a typed smoke test.
- [ ] Add uninstall that removes only Jarvis-owned data after explicit choice.
- [ ] Publish a hardware compatibility matrix for Vulkan/GPU backends.
- [ ] Add release checklist, migration notes, and a reproducible local demo.
Definition of done: all product behaviors in the specification work on Ubuntu
GNOME with one GPU-backed QVAC master, one harness planner, explicit computer-
use grants, and truthful capability status.
+78
View File
@@ -0,0 +1,78 @@
# Agent harness map
Source of truth: `/home/raven/dev/agent-harness` (`~/dev/agent-harness`).
The extracted directory is not currently a git checkout, so Jarvis tracks it by
symlink and records the absolute source path instead of inventing a commit pin.
## Layout and runtime
The harness is a standalone Node.js CommonJS package. Its public entrypoint is
`index.js`, with the CLI at `bin/cli.js`. The README says Node 20 is enough for
the harness itself, while its QVAC inference dependency requires Node 22.17+;
Jarvis therefore requires Node 22.17+.
Important modules:
- `index.js`: public `Agent.create()` / `Agent.load()`, session wrapper, engine and catalog exports.
- `agent/loop.js`: sample → tool call → tool result → repeat loop, permissions, plan mode, compaction, memory, and subagents.
- `agent/custom-tools.js`: per-session JSON-schema tool registry and in-process `execute` handlers.
- `agent/tools.js`: built-in host workspace, memory, planning, web, task, and MCP tools.
- `agent/sessions.js`: persisted session summaries, history, updates, and plan files.
- `agent/memory.js`: local short/long-term memory notes.
- `lib/qvac.js`: lazy `@qvac/sdk` import, model loading, completion streaming, vision attachments, cancellation, and lifecycle close.
- `lib/catalog.js`: friendly model ids mapped to QVAC SDK constants.
## Start and embed
CLI: `node /home/raven/dev/agent-harness/bin/cli.js [options] [prompt]`.
Library integration uses CommonJS from the harness root:
```js
const Agent = require('/home/raven/dev/agent-harness');
await Agent.engine.load({ model: 'qwen3.5-4b', tools: true, device: 'auto' });
const session = await Agent.create({
cwd: process.cwd(),
model: 'qwen3.5-4b',
permissionMode: 'ask',
tools: [{ name, description, parameters, execute }],
});
await session.prompt('hello');
await session.dispose();
await Agent.engine.close();
```
Sessions emit `agent_message_chunk`, `tool_call`, `permission`, `ask_user`,
plan, context, and related loop events. Jarvis adapts the message and tool
events to its D-Bus contract. `session.permit()`, `session.answer()`,
`session.cancel()`, and `session.addTool()` are the control points.
## Tool registry and permissions
Jarvis registers tools through `Agent.create({ tools })`. Each custom tool is a
JSON-schema object with an optional in-process `execute(args)` handler. The
harness caps custom tools at 32 and reserves its built-ins. Permission mode is
`ask` by default; write and dangerous desktop/computer-use tools remain behind
Jarvis confirmation gates.
## Session, memory, and planner
`agent/loop.js` owns the multi-turn agent loop and repeats completion/tool
execution until a final response. `sessions.js` persists conversation state.
`memory.js` provides local memory tools. `plan-mode.js`, `compaction.js`,
`goal.js`, and `tasks.js` provide planning, context management, goals, and
subagents. Jarvis does not create a second planner.
## Model connection
The harness does not use an OpenAI-compatible HTTP URL internally. Its
`lib/qvac.js` imports `@qvac/sdk` in-process and calls `loadModel()` and
`completion()`. Jarvis keeps the optional sibling `qvac serve --openai`
provider for external clients and diagnostics, while the cognition bridge
wraps the harness in-process as required by the extracted package.
## Computer use
The inspected harness has no first-class portal, AT-SPI, libei, or desktop
computer-use backend. Jarvis therefore supplies those as custom tools from
`computer-use/` and `skills/`, while keeping planning in `agent/loop.js`.
+12
View File
@@ -0,0 +1,12 @@
# Computer-use acceptance
Run these on Ubuntu GNOME after `npm run cu-doctor` reports the required portal
and PipeWire services. The current scaffold only exposes the grant/session
budget boundary; portal, AT-SPI, and libei adapters are the next implementation
step.
1. Observe Settings and toggle Night Light.
2. Type a sentence into Text Editor and save it.
3. Open Firefox, locate the address bar, type a URL, and press Enter.
4. Say “hands off” during a run; all actuation must stop.
5. Lock the screen during a run; the grant must die immediately.
+23
View File
@@ -0,0 +1,23 @@
{
"name": "jarvis-qvac",
"version": "0.1.0",
"private": true,
"description": "Local-first GNOME voice assistant powered by QVAC and agent-harness.",
"type": "module",
"engines": { "node": ">=22.17.0" },
"workspaces": ["daemon", "computer-use", "apps/control-center"],
"scripts": {
"start": "node daemon/index.js",
"test": "node --test",
"cu-doctor": "node computer-use/doctor.js",
"gpu-doctor": "node daemon/gpu-doctor.js",
"qvac:doctor": "qvac doctor",
"qvac:serve": "qvac serve --openai --host 127.0.0.1",
"qvac:status": "node -e \"import('./daemon/qvac-master.js').then(({qvacStatus})=>console.log(JSON.stringify(qvacStatus(), null, 2)))\""
},
"dependencies": {
"@qvac/cli": "0.19.0",
"@qvac/sdk": "0.19.0",
"dbus-next": "^0.10.2"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"cacheDirectory": "/home/raven/.cache/jarvis/models",
"serve": {
"host": "127.0.0.1",
"port": 11434,
"models": {
"jarvis-chat": {
"model": "QWEN3_600M_INST_Q4",
"default": true,
"preload": false,
"config": { "ctx_size": 8192 }
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
export const SKILLS = [
['chat', 'Local QVAC completion through the agent harness', 'read'],
['plan', 'Multi-step planning through the extracted harness loop', 'read'],
['embed', 'QVAC embeddings for local memory and retrieval', 'read'],
['remember', 'QVAC RAG workspace ingestion and search', 'write'],
['look-at-this', 'Local screenshot and QVAC multimodal analysis', 'read'],
['dictate', 'Transcribe and inject text into the focused client', 'write'],
['translate', 'Local QVAC/Bergamot translation', 'read'],
['imagine', 'Local QVAC diffusion image generation', 'write'],
['make-video', 'Local QVAC video generation job', 'write'],
['compose', 'Local QVAC AudioGen music job', 'write'],
['speak', 'Local QVAC text-to-speech playback', 'read'],
['assess-model-fit', 'Preflight model memory fit before download', 'read'],
['cu.observe', 'Wayland-first desktop observation bundle', 'computer-use'],
['cu.act', 'AT-SPI semantic desktop action', 'computer-use'],
['cu.click', 'Portal/libei or opted-in fallback click', 'computer-use'],
['cu.type', 'Portal/libei or opted-in fallback Unicode typing', 'computer-use'],
['cu.grant', 'Explicit computer-use grant', 'computer-use'],
['cu.revoke', 'Immediate computer-use revoke', 'dangerous'],
];
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Jarvis QVAC local voice assistant
After=pipewire.service wireplumber.service graphical-session.target
Wants=pipewire.service
[Service]
Type=simple
WorkingDirectory=%h/dev/jarvis
ExecStart=/usr/bin/node %h/dev/jarvis/daemon/index.js
Environment=QVAC_CONFIG_PATH=%h/dev/jarvis/qvac.config.json
Environment=JARVIS_GPU_REQUIRED=1
Environment=JARVIS_QVAC_MODEL=qwen3.5-4b
Environment=JARVIS_QVAC_OWNER=jarvisd
Restart=on-failure
RestartSec=2
[Install]
WantedBy=graphical-session.target
+26
View File
@@ -0,0 +1,26 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { ComputerUseSession } from '../computer-use/session.js';
test('computer use requires an explicit grant and enforces its step budget', () => {
let now = 1000;
const session = new ComputerUseSession({ stepsMax: 2, clock: () => now });
assert.throws(() => session.beginStep(), /inactive/);
assert.deepEqual(session.grant({ persist: true }).restore_token_present, true);
session.beginStep();
session.beginStep();
assert.throws(() => session.beginStep(), /budget/);
assert.equal(session.status().steps_used, 2);
session.revoke();
assert.equal(session.status().active, false);
now += 1;
});
test('computer use expires its wall clock grant', () => {
let now = 0;
const session = new ComputerUseSession({ clock: () => now });
session.grant();
now = 180001;
assert.throws(() => session.beginStep(), /expired/);
assert.equal(session.status().backend, 'none');
});
Vendored Symlink
+1
View File
@@ -0,0 +1 @@
/home/raven/dev/agent-harness