feat(app): improve soul starter observability and steering

This commit is contained in:
Benjamin Shafii
2026-02-19 10:55:47 -08:00
parent 53cf726bad
commit a15e3c4f69
5 changed files with 215 additions and 139 deletions

View File

@@ -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: <ISO-8601 timestamp>
## Goals
-
- <1-3 concrete goals>
## Preferences
-
- <tone/format/boundary preference>
## Current focus
-
- <current initiative>
## Loose ends
-
- <unfinished thread>
## Recurring chores / automations to consider
-
- <repeatable task worth automating>
```
## 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`
- `edit` allow patterns for:
- `.opencode/soul.md` (narrowly allow Soul to update its memory)
- `glob` allow patterns for:
- `.opencode/soul/heartbeat.jsonl`
- `AGENTS.md`
- `_repos/openwork/AGENTS.md`
- `edit` allow patterns:
- `.opencode/soul.md`
- `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
<one-line-json>
EOF
```
- 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 (you may tweak, but keep it non-interactive):
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 = '<pwd>' 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 = '<pwd>' 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 = '<pwd>' 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 = '<pwd>' 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 = '<pwd>' 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).
Append using one heredoc command:
cat <<'EOF' >> .opencode/soul/heartbeat.jsonl
{"ts":"...","workspace":"...","summary":"...","loose_ends":["..."],"next_action":"..."}
EOF
```
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).
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.
Suggested command file (interactive is OK here):
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.
```markdown
---
description: Remove Soul Mode (delete job + remove files)
---
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
Take my soul back.
## G) Schedule the heartbeat
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`.
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.
- 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.
Fast test mode (for debugging):
Use scheduler tools if available (`schedule_job`, `run_job`, `delete_job`).
- 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.
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.
Use the scheduler tools if available (`schedule_job`, `run_job`, `delete_job`).
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.
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).
## H) Final response format
After scheduling, TEST it once:
- 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.
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

View File

@@ -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.")}
>
<Sparkles size={14} />
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
</button>
<button
type="button"
class="rounded-xl border border-dls-border px-3 py-2 text-left text-sm text-dls-text hover:bg-dls-hover disabled:opacity-60"
disabled={props.newTaskDisabled}
onClick={() =>
runPrompt(
"Run a Soul improvement sweep: read .opencode/soul.md and AGENTS.md, query recent sessions/todos/transcript text for this workspace from opencode.db, then propose 3 concrete improvements for process/skills/agents. If safe, update Loose ends and Recurring chores in .opencode/soul.md and explain every change.",
)
}
>
Improvement sweep
</button>
</div>
<div class={`rounded-xl border px-3 py-2 text-xs ${heartbeatStatusCardTone()}`}>
@@ -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.`,
)
}
>

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB