diff --git a/packages/app/pr/screenshots/soul-flow-a-empty-session.png b/packages/app/pr/screenshots/soul-flow-a-empty-session.png new file mode 100644 index 00000000..7e1f198d Binary files /dev/null and b/packages/app/pr/screenshots/soul-flow-a-empty-session.png differ diff --git a/packages/app/pr/screenshots/soul-flow-b-audit.png b/packages/app/pr/screenshots/soul-flow-b-audit.png new file mode 100644 index 00000000..1edfaf16 Binary files /dev/null and b/packages/app/pr/screenshots/soul-flow-b-audit.png differ diff --git a/packages/app/pr/screenshots/soul-flow-c-steering-trigger.png b/packages/app/pr/screenshots/soul-flow-c-steering-trigger.png new file mode 100644 index 00000000..510e9f09 Binary files /dev/null and b/packages/app/pr/screenshots/soul-flow-c-steering-trigger.png differ diff --git a/packages/app/pr/soul-flows-audit-and-steering.md b/packages/app/pr/soul-flows-audit-and-steering.md new file mode 100644 index 00000000..d61f02a8 --- /dev/null +++ b/packages/app/pr/soul-flows-audit-and-steering.md @@ -0,0 +1,34 @@ +# Soul Flows: Enable + Audit + Steering + +## What changed + +- Fixed **Give me a soul** from the empty-session starter so it now falls back to the bundled setup prompt body when the slash command is unavailable. +- Fixed Soul page enable flow so **Enable soul mode** also uses the bundled setup prompt body (instead of assuming `/` exists). +- Updated `runSoulPrompt` to create a session and immediately send the steering/setup prompt (instead of only prefilling composer text). +- Refreshed Soul UI layout to emphasize observability and steering: + - activation audit checklist (memory, instructions wiring, command, scheduler, log, heartbeat proof) + - clearer heartbeat proof panel + - steering checklist state + action triggers mapped to existing Soul actions only + +## Verified flows + +1. Empty session -> **Give me a soul** sends full setup prompt and starts the flow. +2. Soul tab -> **Enable soul mode** sends full setup prompt and starts the flow. +3. Soul setup completion updates Soul tab audit to passing checks and visible heartbeat proof. +4. Soul tab -> **Run heartbeat now** steering trigger opens a new task and sends the scheduler/heartbeat prompt. + +## Validation + +- `pnpm --filter @different-ai/openwork-ui typecheck` ✅ +- `pnpm --filter @different-ai/openwork-ui build` ✅ +- `pnpm --filter @different-ai/openwork-ui test:health` ⚠️ fails in this environment (`/global/health: Unauthorized`) +- Docker + Chrome MCP style verification via browser automation: + - started stack with `packaging/docker/dev-up.sh` + - validated both enable flows and steering trigger end-to-end in the running UI + - validated Soul audit reflects successful setup and heartbeat evidence + +## Evidence + +- `packages/app/pr/screenshots/soul-flow-a-empty-session.png` +- `packages/app/pr/screenshots/soul-flow-b-audit.png` +- `packages/app/pr/screenshots/soul-flow-c-steering-trigger.png` diff --git a/packages/app/src/app/app.tsx b/packages/app/src/app/app.tsx index 952ca673..52e85afb 100644 --- a/packages/app/src/app/app.tsx +++ b/packages/app/src/app/app.tsx @@ -3926,8 +3926,21 @@ export default function App() { function runSoulPrompt(promptText: string) { const text = promptText.trim(); if (!text) return; - setPrompt(text); - void createSessionAndOpen(); + void (async () => { + const sessionId = await createSessionAndOpen(); + if (!sessionId) { + setPrompt(text); + return; + } + + await sendPrompt({ + mode: "prompt", + text, + resolvedText: text, + parts: [{ type: "text", text }], + attachments: [], + }); + })(); } diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index 0a1fa106..ee819919 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -1967,10 +1967,10 @@ export default function SessionView(props: SessionViewProps) { return; } } catch { - // Fall back to slash-command setup below. + // Fall back to prompt-based setup below. } - const text = slashCommand; + const text = SOUL_SETUP_TEMPLATE.body || "Give me a soul."; handleSendPrompt({ mode: "prompt", text, @@ -2826,6 +2826,7 @@ export default function SessionView(props: SessionViewProps) {
Keep your goals and preferences across sessions with light scheduled check-ins. Tradeoff: more autonomy can create extra background runs, but revert is one command. + Audit setup and heartbeat evidence from the Soul section.
diff --git a/packages/app/src/app/pages/soul.tsx b/packages/app/src/app/pages/soul.tsx index f216cc05..0ab4d633 100644 --- a/packages/app/src/app/pages/soul.tsx +++ b/packages/app/src/app/pages/soul.tsx @@ -1,5 +1,5 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"; -import { Activity, HeartPulse, RefreshCw, Sparkles } from "lucide-solid"; +import { Activity, CheckCircle2, Circle, HeartPulse, RefreshCw, Sparkles } from "lucide-solid"; import type { OpenworkSoulHeartbeatEntry, OpenworkSoulStatus } from "../lib/openwork-server"; import soulSetupTemplate from "../data/commands/give-me-a-soul.md?raw"; @@ -27,7 +27,8 @@ const cadenceOptions = [ const SOUL_SETUP_TEMPLATE = (() => { const parsed = parseTemplateFrontmatter(soulSetupTemplate); const name = parsed?.data?.name?.trim() || "give-me-a-soul"; - return { name }; + const body = (parsed?.body ?? soulSetupTemplate).trim(); + return { name, body }; })(); const relativeTime = (value?: string | null) => { @@ -83,6 +84,94 @@ export default function SoulView(props: SoulViewProps) { props.runSoulPrompt(prompt); }; + const enableSoulPrompt = createMemo(() => { + const body = SOUL_SETUP_TEMPLATE.body.trim(); + if (body) return body; + return `/${SOUL_SETUP_TEMPLATE.name}`; + }); + + const latestHeartbeat = createMemo(() => props.heartbeats[0] ?? null); + + const setupAuditItems = createMemo(() => { + const status = props.status; + if (!status) { + return [ + { id: "memory", label: "Soul memory file", passed: false, detail: "Waiting for Soul status." }, + { id: "instructions", label: "Instructions wiring", passed: false, detail: "Waiting for Soul status." }, + { id: "command", label: "Heartbeat command", passed: false, detail: "Waiting for Soul status." }, + { id: "job", label: "Heartbeat schedule", passed: false, detail: "Waiting for Soul status." }, + { id: "log", label: "Heartbeat log", passed: false, detail: "Waiting for Soul status." }, + { id: "proof", label: "Recent heartbeat proof", passed: false, detail: "Run one heartbeat to verify setup." }, + ]; + } + + return [ + { + id: "memory", + label: "Soul memory file", + passed: status.memoryEnabled, + detail: status.memoryEnabled ? status.memoryPath : "Missing .opencode/soul.md", + }, + { + id: "instructions", + label: "Instructions wiring", + passed: status.instructionsEnabled, + detail: status.instructionsEnabled + ? "opencode config loads soul memory" + : "Add .opencode/soul.md to instructions", + }, + { + id: "command", + label: "Heartbeat command", + passed: status.heartbeatCommandExists, + detail: status.heartbeatCommandExists ? "/soul-heartbeat detected" : "Create /soul-heartbeat", + }, + { + id: "job", + label: "Heartbeat schedule", + passed: Boolean(status.heartbeatJob), + detail: status.heartbeatJob?.schedule || "No soul-heartbeat job", + }, + { + id: "log", + label: "Heartbeat log", + passed: status.heartbeatLogExists, + detail: status.heartbeatLogExists ? status.heartbeatPath : "Missing heartbeat.jsonl", + }, + { + id: "proof", + label: "Recent heartbeat proof", + passed: Boolean(status.lastHeartbeatAt), + detail: status.lastHeartbeatAt ? `Latest check-in ${relativeTime(status.lastHeartbeatAt)}` : "No check-ins yet", + }, + ]; + }); + + const steeringAudit = createMemo(() => { + const latest = latestHeartbeat(); + const looseEndCount = latest?.looseEnds.length ?? 0; + return [ + { + id: "heartbeat", + label: "Heartbeat captured", + passed: props.heartbeats.length > 0, + detail: latest?.ts ? `Latest ${relativeTime(latest.ts)}` : "Run heartbeat now", + }, + { + id: "loose-ends", + label: "Loose ends surfaced", + passed: looseEndCount > 0, + detail: looseEndCount > 0 ? `${looseEndCount} loose end${looseEndCount === 1 ? "" : "s"} tracked` : "No loose ends yet", + }, + { + id: "next-action", + label: "Next action ready", + passed: Boolean(latest?.nextAction), + detail: latest?.nextAction || "Generate one with the steering actions", + }, + ]; + }); + const clearHeartbeatTimers = () => { if (heartbeatPollTimer) { clearInterval(heartbeatPollTimer); @@ -173,7 +262,7 @@ export default function SoulView(props: SoulViewProps) {

- Track whether this worker has a soul, monitor heartbeat check-ins, and steer what Soul should focus on next. + Enable Soul from here, audit what is wired, and verify heartbeat proof before steering the next move.

+
+
+ Soul is currently off. Run setup once to create memory, scheduler wiring, and reversible commands for this worker. +
+ +
+ +
+
+

Soul activation audit

+
+ {setupAuditItems().filter((item) => item.passed).length}/{setupAuditItems().length} checks passing +
+
+
+ + {(item) => ( +
+
+ } + > + + +
+
{item.label}
+
{item.detail}
+
+
+
+ )} +
+
+
-

Follow up on heartbeats

-

Recent check-ins, loose ends, and next actions.

+

Heartbeat proof

+

Review recent check-ins, loose ends, and next actions.

Loading... @@ -252,38 +381,48 @@ export default function SoulView(props: SoulViewProps) {
0} + when={latestHeartbeat()} fallback={
- No heartbeat entries yet. Run `/soul-heartbeat` to create the first check-in. + No heartbeat entries yet. Run heartbeat now (or `/soul-heartbeat`) to create proof.
} > -
- + {(entry) => ( +
+
+ + Latest check-in {relativeTime(entry().ts)} +
+
{entry().summary}
+ +
+ Next: {entry().nextAction} +
+
+ 0}> +
+
Loose ends
+
    + + {(item) =>
  • - {item}
  • } +
    +
+
+
+
+ )} + + + 1}> +
+ {(entry) => ( -
-
-
- - {relativeTime(entry.ts)} -
-
+
+
{relativeTime(entry.ts)}
{entry.summary}
- 0}> -
-
Loose ends
-
    - - {(item) =>
  • - {item}
  • } -
    -
-
-
-
- Next: {entry.nextAction} -
+
Next: {entry.nextAction}
)} @@ -292,128 +431,151 @@ export default function SoulView(props: SoulViewProps) {
-
-
-

Steer soul

-

- Adjust focus, boundaries, and cadence. Actions open a task with the right steering prompt. -

-
- -
- - - -
+
+
+
+

Steering checklist

+

+ Trigger each steering step from here and confirm Soul outputs are visible in heartbeat proof. +

+
-
-
{heartbeatStatusTitle()}
-
{heartbeatRunMessage() || "Start a manual heartbeat and watch this card for live status."}
-
+
+ + {(item) => ( +
+ } + > + + +
+
{item.label}
+
{item.detail}
+
+
+ )} +
+
-
- - setFocusInput(event.currentTarget.value)} - placeholder="Ship soul UI for remote workers" - class="w-full rounded-xl border border-dls-border bg-dls-hover/40 px-3 py-2 text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none" - /> - -
+
+ + + +
-
- - setBoundariesInput(event.currentTarget.value)} - placeholder="Keep heartbeat concise and non-destructive" - class="w-full rounded-xl border border-dls-border bg-dls-hover/40 px-3 py-2 text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none" - /> - +
+
{heartbeatStatusTitle()}
+
{heartbeatRunMessage() || "Start a manual heartbeat and watch this card for live status."}
+
-
-
- - Heartbeat cadence -
-
- setFocusInput(event.currentTarget.value)} + placeholder="Ship soul UI for remote workers" + class="w-full rounded-xl border border-dls-border bg-dls-hover/40 px-3 py-2 text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none" + /> + +
+ +
+ + setBoundariesInput(event.currentTarget.value)} + placeholder="Keep heartbeat concise and non-destructive" + class="w-full rounded-xl border border-dls-border bg-dls-hover/40 px-3 py-2 text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none" + />
+ +
+
+ + Heartbeat cadence +
+
+ + +
+