Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .claude/commands/ship-and-babysit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
description: Commit, push to origin (fork), open PR to tinyhumansai/openhuman:main, then poll every ~5min for CodeRabbit comments and CI failures, resolve them, and exit when clean.
allowed-tools: Bash, Read, Edit, Write, Agent, Skill
---

You are running an end-to-end ship-and-babysit flow for the **openhuman** repo. Follow these phases in order. Be concise in user-facing text — one short sentence per phase transition is enough.

Repo facts (from `CLAUDE.md`):
- Upstream: `tinyhumansai/openhuman` (not a fork). PRs target **`main`**.
- Push branches to **`origin`** (the user's own fork of `tinyhumansai/openhuman`). Treat `upstream` as fetch-only.
- PRs are opened with `--head <fork-owner>:<branch>` against `tinyhumansai/openhuman:main`.
- PR template: `.github/PULL_REQUEST_TEMPLATE.md`. Issue templates under `.github/ISSUE_TEMPLATE/`.

**Resolve the fork owner once at the start** and reuse it for the rest of the flow:
```bash
FORK_OWNER=$(git remote get-url origin | sed -E 's#.*[:/]([^/]+)/[^/]+(\.git)?$#\1#')
```
The flow is **fork-only**: `origin` must be the user's fork. If `origin` resolves to `tinyhumansai` (the upstream org), stop and ask the user to add a fork remote — never push branches to the upstream repo.

## Phase 1 — Commit

1. Run `git status`, `git diff` (staged + unstaged), and recent `git log` in parallel to understand pending changes and the repo's commit message style.
2. If there are no changes to commit AND the branch is already pushed AND a PR already exists, skip to Phase 4.
3. If there are uncommitted changes, stage relevant files (avoid secrets / large binaries / `.env`), then create a commit using a conventional prefix (`feat:`, `fix:`, `refactor:`, `chore:`, `docs:`, `test:`). Use a HEREDOC for the message.
4. Never use `--no-verify` to bypass commit hooks for your own changes. If a hook fails on your changes, fix the underlying issue and create a NEW commit (do not amend pushed commits).

## Phase 2 — Push

1. Determine current branch with `git rev-parse --abbrev-ref HEAD`. Confirm it follows the `feat/|fix/|refactor/|chore/|docs/|test/` prefix convention. Never push directly to `main`. If the branch doesn't match the convention, stop and ask the user to either rename it or confirm the deviation — don't auto-rename pushed branches.
2. Push to **`origin`** with `-u` if upstream tracking is missing. Never push to `upstream`. Never force-push to `main`.
3. **Pre-push hook policy** (per `CLAUDE.md`): if a pre-push hook fails on something unrelated to your changes (pre-existing breakage on `main` in code you didn't touch), push with `--no-verify` and call it out in the PR body. If the hook fails on your own changes, fix and re-push. Don't ask — just do the right thing and tell the user what you did.

## Phase 3 — Open PR

1. Verify upstream remote with `git remote -v`. It should point at `tinyhumansai/openhuman`. If missing, ask the user before adding it.
2. Check whether a PR already exists for this branch:
`gh pr list --repo tinyhumansai/openhuman --head <fork-owner>:<branch> --state open --json number,url`
- **If a PR exists**, capture its `number` and `url`, print the URL, skip steps 3–5, and proceed straight to Phase 4 with that PR#.
3. If none exists, draft a title (<70 chars) and a body that follows `.github/PULL_REQUEST_TEMPLATE.md` exactly. Inspect commits with `git log main..HEAD` and the diff with `git diff main...HEAD` to write the summary. If you bypassed a pre-push hook, note it in the PR body.
- When filling the Submission Checklist, write each item as `- [ ] N/A: <reason>` (the item text MUST start with `N/A:` for `scripts/check-pr-checklist.mjs` to count it as satisfied; trailing `— N/A: ...` won't match), or `- [x] <text>` for genuinely checked items.
4. Create the PR:
```bash
gh pr create --repo tinyhumansai/openhuman --base main --head <fork-owner>:<branch> \
--title "..." --body "$(cat <<'EOF'
...template-filled body...
EOF
)"
```
5. Add appropriate labels/type if conventional for this repo.
6. Capture the PR number and URL — you will need them in Phase 4. Print the URL to the user.

## Phase 4 — Babysit loop (~5 minutes)

Repeat the following loop until the exit condition is met. Use `ScheduleWakeup` to pace at **270s** (stays inside the prompt-cache window) — re-enter this phase each tick by passing the same `/ship-and-babysit` invocation back as the prompt.

**Hard cap: 12 ticks (~60 minutes).** After that, stop the loop and ask the user, including PR URL, current CI snapshot, and any unresolved CodeRabbit threads. Maintain an explicit `tickCount` that increments by 1 on every loop entry (regardless of whether you commit or only wait on CI), and pass it through in the `ScheduleWakeup` `reason` (e.g. `"tick 5/12: waiting on CI for PR #1115"`) so the counter is visible across ticks and can't drift if a tick produces no commits.

Each tick:

1. **Fetch CI status**:
`gh pr checks <PR#> --repo tinyhumansai/openhuman --json name,state,link,description`
- `gh pr checks --json` returns a `link` field (an Actions URL like `…/actions/runs/<id>/job/<jobId>`), not a run id directly. Extract the run id with a regex that's robust to trailing slashes (`sed -nE 's#.*/actions/runs/([0-9]+)/.*#\1#p'`) — positional `awk -F/` is brittle when the URL has a trailing slash. Or skip URL parsing entirely and call `gh run list --repo tinyhumansai/openhuman --branch <branch> --json databaseId --limit 1 --jq '.[0].databaseId'`.
- If any check is `FAILURE` or `CANCELLED`, branch by check type: when `link` matches `/actions/runs/<id>/` (Actions-backed), extract `<id>` and fetch logs with `gh run view <id> --log-failed --repo tinyhumansai/openhuman`; when it doesn't (e.g. the `CodeRabbit` virtual check or any other status posted directly via the Checks API without an Actions run), skip `gh run view` and work from the `name`/`state`/`description` fields plus any review comments. Then fix the underlying issue: edit code, commit (conventional prefix), push to `origin`. Do NOT skip hooks or disable failing tests to make CI green.
- For local repro of common failures before pushing fixes:
- Frontend: `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm test:unit`.
- Rust: `cargo check --manifest-path Cargo.toml`, `cargo check --manifest-path app/src-tauri/Cargo.toml`, `pnpm test:rust`.
- Coverage gate is **≥ 80% on changed lines** (`.github/workflows/coverage.yml`) — if coverage fails, add tests for changed lines, not just happy path.
2. **Fetch CodeRabbit review comments**:
`gh api repos/tinyhumansai/openhuman/pulls/<PR#>/comments --paginate`
Filter for comments authored by `coderabbitai` / `coderabbitai[bot]`. Also check issue-level comments: `gh api repos/tinyhumansai/openhuman/issues/<PR#>/comments --paginate`.
- For each unresolved CodeRabbit suggestion: read the file/line referenced and apply the fix if it is correct and in scope. If a suggestion is wrong or out of scope, reply *inside the existing thread* (so the reply attaches to the same conversation, not a brand-new review) before resolving:
```bash
gh api repos/tinyhumansai/openhuman/pulls/comments/<comment_id>/replies \
-X POST \
-f body='**Dismissed:** <reason>'
```
(`<comment_id>` is the top-level review-comment id from `gh api repos/tinyhumansai/openhuman/pulls/<PR#>/comments`. `POST /pulls/<PR#>/reviews` would create a *new* review thread, not a reply.)
- After fixing, commit and push to `origin`.
- Mark the corresponding review thread as resolved via the GraphQL API:
```bash
gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id=<threadId>
```
To list thread IDs (paginated — `reviewThreads` caps at 100 per page, so loop on `pageInfo.hasNextPage` / `endCursor` and feed back as `$cursor` until exhausted, otherwise threads past page 1 silently slip past the exit condition):
```bash
gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$num){reviewThreads(first:100, after:$cursor){pageInfo{hasNextPage endCursor} nodes{id isResolved comments(first:1){nodes{author{login} body}}}}}}}' -F owner=tinyhumansai -F repo=openhuman -F num=<PR#> -F cursor=
```
3. **Exit condition** — stop the loop when ALL of these are true:
- All required checks are `SUCCESS`. `PENDING` keeps the loop running, no exceptions — no "green" claim while CI is mid-run.
- No unresolved CodeRabbit review threads remain.
- No new CodeRabbit issue comments since the last tick that request changes. Track this by remembering the highest CodeRabbit issue-comment `id` seen on the previous tick (the GitHub issue-comment id is monotonic) and only treating ids strictly greater than that marker as new on the current tick.
When the exit condition holds, do NOT call `ScheduleWakeup` — return a final one-line summary with the PR URL and current status.
4. **Pacing**: if exiting, stop. Otherwise call `ScheduleWakeup` with `delaySeconds: 270`, `prompt: "/ship-and-babysit"`, and a specific `reason` like "waiting on CI for PR #123" or "applied 2 CodeRabbit fixes, re-checking".

## Guardrails

- Never push to `upstream` (`tinyhumansai/openhuman`) — only to `origin` (the user's fork). Treat upstream as fetch-only.
- Never force-push to `main`. Never amend pushed commits.
- Never use `--no-verify` to bypass hooks failing on your own changes. The only sanctioned bypass is a pre-push hook failing on pre-existing unrelated breakage — call it out in the PR body when you do.
- Never resolve a CodeRabbit thread without actually addressing it (or replying with a reasoned dismissal).
- If you hit a blocker that needs human input (auth failure, ambiguous CodeRabbit suggestion, conflicting feedback, merge conflict, vendored `tauri-cli` missing), stop the loop and ask the user instead of guessing.
- Do not merge the PR. Stop at "green and clean".
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 79 additions & 30 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,14 @@ async fn restart_app(app: tauri::AppHandle<AppRuntime>) -> Result<(), String> {
log::warn!("[app] hide main window before restart failed: {err}");
}
}

log::info!("[app] restart_app — starting early teardown before restart");
perform_early_teardown_async(&app).await;
log::info!("[app] restart_app — early teardown complete, restarting");

app.restart();
// restart() does not return, but we must satisfy the signature
Ok(())
}

/// Read the authoritative active user id from `active_user.toml` so the
Expand Down Expand Up @@ -429,6 +436,10 @@ async fn apply_app_update(

log::info!("[app-update] install complete — relaunching");
let _ = app.emit("app-update:status", "restarting");

log::info!("[app-update] starting early teardown before restart");
perform_early_teardown_async(&app).await;

// Note: app.restart() never returns. Anything after this is unreachable.
app.restart();
}
Expand Down Expand Up @@ -606,6 +617,10 @@ async fn install_app_update(

log::info!("[app-update] install complete — relaunching");
let _ = app.emit("app-update:status", "restarting");

log::info!("[app-update] starting early teardown before restart");
perform_early_teardown_async(&app).await;

// Note: app.restart() never returns. Anything after this is unreachable.
app.restart();
}
Expand Down Expand Up @@ -916,7 +931,7 @@ fn setup_tray(app: &AppHandle<AppRuntime>) -> tauri::Result<()> {
}
"tray_quit" => {
log::info!("[tray] action=quit source=menu");
app.exit(0);
shutdown_app_sync(app, 0);
}
_ => {}
})
Expand Down Expand Up @@ -982,6 +997,68 @@ fn teardown_cef_prewarm<R: tauri::Runtime>(app: &AppHandle<R>) -> Result<(), Str
Ok(())
}

/// Shared early teardown logic before CEF's shutdown to prevent races and zombie processes.
/// Synchronous version to be called from the main thread (e.g. `RunEvent::ExitRequested` or tray menu events).
fn perform_early_teardown_sync(app_handle: &AppHandle<AppRuntime>) {
log::info!("[app] perform_early_teardown_sync — early teardown");

let _ = teardown_cef_prewarm(app_handle);

if let Some(state) = app_handle.try_state::<webview_accounts::WebviewAccountsState>() {
state.shutdown_all(app_handle);
}

webview_apis::server::stop();

if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
let core = core.inner().clone();
// Aborts the embedded server task. Synchronous and safe on
// the UI thread — `JoinHandle::abort` returns immediately.
tauri::async_runtime::block_on(async move {
core.send_terminate_signal().await;
});
}

// Give CEF's UI message loop a brief window to process the
// queued browser close messages before the runtime calls `cef::shutdown()`.
std::thread::sleep(std::time::Duration::from_millis(50));

log::info!("[app] perform_early_teardown_sync — early teardown complete");
}

/// Shared early teardown logic before CEF's shutdown to prevent races and zombie processes.
/// Asynchronous version to be called from async Tauri commands (e.g. `restart_app`, updates).
async fn perform_early_teardown_async(app_handle: &AppHandle<AppRuntime>) {
log::info!("[app] perform_early_teardown_async — early teardown");

let _ = teardown_cef_prewarm(app_handle);

if let Some(state) = app_handle.try_state::<webview_accounts::WebviewAccountsState>() {
state.shutdown_all(app_handle);
}

webview_apis::server::stop();

if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
let core = core.inner().clone();
core.send_terminate_signal().await;
}

// Give CEF's UI message loop a brief window to process the
// queued browser close messages before the runtime calls `cef::shutdown()`.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;

log::info!("[app] perform_early_teardown_async — early teardown complete");
}

/// Explicitly winds down CEF and Tauri before an app.exit(0)
fn shutdown_app_sync(app_handle: &AppHandle<AppRuntime>, exit_code: i32) {
log::info!("[app] shutdown_app_sync — starting early teardown");
perform_early_teardown_sync(app_handle);
log::info!("[app] shutdown_app_sync — early teardown complete, exiting");
app_handle.exit(exit_code);
}

pub fn run() {
// Initialize Sentry for the Tauri shell (desktop host) process before any
// other startup work. Reads `OPENHUMAN_TAURI_SENTRY_DSN` at runtime first,
Expand Down Expand Up @@ -1675,35 +1752,7 @@ pub fn run() {
// do not wait — that would block the main thread
// and starve CEF's UI loop. The kernel reaps the
// child after Tauri exits.
log::info!("[app] RunEvent::ExitRequested — early teardown");

let _ = teardown_cef_prewarm(app_handle);

if let Some(state) =
app_handle.try_state::<webview_accounts::WebviewAccountsState>()
{
state.shutdown_all(app_handle);
}

webview_apis::server::stop();

if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
let core = core.inner().clone();
// Aborts the embedded server task. Synchronous and safe on
// the UI thread — `JoinHandle::abort` returns immediately.
tauri::async_runtime::block_on(async move {
core.send_terminate_signal().await;
});
}

// Give CEF's UI message loop a brief window to process the
// queued browser close messages before the runtime calls
// `cef::shutdown()`. Without this, a webview that was mid-load
// when the user quit can race the shutdown and leave its
// renderer helper orphaned (re-parented to launchd on macOS).
std::thread::sleep(std::time::Duration::from_millis(50));

log::info!("[app] RunEvent::ExitRequested — early teardown complete");
perform_early_teardown_sync(app_handle);
}
RunEvent::Exit => {
log::info!("[app] RunEvent::Exit — cef::shutdown follows");
Expand Down
30 changes: 15 additions & 15 deletions app/src/components/BottomTabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,21 @@ const tabs = [
),
},
// Memory tab hidden until Intelligence feature is ready (#976)
// {
// id: 'intelligence',
// label: 'Memory',
// path: '/intelligence',
// icon: (
// <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={1.8}
// d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
// />
// </svg>
// ),
// },
{
id: 'intelligence',
label: 'Memory',
path: '/intelligence',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
),
},
{
id: 'notifications',
label: 'Alerts',
Expand Down
8 changes: 8 additions & 0 deletions app/src/constants/onboardingChat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Label applied to the welcome thread created when the user finishes the
* desktop onboarding wizard. The thread is deleted once the welcome agent
* calls `complete_onboarding(action: "complete")`. While it exists, the label
* lets the UI hide all other threads during welcome lockdown and show a stable
* "Onboarding" title.
*/
export const ONBOARDING_WELCOME_THREAD_LABEL = 'onboarding';
Loading