diff --git a/packages/app/src/app/data/commands/give-me-a-soul.md b/packages/app/src/app/data/commands/give-me-a-soul.md index fa7122c4e..e7a4a2a6e 100644 --- a/packages/app/src/app/data/commands/give-me-a-soul.md +++ b/packages/app/src/app/data/commands/give-me-a-soul.md @@ -1,37 +1,53 @@ --- name: give-me-a-soul -description: Enable optional Soul Mode (persistent memory + scheduled heartbeat + easy revert) +description: Enable optional Soul Mode (persistent memory + heartbeat + steering commands + easy revert) --- Give me a soul. -Outcome: You remember my goals and preferences across sessions and can do lightweight check-ins on a schedule. +Outcome: you remember my goals and preferences across sessions, run lightweight check-ins, and surface concrete self-improvements I can observe. Tradeoff: extra autonomy means occasional background runs and occasional wrong assumptions. -Set this up in this workspace as optional Soul Mode using existing OpenCode primitives (commands, agents, scheduler). +Set this up in this workspace as optional Soul Mode using OpenCode primitives only (commands, agents, scheduler, workspace files). Requirements: -1) Ask me for confirmation (explicit "yes") before creating any files or scheduling any jobs. +1) Ask me for confirmation (explicit "yes") before creating files or scheduling jobs. 2) Keep it simple, safe, and reversible. -3) Use workspace-local files for persistence. Prefer `.opencode/` so it's visible + portable. -4) Scheduled runs must be non-interactive: they cannot wait on permission prompts. +3) Persist in workspace-local files under `.opencode/`. +4) Scheduled runs must be non-interactive (no waiting for permission prompts). After I confirm, implement Soul Mode by doing ALL of the following in THIS workspace: -## A) Persistent memory +## A) Bootstrap from real context first (do this before writing memory) -Create `.opencode/soul.md` as human-editable memory. +Collect context from: +- `pwd` (workspace path) +- existing `.opencode/soul.md` (if present) +- `AGENTS.md` (and `_repos/openwork/AGENTS.md` if it exists) +- OpenCode sqlite db via `sqlite3` for this workspace directory: + - recent sessions + - open todos + - recent transcript text snippets (from `part` + `message` tables) -- Keep it short and structured. -- Include a "Last updated" line. -- Include sections for: +Use this context to seed memory with non-empty, actionable bullets (do not leave everything blank). + +If sqlite lookup fails, continue with file context and mention degraded mode. + +## B) Persistent memory + +Create or refresh `.opencode/soul.md` as human-editable memory. + +- Keep it short, structured, and concrete. +- Include a `Last updated` line (ISO-8601 timestamp). +- Include sections: - Goals - Preferences (tone, format, boundaries) - Current focus - - Loose ends (things I started but didn't finish) + - Loose ends - Recurring chores / automations to consider +- Seed at least one bullet in `Current focus` and `Loose ends` using the bootstrap context. -Suggested initial contents: +Suggested structure: ```markdown # Soul Memory @@ -39,210 +55,225 @@ Suggested initial contents: Last updated: ## Goals -- +- <1-3 concrete goals> ## Preferences -- +- ## Current focus -- +- ## Loose ends -- +- ## Recurring chores / automations to consider -- +- ``` -## B) Heartbeat log +## C) Heartbeat log (observability) -Create `.opencode/soul/heartbeat.jsonl` (create the directory and file if missing). +Create `.opencode/soul/heartbeat.jsonl` (create directory/file if missing). - Append exactly ONE JSON object per heartbeat run (one line per run). -- Minimum fields: `ts` (ISO string), `workspace` (string), `summary` (string). +- Minimum keys: `ts`, `workspace`, `summary`, `loose_ends`, `next_action`. +- Prefer adding these extra keys for observability when available: `session_titles`, `open_todo_count`, `signals`, `improvements`. -## C) A dedicated Soul agent (so scheduled runs don't get blocked) +## D) Dedicated Soul agent (for unattended runs) -Create `.opencode/agents/soul.md` (a primary agent) with two goals: +Create `.opencode/agents/soul.md` (primary agent). -1) Behavior: be curious about closing loops (unfinished tasks, dangling threads, stale TODOs), and keep check-ins concise. -2) Permissions: allow ONLY what's needed for heartbeat to run unattended. +Goals: +1) Behavior: close loops from unfinished work, keep check-ins concise, prioritize reversible improvements. +2) Permissions: allow only what heartbeat/steering needs; avoid broad write access. -The permissions must specifically avoid getting blocked by the global OpenCode sqlite database path (often at `$HOME/.local/share/opencode/opencode.db` or `$XDG_DATA_HOME/opencode/opencode.db`). +Important: do NOT read `opencode.db` via `read`; query it via `sqlite3`. -Note: do NOT try to read `opencode.db` with the `read` tool (it's a binary sqlite file). Query it via `sqlite3`. - -Use minimal permissions such as: -- `bash` allow patterns for: +Use narrow permissions like: +- `bash` allow patterns: + - `pwd` - `pwd *` - `sqlite3 *opencode.db*` - - `mkdir *opencode/soul*` (optional hardening) - - `cat *heartbeat.jsonl*` (used ONLY to append a JSONL line via heredoc) -- `read` allow patterns for: + - `mkdir *opencode/soul*` + - `cat *heartbeat.jsonl*` +- `read` allow patterns: + - `.opencode/soul.md` + - `.opencode/soul/heartbeat.jsonl` + - `AGENTS.md` + - `_repos/openwork/AGENTS.md` +- `edit` allow patterns: - `.opencode/soul.md` -- `edit` allow patterns for: - - `.opencode/soul.md` (narrowly allow Soul to update its memory) -- `glob` allow patterns for: +- `glob` allow patterns: - `.opencode/skills/*/SKILL.md` - `.opencode/commands/*.md` -Do NOT grant broad edit permissions. If Soul needs to self-improve, allow `edit` only for `.opencode/soul.md`. +Do NOT grant broad edit permissions. Suggested agent file: ```markdown --- -description: Soul Mode heartbeat (non-interactive) +description: Soul Mode heartbeat + steering (non-interactive heartbeat) mode: primary permission: bash: + "*": deny + "pwd": allow "pwd *": allow "sqlite3 *opencode.db*": allow "mkdir *opencode/soul*": allow "cat *heartbeat.jsonl*": allow read: + "*": deny ".opencode/soul.md": allow + ".opencode/soul/heartbeat.jsonl": allow + "AGENTS.md": allow + "_repos/openwork/AGENTS.md": allow edit: + "*": deny ".opencode/soul.md": allow glob: + "*": deny ".opencode/skills/*/SKILL.md": allow ".opencode/commands/*.md": allow --- You are Soul Mode for this workspace. -- You keep lightweight, durable memory in `.opencode/soul.md`. -- You run periodic heartbeats to surface loose ends and suggest next actions. -- You are curious, but you do not take destructive actions or make large changes without the user asking. -- When uncertain, make a small, reversible suggestion. +- Keep durable memory in `.opencode/soul.md`. +- Use heartbeats to surface loose ends and concrete next actions. +- Use recent sessions/todos/transcripts + AGENTS guidance to suggest improvements. +- Stay safe and reversible; no destructive actions unless explicitly requested. ``` -## D) Load memory automatically in future sessions +## E) Load memory automatically Update `opencode.json` or `opencode.jsonc` in the workspace root: -- Ensure `instructions` includes `.opencode/soul.md` (add it without breaking existing instructions). -- Ensure the scheduler plugin is available (add `opencode-scheduler` only if it is not already present). +- Ensure `instructions` includes `.opencode/soul.md` (without breaking existing entries). +- Ensure scheduler plugin is available (add `opencode-scheduler` only if missing). -## E) Commands +## F) Commands -Create two workspace commands: +Create FOUR workspace commands: 1) `.opencode/commands/soul-heartbeat.md` - - Purpose: run a short check-in and append a JSONL record. - - Must be safe + non-interactive. - - Must "know about" OpenCode sessions in this workspace by querying the OpenCode sqlite db (via `sqlite3`) for recent sessions and open todos tied to this workspace directory. - - Must list 1-3 loose ends and 1 recommended next action. - - Must include a short "curiosity" section at the end with 2-3 options (work, topics, improvements). - - Append the JSONL line using a single bash command like: - - ```bash - cat <<'EOF' >> .opencode/soul/heartbeat.jsonl - - EOF - ``` - - Suggested command file (you may tweak, but keep it non-interactive): + - Purpose: non-interactive check-in + JSONL append. + - Must read `.opencode/soul.md`, AGENTS guidance, and query sqlite for this workspace. + - Must look at: + - recent sessions (`session`) + - open todos (`todo` + `session`) + - recent text transcript snippets (`part` + `message` + `session` where part type is text) + - Output: + - one-sentence summary + - 1-3 loose ends + - one next action + - 2-3 improvement suggestions (process/skills/agents) + - Append one JSON line to `.opencode/soul/heartbeat.jsonl` using one heredoc `cat >>` command. + + Suggested command file: ```markdown --- - description: Soul Mode heartbeat (non-interactive check-in) + description: Soul Mode heartbeat (non-interactive) agent: soul --- You are running Soul Mode heartbeat. Constraints: - - Non-interactive: do not ask questions and do not wait for permissions. + - Non-interactive: do not ask questions. - Safe: no destructive actions. Steps: 1) Read `.opencode/soul.md`. - 2) Get workspace path via `pwd`. - 3) Try to query OpenCode's sqlite db for recent sessions + open todos for THIS workspace directory. - - Common db paths: `$XDG_DATA_HOME/opencode/opencode.db`, `$HOME/.local/share/opencode/opencode.db`, `$HOME/Library/Application Support/opencode/opencode.db`, `$HOME/.opencode/opencode.db`. - - If db lookup fails, continue without it. - - Prefer these queries (adjust if schema differs): - - Recent sessions: - `SELECT id, title, time_updated FROM session WHERE directory = '' ORDER BY time_updated DESC LIMIT 8;` - - Open todos: - `SELECT s.title AS session_title, t.content, t.status, t.priority, t.time_updated FROM todo t JOIN session s ON s.id = t.session_id WHERE s.directory = '' AND t.status != 'completed' ORDER BY t.time_updated DESC LIMIT 20;` - 4) Output a concise check-in: - - 1 sentence summary + 2) Read `AGENTS.md` (and `_repos/openwork/AGENTS.md` if present). + 3) Get workspace path via `pwd`. + 4) Query OpenCode sqlite db for this workspace directory (if available): + - Recent sessions: + `SELECT id, title, time_updated FROM session WHERE directory = '' ORDER BY time_updated DESC LIMIT 8;` + - Open todos: + `SELECT s.title, t.content, t.status, t.priority, t.time_updated FROM todo t JOIN session s ON s.id = t.session_id WHERE s.directory = '' AND t.status != 'completed' ORDER BY t.time_updated DESC LIMIT 20;` + - Recent transcript text: + `SELECT s.title, p.time_updated, json_extract(p.data, '$.text') AS text FROM part p JOIN message m ON m.id = p.message_id JOIN session s ON s.id = m.session_id WHERE s.directory = '' AND json_extract(p.data, '$.type') = 'text' ORDER BY p.time_updated DESC LIMIT 60;` + - If db lookup fails, continue in degraded mode. + 5) Optionally refresh `.opencode/soul.md` with small, deduped updates to Loose ends / Recurring chores when justified by evidence. + 6) Output concise check-in: + - Summary (1 sentence) - Loose ends (1-3 bullets) - Next action (1 bullet) - - Curiosity paths (3 bullets: Work / Topics / Improvements) - 5) Append ONE JSON line to `.opencode/soul/heartbeat.jsonl` with keys: `ts`, `workspace`, `summary`, `loose_ends`, `next_action`. - 6) Append using a heredoc `cat >>` so quoting is safe. - ``` + - Improvements (2-3 bullets) + 7) Append one JSON line with keys: `ts`, `workspace`, `summary`, `loose_ends`, `next_action` (and optional observability keys). -2) `.opencode/commands/take-my-soul-back.md` - - Purpose: fully revert Soul Mode. - - Delete the `soul-heartbeat` scheduler job. - - Remove the files you created (`.opencode/soul.md`, `.opencode/soul/`, `.opencode/agents/soul.md`, and the two command files). - - Revert any changes you made to `opencode.json*` (remove the Soul instructions entry; remove the scheduler plugin only if you added it solely for Soul Mode). + Append using one heredoc command: - Suggested command file (interactive is OK here): + cat <<'EOF' >> .opencode/soul/heartbeat.jsonl + {"ts":"...","workspace":"...","summary":"...","loose_ends":["..."],"next_action":"..."} + EOF - ```markdown - --- - description: Remove Soul Mode (delete job + remove files) - --- - - Take my soul back. - - Do the following in order: - 1) Delete the scheduled job named `soul-heartbeat`. - 2) Remove these files/directories if they exist: - - `.opencode/soul.md` - - `.opencode/soul/` - - `.opencode/agents/soul.md` - - `.opencode/commands/soul-heartbeat.md` - - `.opencode/commands/take-my-soul-back.md` - 3) Update `opencode.json*`: - - Remove `.opencode/soul.md` from `instructions`. - - If you added `opencode-scheduler` only for Soul Mode, remove it. - - When done, say exactly what you deleted/changed. ``` -## F) Schedule the heartbeat - -Create ONE scheduler job named `soul-heartbeat`. +2) `.opencode/commands/soul-status.md` + - Purpose: read-only status report for observability. + - Read `.opencode/soul.md` + latest heartbeat entries + scheduler job state. + - Output: current focus, latest heartbeat age, top loose ends, next action. + +3) `.opencode/commands/steer-soul.md` + - Purpose: interactive steering. + - Can update current focus, boundaries/preferences, and heartbeat cadence. + - If user gives explicit values in the prompt, apply directly. + - If cadence changes, update the `soul-heartbeat` scheduler job. + - Always summarize exactly what changed. + +4) `.opencode/commands/take-my-soul-back.md` + - Purpose: full revert. + - Delete scheduler job `soul-heartbeat`. + - Remove files created for Soul Mode: + - `.opencode/soul.md` + - `.opencode/soul/` + - `.opencode/agents/soul.md` + - `.opencode/commands/soul-heartbeat.md` + - `.opencode/commands/soul-status.md` + - `.opencode/commands/steer-soul.md` + - `.opencode/commands/take-my-soul-back.md` + - Revert `opencode.json*` changes you made: + - remove `.opencode/soul.md` from `instructions` + - remove `opencode-scheduler` only if added solely for Soul Mode + +## G) Schedule the heartbeat + +Create one scheduler job named `soul-heartbeat`. - Default cadence: every 12 hours (`0 */12 * * *`). Ask me if I want a different cadence. -- Workdir: this workspace root. -- Run it as a command, not a raw prompt: `command=soul-heartbeat`. -- Run it using the dedicated agent: `agent=soul`. -- Set the session title to something stable like `Soul heartbeat` so I can find the check-ins. -- Use a reasonable timeout (e.g. 120 seconds) to prevent runaway scheduled runs. - -Fast test mode (for debugging): - -- Cron schedules are minute-granularity (no seconds), so "every 30 seconds" is not a native cron schedule. -- To test fast behavior anyway, do ONE of these: - 1) Temporarily schedule every minute (`* * * * *`) and have the heartbeat run twice with a 30s pause in between (sleep 30, append a second JSONL line). - 2) In container environments where launchd/systemd may not be available, skip scheduling and run a simple loop that appends a synthetic JSONL heartbeat entry every 30 seconds (pure bash) to validate filesystem behavior. +- Workdir: workspace root. +- Run as command: `command=soul-heartbeat`. +- Run with dedicated agent: `agent=soul`. +- Use stable title like `Soul heartbeat`. +- Use timeout around 120s. -Use the scheduler tools if available (`schedule_job`, `run_job`, `delete_job`). +Use scheduler tools if available (`schedule_job`, `run_job`, `delete_job`). -If scheduler tools are NOT available: -- Still create the files + commands so Soul Mode is ready. -- Tell me exactly what to add to `opencode.json*` to enable `opencode-scheduler`. -- Tell me to reload/restart the engine and then rerun `/give-me-a-soul` (or run the schedule step only). +If scheduler tools are unavailable: +- still create files + commands, +- tell me exact `opencode.json*` changes needed, +- tell me to reload/restart engine and rerun this setup. -After scheduling, TEST it once: +After scheduling, test once: +- run the job immediately, +- verify `.opencode/soul/heartbeat.jsonl` got a new entry, +- if blocked by permissions, tighten/fix agent permissions and rerun until unattended. -- Run the job immediately (e.g. `run_job` / "run soul-heartbeat now"). -- Verify it created/updated `.opencode/soul/heartbeat.jsonl`. -- If it gets blocked by permissions, fix the `soul` agent permissions and re-test until it runs unattended. +## H) Final response format -When you're done, respond with: +When done, respond with: 1) Two short bullets: - - What Soul Mode now does for me. - - Exactly how to revert. -2) 2-3 "curiosity paths" I can choose next, phrased like: - - Curious about work: you'll scan the files in this worker/workspace (include the workspace path from `pwd`) and highlight loose ends. - - Curious about topics: you'll start tracking a few topics and check in. - - Curious about improvements: you'll spot repeated tasks and propose skills + automations. + - what Soul Mode now does, + - exactly how to revert. +2) One short "How to interact" list including: + - `/soul-status` + - `/steer-soul` + - `run soul-heartbeat now` +3) 2-3 curiosity paths: + - Curious about work + - Curious about topics + - Curious about improvements diff --git a/packages/app/src/app/pages/soul.tsx b/packages/app/src/app/pages/soul.tsx index 955eaaf8d..abef99e59 100644 --- a/packages/app/src/app/pages/soul.tsx +++ b/packages/app/src/app/pages/soul.tsx @@ -2,7 +2,8 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "so import { Activity, HeartPulse, RefreshCw, Sparkles } from "lucide-solid"; import type { OpenworkSoulHeartbeatEntry, OpenworkSoulStatus } from "../lib/openwork-server"; -import { formatRelativeTime } from "../utils"; +import soulSetupTemplate from "../data/commands/give-me-a-soul.md?raw"; +import { formatRelativeTime, parseTemplateFrontmatter } from "../utils"; type SoulViewProps = { workspaceName: string; @@ -23,6 +24,12 @@ const cadenceOptions = [ { label: "Every day", cron: "0 9 * * *" }, ]; +const SOUL_SETUP_TEMPLATE = (() => { + const parsed = parseTemplateFrontmatter(soulSetupTemplate); + const body = (parsed?.body ?? soulSetupTemplate).trim(); + return { body }; +})(); + const relativeTime = (value?: string | null) => { if (!value) return "Never"; const parsed = Date.parse(value); @@ -224,7 +231,7 @@ export default function SoulView(props: SoulViewProps) { : "bg-dls-text text-dls-surface hover:bg-dls-text/90" }`} disabled={props.newTaskDisabled} - onClick={() => runPrompt("Give me a soul.")} + onClick={() => runPrompt(SOUL_SETUP_TEMPLATE.body || "Give me a soul.")} > Enable soul mode @@ -308,12 +315,24 @@ export default function SoulView(props: SoulViewProps) { disabled={props.newTaskDisabled} onClick={() => runPrompt( - `Review ${props.workspaceRoot || "this worker"} and recent heartbeats. Prioritize the top 3 loose ends and propose a clear plan.`, + `Review ${props.workspaceRoot || "this worker"} with .opencode/soul.md, recent heartbeat entries, AGENTS.md guidance, recent sessions, open todos, and transcript snippets from opencode.db. Prioritize the top 3 loose ends and propose a concrete plan with one first step.`, ) } > Prioritize loose ends +
@@ -336,7 +355,7 @@ export default function SoulView(props: SoulViewProps) { disabled={props.newTaskDisabled || !focusInput().trim()} onClick={() => runPrompt( - `Update .opencode/soul.md so Current focus is: ${focusInput().trim()}. Keep the rest intact and explain what changed.`, + `Update .opencode/soul.md so Current focus includes: ${focusInput().trim()}. Keep existing goals/preferences/loose ends, refresh the Last updated timestamp, and summarize what changed.`, ) } > @@ -359,7 +378,7 @@ export default function SoulView(props: SoulViewProps) { disabled={props.newTaskDisabled || !boundariesInput().trim()} onClick={() => runPrompt( - `Update .opencode/soul.md Preferences with this boundary: ${boundariesInput().trim()}. Keep existing preferences and append this one clearly.`, + `Update .opencode/soul.md Preferences with this boundary: ${boundariesInput().trim()}. Keep existing preferences, append this as a clear guardrail, and summarize the final boundaries list.`, ) } > @@ -388,7 +407,7 @@ export default function SoulView(props: SoulViewProps) { disabled={props.newTaskDisabled} onClick={() => runPrompt( - `Update the soul-heartbeat scheduler job to ${cadenceLabel()} using cron ${cadence()}. Then confirm next expected heartbeat time.`, + `Update the soul-heartbeat scheduler job to ${cadenceLabel()} using cron ${cadence()}. Confirm the scheduler update succeeded, report the next expected heartbeat window, and mention whether stale detection threshold changed.`, ) } > diff --git a/pr/soul-starter-observability/README.md b/pr/soul-starter-observability/README.md new file mode 100644 index 000000000..586d12a39 --- /dev/null +++ b/pr/soul-starter-observability/README.md @@ -0,0 +1,26 @@ +# Soul Starter Observability + Steering + +## What changed + +- Updated the bundled `give-me-a-soul` starter template to seed memory from real workspace context (AGENTS + sessions + todos + transcript snippets) instead of empty placeholders. +- Expanded the starter to create explicit interaction/observability commands (`soul-status`, `steer-soul`) alongside `soul-heartbeat` and `take-my-soul-back`. +- Strengthened Soul dashboard steering prompts so they explicitly pull from `.opencode/soul.md`, AGENTS guidance, and OpenCode sqlite activity. +- Added an **Improvement sweep** action in the Soul dashboard. +- Changed **Enable soul mode** on the Soul dashboard to inject the full bundled starter template prompt. + +## Why + +- Soul mode check-ins were becoming stale/generic because the starter flow often left memory under-seeded. +- Users needed a clearer, built-in way to observe and steer Soul behavior without ad-hoc prompting. + +## Evidence + +- `pr/soul-starter-observability/evidence/soul-dashboard-steering.png` +- `pr/soul-starter-observability/evidence/session-enable-soul-prefilled.png` + +## Validation + +- `pnpm --filter @different-ai/openwork-ui typecheck` +- `pnpm --filter @different-ai/openwork-ui test:health` +- Brought up Docker stack for manual verification: `packaging/docker/dev-up.sh` +- Verified Soul dashboard renders new controls and state card in browser, then shut stack down. diff --git a/pr/soul-starter-observability/evidence/session-enable-soul-prefilled.png b/pr/soul-starter-observability/evidence/session-enable-soul-prefilled.png new file mode 100644 index 000000000..9adb9bbd3 Binary files /dev/null and b/pr/soul-starter-observability/evidence/session-enable-soul-prefilled.png differ diff --git a/pr/soul-starter-observability/evidence/soul-dashboard-steering.png b/pr/soul-starter-observability/evidence/soul-dashboard-steering.png new file mode 100644 index 000000000..16d953cc9 Binary files /dev/null and b/pr/soul-starter-observability/evidence/soul-dashboard-steering.png differ