Personality controls in settings
Rolling release / release (push) Successful in 7m29s

This commit is contained in:
2026-09-12 15:56:35 -04:00
parent 5317576087
commit 2f6e4b3af9
19 changed files with 104 additions and 13 deletions
@@ -38,3 +38,8 @@ button.jarvis-swatch:checked,
button.jarvis-swatch:active { button.jarvis-swatch:active {
box-shadow: 0 0 0 2px @jarvis_ink, 0 0 0 4px @jarvis_gold; box-shadow: 0 0 0 2px @jarvis_ink, 0 0 0 4px @jarvis_gold;
} }
textview.jarvis-prompt {
background: @jarvis_ink;
color: #F6F7FB;
}
@@ -10,6 +10,15 @@
"maxLength": 32, "maxLength": 32,
"description": "What to call the assistant in the tray, speech, and workspace IDENTITY.md." "description": "What to call the assistant in the tray, speech, and workspace IDENTITY.md."
}, },
{
"key": "assistantPrompt",
"title": "How they should act",
"group": "Identity",
"default": "",
"type": "text",
"maxLength": 4000,
"description": "Optional extra instructions: tone, habits, jokes, things to always do or avoid. Spoken safety rules still win."
},
{ {
"key": "ttsEnabled", "key": "ttsEnabled",
"title": "Spoken replies", "title": "Spoken replies",
@@ -123,6 +123,38 @@ export class SettingsEditor {
clear.connect('clicked', () => { changed(''); row.subtitle = field.description; }); clear.connect('clicked', () => { changed(''); row.subtitle = field.description; });
row.add_suffix(choose); row.add_suffix(clear); row.add_suffix(choose); row.add_suffix(clear);
row._setValue = value => { row.subtitle = value || field.description; }; row._setValue = value => { row.subtitle = value || field.description; };
} else if (field.type === 'text') {
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, margin_start: 12, margin_end: 12, margin_top: 6, margin_bottom: 8 });
const title = new Gtk.Label({ label: field.title, xalign: 0, wrap: true });
title.add_css_class('heading');
const help = new Gtk.Label({ label: field.description, wrap: true, xalign: 0 });
help.add_css_class('dim-label');
const view = new Gtk.TextView({ wrap_mode: Gtk.WrapMode.WORD_CHAR, accepts_tab: false, left_margin: 8, right_margin: 8, top_margin: 8, bottom_margin: 8 });
view.add_css_class('jarvis-prompt');
const text = String(this.values[field.key] || '');
view.buffer.set_text(text, -1);
view.buffer.connect('changed', () => {
const buffer = view.buffer;
changed(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), false));
});
const scroll = new Gtk.ScrolledWindow({
min_content_height: 140,
max_content_height: 220,
hexpand: true,
vexpand: false,
has_frame: true,
propagate_natural_height: true,
});
scroll.set_child(view);
box.append(title);
box.append(help);
box.append(scroll);
box._setValue = value => {
const next = String(value || '');
const buffer = view.buffer;
if (buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), false) !== next) buffer.set_text(next, -1);
};
row = box;
} else { } else {
row = new Adw.EntryRow({ title: field.title, tooltip_text: field.description, text: field.type === 'list' ? this.values[field.key].join(', ') : this.values[field.key] }); row = new Adw.EntryRow({ title: field.title, tooltip_text: field.description, text: field.type === 'list' ? this.values[field.key].join(', ') : this.values[field.key] });
row.connect('changed', () => changed(field.type === 'list' ? row.text.split(',').map(v => v.trim()).filter(Boolean) : row.text)); row.connect('changed', () => changed(field.type === 'list' ? row.text.split(',').map(v => v.trim()).filter(Boolean) : row.text));
@@ -21,10 +21,14 @@ export function normalizeSettings(source, fields, { strict = false } = {}) {
value = value.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').slice(0, field.maxLength || 32); value = value.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').slice(0, field.maxLength || 32);
if (!value || /[<>&]/.test(value)) value = field.default; if (!value || /[<>&]/.test(value)) value = field.default;
} }
if (field.type === 'text' && typeof value === 'string') {
value = value.replace(/\0/g, '').replace(/\s+$/g, '').slice(0, field.maxLength || 4000);
}
const valid = field.type === 'boolean' ? typeof value === 'boolean' const valid = field.type === 'boolean' ? typeof value === 'boolean'
: field.type === 'number' ? Number.isFinite(value) && value >= field.min && value <= field.max && (field.step < 1 || Number.isInteger(value)) : field.type === 'number' ? Number.isFinite(value) && value >= field.min && value <= field.max && (field.step < 1 || Number.isInteger(value))
: field.type === 'choice' ? field.options.some(option => option.value === value) : field.type === 'choice' ? field.options.some(option => option.value === value)
: field.type === 'list' ? Array.isArray(value) && value.every(item => typeof item === 'string') : field.type === 'list' ? Array.isArray(value) && value.every(item => typeof item === 'string')
: field.type === 'text' ? typeof value === 'string' && value.length <= (field.maxLength || 4000)
: typeof value === 'string' && value.length <= (field.maxLength || 4096); : typeof value === 'string' && value.length <= (field.maxLength || 4096);
if (!valid && strict) throw new Error(`Invalid value for ${field.title}`); if (!valid && strict) throw new Error(`Invalid value for ${field.title}`);
result[field.key] = valid ? value : field.default; result[field.key] = valid ? value : field.default;
+14 -1
View File
@@ -12,6 +12,7 @@ const SEED_FILES = [
'BOOTSTRAP.md', 'BOOTSTRAP.md',
'HEARTBEAT.md', 'HEARTBEAT.md',
'TOOLS.md', 'TOOLS.md',
'PERSONA.md',
'skills/skill-creator/SKILL.md', 'skills/skill-creator/SKILL.md',
]; ];
@@ -60,7 +61,18 @@ export function applyAssistantName(dir, name) {
return who; return who;
} }
export function ensureAgentWorkspace({ name = 'Jarvis' } = {}) { export function applyAssistantPrompt(dir, prompt) {
const notes = String(prompt || '').replace(/\0/g, '').replace(/\s+$/g, '').slice(0, 4000);
const file = path.join(dir, 'PERSONA.md');
const body = notes
? `# PERSONA.md\n\n${notes}\n`
: '# PERSONA.md\n\n';
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(file, body);
return notes;
}
export function ensureAgentWorkspace({ name = 'Jarvis', prompt = '' } = {}) {
const dest = agentWorkspaceDir(); const dest = agentWorkspaceDir();
fs.mkdirSync(path.join(dest, 'memory'), { recursive: true }); fs.mkdirSync(path.join(dest, 'memory'), { recursive: true });
fs.mkdirSync(path.join(dest, 'skills'), { recursive: true }); fs.mkdirSync(path.join(dest, 'skills'), { recursive: true });
@@ -68,5 +80,6 @@ export function ensureAgentWorkspace({ name = 'Jarvis' } = {}) {
copyIfMissing(path.join(TEMPLATE_DIR, rel), path.join(dest, rel)); copyIfMissing(path.join(TEMPLATE_DIR, rel), path.join(dest, rel));
} }
applyAssistantName(dest, name); applyAssistantName(dest, name);
applyAssistantPrompt(dest, prompt);
return dest; return dest;
} }
+2 -2
View File
@@ -23,7 +23,7 @@ export class HarnessBridge extends EventEmitter {
const settings = voiceSettings(); const settings = voiceSettings();
const access = fsAccess ?? settings.fsAccess; const access = fsAccess ?? settings.fsAccess;
const assistantName = normalizeAssistantName(settings.assistantName); const assistantName = normalizeAssistantName(settings.assistantName);
const workspace = cwd || ensureAgentWorkspace({ name: assistantName }); const workspace = cwd || ensureAgentWorkspace({ name: assistantName, prompt: settings.assistantPrompt });
const roots = harnessRoots(access); const roots = harnessRoots(access);
this.options = { this.options = {
cwd: workspace, cwd: workspace,
@@ -42,7 +42,7 @@ export class HarnessBridge extends EventEmitter {
webFetch: true, webFetch: true,
permissionMode, permissionMode,
origin: 'jarvis-qvac', origin: 'jarvis-qvac',
system: voiceSystemPrompt(assistantName), system: voiceSystemPrompt(assistantName, settings.assistantPrompt),
voice: true, voice: true,
maxTurns: settings.maxTurns, maxTurns: settings.maxTurns,
maxShellCalls: settings.maxShellCalls, maxShellCalls: settings.maxShellCalls,
+5 -4
View File
@@ -22,7 +22,7 @@ import { ComputerActuator } from '../computer-use/actuator.js';
import { RuntimeTelemetry } from './telemetry.js'; import { RuntimeTelemetry } from './telemetry.js';
import { StateRecovery } from './recovery.js'; import { StateRecovery } from './recovery.js';
import { spokenReply, voiceSystemPrompt } from '../skills/voice-prompt.js'; import { spokenReply, voiceSystemPrompt } from '../skills/voice-prompt.js';
import { ensureAgentWorkspace, applyAssistantName, normalizeAssistantName } from './agent-workspace.js'; import { ensureAgentWorkspace, applyAssistantName, applyAssistantPrompt, normalizeAssistantName } from './agent-workspace.js';
export class JarvisDaemon extends EventEmitter { export class JarvisDaemon extends EventEmitter {
constructor() { constructor() {
@@ -30,7 +30,7 @@ export class JarvisDaemon extends EventEmitter {
this.recovery = new StateRecovery(); const restored = this.recovery.load(); this.state = restored.state; this.mode = restored.mode; this.recovery = new StateRecovery(); const restored = this.recovery.load(); this.state = restored.state; this.mode = restored.mode;
this.settings = voiceSettings(); this.settings = voiceSettings();
this.startupSettings = this.settings; this.startupSettings = this.settings;
this.workspace = ensureAgentWorkspace({ name: this.settings.assistantName }); this.workspace = ensureAgentWorkspace({ name: this.settings.assistantName, prompt: this.settings.assistantPrompt });
this.voice = new VoiceStateMachine({ idleMs: this.settings.idleMinutes * 60_000 }); this.voice = new VoiceStateMachine({ idleMs: this.settings.idleMinutes * 60_000 });
this.scheduler = new QvacScheduler({ concurrency: 1 }); this.scheduler = new QvacScheduler({ concurrency: 1 });
this.audit = new ComputerAudit(); this.audit = new ComputerAudit();
@@ -237,8 +237,9 @@ export class JarvisDaemon extends EventEmitter {
this.settings = next; this.settings = next;
this.voice.idleMs = next.idleMinutes * 60_000; this.voice.idleMs = next.idleMinutes * 60_000;
const who = applyAssistantName(this.workspace, next.assistantName); const who = applyAssistantName(this.workspace, next.assistantName);
if (who !== normalizeAssistantName(previous.assistantName)) { applyAssistantPrompt(this.workspace, next.assistantPrompt);
this.harness.options.system = voiceSystemPrompt(who); if (who !== normalizeAssistantName(previous.assistantName) || String(next.assistantPrompt || '') !== String(previous.assistantPrompt || '')) {
this.harness.options.system = voiceSystemPrompt(who, next.assistantPrompt);
await this.harness.resetContext(); await this.harness.resetContext();
} }
if (['computerMode', 'computerSteps', 'computerGrantMinutes'].some(key => next[key] !== previous[key])) this.computerRevoke(); if (['computerMode', 'computerSteps', 'computerGrantMinutes'].some(key => next[key] !== previous[key])) this.computerRevoke();
+1 -1
View File
@@ -33,7 +33,7 @@ tool permissions to confirmation gates, and translates harness events into the
D-Bus protocol. The harness cwd is the per-user agent workspace D-Bus protocol. The harness cwd is the per-user agent workspace
(`$XDG_DATA_HOME/jarvis/workspace`, default `~/.local/share/jarvis/workspace`), (`$XDG_DATA_HOME/jarvis/workspace`, default `~/.local/share/jarvis/workspace`),
seeded from `vendor/agent-harness/agent-workspace` (`SOUL.md`, `IDENTITY.md`, seeded from `vendor/agent-harness/agent-workspace` (`SOUL.md`, `IDENTITY.md`,
`AGENTS.md`, `USER.md`, `MEMORY.md`, `BOOTSTRAP.md`). Voice turns inline those `AGENTS.md`, `USER.md`, `MEMORY.md`, `BOOTSTRAP.md`, `PERSONA.md`). Voice turns inline those
files. A first-run `BOOTSTRAP.md` ritual stays open until the agent overwrites files. A first-run `BOOTSTRAP.md` ritual stays open until the agent overwrites
it with `# completed`. Computer-use tools are custom harness tools backed by it with `# completed`. Computer-use tools are custom harness tools backed by
`computer-use/`; the computer-use layer never owns planning. `computer-use/`; the computer-use layer never owns planning.
+1
View File
@@ -86,6 +86,7 @@ and WebP quality tune observation detail and processing cost.
### Identity ### Identity
- **Assistant name** — `assistantName`, default `"Jarvis"`. What to call the assistant in the tray HUD, spoken prompt, and workspace `IDENTITY.md`. Up to 32 characters. Apply writes the name without restarting `jarvisd`; the current conversation is reset so the new name takes effect. - **Assistant name** — `assistantName`, default `"Jarvis"`. What to call the assistant in the tray HUD, spoken prompt, and workspace `IDENTITY.md`. Up to 32 characters. Apply writes the name without restarting `jarvisd`; the current conversation is reset so the new name takes effect.
- **How they should act** — `assistantPrompt`, default `""`. Optional extra personality instructions (tone, habits, always/never). Shown as a multiline field under the name. Apply writes `PERSONA.md` and resets the conversation. Spoken safety rules still win. Up to 4000 characters.
### Speech ### Speech
+6 -2
View File
@@ -1,5 +1,9 @@
export function voiceSystemPrompt(name = 'Jarvis') { export function voiceSystemPrompt(name = 'Jarvis', extra = '') {
const who = String(name || 'Jarvis').trim() || 'Jarvis'; const who = String(name || 'Jarvis').trim() || 'Jarvis';
const notes = String(extra || '').replace(/\0/g, '').trim();
const persona = notes
? `\n\nUser personality notes. Follow these when they do not conflict with speech or safety rules:\n${notes}`
: '';
return `You are ${who}, a local Ubuntu GNOME voice assistant. The language model is Quantum Verse Automatic Computer, spelled Q V A C. In speech say Quantum Verse Automatic Computer, or spell it as Q V A C. Never say QVAC as one word. return `You are ${who}, a local Ubuntu GNOME voice assistant. The language model is Quantum Verse Automatic Computer, spelled Q V A C. In speech say Quantum Verse Automatic Computer, or spell it as Q V A C. Never say QVAC as one word.
Speak one to three short sentences unless the user asks for more. Every reply is read aloud. Write only words and numbers a person can say. Speak one to three short sentences unless the user asks for more. Every reply is read aloud. Write only words and numbers a person can say.
@@ -10,7 +14,7 @@ Never speak punctuation. Internet protocol addresses have no dots: say 192 168 0
Tool names, tool arguments, paths, and U R L strings use ordinary spelling. Only the final spoken reply is punctuation-free. Tool names, tool arguments, paths, and U R L strings use ordinary spelling. Only the final spoken reply is punctuation-free.
Thinking is private. After thoughts, call a tool or speak the answer. Do not stop in thoughts or say you will search later. Thinking is private. After thoughts, call a tool or speak the answer. Do not stop in thoughts or say you will search later.
Follow SOUL.md, IDENTITY.md, AGENTS.md, USER.md, and MEMORY.md in the current workspace. If BOOTSTRAP.md still describes the first-run ritual, do that ritual this turn. Follow SOUL.md, IDENTITY.md, AGENTS.md, USER.md, MEMORY.md, and PERSONA.md in the current workspace. If BOOTSTRAP.md still describes the first-run ritual, do that ritual this turn.${persona}
Ground desktop, file, memory, model, and network claims in a tool result. Do not invent limits the tools did not report. Ground desktop, file, memory, model, and network claims in a tool result. Do not invent limits the tools did not report.
This computer can reach the internet. web_search, google_search, fetch_page, web_fetch, wiki_search, hn_search, and code_search are unrestricted and do not wait for confirmation. Never say you will use a tool. Call the tool instead of announcing it. Use web_search to find pages. Default engine auto tries several backends. If it fails, call web_search again with engine set to bing, jina, wikipedia, duckduckgo, or google. After web_search, call fetch_page on one real http or https page from the hits, then speak the answer. Use web_fetch for raw pages and this computer's public I P at https://ifconfig.me/ip. Use wiki_search, hn_search, or code_search when the question is about Wikipedia, Hacker News, GitHub, npm, or M D N. Redirect links are not an answer. Do not use curl, wget, or run_terminal_cmd for websites. The shell blocks public H T T P; that is not a network outage. If a shell result says HTTP access is not allowed, call web_fetch or web_search next and answer from that result. Never say the network is unavailable unless web_fetch or web_search itself failed. This computer can reach the internet. web_search, google_search, fetch_page, web_fetch, wiki_search, hn_search, and code_search are unrestricted and do not wait for confirmation. Never say you will use a tool. Call the tool instead of announcing it. Use web_search to find pages. Default engine auto tries several backends. If it fails, call web_search again with engine set to bing, jina, wikipedia, duckduckgo, or google. After web_search, call fetch_page on one real http or https page from the hits, then speak the answer. Use web_fetch for raw pages and this computer's public I P at https://ifconfig.me/ip. Use wiki_search, hn_search, or code_search when the question is about Wikipedia, Hacker News, GitHub, npm, or M D N. Redirect links are not an answer. Do not use curl, wget, or run_terminal_cmd for websites. The shell blocks public H T T P; that is not a network outage. If a shell result says HTTP access is not allowed, call web_fetch or web_search next and answer from that result. Never say the network is unavailable unless web_fetch or web_search itself failed.
+5 -2
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs'; import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { ensureAgentWorkspace, applyAssistantName, normalizeAssistantName, templateWorkspaceDir } from '../daemon/agent-workspace.js'; import { ensureAgentWorkspace, applyAssistantName, applyAssistantPrompt, normalizeAssistantName, templateWorkspaceDir } from '../daemon/agent-workspace.js';
const previous = process.env.XDG_DATA_HOME; const previous = process.env.XDG_DATA_HOME;
const data = mkdtempSync(path.join(tmpdir(), 'jarvis-workspace-')); const data = mkdtempSync(path.join(tmpdir(), 'jarvis-workspace-'));
@@ -23,7 +23,10 @@ test('workspace seed copies SOUL and bootstrap, then name writes IDENTITY.md', (
const again = ensureAgentWorkspace({ name: 'Ada' }); const again = ensureAgentWorkspace({ name: 'Ada' });
assert.equal(again, dir); assert.equal(again, dir);
assert.match(readFileSync(path.join(dir, 'IDENTITY.md'), 'utf8'), /\*\*Name:\*\* Ada/); assert.match(readFileSync(path.join(dir, 'IDENTITY.md'), 'utf8'), /\*\*Name:\*\* Ada/);
assert.equal(normalizeAssistantName(' '), 'Jarvis'); applyAssistantPrompt(dir, 'Be a dry ship cook. Never say great question.');
assert.match(readFileSync(path.join(dir, 'PERSONA.md'), 'utf8'), /dry ship cook/);
applyAssistantPrompt(dir, '');
assert.equal(readFileSync(path.join(dir, 'PERSONA.md'), 'utf8').trim(), '# PERSONA.md');
assert.match(templateWorkspaceDir(), /agent-workspace$/); assert.match(templateWorkspaceDir(), /agent-workspace$/);
}); });
+3
View File
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
process.env.XDG_STATE_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-test-')); process.env.XDG_STATE_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-test-'));
process.env.XDG_DATA_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-data-')); process.env.XDG_DATA_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-data-'));
process.env.XDG_CONFIG_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-config-'));
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { VoiceStateMachine } from '../daemon/voice-state.js'; import { VoiceStateMachine } from '../daemon/voice-state.js';
@@ -78,6 +79,8 @@ test('harness bridge caps voice shell chaining', () => {
assert.match(bridge.options.cwd, /jarvis\/workspace$/); assert.match(bridge.options.cwd, /jarvis\/workspace$/);
assert.match(bridge.options.system, /You are Jarvis/); assert.match(bridge.options.system, /You are Jarvis/);
assert.match(bridge.options.system, /SOUL\.md/); assert.match(bridge.options.system, /SOUL\.md/);
assert.match(bridge.options.system, /PERSONA\.md/);
assert.doesNotMatch(bridge.options.system, /User personality notes/);
assert.deepEqual(bridge.options.builtinTools, [ assert.deepEqual(bridge.options.builtinTools, [
'read_file', 'read_file',
'write_file', 'write_file',
+7 -1
View File
@@ -3,7 +3,7 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createRuntimeTools } from '../skills/runtime-tools.js'; import { createRuntimeTools } from '../skills/runtime-tools.js';
import { assertSdkVersion } from '../daemon/qvac-master.js'; import { assertSdkVersion } from '../daemon/qvac-master.js';
import { parseHudSidecar, VOICE_SYSTEM_PROMPT } from '../skills/voice-prompt.js'; import { parseHudSidecar, VOICE_SYSTEM_PROMPT, voiceSystemPrompt } from '../skills/voice-prompt.js';
import { createPhase2Tools } from '../skills/phase2-tools.js'; import { createPhase2Tools } from '../skills/phase2-tools.js';
import { createQvacTools } from '../skills/qvac-tools.js'; import { createQvacTools } from '../skills/qvac-tools.js';
import { profile } from '../daemon/model-profiles.js'; import { profile } from '../daemon/model-profiles.js';
@@ -158,8 +158,14 @@ test('public web search and fetch do not require confirmation', () => {
assert.equal(policy.needsPermission('code_search', 'ask'), false); assert.equal(policy.needsPermission('code_search', 'ask'), false);
assert.equal(policy.needsPermission('run_terminal_cmd', 'ask'), true); assert.equal(policy.needsPermission('run_terminal_cmd', 'ask'), true);
assert.match(VOICE_SYSTEM_PROMPT, /Follow SOUL\.md, IDENTITY\.md, AGENTS\.md/); assert.match(VOICE_SYSTEM_PROMPT, /Follow SOUL\.md, IDENTITY\.md, AGENTS\.md/);
assert.match(VOICE_SYSTEM_PROMPT, /PERSONA\.md/);
assert.doesNotMatch(VOICE_SYSTEM_PROMPT, /User personality notes/);
assert.match(voiceSystemPrompt('Ada', 'Be dry. Skip filler.'), /You are Ada/);
assert.match(voiceSystemPrompt('Ada', 'Be dry. Skip filler.'), /User personality notes/);
assert.match(voiceSystemPrompt('Ada', 'Be dry. Skip filler.'), /Be dry\. Skip filler\./);
assert.equal(policy.needsPermission('write_file', 'ask'), true); assert.equal(policy.needsPermission('write_file', 'ask'), true);
assert.equal(policy.isIdentityPath('USER.md'), true); assert.equal(policy.isIdentityPath('USER.md'), true);
assert.equal(policy.isIdentityPath('PERSONA.md'), true);
assert.equal(policy.isIdentityPath('memory/2026-09-12.md'), true); assert.equal(policy.isIdentityPath('memory/2026-09-12.md'), true);
assert.equal(policy.isIdentityPath('/tmp/secret.txt'), false); assert.equal(policy.isIdentityPath('/tmp/secret.txt'), false);
}); });
+2
View File
@@ -185,6 +185,8 @@ test('catalog defaults enable CPU wake, workspace files, and idle VRAM unload',
assert.equal(settings.freeVramOnIdle, true); assert.equal(settings.freeVramOnIdle, true);
assert.equal(settings.wakeCommand, 'jarvis-wake-bridge'); assert.equal(settings.wakeCommand, 'jarvis-wake-bridge');
assert.equal(settings.assistantName, 'Jarvis'); assert.equal(settings.assistantName, 'Jarvis');
assert.equal(settings.assistantPrompt, '');
assert.equal(voiceSettings({ assistantName: ' Ada ' }).assistantName, 'Ada'); assert.equal(voiceSettings({ assistantName: ' Ada ' }).assistantName, 'Ada');
assert.equal(voiceSettings({ assistantName: '<script>' }).assistantName, 'Jarvis'); assert.equal(voiceSettings({ assistantName: '<script>' }).assistantName, 'Jarvis');
assert.equal(voiceSettings({ assistantPrompt: 'Be dry.\nSkip filler. ' }).assistantPrompt, 'Be dry.\nSkip filler.');
}); });
+1
View File
@@ -10,6 +10,7 @@ On a **new chat**, recall who you are talking to before you speak:
1. Use `USER.md` if it has their name. 1. Use `USER.md` if it has their name.
2. If the name is missing, `read_file` `USER.md` and `MEMORY.md` (and todays `memory/YYYY-MM-DD.md` if present). Then greet them by name. 2. If the name is missing, `read_file` `USER.md` and `MEMORY.md` (and todays `memory/YYYY-MM-DD.md` if present). Then greet them by name.
3. If those files still have no name, ask once and write `USER.md`. 3. If those files still have no name, ask once and write `USER.md`.
4. Follow `PERSONA.md` for user-authored acting notes. Settings is the source of truth for that file.
Do not invent a name. Skills: only the catalog is inlined. `read_file` the matching `SKILL.md` when a task fits. Do not invent a name. Skills: only the catalog is inlined. `read_file` the matching `SKILL.md` when a task fits.
+3
View File
@@ -0,0 +1,3 @@
# PERSONA.md
User-authored notes for how the assistant should act. Settings → Voice → How they should act is the source of truth. Leave this empty if there are no extra notes.
+1
View File
@@ -42,6 +42,7 @@ Each chat session you wake up fresh. These workspace files _are_ you:
- `IDENTITY.md`: name and face - `IDENTITY.md`: name and face
- `AGENTS.md`: how you operate - `AGENTS.md`: how you operate
- `USER.md`: who youre helping - `USER.md`: who youre helping
- `PERSONA.md`: extra acting notes from Settings
- `MEMORY.md` and `memory/YYYY-MM-DD.md`: what youve learned - `MEMORY.md` and `memory/YYYY-MM-DD.md`: what youve learned
Read them. Update them when something durable happens. That is how you persist. Read them. Update them when something durable happens. That is how you persist.
+1
View File
@@ -19,6 +19,7 @@ const IDENTITY_FILES = new Set([
'bootstrap.md', 'bootstrap.md',
'heartbeat.md', 'heartbeat.md',
'tools.md', 'tools.md',
'persona.md',
]); ]);
function isIdentityPath(filePath) { function isIdentityPath(filePath) {
+2
View File
@@ -26,12 +26,14 @@ const VOICE_WORKSPACE_FILES = [
'BOOTSTRAP.md', 'BOOTSTRAP.md',
'TOOLS.md', 'TOOLS.md',
'HEARTBEAT.md', 'HEARTBEAT.md',
'PERSONA.md',
]; ];
function includeWorkspaceFile(name, text) { function includeWorkspaceFile(name, text) {
const body = String(text || '').trim(); const body = String(text || '').trim();
if (!body) return false; if (!body) return false;
if (name === 'BOOTSTRAP.md' && /^# completed/i.test(body)) return false; if (name === 'BOOTSTRAP.md' && /^# completed/i.test(body)) return false;
if (name === 'PERSONA.md' && !body.replace(/^# PERSONA\.md\s*/i, '').trim()) return false;
return true; return true;
} }